From c9a2e73c61fbe43ccc11691bc1b4ec77cdf0676a Mon Sep 17 00:00:00 2001 From: Carlos Tello Date: Mon, 21 Sep 2026 10:17:31 -0300 Subject: [PATCH] Add Ubuntu configuration backup and restore scripts --- README.md | 54 ++++++++++ backup_ubuntu_config.sh | 209 +++++++++++++++++++++++++++++++++++++++ restore_ubuntu_config.sh | 164 ++++++++++++++++++++++++++++++ 3 files changed, 427 insertions(+) create mode 100755 backup_ubuntu_config.sh create mode 100755 restore_ubuntu_config.sh diff --git a/README.md b/README.md index 2427f1b..71c50ca 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,60 @@ openclaw doctor openclaw gateway status ``` +## Respaldo y recuperación + +Para guardar la configuración del servidor antes de realizar cambios, ejecuta +el script como `root`: + +```bash +chmod +x backup_ubuntu_config.sh restore_ubuntu_config.sh +sudo ./backup_ubuntu_config.sh +``` + +El respaldo se guarda por defecto en `/var/backups` e incluye inventario de +hardware, paquetes, servicios, red, UFW, systemd, NVIDIA, Ollama, LiteLLM, +OpenClaw y configuraciones de usuario. No incluye modelos Ollama, el entorno +virtual de LiteLLM, bases de datos ni logs completos. + +Las claves SSH, certificados privados y credenciales de OpenClaw quedan fuera +por defecto. Para incluirlos explícitamente, protege el archivo resultante: + +```bash +sudo ./backup_ubuntu_config.sh --include-secrets +sudo chmod 600 /var/backups/ubuntu-config-*.tar.gz +``` + +En una instalación Ubuntu nueva, instala primero el sistema base y crea los +usuarios necesarios. Después copia el archivo de respaldo y usa el restaurador +en modo vista previa: + +```bash +sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz +``` + +Para aplicar la configuración: + +```bash +sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz --yes +``` + +Puedes reinstalar los paquetes APT y arrancar los servicios habilitados de la +máquina original de forma explícita: + +```bash +sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz \ + --yes --install-packages --start-services +``` + +Revisa siempre los servicios y el firewall después de restaurar: + +```bash +systemctl --failed +systemctl status ollama litellm +sudo ufw status verbose +nvidia-smi +``` + ## Tool calling y rutas de Windows Cuando un modelo envía rutas de Windows dentro de argumentos JSON, las barras diff --git a/backup_ubuntu_config.sh b/backup_ubuntu_config.sh new file mode 100755 index 0000000..b417d58 --- /dev/null +++ b/backup_ubuntu_config.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +readonly SCRIPT_NAME="$(basename "$0")" +readonly DEFAULT_OUTPUT_DIR="/var/backups" + +include_secrets=false +output_dir="$DEFAULT_OUTPUT_DIR" + +show_help() { + cat <&2; exit 2; } + output_dir="$2" + shift 2 + ;; + --include-secrets) + include_secrets=true + shift + ;; + -h|--help) + show_help + exit 0 + ;; + *) + echo "Opción desconocida: $1" >&2 + show_help >&2 + exit 2 + ;; + esac +done + +if [[ "${EUID}" -ne 0 ]]; then + echo "Ejecuta este script con sudo o como root." >&2 + exit 1 +fi + +command -v tar >/dev/null || { echo "tar es obligatorio." >&2; exit 1; } +command -v gzip >/dev/null || { echo "gzip es obligatorio." >&2; exit 1; } + +hostname_value="$(hostname -s 2>/dev/null || echo unknown)" +timestamp="$(date +%Y%m%d-%H%M%S)" +backup_name="ubuntu-config-${hostname_value}-${timestamp}" +work_dir="$(mktemp -d)" +archive_path="${output_dir}/${backup_name}.tar.gz" + +cleanup() { + rm -rf "$work_dir" +} +trap cleanup EXIT + +mkdir -p "$work_dir/metadata" "$work_dir/files" "$work_dir/packages" "$work_dir/services" +mkdir -p "$output_dir" +umask 077 + +copy_path() { + local source="$1" + local destination="$work_dir/files$source" + + if [[ -e "$source" || -L "$source" ]]; then + mkdir -p "$(dirname "$destination")" + cp -a "$source" "$destination" + fi +} + +capture() { + local name="$1" + shift + "$@" > "$work_dir/metadata/$name.txt" 2>&1 || true +} + +capture_shell() { + local name="$1" + local command_text="$2" + bash -c "$command_text" > "$work_dir/metadata/$name.txt" 2>&1 || true +} + +printf 'Creando respaldo en %s\n' "$archive_path" + +printf '%s\n' "hostname=$hostname_value" "created_at=$(date --iso-8601=seconds)" \ + "include_secrets=$include_secrets" > "$work_dir/metadata/backup-info.txt" + +capture_shell os-release 'cat /etc/os-release' +capture_shell kernel 'uname -a' +capture_shell hardware 'lscpu; echo; free -h; echo; lsblk -f' +capture_shell disks 'df -hT; echo; findmnt' +capture_shell network 'ip -brief address; echo; ip route; echo; resolvectl status 2>/dev/null || true' +capture_shell users 'getent passwd | awk -F: '\''$3 >= 1000 || $1 == "root" {print $1 ":" $3 ":" $4 ":" $6}'\''' +capture_shell mounts 'mount' +capture_shell environment 'printenv | sort' +capture_shell nvidia 'command -v nvidia-smi && nvidia-smi -q || true' +capture_shell ollama 'command -v ollama && ollama list || true' +capture_shell versions 'command -v node && node --version || true; command -v npm && npm --version || true; command -v python3 && python3 --version || true; command -v ollama && ollama --version || true; command -v openclaw && openclaw --version || true' +capture_shell ufw 'command -v ufw && ufw status verbose || true' +capture_shell iptables 'command -v iptables-save && iptables-save || true' +capture_shell sysctl 'sysctl -a 2>/dev/null' +capture_shell timers 'systemctl list-timers --all --no-pager' +capture_shell enabled-services 'systemctl list-unit-files --state=enabled --no-legend --no-pager' +capture_shell failed-services 'systemctl --failed --no-pager' + +if command -v dpkg-query >/dev/null; then + dpkg-query -W -f='${binary:Package}\t${Version}\n' > "$work_dir/packages/dpkg-status.tsv" || true +fi +if command -v apt-mark >/dev/null; then + apt-mark showmanual | sort > "$work_dir/packages/apt-manual.txt" || true +fi +if command -v snap >/dev/null; then + snap list > "$work_dir/packages/snap-list.txt" 2>&1 || true +fi +if command -v pip3 >/dev/null; then + pip3 freeze > "$work_dir/packages/pip3-freeze.txt" 2>&1 || true +fi + +systemctl list-unit-files --type=service --no-legend --no-pager \ + > "$work_dir/services/all-service-units.txt" 2>&1 || true +systemctl list-unit-files --state=enabled --type=service --no-legend --no-pager \ + | awk '{print $1}' | sort -u > "$work_dir/services/enabled-service-names.txt" || true + +# Configuración del sistema y de los servicios usados por este proyecto. +for path in \ + /etc/apt \ + /etc/default \ + /etc/environment \ + /etc/fstab \ + /etc/hostname \ + /etc/hosts \ + /etc/issue \ + /etc/netplan \ + /etc/NetworkManager \ + /etc/systemd/system \ + /etc/sysctl.d \ + /etc/modprobe.d \ + /etc/ufw \ + /etc/ollama \ + /etc/nvidia \ + /etc/profile.d + do + copy_path "$path" +done + +# Configuraciones de usuario que no suelen contener credenciales. +while IFS=: read -r username _ uid _ _ home _; do + [[ -d "$home" ]] || continue + [[ "$uid" -ge 1000 || "$username" == root ]] || continue + for relative_path in .bashrc .profile .config/systemd/user litellm_config.yaml; do + copy_path "$home/$relative_path" + done + printf '%s\t%s\t%s\t%s\n' "$username" "$uid" "$(id -g "$username" 2>/dev/null || echo 0)" "$home" \ + >> "$work_dir/metadata/users.tsv" +done < <(getent passwd) + +if [[ "$include_secrets" == true ]]; then + printf 'Incluyendo archivos sensibles: SSH, certificados y configuración privada de OpenClaw.\n' + for path in /etc/ssh /etc/ssl/private /etc/letsencrypt; do + copy_path "$path" + done + while IFS=: read -r username _ uid _ _ home _; do + [[ -d "$home" ]] || continue + [[ "$uid" -ge 1000 || "$username" == root ]] || continue + for relative_path in .ssh .openclaw .npmrc; do + copy_path "$home/$relative_path" + done + done < <(getent passwd) +else + cat > "$work_dir/metadata/excluded-secrets.txt" < "$work_dir/RESTORE.txt" < "$work_dir/metadata/file-list.txt" +cat "$work_dir/metadata/file-list.txt" + +tar -C "$work_dir" -czf "$archive_path" . +chmod 600 "$archive_path" +printf '\nRespaldo creado: %s\n' "$archive_path" +printf 'Protección: permisos 600\n' diff --git a/restore_ubuntu_config.sh b/restore_ubuntu_config.sh new file mode 100755 index 0000000..7412082 --- /dev/null +++ b/restore_ubuntu_config.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +readonly SCRIPT_NAME="$(basename "$0")" + +archive_path="" +confirmed=false +install_packages=false +start_services=false + +show_help() { + cat <&2 + show_help >&2 + exit 2 + ;; + *) + if [[ -n "$archive_path" ]]; then + echo "Solo se admite un archivo de respaldo." >&2 + exit 2 + fi + archive_path="$1" + shift + ;; + esac +done + +if [[ -z "$archive_path" ]]; then + echo "Debes indicar un archivo .tar.gz." >&2 + show_help >&2 + exit 2 +fi + +if [[ "${EUID}" -ne 0 ]]; then + echo "Ejecuta este script con sudo o como root." >&2 + exit 1 +fi + +[[ -f "$archive_path" ]] || { echo "No existe: $archive_path" >&2; exit 1; } +command -v tar >/dev/null || { echo "tar es obligatorio." >&2; exit 1; } + +work_dir="$(mktemp -d)" +cleanup() { + rm -rf "$work_dir" +} +trap cleanup EXIT + +tar -xzf "$archive_path" -C "$work_dir" --no-same-owner + +if [[ ! -d "$work_dir/files" || ! -f "$work_dir/metadata/backup-info.txt" ]]; then + echo "El archivo no parece un respaldo válido de backup_ubuntu_config.sh." >&2 + exit 1 +fi + +printf 'Respaldo: %s\n' "$archive_path" +cat "$work_dir/metadata/backup-info.txt" +printf '\nArchivos de configuración que se restaurarían:\n' +find "$work_dir/files" -mindepth 1 -maxdepth 8 -printf '%P\n' | sort | sed -n '1,200p' + +if [[ "$confirmed" != true ]]; then + cat </dev/null 2>&1; then + chown -R "$username:$(id -gn "$username")" "$home" 2>/dev/null || true + else + echo "Aviso: no existe el usuario $username; revisa $home manualmente." >&2 + fi + done < "$work_dir/metadata/users.tsv" +fi + +systemctl daemon-reload + +if [[ "$start_services" == true && -s "$work_dir/services/enabled-service-names.txt" ]]; then + while IFS= read -r service; do + [[ -n "$service" ]] || continue + systemctl enable "$service" 2>/dev/null || echo "Aviso: no se pudo habilitar $service" >&2 + done < "$work_dir/services/enabled-service-names.txt" + + systemctl restart ollama 2>/dev/null || true + systemctl restart litellm 2>/dev/null || true + systemctl restart openclaw-gateway 2>/dev/null || true +fi + +if command -v ufw >/dev/null 2>&1; then + ufw --force reload 2>/dev/null || true +fi + +cat <