#!/usr/bin/bash set -euo pipefail IFS=$'\n\t' SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ -d "${SCRIPT_DIR}/../../.git" ]] || [[ -f "${SCRIPT_DIR}/../../nodo/config/etc/mirrors.env" ]]; then REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" else REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" fi if [[ -f "${REPO_ROOT}/nodo/config/etc/mirrors.env" ]]; then # shellcheck source=/dev/null source "${REPO_ROOT}/nodo/config/etc/mirrors.env" elif [[ -f "${REPO_ROOT}/config/etc/mirrors.env" ]]; then # shellcheck source=/dev/null source "${REPO_ROOT}/config/etc/mirrors.env" elif [[ -f "${REPO_ROOT}/etc/mirrors.env" ]]; then # shellcheck source=/dev/null source "${REPO_ROOT}/etc/mirrors.env" fi STAGING_DIR="${MIRRORS_STAGING_DIR:-$HOME/ipfs/staging}" SNAPSHOTS_DIR="${MIRRORS_SNAPSHOTS_DIR:-$HOME/ipfs/snapshots}" mkdir -p "${STAGING_DIR}" "${SNAPSHOTS_DIR}" # Definición canónica de módulos gestionados y sus rutas relativas en staging declare -A MODULES=( ["fedora-iso-43"]="fedora/linux/releases/43/Server/x86_64/iso" ["fedora-iso-44"]="fedora/linux/releases/44/Server/x86_64/iso" ["fedora-iso-45"]="fedora/linux/development/45/Server/x86_64/iso" ["fedora-everything-43"]="fedora/linux/releases/43/Everything" ["fedora-everything-44"]="fedora/linux/releases/44/Everything" ["fedora-everything-45"]="fedora/linux/development/45/Everything" ["centos-baseos"]="centos-stream/10-stream/BaseOS" ["centos-appstream"]="centos-stream/10-stream/AppStream" ["centos-crb"]="centos-stream/10-stream/CRB" ["centos-ha"]="centos-stream/10-stream/HighAvailability" ["centos-nfv"]="centos-stream/10-stream/NFV" ["centos-rt"]="centos-stream/10-stream/RT" ["centos-extras"]="centos-stream/SIGs/10-stream/extras" ["centos-iso"]="centos-stream/10-stream/BaseOS/x86_64/iso" ["centos-sig-extras"]="centos-stream/SIGs/10-stream/extras" ["centos-sig-virt"]="centos-stream/SIGs/10-stream/virt" ["centos-sig-kmods"]="centos-stream/SIGs/10-stream/kmods" ["centos-sig-messaging"]="centos-stream/SIGs/10-stream/messaging" ["centos-sig-storage"]="centos-stream/SIGs/10-stream/storage" ["centos-sig-hyperscale"]="centos-stream/SIGs/10-stream/hyperscale" ) # Orden de presentación consistente MODULE_KEYS=( "fedora-iso-43" "fedora-iso-44" "fedora-iso-45" "fedora-everything-43" "fedora-everything-44" "fedora-everything-45" "centos-iso" "centos-baseos" "centos-appstream" "centos-crb" "centos-ha" "centos-nfv" "centos-rt" "centos-sig-extras" "centos-sig-virt" "centos-sig-kmods" "centos-sig-messaging" "centos-sig-storage" "centos-sig-hyperscale" ) resolve_module_path() { local target="$1" if [[ -n "${MODULES[${target}]:-}" ]]; then echo "${STAGING_DIR}/${MODULES[${target}]}" return 0 fi # Si el usuario suministró una ruta directa o relativa if [[ "${target}" = /* ]]; then echo "${target}" else echo "${STAGING_DIR}/${target}" fi } is_btrfs_fs() { local target_dir="$1" local parent_check="${target_dir}" while [[ ! -d "${parent_check}" && "${parent_check}" != "/" ]]; do parent_check="$(dirname "${parent_check}")" done local fstype fstype="$(stat -f -c %T "${parent_check}" 2>/dev/null || echo "unknown")" [[ "${fstype}" == "btrfs" ]] } is_subvolume() { local target_dir="$1" if [[ ! -d "${target_dir}" ]]; then return 1 fi # En sistemas de archivos Btrfs, el inodo raíz de cualquier subvolumen es 256 local inode inode="$(stat -c %i "${target_dir}" 2>/dev/null || echo "0")" [[ "${inode}" == "256" ]] } get_human_size() { local target_dir="$1" if [[ -d "${target_dir}" ]]; then du -sh "${target_dir}" 2>/dev/null | cut -f1 else echo "0 B" fi } count_snapshots() { local mod_key="$1" find "${SNAPSHOTS_DIR}" -maxdepth 1 -mindepth 1 -name "${mod_key}-*" 2>/dev/null | wc -l } cmd_list() { echo "====================================================================================================" echo " Catálogo de Módulos y Estado de Subvolúmenes Btrfs" echo " Directorio raíz: ${STAGING_DIR}" echo " Directorio de instantáneas: ${SNAPSHOTS_DIR}" echo "====================================================================================================" printf "%-22s %-40s %-12s %-10s %s\n" "Identificador" "Ruta relativa" "Subvolumen" "Tamaño" "Instantáneas" echo "----------------------------------------------------------------------------------------------------" for key in "${MODULE_KEYS[@]}"; do local rel_path="${MODULES[${key}]}" local abs_path="${STAGING_DIR}/${rel_path}" local is_sub="no" if is_subvolume "${abs_path}"; then is_sub="sí (Btrfs)" elif [[ -d "${abs_path}" ]]; then is_sub="directorio" else is_sub="no existe" fi local sz sz="$(get_human_size "${abs_path}")" local snaps snaps="$(count_snapshots "${key}")" printf "%-22s %-40s %-12s %-10s %s\n" "${key}" "${rel_path}" "${is_sub}" "${sz}" "${snaps}" done echo "====================================================================================================" } init_single_subvolume() { local key="$1" local rel_path="${MODULES[${key}]:-${key}}" local target_path="${STAGING_DIR}/${rel_path}" if ! is_btrfs_fs "${target_path}"; then echo "ERROR: La ruta ${target_path} no reside sobre un sistema de archivos Btrfs." >&2 return 1 fi if is_subvolume "${target_path}"; then echo "==> Módulo '${key}': ya es un subvolumen Btrfs (${target_path})." return 0 fi local parent_dir parent_dir="$(dirname "${target_path}")" mkdir -p "${parent_dir}" if [[ ! -e "${target_path}" ]]; then echo "==> Módulo '${key}': creando subvolumen Btrfs en ${target_path}..." btrfs subvolume create "${target_path}" return 0 fi # Si existe como directorio convencional, migrar mediante copia por reflink CoW echo "==> Módulo '${key}': migrando directorio a subvolumen con CoW reflink (costo de almacenamiento cero)..." local temp_subvol="${target_path}.tmp_subvol_$$" btrfs subvolume create "${temp_subvol}" if [[ -n "$(ls -A "${target_path}" 2>/dev/null)" ]]; then echo " Clonando metadatos y referencias de bloques..." cp -aT --reflink=always "${target_path}" "${temp_subvol}" fi local backup_old="${target_path}.old_dir_$$" mv "${target_path}" "${backup_old}" mv "${temp_subvol}" "${target_path}" echo " Limpiando directorio antiguo..." rm -rf "${backup_old}" echo "==> Módulo '${key}': migración a subvolumen Btrfs completada exitosamente." } cmd_init() { local target="${1:-all}" if [[ "${target}" == "all" ]]; then echo "==> Inicializando todos los módulos como subvolúmenes Btrfs..." for key in "${MODULE_KEYS[@]}"; do init_single_subvolume "${key}" done elif [[ -n "${MODULES[${target}]:-}" ]]; then init_single_subvolume "${target}" else # Inicialización por ruta directa local mod_key="${target##*/}" init_single_subvolume "${mod_key}" fi } safe_delete_subvolume() { local target_subvol="$1" btrfs property set "${target_subvol}" ro false 2>/dev/null || true if ! btrfs subvolume delete "${target_subvol}" 2>/dev/null; then # shellcheck disable=SC2029 ssh root@localhost btrfs subvolume delete "${target_subvol}" fi } cmd_delete() { local target="${1:-}" if [[ -z "${target}" ]]; then echo "ERROR: Debe especificar el módulo o subvolumen a eliminar." >&2 echo "Uso: $0 delete " >&2 return 1 fi local abs_path abs_path="$(resolve_module_path "${target}")" if [[ ! -e "${abs_path}" ]]; then echo "AVISO: La ruta ${abs_path} no existe en el sistema." return 0 fi if is_subvolume "${abs_path}"; then echo "==> Eliminando subvolumen Btrfs de forma atómica e instantánea: ${abs_path}..." safe_delete_subvolume "${abs_path}" echo "==> Subvolumen eliminado exitosamente." else echo "AVISO: La ruta ${abs_path} es un directorio ordinario, no un subvolumen Btrfs." echo "==> Eliminando contenido mediante borrado recursivo..." rm -rf "${abs_path}" echo "==> Directorio eliminado." fi } cmd_snapshot() { local target="${1:-}" local tag="${2:-$(date -u +"%Y%m%d-%H%M%S")}" if [[ -z "${target}" ]]; then echo "ERROR: Debe especificar el módulo a respaldar mediante instantánea." >&2 echo "Uso: $0 snapshot [etiqueta]" >&2 return 1 fi local abs_path abs_path="$(resolve_module_path "${target}")" if ! is_subvolume "${abs_path}"; then echo "ERROR: La ruta ${abs_path} no es un subvolumen Btrfs activo." >&2 echo "Ejecute previamente '$0 init ${target}' para convertirla en subvolumen." >&2 return 1 fi local snap_name="${target}-${tag}" local snap_dest="${SNAPSHOTS_DIR}/${snap_name}" echo "==> Creando instantánea de solo lectura con CoW (espacio adicional cero): ${snap_dest}..." btrfs subvolume snapshot -r "${abs_path}" "${snap_dest}" echo "==> Instantánea generada: ${snap_name}" } cmd_snapshots() { local filter="${1:-}" echo "====================================================================================================" echo " Instantáneas Btrfs en Almacén Local" echo " Directorio: ${SNAPSHOTS_DIR}" echo "====================================================================================================" printf "%-40s %-12s %s\n" "Nombre de Instantánea" "Tamaño" "Fecha de Creación" echo "----------------------------------------------------------------------------------------------------" local found=0 for snap in "${SNAPSHOTS_DIR}"/*; do if [[ ! -d "${snap}" ]]; then continue fi local name name="$(basename "${snap}")" if [[ -n "${filter}" && "${name}" != *"${filter}"* ]]; then continue fi found=1 local sz sz="$(get_human_size "${snap}")" local ctime ctime="$(stat -c '%y' "${snap}" 2>/dev/null | cut -d'.' -f1)" printf "%-40s %-12s %s\n" "${name}" "${sz}" "${ctime}" done if [[ "${found}" -eq 0 ]]; then echo "(No se encontraron instantáneas registradas)" fi echo "====================================================================================================" } cmd_rollback() { local target="${1:-}" local snap_name="${2:-}" if [[ -z "${target}" || -z "${snap_name}" ]]; then echo "ERROR: Argumentos insuficientes." >&2 echo "Uso: $0 rollback " >&2 return 1 fi local abs_path abs_path="$(resolve_module_path "${target}")" local snap_src="${SNAPSHOTS_DIR}/${snap_name}" if [[ ! -d "${snap_src}" ]]; then echo "ERROR: La instantánea ${snap_src} no existe." >&2 return 1 fi echo "==> Restaurando módulo '${target}' desde instantánea '${snap_name}'..." if is_subvolume "${abs_path}"; then echo " Eliminando subvolumen actual..." safe_delete_subvolume "${abs_path}" elif [[ -d "${abs_path}" ]]; then echo " Eliminando directorio actual..." rm -rf "${abs_path}" fi echo " Creando subvolumen modificable derivado de la instantánea..." btrfs subvolume snapshot "${snap_src}" "${abs_path}" echo "==> Restauración completada exitosamente." } cmd_prune() { local target="${1:-}" local keep="${2:-3}" if [[ -z "${target}" ]]; then echo "ERROR: Debe especificar el módulo para podar instantáneas antiguas." >&2 echo "Uso: $0 prune-snapshots [cantidad-a-conservar]" >&2 return 1 fi local pattern="${target}-*" # shellcheck disable=SC2012 local snaps mapfile -t snaps < <(find "${SNAPSHOTS_DIR}" -maxdepth 1 -mindepth 1 -name "${pattern}" 2>/dev/null | sort) local total="${#snaps[@]}" if [[ "${total}" -le "${keep}" ]]; then echo "==> Módulo '${target}': hay ${total} instantáneas (máximo permitido sin poda: ${keep}). No se requieren acciones." return 0 fi local to_delete=$((total - keep)) echo "==> Podando ${to_delete} instantáneas antiguas de '${target}' (conservando las últimas ${keep})..." for ((i = 0; i < to_delete; i++)); do local snap_del="${snaps[i]}" echo " Eliminando instantánea: $(basename "${snap_del}")" safe_delete_subvolume "${snap_del}" done echo "==> Poda finalizada." } usage() { cat << EOF Herramienta de Gestión de Módulos y Subvolúmenes Btrfs para Espejos MxOS Uso: $0 list Listar módulos, tamaño y estado de subvolúmenes $0 init [modulo|all] Inicializar o migrar módulos a subvolúmenes Btrfs $0 delete Eliminar un módulo entero de forma instantánea y atómica $0 snapshot [etiqueta] Crear una instantánea de solo lectura con CoW $0 snapshots [filtro] Listar instantáneas almacenadas $0 rollback Restaurar un módulo a partir de una instantánea $0 prune-snapshots [conservar] Depurar instantáneas conservando las N más recientes Módulos reconocidos: fedora-iso-43, fedora-iso-44, fedora-iso-45 fedora-everything-43, fedora-everything-44, fedora-everything-45 centos-baseos, centos-appstream, centos-crb, centos-ha, centos-nfv, centos-rt centos-sig-extras, centos-sig-virt, centos-sig-kmods, centos-sig-messaging, centos-sig-storage, centos-sig-hyperscale EOF } main() { local action="${1:-list}" shift || true case "${action}" in list|status) cmd_list "$@" ;; init) cmd_init "$@" ;; delete|remove|rm) cmd_delete "$@" ;; snapshot|snap) cmd_snapshot "$@" ;; snapshots) cmd_snapshots "$@" ;; rollback) cmd_rollback "$@" ;; prune-snapshots|prune) cmd_prune "$@" ;; help|--help|-h) usage ;; *) echo "ERROR: Acción desconocida: ${action}" >&2 usage >&2 exit 1 ;; esac } main "$@"