#!/bin/sh # ════════════════════════════════════════════════════════════════════ # DECODE · Instalador soberano · CEOO Tecnologías · Hecho en México # Panel de control de servidor 100% V++ · https://decode.ceoo.world # # Uso rápido (servidor Linux, como root): # curl -fsSL https://decode.ceoo.world/instalar | sudo bash # # Uso desatendido: # curl -fsSL https://decode.ceoo.world/instalar | sudo bash -s -- \ # --desatendido --hostname panel.midominio.com --admin admin@midominio.com # # POSIX sh · idempotente · verifica requisitos · no destructivo. # ════════════════════════════════════════════════════════════════════ set -eu DECODE_VERSION="0.3.0-instalable" ORIGEN="${DECODE_ORIGEN:-https://decode.ceoo.world}" # ── Estado / flags ──────────────────────────────────────────────── DESATENDIDO=0 DRYRUN=0 HOSTNAME_PANEL="" ADMIN_EMAIL="" ADMIN_PASS="" PUERTO_PANEL="8830" PUERTO_NODO="8832" OMITIR="" # lista separada por comas: nginx,php,mariadb,certbot,firewall,fail2ban FUENTE_LOCAL="" # copiar runtime desde un checkout local en vez de descargar DECODE_HOME="/opt/decode" DECODE_ETC="/etc/decode" DECODE_USER="decode" DECODE_LOG="/var/log/decode" # ── Colores (solo si es TTY) ────────────────────────────────────── if [ -t 1 ]; then C_AC="$(printf '\033[38;5;39m')"; C_OK="$(printf '\033[38;5;41m')" C_WR="$(printf '\033[38;5;214m')"; C_ER="$(printf '\033[38;5;196m')" C_DIM="$(printf '\033[2m')"; C_RS="$(printf '\033[0m')" else C_AC=""; C_OK=""; C_WR=""; C_ER=""; C_DIM=""; C_RS="" fi log() { printf '%s\n' "${C_AC}▸${C_RS} $*"; } ok() { printf '%s\n' "${C_OK}✓${C_RS} $*"; } warn() { printf '%s\n' "${C_WR}!${C_RS} $*"; } die() { printf '%s\n' "${C_ER}✗ $*${C_RS}" >&2; exit 1; } # run CMD... — ejecuta salvo en --dry-run; siempre traza la acción. run() { printf '%s\n' "${C_DIM}\$ $*${C_RS}" if [ "$DRYRUN" = "0" ]; then "$@" fi } # runsh "cadena" — igual pero con pipes/redirecciones (evalúa en sh -c) runsh() { printf '%s\n' "${C_DIM}\$ $1${C_RS}" if [ "$DRYRUN" = "0" ]; then sh -c "$1" fi } omitido() { case ",$OMITIR," in *",$1,"*) return 0 ;; *) return 1 ;; esac } banner() { printf '%s\n' "${C_AC}" cat <<'EOF' ██████ ███████ ██████ ██████ ██████ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ███████ ██████ ██████ ██████ ███████ EOF printf '%s\n' "${C_RS}${C_DIM} Infrastructure Intelligence OS · V++ · CEOO Tecnologías${C_RS}" printf '%s\n\n' "${C_DIM} Panel de control de servidor soberano · v${DECODE_VERSION}${C_RS}" } uso() { cat < Dominio del panel (ej. panel.midominio.com). --admin Correo del administrador inicial. --clave Contraseña admin (si se omite, se genera una). --puerto Puerto interno del núcleo DECODE (def. ${PUERTO_PANEL}). --omitir Servicios a NO instalar: nginx,php,mariadb,certbot,firewall,fail2ban --fuente-local Copia el runtime V++ desde un checkout local (para pruebas/aire). --dry-run Muestra TODO lo que haría, sin ejecutar nada. --ayuda Esta ayuda. Ejemplos: sudo sh instalar.sh sudo sh instalar.sh --desatendido --hostname panel.acme.com --admin ops@acme.com sudo sh instalar.sh --dry-run --hostname panel.acme.com --admin ops@acme.com EOF } # ── Parseo de argumentos ────────────────────────────────────────── while [ $# -gt 0 ]; do case "$1" in --desatendido) DESATENDIDO=1 ;; --dry-run) DRYRUN=1 ;; --hostname) HOSTNAME_PANEL="${2:-}"; shift ;; --admin) ADMIN_EMAIL="${2:-}"; shift ;; --clave) ADMIN_PASS="${2:-}"; shift ;; --puerto) PUERTO_PANEL="${2:-}"; shift ;; --omitir) OMITIR="${2:-}"; shift ;; --fuente-local) FUENTE_LOCAL="${2:-}"; shift ;; --ayuda|-h|--help) uso; exit 0 ;; *) warn "Argumento ignorado: $1" ;; esac shift done banner # ════════════════════════════════════════════════════════════════ # 1) Verificación de requisitos # ════════════════════════════════════════════════════════════════ log "Verificando requisitos del sistema…" # 1.1 root (salvo dry-run) if [ "$DRYRUN" = "0" ] && [ "$(id -u)" != "0" ]; then die "Debes ejecutar como root (usa: sudo). Para ver el plan sin root: --dry-run" fi # 1.2 Sistema operativo OS_ID="desconocido"; OS_FAM="desconocido"; PKG="" if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release OS_ID="${ID:-desconocido}" fi case "$OS_ID" in debian|ubuntu|raspbian|linuxmint|pop) OS_FAM="debian"; PKG="apt-get" ;; almalinux|rhel|centos|rocky|fedora) OS_FAM="rhel"; PKG="dnf" ;; *) if command -v apt-get >/dev/null 2>&1; then OS_FAM="debian"; PKG="apt-get" elif command -v dnf >/dev/null 2>&1; then OS_FAM="rhel"; PKG="dnf" elif command -v yum >/dev/null 2>&1; then OS_FAM="rhel"; PKG="yum" fi ;; esac # 1.3 Arquitectura ARCH="$(uname -m 2>/dev/null || echo desconocida)" case "$ARCH" in x86_64|amd64) ARCH="x86_64" ;; aarch64|arm64) ARCH="arm64" ;; *) warn "Arquitectura $ARCH no verificada oficialmente (se intentará igual)." ;; esac KERNEL="$(uname -s 2>/dev/null || echo desconocido)" if [ "$KERNEL" != "Linux" ]; then warn "Este instalador aprovisiona servidores ${C_AC}Linux${C_RS} (Debian/Ubuntu/AlmaLinux)." warn "Kernel detectado: $KERNEL. En macOS/otros solo funciona en --dry-run (revisión del plan)." if [ "$DRYRUN" = "0" ]; then die "Ejecuta este script en un servidor Linux, o usa --dry-run aquí para revisar el plan." fi fi if [ "$OS_FAM" = "desconocido" ] && [ "$DRYRUN" = "0" ]; then die "No pude determinar el gestor de paquetes (apt/dnf). SO no soportado aún." fi ok "SO: ${OS_ID} · familia: ${OS_FAM} · arch: ${ARCH} · gestor: ${PKG:-n/d}" # ════════════════════════════════════════════════════════════════ # 2) Datos del panel (interactivo o desatendido) # ════════════════════════════════════════════════════════════════ if [ -z "$HOSTNAME_PANEL" ]; then if [ "$DESATENDIDO" = "1" ]; then HOSTNAME_PANEL="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo localhost)" else printf '%s' "Dominio del panel [ej. panel.midominio.com]: " read -r HOSTNAME_PANEL || true [ -z "$HOSTNAME_PANEL" ] && HOSTNAME_PANEL="$(hostname -f 2>/dev/null || echo localhost)" fi fi if [ -z "$ADMIN_EMAIL" ]; then if [ "$DESATENDIDO" = "1" ]; then ADMIN_EMAIL="admin@${HOSTNAME_PANEL}" else printf '%s' "Correo del administrador: " read -r ADMIN_EMAIL || true [ -z "$ADMIN_EMAIL" ] && ADMIN_EMAIL="admin@${HOSTNAME_PANEL}" fi fi # contraseña admin: generar si no se dio gen_pass() { if command -v openssl >/dev/null 2>&1; then openssl rand -base64 18 2>/dev/null | tr -d '/+=' | cut -c1-20 else head -c 16 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' | cut -c1-20 fi } [ -z "$ADMIN_PASS" ] && ADMIN_PASS="$(gen_pass)" ok "Panel: ${HOSTNAME_PANEL} · admin: ${ADMIN_EMAIL} · puerto núcleo: ${PUERTO_PANEL}" # ════════════════════════════════════════════════════════════════ # 3) Índice de paquetes + dependencias base # ════════════════════════════════════════════════════════════════ pkg_install() { # pkg_install pkg1 pkg2 ... if [ "$OS_FAM" = "debian" ]; then run env DEBIAN_FRONTEND=noninteractive "$PKG" install -y "$@" else run "$PKG" install -y "$@" fi } log "Actualizando índice de paquetes…" if [ "$OS_FAM" = "debian" ]; then run env DEBIAN_FRONTEND=noninteractive "$PKG" update -y fi log "Instalando utilidades base (curl, tar, ca-certificates, python3)…" if [ "$OS_FAM" = "debian" ]; then pkg_install curl ca-certificates tar gzip python3 openssl else pkg_install curl ca-certificates tar gzip python3 openssl fi # ════════════════════════════════════════════════════════════════ # 4) Servicios base del panel # ════════════════════════════════════════════════════════════════ # 4.1 nginx if omitido nginx; then warn "Omitiendo nginx (--omitir)."; else log "Instalando y habilitando nginx…" pkg_install nginx run systemctl enable --now nginx || warn "No pude habilitar nginx vía systemctl (¿contenedor sin systemd?)." ok "nginx listo." fi # 4.2 php-fpm if omitido php; then warn "Omitiendo php-fpm (--omitir)."; else log "Instalando PHP-FPM…" if [ "$OS_FAM" = "debian" ]; then pkg_install php-fpm php-cli php-mysql php-curl php-mbstring php-xml php-zip else pkg_install php-fpm php-cli php-mysqlnd php-mbstring php-xml fi run systemctl enable --now php-fpm 2>/dev/null || \ runsh "systemctl enable --now php*-fpm 2>/dev/null || true" ok "PHP-FPM listo." fi # 4.3 MariaDB if omitido mariadb; then warn "Omitiendo MariaDB (--omitir)."; else log "Instalando MariaDB…" if [ "$OS_FAM" = "debian" ]; then pkg_install mariadb-server else pkg_install mariadb-server fi run systemctl enable --now mariadb 2>/dev/null || \ run systemctl enable --now mysqld 2>/dev/null || \ warn "No pude habilitar MariaDB vía systemctl." ok "MariaDB lista (recuerda ejecutar mysql_secure_installation)." fi # 4.4 certbot / ACME (SSL Let's Encrypt) if omitido certbot; then warn "Omitiendo certbot (--omitir)."; else log "Instalando certbot (Let's Encrypt / ACME)…" if [ "$OS_FAM" = "debian" ]; then pkg_install certbot python3-certbot-nginx else pkg_install certbot python3-certbot-nginx || pkg_install certbot fi ok "certbot listo." fi # 4.5 Firewall (ufw en Debian, nftables/firewalld en RHEL) if omitido firewall; then warn "Omitiendo firewall (--omitir)."; else log "Configurando firewall…" if [ "$OS_FAM" = "debian" ]; then pkg_install ufw run ufw allow OpenSSH || run ufw allow 22/tcp run ufw allow 80/tcp run ufw allow 443/tcp runsh "yes | ufw enable || true" else pkg_install firewalld || true run systemctl enable --now firewalld || true run firewall-cmd --permanent --add-service=ssh || true run firewall-cmd --permanent --add-service=http || true run firewall-cmd --permanent --add-service=https || true run firewall-cmd --reload || true fi ok "Firewall configurado (22, 80, 443)." fi # 4.6 fail2ban if omitido fail2ban; then warn "Omitiendo fail2ban (--omitir)."; else log "Instalando fail2ban…" pkg_install fail2ban || warn "fail2ban no disponible en repos; se puede instalar después." run systemctl enable --now fail2ban 2>/dev/null || true ok "fail2ban listo." fi # ════════════════════════════════════════════════════════════════ # 5) Usuario, directorios y runtime DECODE # ════════════════════════════════════════════════════════════════ log "Creando usuario de servicio y directorios DECODE…" if ! id "$DECODE_USER" >/dev/null 2>&1; then run useradd --system --home "$DECODE_HOME" --shell /usr/sbin/nologin "$DECODE_USER" 2>/dev/null \ || run useradd --system --home "$DECODE_HOME" --shell /sbin/nologin "$DECODE_USER" 2>/dev/null \ || warn "No pude crear el usuario $DECODE_USER (quizá ya existe)." fi run mkdir -p "$DECODE_HOME/runtime" "$DECODE_HOME/modules" "$DECODE_ETC" "$DECODE_LOG" # 5.1 Traer el runtime V++ (intérprete) + módulos del panel fetch() { # fetch _dst="$1"; _src="$2" if [ -n "$FUENTE_LOCAL" ]; then run cp "$FUENTE_LOCAL/$_src" "$_dst" else runsh "curl -fsSL '${ORIGEN}/dist/${_src}' -o '${_dst}'" fi } log "Desplegando runtime V++ soberano…" # El intérprete V++ es la ÚNICA dependencia de ejecución del panel. fetch "$DECODE_HOME/runtime/interprete_vpp.py" "interprete_vpp.py" || warn "Runtime V++ pendiente en el origen (ver INSTALACION.md · roadmap /dist)." fetch "$DECODE_HOME/modules/decode_core.◬++" "decode_core.◬++" || warn "Módulo core pendiente en el origen." fetch "$DECODE_HOME/modules/decode_nodo.◬++" "decode_nodo.◬++" || warn "Módulo nodo pendiente en el origen." fetch "$DECODE_HOME/modules/decode_sentinel.◬++" "decode_sentinel.◬++" || warn "Módulo sentinel pendiente en el origen." run chown -R "$DECODE_USER":"$DECODE_USER" "$DECODE_HOME" "$DECODE_ETC" "$DECODE_LOG" 2>/dev/null || true # 5.2 Credenciales admin + token del nodo log "Generando credenciales del administrador…" NODO_TOKEN="$(gen_pass)$(gen_pass)" if [ "$DRYRUN" = "0" ]; then umask 077 cat > "$DECODE_ETC/decode.env" < "$DECODE_ETC/nodo.token" chmod 600 "$DECODE_ETC/decode.env" "$DECODE_ETC/nodo.token" # Semilla de usuario admin (hash lo materializa Sentinel al primer arranque) cat > "$DECODE_ETC/admin.seed.json" < /etc/systemd/system/decode.service < /etc/systemd/system/decode-nodo.service </dev/null || warn "systemd no disponible; arranca manual el núcleo." run systemctl enable --now decode-nodo 2>/dev/null || warn "systemd no disponible; arranca manual el nodo." # ════════════════════════════════════════════════════════════════ # 7) vhost nginx del panel + SSL # ════════════════════════════════════════════════════════════════ if ! omitido nginx; then log "Publicando el panel en nginx (${HOSTNAME_PANEL} → 127.0.0.1:${PUERTO_PANEL})…" if [ "$OS_FAM" = "debian" ]; then NGX_AVAIL="/etc/nginx/sites-available/decode.conf" NGX_ENABLED="/etc/nginx/sites-enabled/decode.conf" else NGX_AVAIL="/etc/nginx/conf.d/decode.conf" NGX_ENABLED="" fi if [ "$DRYRUN" = "0" ]; then cat > "$NGX_AVAIL" </dev/null 2>&1; then ok "Núcleo DECODE respondiendo en :${PUERTO_PANEL}." break fi i=$((i + 1)); sleep 1 done [ "$i" -ge 10 ] && warn "El núcleo no respondió en 10s; revisa: journalctl -u decode -e" fi # ════════════════════════════════════════════════════════════════ # 9) Resumen final # ════════════════════════════════════════════════════════════════ printf '\n%s\n' "${C_OK}══════════════════════════════════════════════════════════${C_RS}" ok "DECODE instalado." cat <