Esta semana he tenido que trabajar en la automatización de un servidor usando un script de bash que requiere la ejecución de otro sistema el cual consta de diferentes pasos y sobre todo trucos.
El problema
Por ejemplo tenemos que ejecutar un comando como root, pero que sea de un usuario específico y después ejecutar varios enters para esperar a que se despliegue un prompt y seleccionar una opción, luego forzar un reinicio del agente para después repetir el proceso, pero con otra opción y esperar a que se reinicie de nuevo.
Al principio tuve problemas porque el otro sistema tenía diferentes bugs como el de tener que presionar enter varias veces y probe varios scripts.
Este fue uno de ellos:
1#!/bin/bash 2 3# Step 1: Change directory 4cd /dir/agent || exit 1 5 6# Step 2: Run set_agent_mode command 7./set_agent_mode 8 9# Step 3: Simulate pressing Enter multiple times10for ((i=0; i<5; i++)); do11 echo ""12 sleep 1 # Adjust sleep duration based on the application's response time13done14 15# Step 4: Wait for prompt and select option 216while true; do17 read -t 1 line18 if [[ $line == *"Choose the action you want to perform:"* ]]; then19 echo "2"20 break21 fi22done
Pero tuvo diferentes errores por ejemplo la simulación de varios enters a veces se seguía derecho e ignoraba la selección entonces agregué while true, pero fue igual lo cual era un error sistemático.
Entonces traté de hacerlo todo de un modo más sencillo y con menos código:
1#!/bin/bash 2 3# Step 1: Change directory 4cd /dir/agent || exit 1 5 6# Step 2: Run set_agent_mode command 7./set_agent_mode <<EOF 8username 9210y11EOF12
En teoría agregando <<EOF ejecuraria en orden lo que quieras y funciono solo al primer string.
Entonces hice otra prueba usando printf como pipe:
1#!/bin/bash2 3# Step 1: Change directory4cd /dir/agent || exit 15 6# Step 2: Run set_agent_mode command7printf "\nusername\n2\y\n" | ./set_agent_mode
Según esto funcionaria, pero tampoco fue una opción y mi manager ya me empezaba a presionar, lo cual sucede muy seguido cuando cree que me estoy empezando a poner muy creativo y querer probar cosas fuera del scope, overengineering le llama. Para este punto con toda la presión y el tiempo limité ya me estaba empezando a preocupar porque no salía y se había agendado un reléase que incluía esta automatización para la siguiente semana.
La frustración
Me dijo que lo revisara con el equipo ya molesto y creyendo que era sencillo, entonces mande un mensaje al equipo y describí los pasos con un diagrama tratando de hacer visible lo más que pudiera el nivel de complejidad, pero como todo es más difícil empezar a explicar el problema a alguien más que continuar con el progreso que llevas así sea poco y solo uno me respondió, pero con más preguntas que respuestas.
Al día siguiente quise probar con Go, pero eso implicaba más cosas por ejemplo que hiciera tests en mí máquina y que después hiciera el build del binario y después lo jalara en el servidor para poder ejecutarlo, porque por razones de compliance no podía instalar Go en él, así que aborte misión con Go, pero este es un ejemplo del script:
1package main 2 3import ( 4 "bufio" 5 "fmt" 6 "os" 7 "os/exec" 8 "strings" 9 "time"10)11 12func main() {13 // Define the command14 cmd := exec.Command("your_command")15 cmd.Stdout = os.Stdout16 cmd.Stderr = os.Stderr17 stdin, err := cmd.StdinPipe()18 if err != nil {19 fmt.Println("Error creating stdin pipe:", err)20 return21 }22 23 // Start the command24 err = cmd.Start()25 if err != nil {26 fmt.Println("Error starting the command:", err)27 return28 }29 30 // Simulate pressing Enter until prompt selection is displayed31 simulateEnters(stdin)32 33 // Select an option by pressing 1 or 234 selectOption(stdin, "1") // Replace with your desired option35 36 // Wait for the command to finish37 err = cmd.Wait()38 if err != nil {39 fmt.Println("Error waiting for the command:", err)40 }41}42 43func simulateEnters(stdinPipe io.WriteCloser) {44 for i := 0; i < 5; i++ {45 fmt.Fprintln(stdinPipe, "")46 time.Sleep(1 * time.Second) // Adjust sleep duration based on your application's response time47 }48}49 50func selectOption(stdinPipe io.WriteCloser, option string) {51 fmt.Fprintln(stdinPipe, option)52 time.Sleep(1 * time.Second) // Adjust sleep duration based on your application's response time53}
Mientras estaba en eso tuve varias juntas, una era para agregar un Docker build de una migración de node 14 a node 18 con TypeScript que también se estaba volviendo un problema y otra para discutir sobre una herramienta CLI para tareas administrativas que estamos desarrollando in-house, esa fue la última junta y después de ver lo que teníamos pasamos a revisar mi problema rápido y uno de ellos tuvo una gran idea no muy enfocada, pero con el concepto basto para que tuviera avance.
La idea
Me comento que él lo veía como error de software del equipo que desarrollo el agente y que también podría revisar el código para ver si tenía algún flag como en el CLI que estábamos desarrollando entonces aplique él:
1vi set_agent_mode
El cual era bash y al principio me resulto un monstruo que tenía más de 800 líneas de código, pero siempre he sido bueno leyendo código así que seguí y efectivamente habían tres flags:
-u = user
-o = option
-h = help
Fue revelador porque haciendo un paréntesis en la escueta documentación que me dieron no mencionaba nada al respecto, entonces probe usando los flags:
1./set_agent_mode -u user -o 2
Pero no funciono por algún bug con el flag -u el cual intente resolver, pero me tomaría más tiempo.
La siguiente prueba fue solo con el flag -o y tratando de hacer pipe con el user:
1echo "user" | ./set_agent_mode -o 2
Lo cual salió mejor porque ingreso el usuario y seleccionaba la opción que quería, pero cuando tenía que aceptar que reiniciara era ignorado.
La solución
Entonces se me ocurrió agregar un nuevo flag -r y que quedara en la validación cuando checaba si ingresabas Y/N para reiniciar.
1#!/bin/bash 2 3user= 4restart= 5option= 6silent=0 7 8# Reads all the flags and assigns them to the variables 9for i in $*10do11 echo $i12 case $i in13 -u) user=$2; shift 2;;14 -o) option=$2; shift 2; silent=1 ;;15 -r) restart=$2; shift 2;;16 -h) Usage; exit 0;;17 --) if [ ! -z "$2" ]; then18 echo "Unknown $2"19 exit 120 fi21 ;;22 esac23echo $restart24done25 26# Asks user for validation27echo "Do you want to restart? [Y/N]"28read answer29 30# Validates if the answer is yes or not31if [ "$answer" = "y" -o "$answer" = "Y" -o "$restart" = "y" ] ; then32 restart_agent_command33fi34 35
Eso me permitio ejecutar el comando del siguiente modo exitosamente:
1echo "user" | ./set_agent_mode -o 2 -r y
Después hubo una discusión con mi jefe sobre modificar ese archivo o no, pero eso ya es otra historia.
Conclusión
Realmente fue algo complicado el trabajar con esto porque ya lo había intentado sin éxito y el otro equipo no era muy cooperativo incluso ellos no sabían que se debía ingresar muchos enters lo cual descubrí en una llamada con ellos donde estaba muy frustrado y ese fue nuestro workaround por un par de años, pero ha sido satisfactorio el haberlo resuelto. También es importante acercarse a otras personas con un punto de vista diferente porque muchas veces la ceguera técnica no te deja ver más allá de lo que tienes enfrente.
Comentarios