#!/bin/sh # lib/adoptar.sh — adoptar una partición en vez de formatearla. # # Vive aparte de `takana-install-partition.sh` por una razón práctica: así se puede **certificar sin # root**. El instalador entero necesita losetup, mount y un disco; estas funciones, sobre un # directorio, se prueban enteras en un segundo (`scripts/adoptar-test.sh`). Un mecanismo que sólo se # puede probar con el hardware delante es un mecanismo que no se prueba. # # Lo sourcea el instalador; usa `$DEST` y las funciones `es_bloque` de quien la carga. # ── ADOPTAR LA PARTICIÓN (`--adopt`) — ni formatear, ni convivir ────────────────────────────── # # El caso más común de todos: **una sola partición, con un Linux adentro y el `/home` adentro de esa # misma partición**. De las tres salidas posibles sólo una sirve: # # · formatear → se pierde el /home, que es lo único irreponible de una máquina. # · instalar encima → quedan DOS juegos de /etc y de paquetes, y nada que diga cuál manda. # No es una opción; no se ofrece. # · **adoptar** → el sistema viejo se muda a `/sistema-anterior` y el /home se queda. # # Lo que la hace barata: mover dentro del mismo filesystem es un `rename(2)` — instantáneo y sin # copiar un byte. Y deja algo MEJOR que un respaldo: el sistema anterior queda entero y accesible, # se le puede sacar un archivo o volver a él. Formatear no deja nada. # # Las guardas, cada una por lo que pasaría sin ella: # 1. El espacio se comprueba **antes de la primera mudanza**, con el tamaño real del árbol nuevo. # Descubrirlo a mitad deja un sistema partido en dos sitios y ninguno arrancable. # 2. Un `mv` por entrada de primer nivel, cada uno atómico: un corte de luz deja un árbol a # medias pero ningún archivo roto, y el estado se puede leer. # 3. Queda una marca con la lista de lo que se movió y cuándo. # 4. **`/home` no se mueve.** Es el punto entero. # 5. `lost+found` tampoco, y si ya hay un `/sistema-anterior` se numera en vez de pisarlo. # 6. Si lo que hay adentro no tiene forma de sistema, no se adopta: se dice, y se usa el camino # de siempre. Mover cuatro directorios de datos a `/sistema-anterior` no le sirve a nadie. # adopt_montar — monta el destino en /run/takana-adopt. Si ya estaba montado, lo usa donde # está y NO lo desmonta después: un `umount` de un montaje que no hicimos puede propagarse. adopt_montar() { _am_modo="$1" # Un DIRECTORIO como destino es el modo de prueba: ya está «montado», se usa tal cual. Es lo que # permite certificar la mudanza entera sin losetup, sin mount y sin root — y lo que hace que # este mecanismo tenga pruebas en vez de tener sólo un banco que casi nadie corre. if [ -d "$DEST" ]; then ADOPT_DIR="$DEST"; ADOPT_MONTE_YO=""; return 0; fi ADOPT_DIR=$(awk -v d="$(readlink -f "$DEST")" '$1==d {print $2; exit}' /proc/mounts 2>/dev/null || true) if [ -n "$ADOPT_DIR" ]; then ADOPT_MONTE_YO="" return 0 fi ADOPT_DIR="/run/takana-adopt" mkdir -p "$ADOPT_DIR" # Un fichero de prueba se monta por loop; un dispositivo, directo. El modo de prueba sin # hardware es el que hace que esto se pueda certificar sin un disco de verdad. if es_bloque "$DEST"; then mount -o "$_am_modo" "$DEST" "$ADOPT_DIR" 2>/dev/null else mount -o "loop,$_am_modo" "$DEST" "$ADOPT_DIR" 2>/dev/null fi || { echo "takana-install-partition: --adopt no puede montar $DEST (probé $_am_modo)." >&2 echo " Sin montarlo no hay nada que adoptar, y NADA se tocó." >&2; exit 1; } ADOPT_MONTE_YO=1 } adopt_desmontar() { [ -n "$ADOPT_MONTE_YO" ] || return 0 umount "$ADOPT_DIR" 2>/dev/null || echo " ⚠ no pude desmontar $ADOPT_DIR." >&2 ADOPT_MONTE_YO="" } # adopt_validar — corre ANTES del plan y ANTES de tocar nada: mira qué hay, elige el subdirectorio y # cosecha los usuarios, para que el plan pueda decirlo todo con el disco todavía intacto. ADOPT_USUARIOS="${TMPDIR:-/tmp}/takana-adopt-usuarios.$$" adopt_validar() { [ -z "$USE_FREE" ] || { echo "takana-install-partition: --adopt y --use-free-space se excluyen." >&2 echo " En un hueco sin asignar no hay ningún sistema que adoptar." >&2; exit 2; } adopt_montar ro,nodev,nosuid,noexec # 1) ¿Tiene forma de sistema? Por la FORMA del árbol y no por un archivo de una distribución # concreta: esto tiene que servir en la máquina de cualquiera, no en la de quien lo escribe. _av_etc=""; [ -d "$ADOPT_DIR/etc" ] && _av_etc=1 _av_prog="" for _av_d in usr bin sbin; do [ -d "$ADOPT_DIR/$_av_d" ] && _av_prog=1; done [ -n "$_av_etc" ] && [ -n "$_av_prog" ] || { echo "takana-install-partition: en $DEST no hay un sistema que adoptar." >&2 echo " Vi: $(ls -A "$ADOPT_DIR" 2>/dev/null | tr '\n' ' ' | cut -c1-200)" >&2 echo " Para instalar acá, hacelo SIN --adopt: se formatea." >&2 adopt_desmontar; exit 1; } # 2) El subdirectorio, sin pisar uno anterior. Que se numere y no se pise es lo que hace que # adoptar dos veces no borre la primera mudanza. ADOPT_SUB="sistema-anterior" if [ -e "$ADOPT_DIR/$ADOPT_SUB" ]; then _av_n=2 while [ -e "$ADOPT_DIR/sistema-anterior-$_av_n" ] && [ "$_av_n" -lt 10 ]; do _av_n=$((_av_n + 1)); done [ "$_av_n" -lt 10 ] || { echo "takana-install-partition: ya hay nueve /sistema-anterior en $DEST. No sigo." >&2 adopt_desmontar; exit 1; } ADOPT_SUB="sistema-anterior-$_av_n" echo " (ya había un /sistema-anterior: el de ahora va a $ADOPT_SUB)" fi # 3) Qué se mueve, y qué no. `/home` y `lost+found` se quedan; lo demás viaja. ADOPT_MUEVE="" for _av_e in "$ADOPT_DIR"/* "$ADOPT_DIR"/.*; do [ -e "$_av_e" ] || continue _av_b=$(basename "$_av_e") case "$_av_b" in .|..|home|lost+found|sistema-anterior|sistema-anterior-[0-9]) continue ;; esac ADOPT_MUEVE="$ADOPT_MUEVE $_av_b" done # 4) Los usuarios del /home que está adentro: el mismo criterio que --keep. : > "$ADOPT_USUARIOS" if [ -d "$ADOPT_DIR/home" ]; then cosechar_usuarios "$ADOPT_DIR/home" "$ADOPT_USUARIOS"; fi adopt_desmontar } # adopt_mudar <árbol> — la mudanza de verdad. Corre DESPUÉS de armar el stage, porque hasta que el # árbol no existe no se sabe cuánto ocupa, y el espacio es la guarda que no se puede hacer a ojo. adopt_mudar() { _ad_t="$1" adopt_montar rw # 1) EL ESPACIO, antes de mover nada. Con el tamaño real del árbol más un margen: un filesystem # al borde del lleno falla en el último fichero, que es cuando ya no hay vuelta atrás. _ad_necesita=$(du -sk "$_ad_t" 2>/dev/null | awk '{print $1}' || echo "") _ad_libre=$(df -k "$ADOPT_DIR" 2>/dev/null | awk 'NR==2 {print $4}' || echo "") case "${_ad_necesita:-x}${_ad_libre:-x}" in *x*) echo "takana-install-partition: no pude medir el espacio de $DEST (du/df). No sigo." >&2 adopt_desmontar; exit 1 ;; esac _ad_con_margen=$(( _ad_necesita + _ad_necesita / 10 + 262144 )) [ "$_ad_libre" -ge "$_ad_con_margen" ] || { echo "takana-install-partition: no entra. El sistema nuevo ocupa $((_ad_necesita / 1024)) MiB" >&2 echo " y en $DEST quedan $((_ad_libre / 1024)) MiB libres (hacen falta" >&2 echo " $((_ad_con_margen / 1024)) MiB con margen)." >&2 echo " NADA se movió: el disco está como estaba." >&2 adopt_desmontar; exit 1; } echo " espacio: ocupa $((_ad_necesita / 1024)) MiB · libres $((_ad_libre / 1024)) MiB ✓" # 2) La marca, ANTES de mover: si se corta la luz a la mitad, lo que quedó a medias se puede # leer. Una marca escrita al final sólo documenta los casos que salieron bien. mkdir -p "$ADOPT_DIR/$ADOPT_SUB" { echo "# El sistema que había en esta partición, movido acá por el instalador de takana." echo "# Está ENTERO: no se copió ni se borró nada, sólo cambiaron de directorio." echo "# Para volver a usarlo, sus archivos están donde siempre, un nivel más adentro." echo "cuando=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo desconocido)" echo "particion=$DEST" echo "se-quedo-donde-estaba=/home" echo "movido=$ADOPT_MUEVE" } > "$ADOPT_DIR/$ADOPT_SUB/MUDANZA.txt" # 3) Un `mv` por entrada, cada uno atómico. Si uno falla se para ahí y se dice cuál: seguir # moviendo después de un fallo reparte el sistema viejo entre dos sitios. for _ad_e in $ADOPT_MUEVE; do mv "$ADOPT_DIR/$_ad_e" "$ADOPT_DIR/$ADOPT_SUB/$_ad_e" || { echo "takana-install-partition: no pude mover «$_ad_e» a $ADOPT_SUB. Paro acá." >&2 echo " Lo que ya se movió está en $ADOPT_SUB y se puede volver a sacar." >&2 adopt_desmontar; exit 1; } done echo " ✓ el sistema anterior está en /$ADOPT_SUB, entero ($(echo $ADOPT_MUEVE | wc -w) entradas)" # 4) El sistema nuevo, encima. `/home` del árbol viene vacío, así que no pisa el que ya está. cp -a "$_ad_t/." "$ADOPT_DIR/" || { echo "takana-install-partition: falló la copia del sistema nuevo." >&2 echo " El anterior sigue entero en /$ADOPT_SUB." >&2 adopt_desmontar; exit 1; } adopt_desmontar # 5) La etiqueta, que es por donde el arranque encuentra la raíz. Sin esto la partición # adoptada tiene el sistema nuevo adentro y nadie la reconoce al bootear. # El initramfs de pivote encuentra la raíz con `findfs LABEL=hammer-root`: sin la etiqueta, el # sistema está adentro y **nadie lo encuentra al arrancar**. Tres caminos, en orden de # preferencia, porque ninguno está siempre: if [ -d "$DEST" ]; then return 0 # modo de prueba: un directorio no tiene etiqueta de filesystem que poner elif [ -n "${E2LABEL:-}" ] && [ -x "$E2LABEL" ]; then "$E2LABEL" "$DEST" hammer-root || { echo "takana-install-partition: no pude ponerle LABEL=hammer-root a $DEST." >&2; exit 1; } elif command -v takana >/dev/null 2>&1; then # Dentro del initramfs no viaja e2fsprogs —busybox no trae `e2label` ni `tune2fs`— pero el # binario `takana` sí. Escribe los 16 bytes de la etiqueta y recalcula el crc32c del # superbloque si el filesystem trae `metadata_csum`; sin ese recálculo el kernel lo monta # de sólo lectura, y el síntoma no menciona la etiqueta por ningún lado. takana fs-label "$DEST" hammer-root || { echo "takana-install-partition: no pude ponerle LABEL=hammer-root a $DEST." >&2; exit 1; } else echo "takana-install-partition: para --adopt hace falta e2label o el binario takana, y no" >&2 echo " encontré ninguno. El sistema anterior sigue entero en" >&2 echo " /$ADOPT_SUB y el nuevo está copiado: falta sólo la etiqueta." >&2 exit 1 fi }