Scripts prácticos: backup automático, monitor del sistema, limpieza de ficheros

Introducción

Esta unidad reúne todos los conceptos de la sección en tres scripts completos y funcionales: un sistema de backup con rotación, un monitor de recursos del sistema y un limpiador de ficheros antiguos. Cada script puede usarse directamente o adaptarse como punto de partida.

Script 1: Backup automático con rotación

Crea copias de seguridad comprimidas con fecha, mantiene solo los últimos N backups y registra todo en un fichero de log.


#!/usr/bin/env bash
# backup.sh — Backup con rotación y log
set -euo pipefail

# ── Configuración ────────────────────────────────────────
readonly ORIGEN="/var/www/html"
readonly DESTINO="/backup"
readonly MAX_BACKUPS=7
readonly LOG="/var/log/backup.log"
readonly FECHA=$(date '+%Y%m%d_%H%M%S')
readonly FICHERO_BACKUP="backup_{$FECHA}.tar.gz"

# ── Funciones de log ─────────────────────────────────────
log() {
    local MSG="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
    echo "$MSG"
    echo "$MSG" >> "$LOG"
}

error() {
    log "ERROR: $*"
    exit 1
}

# ── Validaciones ─────────────────────────────────────────
[ -d "$ORIGEN" ]  || error "El directorio origen no existe: $ORIGEN"
[ -d "$DESTINO" ] || mkdir -p "$DESTINO"

# ── Limpieza al salir ─────────────────────────────────────
TMPFILE=""
cleanup() {
    [ -n "$TMPFILE" ] && rm -f "$TMPFILE"
}
trap cleanup EXIT

# ── Crear backup ─────────────────────────────────────────
log "Iniciando backup de $ORIGEN"

TMPFILE=$(mktemp --suffix=.tar.gz)
if tar -czf "$TMPFILE" -C "$(dirname "$ORIGEN")" "$(basename "$ORIGEN")"; then
    mv "$TMPFILE" "$DESTINO/$FICHERO_BACKUP"
    TMPFILE=""   # ya no necesita limpiarse
    TAMANIO=$(du -sh "$DESTINO/$FICHERO_BACKUP" | cut -f1)
    log "Backup creado: $FICHERO_BACKUP ($TAMANIO)"
else
    error "Fallo al crear el backup"
fi

# ── Rotación: eliminar backups antiguos ───────────────────
log "Rotando backups (máximo $MAX_BACKUPS)..."
TOTAL=$(find "$DESTINO" -maxdepth 1 -name "backup_*.tar.gz" | wc -l)

if [ "$TOTAL" -gt "$MAX_BACKUPS" ]; then
    ELIMINAR=$((TOTAL - MAX_BACKUPS))
    find "$DESTINO" -maxdepth 1 -name "backup_*.tar.gz" \
        | sort | head -n "$ELIMINAR" \
        | while read -r fichero; do
            rm "$fichero"
            log "Eliminado backup antiguo: $(basename "$fichero")"
        done
fi

log "Backup completado. Total backups: $(find "$DESTINO" -name "backup_*.tar.gz" | wc -l)"
          

Automatizar con cron:


# Ejecutar a las 2:00 de la madrugada todos los días
0 2 * * * /usr/local/bin/backup.sh
          

Script 2: Monitor de recursos del sistema

Comprueba CPU, memoria, disco y procesos zombis. Envía alertas a stderr (o a un fichero de log) si algún umbral se supera.


#!/usr/bin/env bash
# monitor.sh — Monitor de recursos con alertas
set -euo pipefail

# ── Umbrales ─────────────────────────────────────────────
readonly UMBRAL_CPU=80       # % de CPU
readonly UMBRAL_MEM=85       # % de memoria
readonly UMBRAL_DISCO=90     # % de disco
readonly LOG="/var/log/monitor.log"

# ── Colores ──────────────────────────────────────────────
ROJO='\033[0;31m'
VERDE='\033[0;32m'
AMARILLO='\033[1;33m'
NC='\033[0m'   # sin color

# ── Funciones ────────────────────────────────────────────
log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG"
}

alerta() {
    echo -e "{$ROJO}[ALERTA]{$NC} $*"
    log "ALERTA: $*"
}

ok() {
    echo -e "{$VERDE}[OK]{$NC}    $*"
}

# ── Comprobaciones ───────────────────────────────────────
comprobar_cpu() {
    local USO
    # awk calcula el porcentaje de uso: 100 - %idle
    USO=$(top -bn1 | grep "Cpu(s)" | awk '{print 100 - $8}' | cut -d. -f1)
    if [ "$USO" -ge "$UMBRAL_CPU" ]; then
        alerta "CPU al {$USO}% (umbral: {$UMBRAL_CPU}%)"
        log "  Top procesos por CPU:"
        ps aux --sort=-%cpu | head -5 | awk '{printf "  %-10s %5s%%  %s\n", $1, $3, $11}' | tee -a "$LOG"
    else
        ok "CPU al {$USO}%"
    fi
}

comprobar_memoria() {
    local TOTAL DISPONIBLE USO
    TOTAL=$(grep MemTotal /proc/meminfo | awk '{print $2}')
    DISPONIBLE=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
    USO=$(( (TOTAL - DISPONIBLE) * 100 / TOTAL ))
    if [ "$USO" -ge "$UMBRAL_MEM" ]; then
        alerta "Memoria al {$USO}% (umbral: {$UMBRAL_MEM}%)"
        log "  Top procesos por memoria:"
        ps aux --sort=-%mem | head -5 | awk '{printf "  %-10s %5s%%  %s\n", $1, $4, $11}' | tee -a "$LOG"
    else
        ok "Memoria al {$USO}%"
    fi
}

comprobar_disco() {
    local ALERTA=false
    while IFS= read -r linea; do
        local USO PUNTO_MONTAJE
        USO=$(echo "$linea" | awk '{print $5}' | tr -d '%')
        PUNTO_MONTAJE=$(echo "$linea" | awk '{print $6}')
        if [ "$USO" -ge "$UMBRAL_DISCO" ]; then
            alerta "Disco {$PUNTO_MONTAJE} al {$USO}% (umbral: {$UMBRAL_DISCO}%)"
            ALERTA=true
        else
            ok "Disco {$PUNTO_MONTAJE} al {$USO}%"
        fi
    done < <(df -h | grep '^/dev' | grep -v tmpfs)

    $ALERTA || true
}

comprobar_zombis() {
    local ZOMBIS
    ZOMBIS=$(ps aux | awk '$8 == "Z"' | wc -l)
    if [ "$ZOMBIS" -gt 0 ]; then
        alerta "{$ZOMBIS} proceso(s) zombi detectado(s)"
        ps aux | awk '$8 == "Z" {print}' | tee -a "$LOG"
    else
        ok "Sin procesos zombi"
    fi
}

# ── Ejecución ────────────────────────────────────────────
echo "=== Monitor de sistema: $(hostname) — $(date) ==="
comprobar_cpu
comprobar_memoria
comprobar_disco
comprobar_zombis
echo "=== Fin del informe ==="
          

Script 3: Limpieza de ficheros antiguos

Elimina ficheros más antiguos de N días en directorios configurados, con modo de prueba (dry-run) para ver qué se borraría sin hacerlo.


#!/usr/bin/env bash
# limpieza.sh — Limpieza de ficheros antiguos
set -euo pipefail

# ── Uso ──────────────────────────────────────────────────
usage() {
    cat <<EOF
Uso: $0 [opciones]
  -d DIR    Directorio a limpiar (puede usarse varias veces)
  -a DIAS   Eliminar ficheros más antiguos de DIAS días (por defecto: 30)
  -p PATRÓN Patrón de ficheros a eliminar (por defecto: *)
  -n        Dry-run: mostrar qué se eliminaría sin borrar
  -h        Mostrar esta ayuda
EOF
    exit 0
}

# ── Valores por defecto ───────────────────────────────────
DIRECTORIOS=()
DIAS=30
PATRON="*"
DRY_RUN=false

# ── Argumentos ───────────────────────────────────────────
while getopts "d:a:p:nh" opt; do
    case $opt in
        d) DIRECTORIOS+=("$OPTARG") ;;
        a) DIAS="$OPTARG" ;;
        p) PATRON="$OPTARG" ;;
        n) DRY_RUN=true ;;
        h) usage ;;
        ?) echo "Opción desconocida: -$OPTARG" >&2; exit 2 ;;
    esac
done

# ── Validaciones ─────────────────────────────────────────
if [ {$#DIRECTORIOS[@]} -eq 0 ]; then
    echo "Error: se requiere al menos un directorio (-d)" >&2
    exit 2
fi

if ! [[ "$DIAS" =~ ^[0-9]+$ ]]; then
    echo "Error: DIAS debe ser un número entero positivo" >&2
    exit 2
fi

# ── Limpiar un directorio ─────────────────────────────────
limpiar_directorio() {
    local DIR="$1"
    local ELIMINADOS=0
    local ESPACIO_LIBERADO=0

    if [ ! -d "$DIR" ]; then
        echo "Aviso: el directorio no existe: $DIR" >&2
        return
    fi

    echo "Procesando: $DIR (ficheros con más de $DIAS días, patrón: $PATRON)"

    while IFS= read -r -d '' fichero; do
        local TAMANIO
        TAMANIO=$(du -b "$fichero" 2>/dev/null | cut -f1 || echo 0)
        if $DRY_RUN; then
            echo "  [DRY-RUN] Se eliminaría: $fichero ($(du -sh "$fichero" | cut -f1))"
        else
            rm -f "$fichero"
            echo "  Eliminado: $fichero"
        fi
        ((ELIMINADOS++))
        ((ESPACIO_LIBERADO += TAMANIO))
    done < <(find "$DIR" -maxdepth 1 -name "$PATRON" -type f -mtime +"$DIAS" -print0)

    echo "  Total: $ELIMINADOS ficheros$({$DRY_RUN} && echo " (simulado)" || echo " eliminados")"
    if [ "$ESPACIO_LIBERADO" -gt 0 ]; then
        echo "  Espacio liberado: $((ESPACIO_LIBERADO / 1024 / 1024)) MB"
    fi
}

# ── Ejecución ────────────────────────────────────────────
$DRY_RUN && echo "[DRY-RUN activado — no se eliminará nada]"
echo "Inicio: $(date)"

TOTAL_LIBERADO=0
for dir in "{${DIRECTORIOS[@]}}"; do
    limpiar_directorio "$dir"
done

echo "Fin: $(date)"
          

Ejemplos de uso:


# Ver qué se eliminaría (sin borrar nada)
./limpieza.sh -d /tmp -d /var/log -a 30 -p "*.log" -n

# Eliminar realmente
./limpieza.sh -d /tmp -a 7 -p "*.tmp"

# Múltiples directorios y patrones personalizados
./limpieza.sh -d /var/log/nginx -d /var/log/apache2 -a 60 -p "*.gz"

# En cron: limpiar /tmp cada domingo a las 3:00
0 3 * * 0 /usr/local/bin/limpieza.sh -d /tmp -a 7 >> /var/log/limpieza.log 2>&1
          

Resumen de la sección

A lo largo de esta sección se han cubierto los pilares del scripting bash:

  • Variables de entorno, configuración de shell y aliases
  • Estructura de un script: shebang, variables, expansiones, aritmética
  • Argumentos posicionales, $@, getopts
  • Comparaciones con [ ], [[ ]] y el comando test
  • Condicionales if/elif/else/fi y case/esac
  • Bucles for, while, until, break, continue
  • Funciones, local, return y librerías reutilizables
  • Entrada/salida con read, echo, printf y here-docs
  • Manejo de errores con set -euo pipefail, trap y depuración
  • Scripts completos: backup, monitorización y limpieza

Un buen script de producción siempre tiene: set -euo pipefail, trap EXIT para limpieza, validación de argumentos y mensajes de error en stderr. Los tres scripts de esta unidad sirven como plantillas reutilizables.

Conceptos TCP/IP: dirección, máscara, gateway, DNS

Índice de la sección

Índice del curso