diff --git a/scripts/hammer-live-install.sh b/scripts/hammer-live-install.sh index b098b2d1..491f9426 100755 --- a/scripts/hammer-live-install.sh +++ b/scripts/hammer-live-install.sh @@ -18,14 +18,254 @@ # Cadena de arranque del disco resultante: BIOS → MBR (boot.img) → core.img (hueco post-MBR) → # (hd0,msdos1)/boot/grub/grub.cfg → linux /boot/bzImage root=/dev/1 → wrapper /sbin/init. # -# Uso (en el live): hammer-install /dev/vdb [ROOT_MB] [STORE_MB] +# Modo de uso: +# INTERACTIVO (sin argumentos, con TTY): hammer-install +# TUI en busybox sh: detecta discos, el usuario elige destino + confirma el borrado, recoge +# hostname / credencial de root / zona horaria / teclado / tamaños de partición, y recién ahí +# particiona+instala. Es la cara humana del instalador (el /init del ISO installer lo lanza). +# DESATENDIDO (con device): hammer-install /dev/vdb [ROOT_MB] [STORE_MB] +# Autoinstalador (lo usa iso-install-test.sh con AUTO_INSTALL). La config sale de env: +# HAMMER_HOSTNAME / HAMMER_ROOT_PW / HAMMER_ROOT_AUTHKEYS / HAMMER_TZ / HAMMER_KEYMAP. set -eu -DEV="${1:-/dev/vdb}" -ROOT_MB="${2:-1500}" -STORE_MB="${3:-400}" # /var/lib/hammer toma el resto +DEV="${1:-}" +ROOT_MB="${2:-}" +STORE_MB="${3:-}" # /var/lib/hammer toma el resto PAYLOAD=/usr/lib/hammer/install -BB=/bin/busybox +BB="${BB:-/bin/busybox}" +SYS_BLOCK="${SYS_BLOCK:-/sys/block}" # override para tests +MOUNTS="${MOUNTS:-/proc/mounts}" # override para tests + +# bb: usa el busybox del live si existe; si no (p.ej. tests en el host), cae a las herramientas del PATH. +# Sólo lo usan las funciones NUEVAS (config/TUI). Las ramas EFI/BIOS validadas siguen con "$BB" literal +# y jamás corren en el host (el dry-run sale antes). +bb() { if [ -x "$BB" ]; then "$BB" "$@"; else "$@"; fi; } + +_human() { # bytes → humano + _b="$1" + if [ "$_b" -ge 1073741824 ]; then printf '%s.%s GiB' "$((_b/1073741824))" "$(( (_b%1073741824)*10/1073741824 ))" + elif [ "$_b" -ge 1048576 ]; then printf '%s MiB' "$((_b/1048576))" + else printf '%s B' "$_b"; fi +} + +_is_mounted() { # ¿alguna partición del disco $1 aparece en MOUNTS? (heurística "es el medio live") + _d="$1" + while read -r _src _rest; do + case "$_src" in "/dev/$_d"|"/dev/$_d"[0-9]*|"/dev/$_d"p[0-9]*) return 0 ;; esac + done < "$MOUNTS" 2>/dev/null + return 1 +} + +list_disks() { # emite por disco candidato: namebytesmodelmounted(0|1) + for _p in "$SYS_BLOCK"/*; do + [ -d "$_p" ] || continue + _n=${_p##*/} + case "$_n" in loop*|ram*|sr*|fd*|dm-*|md*|zram*) continue ;; esac + [ -r "$_p/size" ] || continue + _sz=$(cat "$_p/size" 2>/dev/null || echo 0); [ "$_sz" -gt 0 ] || continue + _model=$(cat "$_p/device/model" 2>/dev/null | tr -d '\n'); _model=$(printf '%s' "$_model" | sed 's/[[:space:]]*$//') + _m=0; _is_mounted "$_n" && _m=1 + printf '%s\t%s\t%s\t%s\n' "$_n" "$((_sz*512))" "${_model:-disco}" "$_m" + done +} + +ask() { # ask VAR "prompt" "default" + if [ -n "$3" ]; then printf '%s [%s]: ' "$2" "$3"; else printf '%s: ' "$2"; fi + IFS= read -r _ans || _ans="" + [ -n "$_ans" ] || _ans="$3" + eval "$1=\$_ans" +} + +read_secret() { # read_secret VAR "prompt" (sin eco si hay stty) + printf '%s: ' "$2" + bb stty -echo 2>/dev/null || true + IFS= read -r _sec || _sec="" + bb stty echo 2>/dev/null || true + echo + eval "$1=\$_sec" +} + +have_cryptpw() { bb 2>&1 | grep -qw cryptpw; } + +# Reescribe el campo de contraseña de un usuario en $mnt/etc/shadow (o crea el fichero). +_set_shadow() { + _u="$1"; _h="$2"; _f="$3/etc/shadow" + if [ ! -f "$_f" ]; then + bb mkdir -p "$3/etc" + printf '%s:%s:19000:0:99999:7:::\n' "$_u" "$_h" > "$_f"; bb chmod 600 "$_f"; return + fi + _t="$_f.hammer-tmp" + bb awk -F: -v u="$_u" -v h="$_h" 'BEGIN{OFS=":"} + { if ($1==u){ $2=h; seen=1 } print } + END{ if(!seen) print u,h,"19000","0","99999","7","","","" }' "$_f" > "$_t" && bb mv "$_t" "$_f" + bb chmod 600 "$_f" +} + +# Aplica la config recogida al rootfs YA copiado y montado en $1 (llamada por ambas ramas tras la copia). +apply_post_install_config() { + _mnt="$1" + bb mkdir -p "$_mnt/etc/hammer" + if [ -n "${CFG_HOSTNAME:-}" ]; then + printf '%s\n' "$CFG_HOSTNAME" > "$_mnt/etc/hostname" + if ! grep -q "[[:space:]]$CFG_HOSTNAME\$" "$_mnt/etc/hosts" 2>/dev/null; then + printf '127.0.0.1\tlocalhost %s\n::1\t\tlocalhost %s\n' "$CFG_HOSTNAME" "$CFG_HOSTNAME" >> "$_mnt/etc/hosts" + fi + fi + if [ -n "${CFG_ROOT_HASH:-}" ]; then _set_shadow root "$CFG_ROOT_HASH" "$_mnt"; fi + if [ -n "${CFG_ROOT_AUTHKEYS:-}" ]; then + bb mkdir -p "$_mnt/root/.ssh"; bb chmod 700 "$_mnt/root/.ssh" + printf '%s\n' "$CFG_ROOT_AUTHKEYS" >> "$_mnt/root/.ssh/authorized_keys" + bb chmod 600 "$_mnt/root/.ssh/authorized_keys" + fi + if [ -n "${CFG_TZ:-}" ] && [ -e "$_mnt/usr/share/zoneinfo/$CFG_TZ" ]; then + bb ln -sf "/usr/share/zoneinfo/$CFG_TZ" "$_mnt/etc/localtime" + printf '%s\n' "$CFG_TZ" > "$_mnt/etc/timezone" + fi + _ts=$(bb date -u +%FT%TZ 2>/dev/null || echo unknown) + { + echo "# escrito por hammer-install en $_ts" + echo "HOSTNAME=${CFG_HOSTNAME:-}" + echo "TIMEZONE=${CFG_TZ:-}" + echo "KEYMAP=${CFG_KEYMAP:-}" # best-effort: el producto aún no trae kbd/loadkeys + echo "TARGET=${DEV:-}" + } > "$_mnt/etc/hammer/install.conf" +} + +# ---- recolección de config: interactiva (TUI) o desde env (desatendido) ---- +CFG_HOSTNAME="${HAMMER_HOSTNAME:-hammer}" +CFG_ROOT_AUTHKEYS="${HAMMER_ROOT_AUTHKEYS:-}" +CFG_ROOT_HASH="${HAMMER_ROOT_HASH:-}" +CFG_TZ="${HAMMER_TZ:-}" +CFG_KEYMAP="${HAMMER_KEYMAP:-us}" + +interactive_setup() { + echo + echo " # instalador de hammer" + echo " ─────────────────────────" + echo + _disks=$(list_disks) + [ -n "$_disks" ] || { echo "No detecté discos instalables (¿el medio live es el único dispositivo?)."; exit 1; } + echo "Discos detectados:" + OLDIFS=$IFS; IFS=' +'; set -- + for _l in $_disks; do set -- "$@" "$_l"; done + IFS=$OLDIFS + _i=1 + for _l in "$@"; do + _n=$(printf '%s' "$_l" | cut -f1); _by=$(printf '%s' "$_l" | cut -f2) + _mo=$(printf '%s' "$_l" | cut -f3); _mm=$(printf '%s' "$_l" | cut -f4) + _w=""; [ "$_mm" = 1 ] && _w=" ⚠ montado (¿medio live?)" + printf ' %s) /dev/%-8s %10s %s%s\n' "$_i" "$_n" "$(_human "$_by")" "$_mo" "$_w" + _i=$((_i+1)) + done + _total=$# + while :; do + ask _sel "Disco destino (1-$_total)" "" + case "$_sel" in ''|*[!0-9]*) echo " número inválido"; continue ;; esac + [ "$_sel" -ge 1 ] && [ "$_sel" -le "$_total" ] && break + echo " fuera de rango" + done + eval "_line=\${$_sel}" + _name=$(printf '%s' "$_line" | cut -f1); _bytes=$(printf '%s' "$_line" | cut -f2) + DEV="/dev/$_name" + _disk_mb=$((_bytes/1048576)) + + # --- tamaños de partición --- + _esp=64; _slack=16; _avail=$((_disk_mb - _esp - _slack)) + [ "$_avail" -ge 600 ] || { echo "Disco demasiado chico ($(_human "$_bytes")); se necesitan ~700 MiB."; exit 1; } + _defr=4096; [ "$_defr" -gt $((_avail*6/10)) ] && _defr=$((_avail*6/10)) + _defs=1024; [ "$_defs" -gt $((_avail/5)) ] && _defs=$((_avail/5)) + while :; do + ask ROOT_MB "Tamaño de / en MB" "$_defr" + ask STORE_MB "Tamaño de /store en MB" "$_defs" + case "$ROOT_MB$STORE_MB" in *[!0-9]*) echo " usá sólo números"; continue ;; esac + if [ $((ROOT_MB + STORE_MB + _esp + _slack)) -le "$_disk_mb" ]; then break; fi + echo " no entra en el disco ($_disk_mb MiB) — / + /store + ESP deben sumar menos" + done + + # --- identidad --- + ask CFG_HOSTNAME "Hostname" "${CFG_HOSTNAME:-hammer}" + echo "Credencial de root:" + echo " 1) contraseña" + echo " 2) clave SSH (authorized_keys)" + echo " 3) omitir" + ask _cred "Opción" "1" + case "$_cred" in + 1) if have_cryptpw; then + while :; do + read_secret _pw1 "Contraseña de root" + read_secret _pw2 "Repetir" + [ "$_pw1" = "$_pw2" ] && [ -n "$_pw1" ] && break + echo " no coinciden (o vacía) — reintentá" + done + CFG_ROOT_HASH=$(printf '%s' "$_pw1" | bb cryptpw -m sha512 2>/dev/null || true) + [ -n "$CFG_ROOT_HASH" ] || echo " ⚠ no pude generar el hash; root queda como está" + else + echo " (cryptpw no disponible en este busybox — usá la opción de clave SSH)" + ask CFG_ROOT_AUTHKEYS "Pegá una línea authorized_keys" "" + fi ;; + 2) ask CFG_ROOT_AUTHKEYS "Pegá una línea authorized_keys (o ruta a un fichero .pub)" "" + if [ -n "$CFG_ROOT_AUTHKEYS" ] && [ -f "$CFG_ROOT_AUTHKEYS" ]; then + CFG_ROOT_AUTHKEYS=$(cat "$CFG_ROOT_AUTHKEYS") + fi ;; + *) echo " (root sin credencial nueva)" ;; + esac + + # --- locale (best-effort) --- + _zi="${ZONEINFO:-/usr/share/zoneinfo}" + if [ -d "$_zi" ]; then + ask CFG_TZ "Zona horaria (ej. America/La_Paz; vacío = UTC)" "${CFG_TZ:-}" + if [ -n "$CFG_TZ" ] && [ ! -e "$_zi/$CFG_TZ" ]; then + echo " ⚠ zona desconocida, se ignora"; CFG_TZ="" + fi + else + echo "(tzdata no instalado — la zona horaria se omite)"; CFG_TZ="" + fi + ask CFG_KEYMAP "Layout de teclado" "${CFG_KEYMAP:-us}" + + # --- resumen + confirmación fuerte --- + _cd="sin cambios"; [ -n "$CFG_ROOT_HASH" ] && _cd="contraseña"; [ -n "$CFG_ROOT_AUTHKEYS" ] && _cd="clave SSH" + echo + echo "Resumen de instalación:" + echo " destino : $DEV ($(_human "$_bytes"))" + echo " / = ${ROOT_MB} MiB /store = ${STORE_MB} MiB /var/lib/hammer = resto" + echo " hostname: $CFG_HOSTNAME" + echo " root : $_cd" + echo " zona : ${CFG_TZ:-UTC} teclado: $CFG_KEYMAP" + echo + echo " ⚠ ESTO BORRA TODO EN $DEV." + ask _ok "Escribí el nombre del disco ($DEV) para confirmar, o Enter para cancelar" "" + [ "$_ok" = "$DEV" ] || { echo "Cancelado — no se tocó ningún disco."; exit 1; } +} + +# --- resolución del modo: interactivo (sin device + TTY) vs desatendido (device dado) --- +if [ -z "$DEV" ]; then + if [ -t 0 ] || [ "${HAMMER_INSTALL_INTERACTIVE:-}" = 1 ]; then + interactive_setup + else + echo "uso: hammer-install [ROOT_MB] [STORE_MB] (o sin args con un TTY para el modo interactivo)" >&2 + exit 2 + fi +fi +ROOT_MB="${ROOT_MB:-1500}" +STORE_MB="${STORE_MB:-400}" +# En modo desatendido con contraseña por env, derivar el hash acá (interactivo ya lo hizo). +if [ -z "$CFG_ROOT_HASH" ] && [ -n "${HAMMER_ROOT_PW:-}" ] && have_cryptpw; then + CFG_ROOT_HASH=$(printf '%s' "$HAMMER_ROOT_PW" | bb cryptpw -m sha512 2>/dev/null || true) +fi + +# --- gancho de prueba: valida TUI + config sin tocar disco (HAMMER_INSTALL_DRYRUN=1) --- +if [ "${HAMMER_INSTALL_DRYRUN:-}" = 1 ]; then + echo "== DRYRUN: plan de instalación ==" + echo " DEV=$DEV ROOT_MB=$ROOT_MB STORE_MB=$STORE_MB" + echo " HOSTNAME=$CFG_HOSTNAME TZ=${CFG_TZ:-UTC} KEYMAP=$CFG_KEYMAP" + echo " ROOT_HASH=$([ -n "$CFG_ROOT_HASH" ] && echo set || echo -) AUTHKEYS=$([ -n "$CFG_ROOT_AUTHKEYS" ] && echo set || echo -)" + if [ -n "${HAMMER_INSTALL_TESTROOT:-}" ]; then + apply_post_install_config "$HAMMER_INSTALL_TESTROOT" + echo " config aplicada a $HAMMER_INSTALL_TESTROOT" + fi + exit 0 +fi [ -b "$DEV" ] || { echo "hammer-install: $DEV no es un block device" >&2; exit 1; } [ -d "$PAYLOAD" ] || { echo "hammer-install: falta el payload $PAYLOAD (¿ISO sin INSTALLER=1?)" >&2; exit 1; } @@ -177,6 +417,8 @@ echo "HAMMER-EFI-INIT-OK: arje-zero PID1" > /dev/ttyS0 2>/dev/null || true exec /usr/bin/arje-zero INIT "$BB" chmod +x "$MNT/sbin/init" + # config del usuario (hostname / root / zona / teclado) recogida al inicio + apply_post_install_config "$MNT" "$BB" sync "$BB" umount "$MNT" @@ -294,6 +536,8 @@ cat > "$MNT/sbin/init" <<'INIT' exec /usr/bin/arje-zero INIT "$BB" chmod +x "$MNT/sbin/init" +# config del usuario (hostname / root / zona / teclado) recogida al inicio +apply_post_install_config "$MNT" "$BB" sync "$BB" umount "$MNT" diff --git a/scripts/install-tui-test.sh b/scripts/install-tui-test.sh new file mode 100755 index 00000000..32a3c7ac --- /dev/null +++ b/scripts/install-tui-test.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# install-tui-test.sh — valida la capa NUEVA del instalador interactivo (hammer-live-install.sh) SIN +# tocar disco: usa el gancho HAMMER_INSTALL_DRYRUN=1, un /sys/block falso y un rootfs de scratch. +# Ejercita (1) el modo interactivo (detección de discos + prompts + confirmación fuerte) y (2) el modo +# desatendido por env; en ambos verifica que apply_post_install_config escribe hostname/hosts/shadow/ +# authorized_keys/localtime/install.conf correctamente. Las ramas EFI/BIOS reales (particionado) las +# cubre iso-install-test.sh / efi-install-test.sh en QEMU; acá validamos la lógica de UI + config. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" +INST="$ROOT/scripts/hammer-live-install.sh" +W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT +fail() { echo "✗ FALLO: $*" >&2; exit 1; } + +# --- sysfs falso: vda (montado = simula el medio live) + vdb (target) --- +SB="$W/sys-block" +mkdir -p "$SB/vda/device" "$SB/vdb/device" +echo $((8*1024*1024*1024/512)) > "$SB/vda/size" # 8 GiB +printf 'LIVE-USB\n' > "$SB/vda/device/model" +echo $((16*1024*1024*1024/512)) > "$SB/vdb/size" # 16 GiB +printf 'TARGET-SSD\n' > "$SB/vdb/device/model" +MOUNTS="$W/mounts"; printf '/dev/vda1 / ext4 ro 0 0\n' > "$MOUNTS" + +# --- zoneinfo falso (hermético) --- +ZI="$W/zoneinfo"; mkdir -p "$ZI/America"; : > "$ZI/America/La_Paz" + +# --- rootfs de scratch #1 (destino de la config del modo interactivo) --- +R1="$W/root1"; mkdir -p "$R1/etc" "$R1/usr/share/zoneinfo/America" +printf 'root:!:19000:0:99999:7:::\ndaemon:!:19000::::::\n' > "$R1/etc/shadow" +printf '127.0.0.1\tlocalhost\n' > "$R1/etc/hosts" +: > "$R1/usr/share/zoneinfo/America/La_Paz" + +echo "== test 1: modo INTERACTIVO (TUI dry-run) ==" +# Respuestas: disco #2 (vdb) · root 2048M · store 512M · hostname midistro · cred 2 (ssh) · authkeys · +# zona America/La_Paz · teclado (default us) · confirmación "/dev/vdb" +OUT1="$W/out1.txt" +BB=/nonexistent SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$ZI" \ + HAMMER_INSTALL_INTERACTIVE=1 HAMMER_INSTALL_DRYRUN=1 HAMMER_INSTALL_TESTROOT="$R1" \ + sh "$INST" >"$OUT1" 2>&1 <<'ANSWERS' || { cat "$OUT1"; fail "el instalador salió con error"; } +2 +2048 +512 +midistro +2 +ssh-ed25519 AAAATESTKEY user@host +America/La_Paz + +/dev/vdb +ANSWERS + +grep -q 'TARGET-SSD' "$OUT1" || { cat "$OUT1"; fail "no listó el disco vdb con su modelo"; } +grep -q 'montado' "$OUT1" || { cat "$OUT1"; fail "no marcó vda como montado (medio live)"; } +grep -q 'DRYRUN' "$OUT1" || { cat "$OUT1"; fail "no llegó al plan dry-run (¿se cortó antes?)"; } + +[ "$(cat "$R1/etc/hostname")" = midistro ] || fail "hostname no quedó en midistro" +grep -q 'midistro' "$R1/etc/hosts" || fail "hosts no menciona el hostname" +grep -q 'AAAATESTKEY' "$R1/root/.ssh/authorized_keys" || fail "authorized_keys no escrito" +[ -L "$R1/etc/localtime" ] || fail "localtime no es symlink" +case "$(readlink "$R1/etc/localtime")" in */America/La_Paz) : ;; *) fail "localtime no apunta a La_Paz"; esac +grep -q 'HOSTNAME=midistro' "$R1/etc/hammer/install.conf" || fail "install.conf sin hostname" +grep -q 'KEYMAP=us' "$R1/etc/hammer/install.conf" || fail "install.conf sin keymap default" +# root en shadow debe seguir intacto (interactivo eligió ssh, no tocó la contraseña) +grep -q '^root:!:' "$R1/etc/shadow" || fail "shadow de root alterado sin querer" +echo " ✓ interactivo: disco elegido + confirmado, config escrita (hostname/hosts/ssh/tz/keymap)" + +echo "== test 2: confirmación ERRÓNEA cancela sin tocar nada ==" +OUT2="$W/out2.txt" +if BB=/nonexistent SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$ZI" \ + HAMMER_INSTALL_INTERACTIVE=1 HAMMER_INSTALL_DRYRUN=1 HAMMER_INSTALL_TESTROOT="$W/root-none" \ + sh "$INST" >"$OUT2" 2>&1 <<'ANSWERS' +2 +2048 +512 +h +3 +us +sda-equivocado +ANSWERS +then fail "debió cancelar con confirmación errónea (exit != 0)"; fi +grep -qi 'Cancelado' "$OUT2" || { cat "$OUT2"; fail "no reportó cancelación"; } +[ ! -d "$W/root-none/etc" ] || fail "escribió config pese a cancelar" +echo " ✓ confirmación errónea ⇒ aborta sin escribir" + +echo "== test 3: modo DESATENDIDO por env (device + HAMMER_ROOT_HASH) ==" +R3="$W/root3"; mkdir -p "$R3/etc" +printf 'root:!:19000:0:99999:7:::\n' > "$R3/etc/shadow" +OUT3="$W/out3.txt" +BB=/nonexistent HAMMER_INSTALL_DRYRUN=1 HAMMER_INSTALL_TESTROOT="$R3" \ + HAMMER_HOSTNAME=srv01 HAMMER_ROOT_HASH='$6$salt$deadbeefHASH' HAMMER_TZ=UTC HAMMER_KEYMAP=es \ + sh "$INST" /dev/vdb 3000 700 >"$OUT3" 2>&1 || { cat "$OUT3"; fail "desatendido salió con error"; } +grep -q 'DEV=/dev/vdb ROOT_MB=3000 STORE_MB=700' "$OUT3" || { cat "$OUT3"; fail "no respetó device/tamaños posicionales"; } +grep -q '^root:\$6\$salt\$deadbeefHASH:' "$R3/etc/shadow" || { cat "$R3/etc/shadow"; fail "no reemplazó el hash de root"; } +[ "$(cat "$R3/etc/hostname")" = srv01 ] || fail "hostname desatendido incorrecto" +grep -q 'KEYMAP=es' "$R3/etc/hammer/install.conf" || fail "keymap por env no propagado" +echo " ✓ desatendido: hash de root reemplazado + hostname/keymap por env" + +echo "== test 4: sin device y sin TTY ⇒ error de uso (no cuelga) ==" +if BB=/nonexistent sh "$INST" /dev/null 2>&1; then fail "debió fallar sin device ni TTY"; fi +echo " ✓ uso inválido reportado" + +echo +echo "✓✓ install-tui-test: TODOS los casos verdes" diff --git a/scripts/iso-image.sh b/scripts/iso-image.sh index b7f355bc..cb216f77 100755 --- a/scripts/iso-image.sh +++ b/scripts/iso-image.sh @@ -77,6 +77,15 @@ cp -al "$ROOTFS" "$RFS/root" echo "if /usr/bin/hammer-install '${AUTO_INSTALL}'; then echo 'HAMMER-INSTALL-OK'; else echo 'HAMMER-INSTALL-FAIL'; fi" echo '/bin/busybox sync' echo '/bin/busybox poweroff -f' + elif [ "${INSTALLER:-0}" = 1 ] && [ "${INSTALLER_INTERACTIVE:-1}" = 1 ]; then + # ISO instalador SIN AUTO_INSTALL ⇒ recibir al usuario con la TUI del instalador en la consola. + # Tras instalar (o cancelar) ofrece reiniciar o caer al live. HAMMER_INSTALL_INTERACTIVE fuerza el + # modo aunque /dev/console no pase el test -t (a esta altura stdin puede no ser un tty). + echo '[ -e /dev/console ] && exec 0/dev/console 2>/dev/console' + echo 'HAMMER_INSTALL_INTERACTIVE=1 /usr/bin/hammer-install || true' + echo 'echo; echo "Instalación finalizada. [r] reiniciar · [Enter] shell en vivo"' + echo 'read -r _a || _a=""' + echo 'case "$_a" in r|R) /bin/busybox reboot -f ;; *) exec /sbin/init ;; esac' else echo 'exec /sbin/init' fi