#!/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' # Registrar nombres, no valores que puedan contener credenciales. capture_shell environment 'printenv | cut -d= -f1 | sort -u' 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'