diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0d65bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.backup/ +__pycache__/ +*.pyc +.DS_Store diff --git a/README.md b/README.md index 67fd242..668a08f 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,61 @@ Tecnotel Servizi SRL — [www.tecnotelsrl.com](https://www.tecnotelsrl.com) Repository pubblico contenente l'installer di prima fase e il Web Setup Wizard di ARGOS SOC. +## Installazione rapida (one-liner) + +Su una macchina Ubuntu 24.04 LTS vergine, esegui: + +```bash +curl -fsSL https://argos-update.tecnotelsrl.com/tecnotel/argos-setup/raw/branch/main/bootstrap.sh | sudo bash +``` + +Il bootstrap scaricherà il repository e avvierà `first-setup.sh`. + +## Installazione manuale + +Se preferisci eseguire gli step separatamente: + +```bash +sudo apt update && sudo apt install -y git +git clone https://argos-update.tecnotelsrl.com/tecnotel/argos-setup.git /opt/argos-setup-pkg +cd /opt/argos-setup-pkg +sudo bash first-setup.sh +``` + ## Contenuto | File | Scopo | |---|---| -| `first-setup.sh` | Installer ambiente di prima fase: sistema base, utenti, firewall, virtualenv, nginx. | -| `setup_server.py` | Backend Flask del Web Installer (porta 8888). | -| `setup.html` | Frontend del Web Installer, wizard in 5 step. | -| `gen_config.py` | Helper per generazione iniziale di `argos.json`. | +| `bootstrap.sh` | One-liner installer: scarica il repo e avvia first-setup.sh | +| `first-setup.sh` | Installer ambiente base: sistema, utenti, firewall, nginx temp, avvia wizard web | +| `setup_server.py` | Backend Python del Web Installer (porta 8888, self-contained) | +| `setup.html` | Frontend del Web Installer — wizard in 6 step | +| `gen_config.py` | Helper per generazione iniziale di `argos.json` | ## Flusso d'installazione -1. Il cliente esegue `first-setup.sh` per predisporre l'ambiente base (Ubuntu 24.04). -2. Al termine dello script viene avviato il Web Installer su `http://IP:8888`. -3. Il wizard completa la configurazione: cliente, rete/SSL, SIEM, utente admin. -4. Al termine del wizard, `argos-setup` richiede la licenza ARGOS per sbloccare il clone del repository privato `tecnotel/argos` e completare l'installazione dei componenti runtime. +1. Il cliente esegue il bootstrap (o il clone manuale + first-setup.sh). +2. `first-setup.sh` predispone l'ambiente base Ubuntu 24.04 (pacchetti, utente argos, cartelle, firewall) e avvia il Web Installer su `http://IP:8888`. +3. Il cliente apre il browser e segue il wizard in 6 step: + 1. **Licenza ARGOS** — il wizard mostra il `machine_id` di questo server. Il cliente lo invia a Tecnotel, riceve `license.json`, lo carica nel wizard. + 2. **Informazioni cliente** (nome, dominio, logo) + 3. **Rete & SSL** (hostname, certificato) + 4. **SIEM** (OpenSearch) + 5. **Utente admin** + 6. **Riepilogo & Installazione** — il wizard clona il repository privato `tecnotel/argos` usando il token della licenza, crea virtualenv, configura servizi e avvia ARGOS SOC. + +Al termine il Web Installer viene disattivato automaticamente e la porta 8888 chiusa. + +## Requisiti + +- Ubuntu 24.04 LTS (verificato dallo script) +- Accesso root (sudo) +- Connessione internet verso `argos-update.tecnotelsrl.com` +- Una licenza ARGOS valida emessa da Tecnotel per il `machine_id` del server ## Repository correlati -- [`tecnotel/argos`](https://argos-update.tecnotelsrl.com/tecnotel/argos) — codice runtime di ARGOS SOC (privato). +- [`tecnotel/argos`](https://argos-update.tecnotelsrl.com/tecnotel/argos) — codice runtime di ARGOS SOC (privato, accessibile solo con licenza). ## Versioning diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 0000000..28ccd29 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# ══════════════════════════════════════════════════════════════════════════════ +# ARGOS SOC — Bootstrap Installer (one-liner) +# Tecnotel Servizi SRL — Ubuntu 24.04 LTS +# +# Uso tramite one-liner: +# curl -fsSL https://argos-update.tecnotelsrl.com/tecnotel/argos-setup/raw/branch/main/bootstrap.sh | sudo bash +# +# Oppure manuale (raccomandato per verifica): +# curl -fsSLo /tmp/argos-bootstrap.sh https://argos-update.tecnotelsrl.com/tecnotel/argos-setup/raw/branch/main/bootstrap.sh +# less /tmp/argos-bootstrap.sh # verifica cosa fa +# sudo bash /tmp/argos-bootstrap.sh +# ══════════════════════════════════════════════════════════════════════════════ +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" >&2; exit 1; } + +# ── Check privilegi e OS ────────────────────────────────────────────────────── +[[ $EUID -ne 0 ]] && error "Eseguire come root (sudo)." + +if [[ ! -f /etc/os-release ]]; then + error "OS non riconosciuto: /etc/os-release mancante." +fi +. /etc/os-release +if [[ "$ID" != "ubuntu" || "$VERSION_ID" != "24.04" ]]; then + error "Richiesto Ubuntu 24.04 LTS (trovato: $ID $VERSION_ID)." +fi + +echo "" +echo -e "${BLUE}╔══════════════════════════════════════════════════╗${NC}" +echo -e "${BLUE}║ ARGOS SOC — Bootstrap Installer ║${NC}" +echo -e "${BLUE}║ Tecnotel Servizi SRL ║${NC}" +echo -e "${BLUE}╚══════════════════════════════════════════════════╝${NC}" +echo "" + +# ── Variabili ──────────────────────────────────────────────────────────────── +SETUP_REPO_URL="https://argos-update.tecnotelsrl.com/tecnotel/argos-setup.git" +SETUP_DIR="/opt/argos-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 gia' presente ($(git --version | awk '{print $3}'))" +fi + +# ── Scarica o aggiorna argos-setup ─────────────────────────────────────────── +if [[ -d "$SETUP_DIR/.git" ]]; then + info "argos-setup gia' presente in $SETUP_DIR — aggiorno..." + git -C "$SETUP_DIR" pull --ff-only origin main + success "argos-setup aggiornato" +else + info "Scarico argos-setup da $SETUP_REPO_URL..." + rm -rf "$SETUP_DIR" + git clone --depth=1 "$SETUP_REPO_URL" "$SETUP_DIR" + success "argos-setup scaricato in $SETUP_DIR" +fi + +# ── Verifica presenza file attesi ──────────────────────────────────────────── +for f in first-setup.sh setup_server.py setup.html; do + if [[ ! -f "$SETUP_DIR/$f" ]]; then + error "File $f mancante in $SETUP_DIR — repo argos-setup incompleto?" + fi +done + +# ── Lancia first-setup.sh ──────────────────────────────────────────────────── +info "Avvio installer principale..." +echo "" +cd "$SETUP_DIR" +exec bash ./first-setup.sh diff --git a/first-setup.sh b/first-setup.sh old mode 100644 new mode 100755 index 9d2b704..77bbb42 --- a/first-setup.sh +++ b/first-setup.sh @@ -2,7 +2,7 @@ # ══════════════════════════════════════════════════════════════════════════════ # ARGOS SOC — Installer ambiente # Tecnotel Servizi SRL — Ubuntu 24.04 LTS -# Uso: sudo bash install.sh +# Uso: sudo bash first-setup.sh # ══════════════════════════════════════════════════════════════════════════════ set -euo pipefail @@ -14,20 +14,18 @@ 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 install.sh" +[[ $EUID -ne 0 ]] && error "Eseguire come root: sudo bash first-setup.sh" . /etc/os-release [[ "$ID" != "ubuntu" || "$VERSION_ID" != "24.04" ]] && error "Richiesto Ubuntu 24.04 LTS" echo "" echo -e "${BLUE}╔══════════════════════════════════════════╗${NC}" -echo -e "${BLUE}║ ARGOS SOC — Installer v1.0.0 ║${NC}" +echo -e "${BLUE}║ ARGOS SOC — Setup ambiente base ║${NC}" echo -e "${BLUE}║ Tecnotel Servizi SRL ║${NC}" echo -e "${BLUE}╚══════════════════════════════════════════╝${NC}" echo "" -GITEA_REPO="https://3eefb5a2802e8c5a9395396b1bb98e2d5fe46101@argos-update.tecnotelsrl.com:3443/tecnotel/argos.git" - # ══════════════════════════════════════════════════════════════════════════════ section "1. Sistema base" # ══════════════════════════════════════════════════════════════════════════════ @@ -36,7 +34,7 @@ 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 python3-pip python3-venv python3-cryptography \ nginx certbot python3-certbot-nginx \ ufw fail2ban \ build-essential libssl-dev libffi-dev python3-dev \ @@ -92,33 +90,7 @@ chmod 755 /opt/argos/feeds success "Struttura /opt/argos/ creata" # ══════════════════════════════════════════════════════════════════════════════ -section "5. Clone repository" -# ══════════════════════════════════════════════════════════════════════════════ -git config --global --add safe.directory /opt/argos/app 2>/dev/null || true -if [[ -d /opt/argos/app/.git ]]; then - warn "Repository già presente — aggiorno" - git -C /opt/argos/app pull origin main -else - git clone "$GITEA_REPO" /opt/argos/app -fi -chown -R argos:argos /opt/argos/app -success "Repository clonato in /opt/argos/app/" - -# ══════════════════════════════════════════════════════════════════════════════ -section "6. Python virtualenv" -# ══════════════════════════════════════════════════════════════════════════════ -python3 -m venv /opt/argos/app/backend/venv -/opt/argos/app/backend/venv/bin/pip install --upgrade pip -q -if [[ -f /opt/argos/app/backend/requirements.txt ]]; then - /opt/argos/app/backend/venv/bin/pip install -r /opt/argos/app/backend/requirements.txt -q - success "Dipendenze Python installate" -else - warn "requirements.txt non trovato — installare manualmente dopo" -fi -chown -R argos:argos /opt/argos/app/backend/venv - -# ══════════════════════════════════════════════════════════════════════════════ -section "7. Firewall UFW" +section "5. Firewall UFW" # ══════════════════════════════════════════════════════════════════════════════ ufw --force reset >/dev/null ufw default deny incoming >/dev/null @@ -131,13 +103,13 @@ ufw --force enable >/dev/null success "Firewall UFW configurato" # ══════════════════════════════════════════════════════════════════════════════ -section "8. Fail2ban" +section "6. Fail2ban" # ══════════════════════════════════════════════════════════════════════════════ systemctl enable --now fail2ban >/dev/null 2>&1 success "Fail2ban attivo" # ══════════════════════════════════════════════════════════════════════════════ -section "9. Nginx temporaneo" +section "7. Nginx temporaneo" # ══════════════════════════════════════════════════════════════════════════════ rm -f /etc/nginx/sites-enabled/default cat > /etc/nginx/sites-available/argos-setup << 'NGINX' @@ -153,11 +125,18 @@ nginx -t && systemctl restart nginx success "Nginx temporaneo configurato" # ══════════════════════════════════════════════════════════════════════════════ -section "10. Web Installer" +section "8. Web Installer" # ══════════════════════════════════════════════════════════════════════════════ -# Copia file setup -cp /opt/argos/app/scripts/setup_server.py /opt/argos/setup/ -cp /opt/argos/app/scripts/setup.html /opt/argos/setup/ +# Copia file setup dalla directory dello script (argos-setup tarball) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +for f in setup_server.py setup.html gen_config.py; do + if [[ -f "$SCRIPT_DIR/$f" ]]; then + cp "$SCRIPT_DIR/$f" /opt/argos/setup/ + else + error "File $f non trovato in $SCRIPT_DIR — installare argos-setup completo" + fi +done +chown -R root:root /opt/argos/setup # Systemd service web installer cat > /etc/systemd/system/argos-setup.service << 'EOF' @@ -169,7 +148,7 @@ After=network.target Type=simple User=root WorkingDirectory=/opt/argos/setup -ExecStart=/opt/argos/app/backend/venv/bin/python3 /opt/argos/setup/setup_server.py +ExecStart=/usr/bin/python3 /opt/argos/setup/setup_server.py Restart=on-failure RestartSec=3 StandardOutput=journal diff --git a/setup.html b/setup.html index 2aca48b..80a8561 100644 --- a/setup.html +++ b/setup.html @@ -95,11 +95,12 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
@@ -108,10 +109,62 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
+
- +
+
🔑 Licenza ARGOS
+
ARGOS SOC richiede una licenza valida emessa da Tecnotel Servizi SRL, + vincolata all'identificativo hardware di questo server.
+ +
+
1. Identificativo hardware del server
+
+ +
+ + +
+
Invia questo ID a Tecnotel Servizi SRL per ricevere la licenza. + Email: info@tecnotelsrl.com
+
+ +
2. Carica license.json ricevuta
+
+ +
📜
+
Trascina license.json o clicca per selezionare
+
File JSON firmato Ed25519 — max 10KB
+
+ + + + +
+ + +
+ + +
👤 Informazioni cliente
Dati dell'organizzazione che utilizzerà ARGOS SOC.
@@ -129,11 +182,11 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
- +
- -
+ +
🌐 Rete & SSL
Configurazione dominio e certificato SSL. Il DNS deve già puntare a questo server.
@@ -152,11 +205,11 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
- +
- -
+ +
🔍 SIEM — OpenSearch
Connessione al backend SIEM. Le altre integrazioni si configurano dall'interfaccia ARGOS dopo l'installazione.
@@ -169,7 +222,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
-
+
🔑 Utente amministratore
Crea il primo account admin per accedere a ARGOS SOC.
@@ -183,8 +236,8 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
- -
+ +
🚀 Riepilogo & Installazione
Verifica i dati e avvia l'installazione.
@@ -235,6 +288,91 @@ let keyUploaded = false; let installing = false; let installDomain = ''; +// ── Licenza ARGOS: fetch machine_id e gestione upload ──────────────────────── +let licenseValid = false; + +async function loadMachineId() { + try { + const r = await fetch('/api/machine-id'); + const d = await r.json(); + const el = document.getElementById('machine-id-display'); + if (d && d.machine_id) { + el.value = d.machine_id; + } else { + el.value = 'Errore caricamento'; + } + } catch (e) { + document.getElementById('machine-id-display').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-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'; + } catch (e) { + errEl.textContent = 'Errore upload: ' + e.message; + errEl.style.display = 'block'; + } +} + +// Al caricamento pagina: fetch machine_id +window.addEventListener('DOMContentLoaded', loadMachineId); + + + function goTab(n) { if (n > currentTab) document.getElementById('tab-' + currentTab).classList.add('done'); document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); @@ -254,7 +392,7 @@ function goToInstall() { const err = document.getElementById('pw-error'); if (pw1 !== pw2) { err.style.display = 'block'; return; } err.style.display = 'none'; - goTab(4); + goTab(5); buildSummary(); } diff --git a/setup_server.py b/setup_server.py index f9eb864..c256b52 100644 --- a/setup_server.py +++ b/setup_server.py @@ -26,11 +26,121 @@ SETUP_DIR = Path("/opt/argos/setup") APP_USER = "argos" PORT = 8888 +# ── Licenza ARGOS — pubkey per verifica firma Ed25519 ───────────────────────── +# Stessa pubkey hardcoded in backend/core.py. Raw Ed25519 (32 byte) base64. +# Il setup_server la usa per verificare licenze ricevute dal cliente PRIMA +# di procedere con l'installazione. +_LICENSE_PUBLIC_KEY_B64 = "GMRsZMoxOlCBiJU66EsQcj0ZO0gVd0GHB5LelEo/hns=" + +# ── Clone argos: username Basic Auth bot Gitea (costante del sistema) ───────── +GITEA_BOT_USER = "argos-portal-bot" +GITEA_REPO_PATH = "/tecnotel/argos.git" + install_log = [] install_done = False install_error = False +def get_machine_id() -> str: + """Fingerprint univoco del server (stesso algoritmo di core.py). + SHA256 hex di: /etc/machine-id | hostname | MAC prima interfaccia fisica. + """ + import socket as _sock + import subprocess as _sp + 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 = _sp.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 = _sp.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("") + combined = "|".join(parts) + return hashlib.sha256(combined.encode()).hexdigest() + + +def verify_license(raw_bytes): + """Verifica firma Ed25519 + machine_id match + scadenza. + + Ritorna tupla (ok, license_dict, error_message). + Se ok=True, license_dict contiene il payload completo (inclusi gitea_url, + gitea_token se presenti). Se ok=False, error_message e' umano-friendly. + """ + try: + raw = json.loads(raw_bytes) + except Exception as e: + return (False, None, f"File non e' JSON valido: {e}") + + if not isinstance(raw, dict): + return (False, None, "Formato licenza non riconosciuto") + + # Verifica firma + try: + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + from cryptography.exceptions import InvalidSignature + import base64 + 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 # ripristina + pub_bytes = base64.b64decode(_LICENSE_PUBLIC_KEY_B64) + pub = Ed25519PublicKey.from_public_bytes(pub_bytes) + try: + pub.verify(base64.b64decode(sig), payload.encode()) + except InvalidSignature: + return (False, None, "Firma non valida. La licenza potrebbe essere manipolata o emessa da altro vendor.") + except ImportError: + return (False, None, "Libreria 'cryptography' non disponibile. Installa: apt install python3-cryptography") + except Exception as e: + return (False, None, f"Errore verifica firma: {e}") + + # Verifica machine_id + 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 e' {cur_machine[:12]}... " + "La licenza non e' valida per questa macchina.") + + # Verifica scadenza + expires = raw.get("expires_at", "") + if expires: + today = datetime.now().strftime("%Y-%m-%d") + if expires < today: + return (False, None, f"Licenza scaduta il {expires}") + + # Verifica presenza credenziali Gitea (la licenza deve averle per permettere clone) + if not raw.get("gitea_url") or not raw.get("gitea_token"): + return (False, None, + "Licenza priva di credenziali Gitea. " + "Contatta Tecnotel per riemetterla con il nuovo formato.") + + return (True, raw, "") + + + def log(msg): ts = datetime.now().strftime("%H:%M:%S") line = f"[{ts}] {msg}" @@ -166,7 +276,51 @@ def install(data): try: log("=== AVVIO INSTALLAZIONE ARGOS SOC ===") - # 1. argos.json + # 0. Carica licenza (gia' validata da /api/license/upload) + log("── Verifica licenza ARGOS ──") + lic_path = SETUP_DIR / "license.json" + if not lic_path.exists(): + raise RuntimeError( + "License.json non trovata in /opt/argos/setup/. " + "Caricare una licenza valida prima di avviare l'installazione." + ) + 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", "") + # Deriva host dal gitea_url (es: https://host/api/v1 -> https://host) + 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 repository argos (URL autenticato temporaneo: token NON in .git/config) + log("── Clone repository ARGOS ──") + if APP_DIR.exists() and (APP_DIR / ".git").exists(): + log("Repository gia' 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) + # Clona con URL autenticato + run(f"git clone {auth_url} {APP_DIR}") + # Dopo clone, ripulisci .git/config rimuovendo il token + clean_url = f"{gitea_host}{GITEA_REPO_PATH}" + run(f"git -C {APP_DIR} remote set-url origin {clean_url}") + run(f"chown -R {APP_USER}:{APP_USER} {APP_DIR}") + log("Repository ARGOS clonato") + + # 2. Python virtualenv + log("── Creazione 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") + req_file = APP_DIR / "backend/requirements.txt" + if req_file.exists(): + run(f"{venv_dir}/bin/pip install -r {req_file} -q") + run(f"chown -R {APP_USER}:{APP_USER} {venv_dir}") + log("Virtualenv Python pronto") + + # 3. argos.json log("── Generazione argos.json ──") CONFIG_DIR.mkdir(parents=True, exist_ok=True) config = generate_argos_json(data) @@ -177,7 +331,7 @@ def install(data): chown(CONFIG_DIR) log("argos.json creato") - # 2. integrations.json + # 4. integrations.json log("── Generazione integrations.json ──") integ = generate_integrations_json() # Aggiorna pdf con nome organizzazione @@ -191,14 +345,14 @@ def install(data): chown(integ_path) log("integrations.json creato") - # 3. modules.json + # 5. modules.json mods = APP_DIR / "config/modules.json.example" if mods.exists(): shutil.copy(mods, CONFIG_DIR / "modules.json") chown(CONFIG_DIR / "modules.json") log("modules.json copiato") - # 4. Logo cliente + # 6. Logo cliente logo_src = SETUP_DIR / "logo_cliente.png" if logo_src.exists(): DATA_DIR.mkdir(parents=True, exist_ok=True) @@ -206,7 +360,7 @@ def install(data): chown(CONFIG_DIR / "assets" / "logo_cliente.png") log("Logo cliente copiato") - # 5. Build frontend + # 7. Build frontend log("── Build frontend React ──") run(f"cd {APP_DIR}/frontend && npm install --silent") run(f"cd {APP_DIR}/frontend && npm run build") @@ -216,7 +370,7 @@ def install(data): run(f"chmod -R 755 {APP_DIR}/frontend/dist/") log("Frontend buildato") - # 6. Dipendenze Python + # 8. Dipendenze Python (re-run in caso di aggiornamenti) log("── Dipendenze Python ──") venv_pip = APP_DIR / "backend/venv/bin/pip" req = APP_DIR / "backend/requirements.txt" @@ -224,11 +378,11 @@ def install(data): run(f"{venv_pip} install -r {req} -q") log("Dipendenze Python installate") - # 7. Utente admin + # 9. Utente admin log("── Creazione utente admin ──") create_admin_user(data) - # 8. SSL + # 10. SSL log("── Configurazione SSL ──") domain = data.get("domain", "").strip() aliases = data.get("aliases", "").strip() @@ -257,13 +411,13 @@ def install(data): ssl_key = f"/etc/letsencrypt/live/{domain}/privkey.pem" log("Certificato Let's Encrypt ottenuto") - # 9. Nginx finale + # 11. Nginx finale log("── Nginx configurazione finale ──") _write_nginx_final(all_names, ssl_crt, ssl_key) run("nginx -t && systemctl restart nginx") log("Nginx configurato") - # 10. Systemd services + # 12. Systemd services log("── Creazione e avvio servizi ──") _write_services() run("systemctl daemon-reload") @@ -271,7 +425,15 @@ def install(data): run(f"systemctl enable --now {svc}") log(f"{svc} avviato") - # 11. Chiudi web installer + # 13. Copia licenza + chiudi web installer + log("── Copia licenza in posizione finale ──") + DATA_DIR.mkdir(parents=True, exist_ok=True) + 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}") + log("── Chiusura web installer ──") run("systemctl disable --now argos-setup", check=False) run("ufw delete allow 8888/tcp", check=False) @@ -435,6 +597,8 @@ class SetupHandler(BaseHTTPRequestHandler): 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() @@ -462,6 +626,28 @@ class SetupHandler(BaseHTTPRequestHandler): SETUP_DIR.mkdir(parents=True, exist_ok=True) (SETUP_DIR / "logo_cliente.png").write_bytes(body) 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 + # Salva licenza valida nel SETUP_DIR per essere usata durante install() + SETUP_DIR.mkdir(parents=True, exist_ok=True) + lic_path = SETUP_DIR / "license.json" + lic_path.write_bytes(body) + os.chmod(lic_path, 0o600) + # Risposta: solo dati sommari (non rinvia token in plain) + 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", ""), + "has_gitea": bool(lic.get("gitea_token")), + } + }) else: self.send_response(404); self.end_headers()