#!/bin/bash
# Installe une VM Kanadr sous Proxmox VE en une commande, depuis le shell de l'hôte (D1, D109) :
#
#   curl -fsSL https://apt.kanadr.com/images/proxmox.sh | bash -s -- --id 120 \
#       --nom pbx.operateur.exemple --ip 192.0.2.50/24 --passerelle 192.0.2.254 \
#       --cle "ssh-ed25519 AAAA… admin@operateur"
#
# Télécharge la dernière image (ou --version), vérifie son empreinte SHA-256, crée la VM (agent
# invité activé), importe et attache le disque, règle cloud-init (compte, clé SSH, adresse),
# démarre, attend la fin du premier démarrage par l'agent invité, puis affiche le lien de mise en
# route (sous le nom et sous l'adresse IP) et la commande SSH. S'arrête net sur toute erreur ;
# une VM créée mais pas démarrée est supprimée, une VM démarrée est laissée et le message le dit.
# Publié à côté de l'image par ci/image/publier.sh ; --essai affiche les commandes sans rien
# lancer (vérifié par ci/image/tester-proxmox.sh).
set -euo pipefail

aide() {
    cat <<'AIDE'
Installe une VM Kanadr sous Proxmox VE (shell de l'hôte, en root).

Usage : proxmox.sh --id <VMID> --nom <nom complet> (--ip <adresse/préfixe> --passerelle <IP> | --dhcp)
                   (--cle "<clé SSH publique>" | --fichier-cle <fichier>) [options]

  --id N               VMID libre (100 ou plus)
  --nom NOM            nom complet de la machine (pbx.operateur.exemple) : adresse du portail
  --ip A/P             adresse fixe et préfixe (192.0.2.50/24) ; ou --dhcp
  --passerelle IP      passerelle, avec --ip
  --dns "IP IP"        serveurs DNS (défaut : ceux de l'hôte Proxmox)
  --dhcp               adresse donnée par le DHCP
  --cle "ssh-…"        clé SSH publique du compte d'administration
  --fichier-cle F      ou un fichier de clés (authorized_keys)
  --utilisateur U      compte d'administration, avec sudo (défaut : debian)
  --stockage S         stockage des disques (défaut : local-lvm)
  --pont P             pont réseau (défaut : vmbr0)
  --disque T           taille du disque système, en Go (défaut : 32G, au moins 12G)
  --enregistrements T  second disque, pour les enregistrements d'appels (défaut : 50G ; 0 : aucun)
  --coeurs N           vCPU (défaut : 2)
  --memoire Mo         mémoire en Mo (défaut : 4096)
  --version V          version de l'image (défaut : la dernière publiée)
  --essai              affiche les commandes sans rien lancer
  --aide               cette aide
AIDE
}

BASE=${KANADR_IMAGES:-https://apt.kanadr.com/images}
# Taille du disque de l'image, en Go.
DISQUE_IMAGE=12
MARQUEUR=/var/lib/kanadr-systeme/premier-demarrage-fait

erreur() {
    printf 'ERREUR : %s\n' "$*" >&2
    exit 1
}

vmid="" nom="" ip="" passerelle="" dns="" cle="" fichier_cle="" utilisateur=debian
stockage=local-lvm pont=vmbr0 dhcp=0 disque=32G enregistrements=50G coeurs=2 memoire=4096
version="" essai=0

lire_options() {
    local option valeur
    while [ $# -gt 0 ]; do
        option=$1
        valeur=""
        case "$option" in
            --id | --nom | --ip | --passerelle | --dns | --cle | --fichier-cle | --utilisateur \
                | --stockage | --pont | --disque | --enregistrements | --coeurs | --memoire \
                | --version)
                [ $# -ge 2 ] || erreur "$option : valeur attendue"
                valeur=$2
                shift
                ;;
        esac
        shift
        case "$option" in
            --id) vmid=$valeur ;;
            --nom) nom=$(printf '%s' "$valeur" | tr '[:upper:]' '[:lower:]') ;;
            --ip) ip=$valeur ;;
            --passerelle) passerelle=$valeur ;;
            --dns) dns=$(printf '%s' "$valeur" | tr ',' ' ' | xargs) ;;
            --dhcp) dhcp=1 ;;
            --cle) cle=$valeur ;;
            --fichier-cle) fichier_cle=$valeur ;;
            --utilisateur) utilisateur=$valeur ;;
            --stockage) stockage=$valeur ;;
            --pont) pont=$valeur ;;
            --disque) disque=$valeur ;;
            --enregistrements) enregistrements=$valeur ;;
            --coeurs) coeurs=$valeur ;;
            --memoire) memoire=$valeur ;;
            --version) version=$valeur ;;
            --essai) essai=1 ;;
            --aide | --help | -h)
                aide
                exit 0
                ;;
            *) erreur "option inconnue : $option (--aide pour la liste)" ;;
        esac
    done
}

ipv4() { # <adresse> : IPv4 en notation pointée
    local IFS=. octet
    [[ $1 =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
    for octet in $1; do
        [ "$((10#$octet))" -le 255 ] || return 1
    done
}

taille_go() { # <taille> : nombre de Go (« 32G », « 32 »), ou rien
    [[ $1 =~ ^([0-9]+)[Gg]?$ ]] && echo "$((10#${BASH_REMATCH[1]}))"
}

valider() {
    [ -n "$vmid" ] || erreur "--id manquant (VMID de la nouvelle VM, 100 ou plus)"
    [[ $vmid =~ ^[1-9][0-9]{2,8}$ ]] || erreur "--id $vmid : VMID de 100 à 999999999 attendu"
    [ -n "$nom" ] || erreur "--nom manquant (nom complet de la machine, pbx.operateur.exemple)"
    local etiquette='[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?'
    if ! [[ $nom =~ ^$etiquette(\.$etiquette)+$ ]] || [ "${#nom}" -gt 253 ]; then
        erreur "--nom $nom : nom complet attendu (pbx.operateur.exemple), il donne l'adresse du portail"
    fi
    if [ "$dhcp" = 1 ]; then
        [ -z "$ip" ] || erreur "--ip et --dhcp : choisir l'un ou l'autre"
        [ -z "$passerelle" ] || erreur "--passerelle va avec --ip, pas avec --dhcp"
    else
        [ -n "$ip" ] || erreur "--ip manquant (adresse fixe, 192.0.2.50/24), ou --dhcp"
        local prefixe=${ip##*/}
        if ! [[ $ip =~ ^[0-9.]+/[0-9]{1,2}$ ]] || ! ipv4 "${ip%/*}" \
            || [ "$((10#$prefixe))" -lt 1 ] || [ "$((10#$prefixe))" -gt 32 ]; then
            erreur "--ip $ip : adresse IPv4 et préfixe attendus (192.0.2.50/24)"
        fi
        [ -n "$passerelle" ] || erreur "--passerelle manquant (avec --ip)"
        ipv4 "$passerelle" || erreur "--passerelle $passerelle : adresse IPv4 attendue"
    fi
    local serveur
    for serveur in $dns; do
        ipv4 "$serveur" || erreur "--dns $serveur : adresse IPv4 attendue"
    done
    if [ -n "$cle" ] && [ -n "$fichier_cle" ]; then
        erreur "--cle et --fichier-cle : choisir l'un ou l'autre"
    elif [ -n "$cle" ]; then
        [[ $cle =~ ^(ssh-(ed25519|rsa)|ecdsa-sha2-nistp(256|384|521)|sk-(ssh-ed25519|ecdsa-sha2-nistp256)@openssh\.com)\ [A-Za-z0-9+/=]+(\ .*)?$ ]] \
            || erreur "--cle : clé SSH publique attendue (« ssh-ed25519 AAAA… commentaire »)"
    elif [ -n "$fichier_cle" ]; then
        [ -r "$fichier_cle" ] || erreur "--fichier-cle $fichier_cle : fichier illisible"
        grep -Eq '^(ssh-|ecdsa-|sk-)' "$fichier_cle" \
            || erreur "--fichier-cle $fichier_cle : aucune clé SSH publique"
    else
        erreur "--cle (ou --fichier-cle) manquant : sans clé, personne ne peut se connecter à la VM"
    fi
    if ! [[ $utilisateur =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || [ "$utilisateur" = root ]; then
        erreur "--utilisateur $utilisateur : nom de compte attendu (pas root)"
    fi
    [[ $stockage =~ ^[A-Za-z][A-Za-z0-9_.-]*$ ]] || erreur "--stockage $stockage : nom invalide"
    [[ $pont =~ ^[A-Za-z][A-Za-z0-9_.-]*$ ]] || erreur "--pont $pont : nom invalide"
    disque_go=$(taille_go "$disque") || erreur "--disque $disque : taille en Go attendue (32G)"
    [ "$disque_go" -ge "$DISQUE_IMAGE" ] \
        || erreur "--disque $disque : au moins ${DISQUE_IMAGE}G (taille de l'image)"
    enregistrements_go=$(taille_go "$enregistrements") \
        || erreur "--enregistrements $enregistrements : taille en Go attendue (50G, ou 0)"
    [[ $coeurs =~ ^[1-9][0-9]{0,2}$ ]] || erreur "--coeurs $coeurs : nombre attendu"
    [[ $memoire =~ ^[1-9][0-9]{2,6}$ ]] || erreur "--memoire $memoire : mémoire en Mo attendue (4096)"
    [ "$memoire" -ge 2048 ] || erreur "--memoire $memoire : au moins 2048 Mo"
    [ -z "$version" ] || [[ $version =~ ^[0-9][0-9A-Za-z.~+-]*$ ]] \
        || erreur "--version $version : version invalide"
}

# Commande sur l'hôte, ou seulement affichée (--essai), sur la sortie d'origine (descripteur 3) :
# visible même quand la commande réelle serait rendue muette.
lancer() {
    if [ "$essai" = 1 ]; then
        local mot ligne="+"
        for mot in "$@"; do
            if [[ $mot =~ ^[A-Za-z0-9_./:=,@+-]+$ ]]; then
                ligne+=" $mot"
            else
                ligne+=" '${mot//\'/\'\\\'\'}'"
            fi
        done
        printf '%s\n' "$ligne" >&3
    else
        "$@"
    fi
}

verifier_hote() {
    [ "$(id -u)" = 0 ] || erreur "à lancer en root, dans le shell de l'hôte Proxmox"
    local outil
    for outil in qm pvesm pvesh curl sha256sum perl ip; do
        command -v "$outil" >/dev/null || erreur "$outil introuvable : lancer sur un hôte Proxmox VE"
    done
    if pvesh get /cluster/nextid --vmid "$vmid" >/dev/null 2>&1 \
        && ! qm status "$vmid" >/dev/null 2>&1; then
        :
    else
        erreur "VMID $vmid déjà pris (VM ou conteneur) : choisir un autre --id"
    fi
    local ligne type statut disponible besoin
    ligne=$(pvesm status --storage "$stockage" 2>/dev/null | awk -v s="$stockage" '$1 == s') || true
    [ -n "$ligne" ] || erreur "stockage inconnu : $stockage (« pvesm status » liste ceux de l'hôte)"
    pvesm status --storage "$stockage" --content images 2>/dev/null \
        | awk -v s="$stockage" '$1 == s' | grep -q . \
        || erreur "le stockage $stockage n'accepte pas les disques de VM (contenu « images »)"
    read -r _ type statut _ _ disponible _ <<<"$ligne"
    [ "$statut" = active ] || erreur "le stockage $stockage n'est pas actif ($statut)"
    # Disponible en Kio ; besoin : disque système, second disque.
    besoin=$(((disque_go + enregistrements_go) * 1024 * 1024))
    if [ "$disponible" -lt "$besoin" ]; then
        case "$type" in
            lvmthin | dir | nfs | cifs | btrfs | glusterfs | rbd | cephfs)
                # Allocation fine : seul l'espace écrit compte ; l'image en écrit environ 4 Go.
                [ "$disponible" -ge $((DISQUE_IMAGE * 1024 * 1024)) ] \
                    || erreur "place insuffisante sur $stockage : $((disponible / 1048576)) Go libres"
                echo "ATTENTION : $stockage ($type, allocation fine) : $((disponible / 1048576)) Go libres pour $((disque_go + enregistrements_go)) Go de disques."
                ;;
            *)
                erreur "place insuffisante sur $stockage : $((disponible / 1048576)) Go libres, $((disque_go + enregistrements_go)) Go demandés (--disque, --enregistrements)"
                ;;
        esac
    fi
    ip link show dev "$pont" >/dev/null 2>&1 || erreur "pont réseau inconnu : $pont (--pont)"
}

# Image : empreintes, puis qcow2 vérifié dans $travail. Affiche le nom du qcow2.
telecharger() {
    local empreintes taille libre
    if [ -n "$version" ]; then
        empreintes=kanadr-$version.sha256
    else
        empreintes=kanadr.sha256
    fi
    if [ "$essai" = 1 ]; then
        lancer curl -fsSL -o "$travail/$empreintes" "$BASE/$empreintes"
        lancer curl -fL --progress-bar -o "$travail/kanadr-VERSION.qcow2" \
            "$BASE/kanadr-VERSION.qcow2"
        lancer sha256sum -c --ignore-missing "$empreintes"
        echo "kanadr-VERSION.qcow2"
        return
    fi
    curl -fsSL -o "$travail/$empreintes" "$BASE/$empreintes" \
        || erreur "image introuvable : $BASE/$empreintes"
    image=$(awk '$2 ~ /^kanadr-[0-9][0-9A-Za-z.~+-]*\.qcow2$/ { print $2; exit }' \
        "$travail/$empreintes")
    [ -n "$image" ] || erreur "$empreintes : aucune image qcow2"
    taille=$(curl -fsSLI "$BASE/$image" | tr -d '\r' \
        | awk 'tolower($1) == "content-length:" { t = $2 } END { print t + 0 }')
    libre=$(df -Pk "$travail" | awk 'NR == 2 { print $4 }')
    [ "$((libre * 1024))" -gt "$((taille + 512 * 1024 * 1024))" ] \
        || erreur "place insuffisante dans $travail pour l'image ($((taille / 1048576)) Mo) : TMPDIR=<répertoire> pour en choisir un autre"
    echo "Téléchargement de $image ($((taille / 1048576)) Mo)…" >&2
    curl -fL --progress-bar -o "$travail/$image" "$BASE/$image" >&2 \
        || erreur "téléchargement de $BASE/$image interrompu"
    (cd "$travail" && grep -F "  $image" "$empreintes" | sha256sum -c --quiet - >/dev/null 2>&1) \
        || erreur "empreinte SHA-256 fausse pour $image : fichier corrompu ; aucune VM créée"
    echo "Empreinte SHA-256 vérifiée." >&2
    echo "$image"
}

# Commande dans la VM par l'agent invité : sa sortie, et son code de sortie.
dans_vm() {
    local json
    json=$(qm guest exec "$vmid" --timeout 60 -- "$@" 2>/dev/null) || return 1
    printf '%s' "$json" | perl -MJSON::PP -0777 -ne '
        my $j = eval { decode_json($_) } or exit 2;
        print $j->{"out-data"} // "";
        exit((($j->{exited} // 0) && ($j->{exitcode} // 1) == 0) ? 0 : 1);'
}

# Première adresse IPv4 de la VM vue par l'agent invité.
adresse_de_la_vm() {
    qm guest cmd "$vmid" network-get-interfaces 2>/dev/null | perl -MJSON::PP -0777 -ne '
        my $j = eval { decode_json($_) } or exit 1;
        for my $i (@$j) {
            next if $i->{name} eq "lo";
            for my $a (@{ $i->{"ip-addresses"} // [] }) {
                if ($a->{"ip-address-type"} eq "ipv4") { print $a->{"ip-address"}; exit 0 }
            }
        }
        exit 1;'
}

attendre() { # <secondes> <description> <commande…> : jusqu'au succès de la commande
    local delai=$1 quoi=$2 debut=$SECONDS
    shift 2
    until "$@"; do
        if [ $((SECONDS - debut)) -ge "$delai" ]; then
            echo >&2
            return 1
        fi
        printf '.' >&2
        sleep 5
    done
    [ $((SECONDS - debut)) -lt 5 ] || echo >&2
    echo "$quoi" >&2
}

agent_pret() {
    qm guest cmd "$vmid" ping >/dev/null 2>&1
}

premier_demarrage_fini() {
    if dans_vm systemctl is-failed --quiet kanadr-premier-demarrage.service >/dev/null; then
        erreur "premier démarrage en échec dans la VM $vmid : se connecter et lire « journalctl -u kanadr-premier-demarrage »"
    fi
    dans_vm test -e "$MARQUEUR" >/dev/null
}

lien_pret() {
    texte=$(dans_vm /usr/sbin/kanadr-message-mise-en-route --texte) || return 1
    [[ $texte == *"/mise-en-route#"* ]]
}

creee=0
demarree=0
travail=""
fin() {
    local statut=$1
    [ -z "$travail" ] || rm -rf "$travail"
    [ "$statut" != 0 ] || return 0
    if [ "$creee" = 1 ] && [ "$demarree" = 0 ]; then
        echo "La VM $vmid, incomplète, est supprimée." >&2
        qm destroy "$vmid" --purge 1 --destroy-unreferenced-disks 1 >/dev/null 2>&1 \
            || echo "ERREUR : la supprimer à la main : qm destroy $vmid --purge" >&2
    elif [ "$demarree" = 1 ]; then
        echo "La VM $vmid est créée et démarrée : voir sa console dans Proxmox, ou la supprimer : qm stop $vmid && qm destroy $vmid --purge" >&2
    fi
}

principal() {
    lire_options "$@"
    valider
    trap 'fin $?' EXIT
    travail=$(mktemp -d "${TMPDIR:-/var/tmp}/kanadr.XXXXXX")
    if [ "$essai" = 1 ]; then
        echo "Essai : commandes affichées, rien n'est lancé."
    else
        verifier_hote
    fi

    local image cles ipconfig
    image=$(telecharger)
    cles=$travail/cles.pub
    if [ -n "$cle" ]; then
        printf '%s\n' "$cle" > "$cles"
    else
        grep -E '^(ssh-|ecdsa-|sk-)' "$fichier_cle" > "$cles"
    fi
    chmod 0600 "$cles"
    if [ "$dhcp" = 1 ]; then
        ipconfig=ip=dhcp
    else
        ipconfig="ip=$ip,gw=$passerelle"
    fi

    echo "Création de la VM $vmid ($nom)…"
    lancer qm create "$vmid" --name "$nom" --cores "$coeurs" --memory "$memoire" \
        --net0 "virtio,bridge=$pont" --scsihw virtio-scsi-single --ostype l26 \
        --agent enabled=1 --description "Kanadr"
    creee=1
    lancer qm disk import "$vmid" "$travail/$image" "$stockage" >/dev/null
    local volume="$stockage:vm-$vmid-disk-0"
    if [ "$essai" = 0 ]; then
        volume=$(qm config "$vmid" | awk '$1 == "unused0:" { print $2 }')
        [ -n "$volume" ] || erreur "disque importé introuvable dans la configuration de la VM $vmid"
    fi
    local reglages=(--scsi0 "$volume,discard=on,ssd=1" --boot order=scsi0
        --ide2 "$stockage:cloudinit" --ciuser "$utilisateur" --sshkeys "$cles"
        --ipconfig0 "$ipconfig")
    [ -z "$dns" ] || reglages+=(--nameserver "$dns")
    # Proxmox 8 : sans --ciupgrade 0, cloud-init lance une mise à jour des paquets au premier
    # démarrage (plus long, et la mise à jour de Kanadr se fait par apt, à la main).
    if [ "$essai" = 1 ] || qm help set 2>/dev/null | grep -q -- '--ciupgrade'; then
        reglages+=(--ciupgrade 0)
    fi
    lancer qm set "$vmid" "${reglages[@]}" >/dev/null
    if [ "$enregistrements_go" -gt 0 ]; then
        lancer qm set "$vmid" --scsi1 "$stockage:$enregistrements_go,discard=on,ssd=1" >/dev/null
    fi
    if [ "$disque_go" -gt "$DISQUE_IMAGE" ]; then
        lancer qm disk resize "$vmid" scsi0 "${disque_go}G" >/dev/null
    fi
    lancer qm start "$vmid"
    demarree=1

    if [ "$essai" = 1 ]; then
        lancer qm guest exec "$vmid" -- test -e "$MARQUEUR"
        lancer qm guest exec "$vmid" -- /usr/sbin/kanadr-message-mise-en-route --texte
        echo "Essai terminé : aucune VM créée."
        return 0
    fi

    echo "VM $vmid démarrée ; premier démarrage en cours (une à trois minutes)."
    attendre 300 "Agent invité joignable." agent_pret \
        || erreur "l'agent invité de la VM $vmid ne répond pas (5 min) : voir sa console"
    attendre 900 "Premier démarrage terminé." premier_demarrage_fini \
        || erreur "premier démarrage non terminé après 15 min : voir la console de la VM $vmid"
    texte=""
    attendre 180 "Lien de mise en route prêt." lien_pret \
        || erreur "lien de mise en route absent : dans la VM, « sudo kanadr mise-en-route »"

    local adresse=${ip%/*}
    [ -n "$adresse" ] || adresse=$(adresse_de_la_vm) || adresse="<adresse de la VM>"
    echo
    echo "VM $vmid prête : $nom ($adresse)"
    echo
    printf '%s\n' "$texte"
    echo
    echo "Se connecter en SSH : ssh $utilisateur@$adresse"
    echo "Vérifier la machine : sudo kanadr verifier"
}

# Tout le script est lu avant de commencer (curl … | bash) ; rien ne lit l'entrée standard.
exec 3>&1
principal "$@" </dev/null
