6.5 Scripting Bash para Sysadmins
Un sysadmin que no sabe scripting repite a mano lo que podría automatizar. El examen LFCS incluye tareas donde debes escribir scripts funcionales en el terminal. No hace falta ser un programador: con dominar las construcciones básicas de Bash puedes automatizar el 90% de las tareas de administración de sistemas.
1. Estructura de un script Bash
Sección titulada “1. Estructura de un script Bash"#!/usr/bin/env bash# ^ Shebang: indica al SO con qué intérprete ejecutar este fichero
# Descripción: qué hace el script, autor, fecha
set -euo pipefail# -e: salir si cualquier comando falla# -u: tratar variables no definidas como error# -o pipefail: un pipe falla si falla cualquier etapa
# Cuerpo del scriptecho "Hola desde bash"Hacer ejecutable y ejecutar:
chmod +x mi_script.sh./mi_script.sh
# O ejecutar sin hacerlo ejecutable:bash mi_script.sh2. Variables
Sección titulada “2. Variables"# Asignar (sin espacios alrededor del =)nombre="servidor-web"puerto=8080fecha=$(date +%Y-%m-%d) # Captura la salida de un comando
# Usar (siempre entre comillas dobles para evitar word splitting)echo "El servidor es: $nombre"echo "Puerto: ${puerto}" # Las llaves son opcionales pero clarifican el límite
# Variables especiales del sistemaecho "Mi PID: $$"echo "Usuario: $USER"echo "Directorio home: $HOME"echo "Directorio actual: $PWD"3. Argumentos del script
Sección titulada “3. Argumentos del script"Cuando ejecutas ./script.sh arg1 arg2 arg3:
| Variable | Valor |
|---|---|
$0 | Nombre del script |
$1, $2… | Argumentos posicionales |
$# | Número de argumentos |
$@ | Todos los argumentos (como lista) |
$* | Todos los argumentos (como cadena) |
#!/usr/bin/env bashset -euo pipefail
# Verificar que se pasaron los argumentos necesariosif [[ $# -lt 2 ]]; then echo "Uso: $0 <usuario> <directorio>" exit 1fi
usuario=$1directorio=$2
echo "Creando directorio $directorio para el usuario $usuario"mkdir -p "$directorio"chown "$usuario:$usuario" "$directorio"4. Códigos de salida y $?
Sección titulada “4. Códigos de salida y $?"Cada comando devuelve un código de salida: 0 = éxito, cualquier otro valor = error.
# $? contiene el código de salida del último comandols /etc/passwdecho "Código de salida: $?" # 0
ls /no/existeecho "Código de salida: $?" # 2
# Comprobar explícitamenteif grep -q "root" /etc/passwd; then echo "root existe en el sistema"fi
# Forzar un código de salida en el scriptexit 0 # Éxitoexit 1 # Error genéricoexit 2 # Error de uso incorrecto5. Condicionales
Sección titulada “5. Condicionales"if / elif / else
Sección titulada “if / elif / else"#!/usr/bin/env bashdisco_uso=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [[ $disco_uso -gt 90 ]]; then echo "CRÍTICO: Disco al ${disco_uso}%" exit 2elif [[ $disco_uso -gt 75 ]]; then echo "AVISO: Disco al ${disco_uso}%" exit 1else echo "OK: Disco al ${disco_uso}%" exit 0fiOperadores de comparación
Sección titulada “Operadores de comparación"| Números | Cadenas | Ficheros |
|---|---|---|
-eq (igual) | = o == | -f (es fichero regular) |
-ne (distinto) | != | -d (es directorio) |
-lt (menor) | -z (cadena vacía) | -e (existe) |
-gt (mayor) | -n (no vacía) | -r (tiene permiso lectura) |
-le (menor o igual) | -w (tiene permiso escritura) | |
-ge (mayor o igual) | -x (es ejecutable) |
# Comprobar si un fichero existeif [[ -f /etc/nginx/nginx.conf ]]; then echo "Nginx está configurado"fi
# Comprobar si un directorio existeif [[ ! -d /var/backups ]]; then mkdir -p /var/backupsfi
# Comprobar si un servicio está activoif systemctl is-active --quiet nginx; then echo "nginx está corriendo"fi6. Bucles
Sección titulada “6. Bucles"# Iterar sobre una listafor servicio in nginx ssh cron; do estado=$(systemctl is-active "$servicio" 2>/dev/null || echo "inactivo") echo "$servicio: $estado"done
# Iterar sobre ficherosfor log in /var/log/*.log; do echo "Procesando: $log" wc -l "$log"done
# Iterar con rango numéricofor i in {1..5}; do echo "Intento $i"done# Leer fichero línea a líneawhile IFS= read -r linea; do echo "Procesando: $linea"done < /etc/hosts
# Esperar hasta que un servicio esté activointentos=0while ! systemctl is-active --quiet nginx; do intentos=$((intentos + 1)) if [[ $intentos -ge 10 ]]; then echo "ERROR: nginx no arrancó después de 10 intentos" exit 1 fi echo "Esperando nginx... (intento $intentos)" sleep 2doneecho "nginx activo"7. Funciones
Sección titulada “7. Funciones"#!/usr/bin/env bashset -euo pipefail
# Definir función (debe definirse antes de usarse)log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $*"}
log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2}
verificar_servicio() { local servicio=$1 # local = variable local a la función if systemctl is-active --quiet "$servicio"; then log_info "$servicio está activo" return 0 else log_error "$servicio NO está activo" return 1 fi}
# Llamar las funcionesverificar_servicio "nginx"verificar_servicio "ssh"8. Scripts sysadmin reales
Sección titulada “8. Scripts sysadmin reales"Script de backup incremental diario
Sección titulada “Script de backup incremental diario"#!/usr/bin/env bashset -euo pipefail
ORIGEN="/var/www"DESTINO="/backup/www"FECHA=$(date +%Y-%m-%d)LOG="/var/log/backup.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
# Crear directorio de destino si no existemkdir -p "$DESTINO"
log "Iniciando backup de $ORIGEN"
if rsync -av --delete "$ORIGEN/" "$DESTINO/"; then log "Backup completado: $DESTINO" exit 0else log "ERROR: Backup fallido" exit 1fiScript de auditoría de usuarios sin contraseña
Sección titulada “Script de auditoría de usuarios sin contraseña"#!/usr/bin/env bashset -euo pipefail
echo "=== Usuarios con contraseña vacía o sin contraseña ==="while IFS=: read -r usuario _ uid _ _ _ _; do if [[ $uid -ge 1000 ]]; then estado=$(passwd -S "$usuario" 2>/dev/null | awk '{print $2}') if [[ "$estado" == "NP" || "$estado" == "L" ]]; then echo " $usuario (UID $uid): $estado" fi fidone < /etc/passwdScript de monitorización de disco con alerta
Sección titulada “Script de monitorización de disco con alerta"#!/usr/bin/env bashset -euo pipefail
UMBRAL=80DESTINATARIO="root"
while IFS= read -r linea; do uso=$(echo "$linea" | awk '{print $5}' | tr -d '%') punto=$(echo "$linea" | awk '{print $6}')
if [[ $uso -gt $UMBRAL ]]; then mensaje="ALERTA: $punto al ${uso}% de capacidad" echo "$mensaje" echo "$mensaje" | mail -s "Alerta disco $(hostname)" "$DESTINATARIO" fidone < <(df -h | grep -v "^Filesystem\|tmpfs\|cdrom")9. Buenas prácticas en scripts de producción
Sección titulada “9. Buenas prácticas en scripts de producción"#!/usr/bin/env bashset -euo pipefail
# 1. Verificar que el script se ejecuta como root si es necesarioif [[ $EUID -ne 0 ]]; then echo "Este script debe ejecutarse como root" >&2 exit 1fi
# 2. Usar rutas absolutas para comandos críticos/usr/bin/systemctl restart nginx
# 3. Redirigir stderr a un logexec 2>>/var/log/mi_script_errores.log
# 4. Limpiar recursos al salir (trap)tmp_dir=$(mktemp -d)trap "rm -rf $tmp_dir" EXIT # Se ejecuta siempre al salir
# Usar $tmp_dir con confianzaecho "datos temporales" > "$tmp_dir/trabajo.txt"
# 5. Validar entrada antes de usarlaif [[ ! "$1" =~ ^[a-zA-Z0-9_-]+$ ]]; then echo "Nombre de usuario inválido" >&2 exit 1fi