Tenemos nueve scripts que funcionan, cada uno con sus opciones, su configuración y su manera de invocarse. Eso no es un producto: es una carpeta. Un compañero que entra mañana no sabe cuál ejecutar, la ayuda está repartida en nueve --help distintos, la instalación es «copia esto y acuérdate de los permisos», y nadie sabe qué versión corre en srv-veloz-02. Este último proyecto convierte la colección en veloz-ops: un único comando con subcomandos, instalable, configurable, versionado, probado, desplegado en la flota y documentado. Es el cierre del curso, y el paso que separa «sé programar en Bash» de «he entregado algo».
Contenido
- El toolkit visto entero
- El patrón de comando único con subcomandos
- Reorganización final del repositorio
- Configuración unificada y
veloz-ops config - Autocompletado de Bash
- Instalación idempotente
- Versionado semántico y
veloz-ops version - Calidad antes de desplegar
- Despliegue en la flota y vuelta atrás
- Documentación: README y runbook
- El toolkit visto entero
flowchart TD
U["veloz-ops (comando unico)"]
U --> S2[estado] --> L2[libexec/info-sistema.sh]
U --> S3[logs] --> L3[libexec/analiza-logs.sh]
U --> S4[respaldo] --> L4[libexec/respaldo.sh]
U --> S5[red] --> L5[libexec/monitor-red.sh]
U --> S6[flota] --> L6[libexec/flota.sh]
L2 & L3 & L4 & L5 & L6 --> LIB["lib/comun.sh<br/>veloz_log · veloz_requiere · veloz_api_get"]
LIB --> CFG["etc/veloz-ops.conf + etc/*.d/"]
L3 --> D1[("/var/log/veloz/*.log")]
L4 --> D2[("/srv/veloz/datos + /respaldos")]
L5 --> D3(["veloz-api :8080 · flota SSH"])
TMR["systemd timers"] --> U
El diagrama muestra la propiedad que buscamos: una puerta de entrada, una librería común, una configuración. Los scripts pasan a libexec/ porque dejan de ser interfaz pública; nadie los invoca directamente, los invoca veloz-ops.
- El patrón de comando único con subcomandos
Es el patrón de git, systemctl o docker, y en Bash se implementa con case y funciones (04-05):
#!/usr/bin/env bash
# veloz-ops - punto de entrada unico del toolkit.
set -euo pipefail
export LC_ALL=C
readonly RAIZ=${VELOZ_RAIZ:-/opt/veloz-ops}
readonly LIBEXEC=$RAIZ/libexec
# shellcheck source=lib/comun.sh
. "$RAIZ/lib/comun.sh"
ayuda() {
cat <<'EOF'
Uso: veloz-ops <subcomando> [opciones]
estado Informacion del sistema (09-01)
logs Analisis de registros (09-02)
respaldo Respaldos, verificacion y restauracion (09-03)
red Monitorizacion de red y servicio (09-04)
flota Ejecuta una orden en los tres servidores
config Configuracion efectiva y su origen; version; ayuda <sub>
EOF
}
main() {
local sub=${1:-ayuda}; shift || true
case $sub in
estado) exec "$LIBEXEC/info-sistema.sh" "$@" ;;
logs) exec "$LIBEXEC/analiza-logs.sh" "$@" ;;
respaldo) exec "$LIBEXEC/respaldo.sh" "$@" ;;
red) exec "$LIBEXEC/monitor-red.sh" "$@" ;;
flota) exec "$LIBEXEC/flota.sh" "$@" ;;
config) muestra_config "$@" ;;
version) printf 'veloz-ops %s\n' "$VERSION" ;;
ayuda|-h|--help) [[ $# -gt 0 ]] && ayuda_de "$1" || ayuda ;;
*) printf 'Subcomando desconocido: %s\n\n' "$sub" >&2; ayuda >&2; exit 2 ;;
esac
}
main "$@"Tres decisiones. exec reemplaza el proceso en lugar de crear uno hijo: ahorra un proceso y, más importante, el código de salida y las señales llegan al script real sin intermediario, así que Ctrl-C sobre veloz-ops respaldo interrumpe el respaldo de verdad. El case es una lista blanca explícita (08-03): construir la ruta como "$LIBEXEC/$sub.sh" sería más corto y permitiría a cualquiera ejecutar veloz-ops ../../bin/loquesea. Y el subcomando desconocido sale con código 2 y escribe en stderr, siguiendo el convenio de 05-03.
- Reorganización final del repositorio
veloz-ops/ ├── bin/veloz-ops # unico ejecutable en el PATH ├── libexec/ # scripts internos, no en el PATH │ ├── info-sistema.sh analiza-logs.sh respaldo.sh monitor-red.sh flota.sh │ └── acceso.awk # programas awk separados (09-02) ├── lib/comun.sh # funciones veloz_*, se carga con source ├── etc/veloz-ops.conf # 600; mas respaldo.d/ y monitor-red.d/ ├── systemd/*.service *.timer ├── tests/*.bats ├── docs/runbook.md └── completion/ install.sh verificar.sh README.md CHANGELOG.md
El criterio merece la pena enunciarlo: bin/ es lo que el usuario invoca, libexec/ lo que invoca el programa, lib/ lo que se carga con source y nunca se ejecuta, etc/ lo que se edita sin desplegar. Esa última frase decide las dudas: si cambiarlo exige revisión de código, es código; si es una ruta, un umbral o una lista de objetivos, es configuración.
- Configuración unificada y
veloz-ops config
veloz-ops configLa precedencia de 05-06 —defecto < fichero < entorno < opciones— se implementa en un único punto de lib/comun.sh, y se hace auditable guardando el origen de cada valor:
declare -A CFG ORIGEN
veloz_config_carga() {
local f=$1 k v env_var
while IFS='=' read -r k v; do # fichero: pisa los defectos
[[ $k == \#* || -z $k ]] && continue
k=${k// /}; v=${v%\"}; v=${v#\"}; CFG[$k]=$v; ORIGEN[$k]="fichero:$f"
done < "$f"
for k in "${!CFG[@]}"; do # entorno: pisa al fichero
env_var=VELOZ_$k
[[ -n ${!env_var:-} ]] && { CFG[$k]=${!env_var}; ORIGEN[$k]=entorno; }
done
}${!env_var} es la expansión indirecta (03-06): construye el nombre VELOZ_UMBRAL_DISCO y lee esa variable. Guardar el origen junto al valor cuesta un array asociativo más y resuelve la pregunta que consume horas en producción: «el umbral está a 95, pero el fichero dice 85, ¿de dónde sale?». veloz-ops config lo imprime en tres columnas —clave, valor, origen— y por eso es un subcomando de primera clase y no un echo de depuración.
- Autocompletado de Bash
# completion/veloz-ops.bash -> /etc/bash_completion.d/veloz-ops
_veloz_ops() {
local act=${COMP_WORDS[COMP_CWORD]} opciones='estado logs respaldo red flota config version ayuda'
(( COMP_CWORD > 1 )) && case ${COMP_WORDS[1]} in
respaldo) opciones='ejecutar verificar restaurar informe --simular' ;;
estado) opciones='--formato --seccion --ayuda' ;;
red) opciones='comprobar resumen directo --objetivo' ;;
esac
mapfile -t COMPREPLY < <(compgen -W "$opciones" -- "$act")
}
complete -F _veloz_ops veloz-opsCOMP_WORDS y COMP_CWORD son las variables que Bash rellena al pulsar Tab, y compgen -W filtra una lista por el prefijo tecleado. No es cosmética: el autocompletado es la documentación que la gente lee de verdad, y evita la mitad de los errores de tecleo en un subcomando delicado como restaurar.
- Instalación idempotente
install.sh debe poder ejecutarse cien veces con el mismo resultado (07-02):
instalar() {
install -d -m 755 "$PREFIJO"/{bin,libexec,lib,etc,docs}
install -m 755 bin/veloz-ops "$PREFIJO/bin/veloz-ops"
install -m 755 libexec/*.sh "$PREFIJO/libexec/"
install -m 644 libexec/*.awk lib/comun.sh -t "$PREFIJO/lib/"
[[ -e $PREFIJO/etc/veloz-ops.conf ]] ||
install -m 600 etc/veloz-ops.conf.ejemplo "$PREFIJO/etc/veloz-ops.conf"
install -m 644 completion/veloz-ops.bash /etc/bash_completion.d/veloz-ops
ln -sfn "$PREFIJO/bin/veloz-ops" /usr/local/bin/veloz-ops
install -m 644 systemd/*.service systemd/*.timer /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now veloz-respaldo.timer veloz-red.timer
"$PREFIJO/bin/veloz-ops" version
}install en lugar de cp + chmod porque crea el destino y fija los permisos en una sola operación, y es idempotente por naturaleza; ln -sfn sobrescribe el enlace sin quejarse; systemctl enable --now activa y arranca, y repetirlo no hace daño. La línea clave es la condicional: la configuración existente no se pisa nunca. Un instalador que sobrescribe veloz-ops.conf borra los umbrales que alguien ajustó a las tres de la mañana; por eso el repositorio versiona veloz-ops.conf.ejemplo y no el fichero real (08-04). La desinstalación es simétrica: --desinstalar para y deshabilita los timers, borra las unidades, hace daemon-reload, elimina el enlace y el árbol, y conserva etc/ y los respaldos, avisando de dónde quedan.
- Versionado semántico y
veloz-ops version
veloz-ops version| Cambio | Incrementa | Ejemplo |
|---|---|---|
| Se elimina o renombra un subcomando u opción; cambia el formato de un fichero de estado | MAYOR | --seccion pasa a --area |
| Subcomando u opción nuevos, compatibles | MENOR | Se añade veloz-ops red resumen |
| Corrección sin cambio de interfaz | PARCHE | Se arregla el cálculo de la mediana |
La versión sale de Git, no de una constante que alguien olvidará actualizar (08-04):
VERSION=$(git -C "$RAIZ" describe --tags --dirty 2>/dev/null || cat "$RAIZ/VERSION" 2>/dev/null || echo desconocida)git describe --tags --dirty da v1.4.0 en una etiqueta exacta, v1.4.0-7-gab12cd3 si hay siete commits encima —lo que revela al instante que en ese servidor hay algo sin publicar— y añade -dirty si hay cambios sin confirmar, que en producción es una alerta en sí misma. El install.sh escribe además un fichero VERSION para el despliegue por rsync sin repositorio.
- Calidad antes de desplegar
Un solo script recoge todas las comprobaciones del módulo 8:
#!/usr/bin/env bash
# verificar.sh - todo lo que debe pasar antes de desplegar.
set -uo pipefail
fallos=0
mapfile -t SCRIPTS < <(find bin libexec lib -type f)
paso() { printf '\n== %s ==\n' "$1"; shift; "$@" || ((fallos++)); }
paso sintaxis bash -n "${SCRIPTS[@]}"
paso shellcheck shellcheck -x -S style "${SCRIPTS[@]}"
paso formato shfmt -d -i 2 -ci "${SCRIPTS[@]}"
paso pruebas bats tests/
paso ayuda bin/veloz-ops ayuda
exit $(( fallos > 0 ))El orden va de lo rápido a lo lento: bash -n tarda milisegundos y descarta un error de sintaxis antes de gastar treinta segundos en Bats. shellcheck -x sigue los source para analizar también lib/comun.sh (08-05), y -S style sube el listón por encima de los errores. shfmt -d no reformatea, solo muestra la diferencia y falla: en CI hay que fallar, no arreglar por sorpresa. La batería de Bats se amplía a los subcomandos (08-06):
@test "subcomando desconocido sale con 2 y escribe en stderr" {
run bin/veloz-ops noexiste
[ "$status" -eq 2 ]
[[ $output == *"Subcomando desconocido"* ]]
}
@test "respaldo --simular no crea ningun directorio" {
local antes; antes=$(find "$BATS_TMPDIR/respaldos" -type d | wc -l)
run bin/veloz-ops respaldo ejecutar --simular
[ "$status" -eq 0 ]
[ "$(find "$BATS_TMPDIR/respaldos" -type d | wc -l)" -eq "$antes" ]
}La segunda prueba es la más valiosa del toolkit: verifica que el modo simulación no tiene efectos, que es la promesa en la que se apoya cualquiera antes de lanzar un respaldo con --delete. El hook pre-commit (08-04) ejecuta las tres primeras etapas y CI la batería completa en cada rama.
- Despliegue en la flota y vuelta atrás
despliega() { # $1 = etiqueta, p.ej. v1.4.0
local etiqueta=$1 host
git tag -l "$etiqueta" | grep -q . || veloz_morir 2 "etiqueta inexistente: $etiqueta"
./verificar.sh || veloz_morir 3 "no se despliega codigo que no verifica"
for host in srv-veloz-01 srv-veloz-02 srv-veloz-03; do
printf '\n--- %s ---\n' "$host"
ssh -n "$host" "cd /opt/veloz-ops && git fetch --tags -q &&
git checkout -q $etiqueta && sudo ./install.sh" || return 1
ssh -n "$host" 'veloz-ops version && veloz-ops estado --seccion servicios' || return 1
done
}El despliegue es secuencial y con verificación posterior en cada host: si srv-veloz-01 falla, no se toca el segundo, y en el peor caso queda un servidor mal y dos intactos. ssh -n (07-06) impide que el bucle se coma la entrada estándar, el error clásico que hace que solo se despliegue el primer host. Se despliega una etiqueta, nunca una rama: git checkout v1.4.0 da exactamente el código que se probó, y la vuelta atrás es la misma orden con la etiqueta anterior (despliega v1.3.2). Que revertir sea idéntico a desplegar es lo que hace que alguien se atreva a ejecutarlo a las cuatro de la mañana. Con una salvedad para el runbook: revertir el código es trivial, revertir un cambio de formato de los ficheros de estado o del histórico no lo es, y por eso ese tipo de cambio incrementa la versión MAYOR.
- Documentación: README y runbook
Son dos documentos con dos lectores distintos y forman parte del entregable. El README.md lo lee quien llega nuevo, en frío: qué es, qué requiere, cómo se instala en cinco órdenes copiables, los subcomandos con un ejemplo real cada uno y dónde está la configuración. Si alguien no puede instalarlo y ejecutar veloz-ops estado siguiendo solo el README, el README está mal. El docs/runbook.md lo lee alguien medio dormido con una alerta en el móvil: se organiza por alerta, no por script, sin prosa y con órdenes copiables.
## ALERTA: respaldo con fallos en srv-veloz-01
1. Confirmar: veloz-ops respaldo informe
2. Ver el log: journalctl -u veloz-respaldo.service -n 50
3. Si "sin espacio" (codigo 5): df -h /respaldos; revisar RETENCION_* del perfil
4. Si "verificacion fallida" (codigo 4): NO borrar nada; restaurar de srv-veloz-03
5. Escalar a Operaciones si el perfil datos falla dos noches seguidas.Un script sin documentación es un script que solo puede ejecutar quien lo escribió; y quien lo escribió acabará de vacaciones justo la noche en que falle.
Errores Comunes y Consejos
- Construir la ruta del subcomando con el argumento del usuario.
"$LIBEXEC/$1.sh"abre la puerta a../. Lista blanca concase, siempre. - Instaladores que pisan la configuración. Versiona
.conf.ejemplo; el.confreal solo se crea si no existe. - Desplegar una rama en lugar de una etiqueta.
mainsignifica una cosa distinta cada hora; una etiqueta significa siempre lo mismo. - Bucle
for hostconsshsin-n. El primersshconsume la lista y el resto de la flota se queda sin desplegar, en silencio. - Documentar por script en vez de por alerta. A las cuatro de la mañana nadie busca «analiza-logs»; busca el texto que le llegó al móvil.
- Consejo: ejecuta
veloz-ops configen los tres servidores y compara. Las diferencias no explicadas entre nodos son el origen de la mitad de los incidentes de «en este servidor sí funciona».
Ejercicios
- Subcomando
doctor. Verifica la salud de la propia instalación: dependencias (jq,rsync,curl,awk), permisos deetc/(600), timers activos, antigüedad del último respaldo y del último ciclo del monitor. Un símbolo por comprobación y código 0/1/2 global. - Panel del estado de la flota. Un
veloz-ops flota estadoque recoja el JSON deestadode los tres servidores en paralelo y marque las diferencias de versión, kernel y configuración efectiva. - Retos abiertos de ampliación. Empaqueta el toolkit como
.debconpostinst/prermy la configuración marcada comoconffiles; añade unveloz-ops apique exponga los informes por HTTP decidiendo qué no exponer; unifica un--jsonen todos los subcomandos con una prueba Bats que valide cada salida; y lleva el toolkit a Debian o Alpine anotando cada bashismo que haya que resolver (08-07).
Soluciones
1. doctor no comprueba nada por su cuenta: reutiliza lo ya construido.
doctor() {
local fallos=0
chk() { local n=$1; shift
if "$@" >/dev/null 2>&1; then printf ' [OK] %s\n' "$n"
else printf ' [FALLO] %s\n' "$n"; ((fallos++)); fi; }
chk "dependencias" veloz_requiere jq rsync curl awk
chk "permisos de etc" test "$(stat -c %a "$RAIZ/etc/veloz-ops.conf")" = 600
chk "timer de respaldo" systemctl is-active --quiet veloz-respaldo.timer
chk "respaldo reciente" test "$(dias_desde_ultimo_respaldo)" -le 1
(( fallos == 0 )) && return 0 || return 2
}La función anidada chk, que recibe una orden y la ejecuta, evita repetir el mismo if diez veces; y devolver 0/2 permite que el propio doctor se vigile desde vigilante.sh con el convenio de 07-04.
2. flota.sh ya ejecuta en paralelo, así que basta con consolidar los JSON con jq -s:
veloz-ops flota 'veloz-ops estado --formato json' | jq -s '
map({host, version, kernel})
| {nodos: ., kernels_distintos: (map(.kernel) | unique | length)}'Detectar la divergencia —no listar los valores— es lo que convierte el panel en una herramienta útil: si kernels_distintos es mayor que 1, hay un servidor sin actualizar.
Conclusión
Empezamos el curso escribiendo echo "Hola" en un terminal y lo terminamos con un producto instalado en tres servidores: un comando único con subcomandos y ayuda, configuración con precedencia y auditable, autocompletado, instalador idempotente y desinstalador simétrico, versionado semántico atado a las etiquetas de Git, una batería de pruebas que corre en cada commit, despliegue por etiqueta con vuelta atrás y documentación escrita para quien la leerá de madrugada. Ninguna de esas piezas es Bash avanzado: son case, funciones, arrays, printf, source y códigos de salida —lo del módulo 4— aplicados con el criterio del módulo 8. Ese es el mensaje del curso entero: la diferencia entre un script y una herramienta no está en el lenguaje, está en la disciplina.
Conviene terminar con honestidad sobre los límites. Bash es insuperable pegando programas, moviendo ficheros y automatizando el sistema; es exactamente la herramienta correcta para los cinco proyectos de este módulo. Deja de serlo cuando aparecen estructuras de datos anidadas (más de dos niveles de JSON y jq ya no basta), cuando hacen falta pruebas unitarias de lógica de negocio, concurrencia real, aritmética de coma flotante sostenida, o cuando el script pasa de las mil líneas y cada cambio da miedo. Si te encuentras escribiendo un analizador de expresiones o un cliente HTTP con estado en Bash, la señal es clara: toca Python, Go o el lenguaje que use tu equipo. Saber cuándo parar forma parte de dominar la herramienta, y reescribir en otro lenguaje un script de Bash bien estructurado es sencillo precisamente porque estaba bien estructurado.
Y por dónde seguir. El manual de Bash (man bash, y sobre todo su sección de expansiones, que se lee entera una vez en la vida) es la única fuente definitiva. La guía de estilo de shell de Google, para discutir decisiones con argumentos en lugar de con gustos. ShellCheck como maestro permanente: cada aviso que no entiendas, búscalo por su código y aprenderás algo que no sabías. Y sobre todo, lo que de verdad consolida: automatiza lo tuyo. Coge esa tarea que haces a mano cada lunes, escríbela como un script con set -euo pipefail, dale opciones, ponle un --simular, pruébala con Bats, súbela a Git y dispárala con un timer. En eso consiste el oficio, y ya lo sabes hacer entero.
Curso de Programación en Bash
Módulo 1: Introducción a Bash
- ¿Qué es Bash?
- Configurando tu Entorno
- Navegación Básica en la Línea de Comandos
- Entendiendo el Shell
- Encontrar Ayuda: man, help y --help
Módulo 2: Comandos Básicos de Bash
- Operaciones con Archivos y Directorios
- Comandos de Procesamiento de Texto
- Permisos y Propiedad de Archivos
- Redirección y Tuberías
- Comodines y Expansión de Rutas
- Historial y Atajos de Teclado
Módulo 3: Fundamentos de Scripting
- Creando y Ejecutando un Script
- Variables y Constantes
- Operadores Básicos
- Sentencias Condicionales
- Argumentos y Entrada del Usuario
- Comillas, Expansión y Sustitución
Módulo 4: Scripting Intermedio
- Bucles en Bash
- Funciones en Bash
- Arrays y Arrays Asociativos
- Manipulación de Cadenas
- La Sentencia case y los Menús Interactivos
- Aritmética y Cálculos Numéricos
Módulo 5: Técnicas Avanzadas de Scripting
- Operaciones Avanzadas con Archivos
- Gestión de Procesos
- Manejo de Errores y Depuración
- Expresiones Regulares
- Entrada/Salida Avanzada: Descriptores y Here-Documents
- Scripts Modulares y Librerías Reutilizables
Módulo 6: Trabajando con Herramientas Externas
Módulo 7: Automatización y Programación
- Trabajos Cron
- Automatizando Tareas
- Scripts de Respaldo y Restauración
- Monitoreo y Registro
- Servicios y Temporizadores con systemd
- Automatización Remota con SSH
Módulo 8: Mejores Prácticas y Optimización
- Escribiendo Código Legible
- Optimizando Scripts en Bash
- Consideraciones de Seguridad
- Control de Versiones con Git
- Análisis Estático con ShellCheck y shfmt
- Pruebas Automatizadas con Bats
- Portabilidad: POSIX sh frente a Bashismos
