gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 188Rust 55Shell 30Markdown 281211 Definitions

⎇ ff78b3d54fb9786c53bf223dde4d428469948289 scripts/install-tui-test.sh Shell · 207 lines · 12.2 KB History raw

fix(instalador): la cuenta de quien va a usar la maquina se pregunta antes de instalar 0f66c9973 · sergio ·

1#!/usr/bin/env bash
2# install-tui-test.sh — valida la capa NUEVA del instalador interactivo (takana-live-install.sh) SIN
3# tocar disco: usa el gancho TAKANA_INSTALL_DRYRUN=1, un /sys/block falso y un rootfs de scratch.
4# Ejercita (1) el modo interactivo (detección de discos + prompts + confirmación fuerte) y (2) el modo
5# desatendido por env; en ambos verifica que apply_post_install_config escribe hostname/hosts/shadow/
6# authorized_keys/localtime/install.conf correctamente. Las ramas EFI/BIOS reales (particionado) las
7# cubre iso-install-test.sh / efi-install-test.sh en QEMU; acá validamos la lógica de UI + config.
8set -euo pipefail
9ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
10INST="$ROOT/scripts/takana-live-install.sh"
11W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT
12fail() { echo "✗ FALLO: $*" >&2; exit 1; }
13
14# --- sysfs falso: vda (montado = simula el medio live) + vdb (target) ---
15SB="$W/sys-block"
16mkdir -p "$SB/vda/device" "$SB/vdb/device"
17echo $((8*1024*1024*1024/512))  > "$SB/vda/size"   # 8 GiB
18printf 'LIVE-USB\n'             > "$SB/vda/device/model"
19echo $((16*1024*1024*1024/512)) > "$SB/vdb/size"   # 16 GiB
20printf 'TARGET-SSD\n'           > "$SB/vdb/device/model"
21MOUNTS="$W/mounts"; printf '/dev/vda1 / ext4 ro 0 0\n' > "$MOUNTS"
22
23# --- zoneinfo falso (hermético) ---
24ZI="$W/zoneinfo"; mkdir -p "$ZI/America"; : > "$ZI/America/La_Paz"
25
26# --- rootfs de scratch #1 (destino de la config del modo interactivo) ---
27R1="$W/root1"; mkdir -p "$R1/etc" "$R1/usr/share/zoneinfo/America"
28printf 'root:!:19000:0:99999:7:::\ndaemon:!:19000::::::\n' > "$R1/etc/shadow"
29printf '127.0.0.1\tlocalhost\n' > "$R1/etc/hosts"
30: > "$R1/usr/share/zoneinfo/America/La_Paz"
31
32echo "== test 1: modo INTERACTIVO (TUI dry-run) =="
33# Respuestas: disco #2 (vdb) · root 2048M · store 512M · hostname midistro · cred 2 (ssh) · authkeys ·
34#             USUARIO ana · zona America/La_Paz · teclado (default us) · confirmación "/dev/vdb"
35#
36# ⚠ `ana` es la respuesta de la SECCIÓN DE CUENTA (lib/cuenta.sh, 2026-10-15). Este rootfs de
37# scratch no trae `usr/bin/doas`, así que la librería NO pregunta por administración —y eso es
38# determinista a propósito: mira el rootfs del test y no el de la máquina que corre el test—.
39OUT1="$W/out1.txt"
40BB=/nonexistent SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$ZI" \
41  TAKANA_INSTALL_INTERACTIVE=1 TAKANA_INSTALL_DRYRUN=1 TAKANA_INSTALL_TESTROOT="$R1" \
42  sh "$INST" >"$OUT1" 2>&1 <<'ANSWERS' || { cat "$OUT1"; fail "el instalador salió con error"; }
432
442048
45512
46midistro
472
48ssh-ed25519 AAAATESTKEY user@host
49ana
50America/La_Paz
51
52/dev/vdb
53ANSWERS
54
55grep -q 'TARGET-SSD' "$OUT1" || { cat "$OUT1"; fail "no listó el disco vdb con su modelo"; }
56grep -q 'montado' "$OUT1"    || { cat "$OUT1"; fail "no marcó vda como montado (medio live)"; }
57grep -q 'DRYRUN' "$OUT1"     || { cat "$OUT1"; fail "no llegó al plan dry-run (¿se cortó antes?)"; }
58
59[ "$(cat "$R1/etc/hostname")" = midistro ] || fail "hostname no quedó en midistro"
60grep -q 'midistro' "$R1/etc/hosts"          || fail "hosts no menciona el hostname"
61grep -q 'AAAATESTKEY' "$R1/root/.ssh/authorized_keys" || fail "authorized_keys no escrito"
62[ -L "$R1/etc/localtime" ] || fail "localtime no es symlink"
63case "$(readlink "$R1/etc/localtime")" in */America/La_Paz) : ;; *) fail "localtime no apunta a La_Paz"; esac
64grep -q 'HOSTNAME=midistro' "$R1/etc/hammer/install.conf" || fail "install.conf sin hostname"
65grep -q 'KEYMAP=us'         "$R1/etc/hammer/install.conf" || fail "install.conf sin keymap default"
66# root en shadow debe seguir intacto (interactivo eligió ssh, no tocó la contraseña)
67grep -q '^root:!:' "$R1/etc/shadow" || fail "shadow de root alterado sin querer"
68# ── la cuenta (lib/cuenta.sh) ──────────────────────────────────────────────────────────────────
69# Sin `cryptpw` (BB=/nonexistent) no se puede hashear nada, así que lo que se exige acá es que la
70# cuenta EXISTA con su hogar y que el instalador DIGA que queda cerrada. Un instalador que la deja
71# cerrada en silencio es el defecto que esta sección cierra.
72grep -q "^ana:x:1000:1000:" "$R1/etc/passwd" || { cat "$R1/etc/passwd"; fail "el usuario ana no quedó en passwd con uid 1000"; }
73grep -q "^ana:x:1000:" "$R1/etc/group"       || { cat "$R1/etc/group"; fail "el grupo ana no quedó en group"; }
74[ -d "$R1/home/ana" ] || fail "/home/ana no se creó"
75grep -q '^ana:!:' "$R1/etc/shadow" || { cat "$R1/etc/shadow"; fail "ana debía quedar con la cuenta cerrada (no hay cryptpw)"; }
76grep -q 'SIN contraseña posible' "$OUT1" || { cat "$OUT1"; fail "no avisó que la cuenta queda sin contraseña"; }
77grep -q 'INADMINISTRABLE' "$OUT1" || { cat "$OUT1"; fail "no avisó que la máquina queda inadministrable (root cerrado y nadie escala)"; }
78[ ! -e "$R1/etc/doas.conf" ] || fail "escribió doas.conf en una imagen que no trae doas"
79echo "  ✓ interactivo: disco elegido + confirmado, config escrita (hostname/hosts/ssh/tz/keymap)"
80# ── /etc/profile: que exista, y que NO sea de NADIE EN PARTICULAR ─────────────────────────────
81# El segundo control es el que importa para una imagen que instala un desconocido: la primera
82# versión de este PATH llevaba `/home/sergio/.local/bin` escrito a mano, porque nació de arreglar
83# UNA máquina. Un fichero de `/etc` con el hogar de una persona adentro es un defecto que no se ve
84# hasta que lo instala otra.
85[ -f "$R1/etc/profile" ] || fail "/etc/profile no se escribió (sin él nadie lee /etc/profile.d y /usr/local/bin no existe para el shell)"
86grep -q '/usr/local/bin' "$R1/etc/profile" || fail "/etc/profile no garantiza /usr/local/bin"
87grep -q '/etc/profile.d' "$R1/etc/profile" || fail "/etc/profile no sourcea profile.d"
88grep -q '/home/[a-z]' "$R1/etc/profile" && { grep -n '/home/[a-z]' "$R1/etc/profile"; fail "/etc/profile lleva el hogar de UNA persona: no sirve para instalar en otra máquina"; }
89echo "  ✓ la cuenta: usuario creado con uid 1000, su hogar, y los DOS avisos (cerrada / inadministrable)"
90echo "  ✓ /etc/profile: los seis directorios, profile.d sourceado, y SIN el hogar de nadie escrito"
91
92echo "== test 2: confirmación ERRÓNEA cancela sin tocar nada =="
93OUT2="$W/out2.txt"
94if BB=/nonexistent SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$ZI" \
95   TAKANA_INSTALL_INTERACTIVE=1 TAKANA_INSTALL_DRYRUN=1 TAKANA_INSTALL_TESTROOT="$W/root-none" \
96   sh "$INST" >"$OUT2" 2>&1 <<'ANSWERS'
972
982048
99512
100h
1013
102
103us
104sda-equivocado
105ANSWERS
106then fail "debió cancelar con confirmación errónea (exit != 0)"; fi
107grep -qi 'Cancelado' "$OUT2" || { cat "$OUT2"; fail "no reportó cancelación"; }
108[ ! -d "$W/root-none/etc" ] || fail "escribió config pese a cancelar"
109echo "  ✓ confirmación errónea ⇒ aborta sin escribir"
110
111echo "== test 3: modo DESATENDIDO por env (device + HAMMER_ROOT_HASH) =="
112R3="$W/root3"; mkdir -p "$R3/etc"
113printf 'root:!:19000:0:99999:7:::\n' > "$R3/etc/shadow"
114OUT3="$W/out3.txt"
115BB=/nonexistent TAKANA_INSTALL_DRYRUN=1 TAKANA_INSTALL_TESTROOT="$R3" \
116  HAMMER_HOSTNAME=srv01 HAMMER_ROOT_HASH='$6$salt$deadbeefHASH' HAMMER_TZ=UTC HAMMER_KEYMAP=es \
117  sh "$INST" /dev/vdb 3000 700 >"$OUT3" 2>&1 || { cat "$OUT3"; fail "desatendido salió con error"; }
118grep -q 'DEV=/dev/vdb  ROOT_MB=3000  STORE_MB=700' "$OUT3" || { cat "$OUT3"; fail "no respetó device/tamaños posicionales"; }
119grep -q '^root:\$6\$salt\$deadbeefHASH:' "$R3/etc/shadow" || { cat "$R3/etc/shadow"; fail "no reemplazó el hash de root"; }
120[ "$(cat "$R3/etc/hostname")" = srv01 ] || fail "hostname desatendido incorrecto"
121grep -q 'KEYMAP=es' "$R3/etc/hammer/install.conf" || fail "keymap por env no propagado"
122echo "  ✓ desatendido: hash de root reemplazado + hostname/keymap por env"
123
124echo "== test 4: sin device y sin TTY ⇒ error de uso (no cuelga) =="
125if BB=/nonexistent sh "$INST" </dev/null >/dev/null 2>&1; then fail "debió fallar sin device ni TTY"; fi
126echo "  ✓ uso inválido reportado"
127
128echo "== test 5: LA CUENTA COMPLETA, con cryptpw de verdad =="
129# Lo que los tests 1-4 no pueden mostrar: que la contraseña se hashea, que la regla de doas se
130# escribe, y que el usuario entra a los grupos del hardware. Necesita un busybox con `cryptpw`; si
131# no hay, se SALTA diciéndolo — un test que se calla cuando no puede medir es peor que ninguno.
132BBREAL=$(command -v busybox 2>/dev/null || true)
133if [ -n "$BBREAL" ] && printf x | "$BBREAL" cryptpw -m sha512 >/dev/null 2>&1; then
134  R5="$W/root5"; mkdir -p "$R5/etc" "$R5/usr/bin" "$R5/usr/share/zoneinfo"
135  printf 'root:!:19000:0:99999:7:::\n'  > "$R5/etc/shadow"
136  printf 'root:x:0:0:root:/root:/bin/sh\n' > "$R5/etc/passwd"
137  printf 'root:x:0:\nvideo:x:27:\ninput:x:97:\n'  > "$R5/etc/group"
138  printf '127.0.0.1\tlocalhost\n'       > "$R5/etc/hosts"
139  : > "$R5/usr/bin/doas"; chmod 755 "$R5/usr/bin/doas"   # con doas, la librería SÍ pregunta por admin
140  OUT5="$W/out5.txt"
141  # Respuestas: disco 2 · 2048 · 512 · hostname · cred 3 (root sin cambios) · usuario ana ·
142  #             contraseña ×2 · admin 1 · zona (vacío) · teclado (default) · confirmación
143  BB="$BBREAL" SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$R5/usr/share/zoneinfo" \
144    TAKANA_INSTALL_INTERACTIVE=1 TAKANA_INSTALL_DRYRUN=1 TAKANA_INSTALL_TESTROOT="$R5" \
145    sh "$INST" >"$OUT5" 2>&1 <<'ANSWERS' || { cat "$OUT5"; fail "test 5 salió con error"; }
1462
1472048
148512
149midistro
1503
151ana
152clave-de-prueba
153clave-de-prueba
1541
155
156
157/dev/vdb
158ANSWERS
159  grep -qE '^ana:\$6\$' "$R5/etc/shadow" || { cat "$R5/etc/shadow"; fail "la contraseña de ana no quedó hasheada en SHA-512"; }
160  grep -q 'clave-de-prueba' "$R5/etc/shadow" "$OUT5" && fail "LA CONTRASEÑA EN CLARO quedó escrita en algún sitio"
161  grep -q '^permit persist ana$' "$R5/etc/doas.conf" || { cat "$R5/etc/doas.conf" 2>/dev/null; fail "la regla de doas no se escribió"; }
162  [ "$(stat -c%a "$R5/etc/doas.conf")" = 640 ] || fail "doas.conf no quedó 640"
163  grep -q '^video:x:27:ana$' "$R5/etc/group" || { grep '^video' "$R5/etc/group"; fail "ana no entró al grupo video (seatd lo exige por nombre)"; }
164  grep -q '^input:x:97:ana$' "$R5/etc/group" || fail "ana no entró al grupo input"
165  grep -q 'INADMINISTRABLE' "$OUT5" && fail "avisó inadministrable cuando ana SÍ puede escalar"
166  echo "  ✓ cuenta completa: hash SHA-512, nada en claro, permit persist 640, grupos video+input"
167
168  echo "== test 6: el usuario YA EXISTE (cosechado del /home conservado) ⇒ sólo su contraseña =="
169  # Éste es el caso que vivió el operador: `--keep` cosecha `sergio` del /home con el uid de sus
170  # archivos y `!` por contraseña. La sección NO debe crear un segundo usuario ni cambiarle el uid:
171  # si le cambia el uid, sus archivos dejan de ser suyos — el único daño irreparable que hay acá.
172  R6="$W/root6"; mkdir -p "$R6/etc" "$R6/usr/bin" "$R6/home/ana" "$R6/usr/share/zoneinfo"
173  printf 'root:x:0:0:root:/root:/bin/sh\nana:x:1047:1047:ana:/home/ana:/bin/sh\n' > "$R6/etc/passwd"
174  printf 'root:x:0:\nana:x:1047:\n' > "$R6/etc/group"
175  printf 'root:!:19000:0:99999:7:::\nana:!:20000:0:99999:7:::\n' > "$R6/etc/shadow"
176  printf '127.0.0.1\tlocalhost\n' > "$R6/etc/hosts"
177  : > "$R6/usr/bin/doas"; chmod 755 "$R6/usr/bin/doas"
178  : > "$R6/home/ana/lo-que-ya-tenia"
179  OUT6="$W/out6.txt"
180  BB="$BBREAL" SYS_BLOCK="$SB" MOUNTS="$MOUNTS" ZONEINFO="$R6/usr/share/zoneinfo" \
181    TAKANA_INSTALL_INTERACTIVE=1 TAKANA_INSTALL_DRYRUN=1 TAKANA_INSTALL_TESTROOT="$R6" \
182    sh "$INST" >"$OUT6" 2>&1 <<'ANSWERS' || { cat "$OUT6"; fail "test 6 salió con error"; }
1832
1842048
185512
186midistro
1873
188ana
189otra-clave
190otra-clave
1911
192
193
194/dev/vdb
195ANSWERS
196  [ "$(grep -c '^ana:' "$R6/etc/passwd")" = 1 ] || { grep '^ana:' "$R6/etc/passwd"; fail "duplicó el usuario ana"; }
197  grep -q '^ana:x:1047:1047:' "$R6/etc/passwd" || { grep '^ana:' "$R6/etc/passwd"; fail "le CAMBIÓ el uid: sus archivos dejarían de ser suyos"; }
198  grep -qE '^ana:\$6\$' "$R6/etc/shadow" || fail "no le puso contraseña al usuario cosechado"
199  [ -f "$R6/home/ana/lo-que-ya-tenia" ] || fail "tocó el /home que ya existía"
200  grep -q 'ya existe con uid 1047' "$OUT6" || { cat "$OUT6"; fail "no dijo que reusaba el usuario cosechado"; }
201  echo "  ✓ usuario cosechado: uid 1047 intacto, sin duplicar, /home sin tocar, contraseña puesta"
202else
203  echo "  ⊘ SALTADO: no hay un busybox con cryptpw en esta máquina (los tests 5 y 6 no se midieron)"
204fi
205
206echo
207echo "✓✓ install-tui-test: TODOS los casos verdes"