Automatizando la ejecución de un comando con bash y esperar a seleccionar opciones

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.

Automatizando la ejecución de un comando con bash y esperar a seleccionar opciones

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 times
10for ((i=0; i<5; i++)); do
11 echo ""
12 sleep 1 # Adjust sleep duration based on the application's response time
13done
14
15# Step 4: Wait for prompt and select option 2
16while true; do
17 read -t 1 line
18 if [[ $line == *"Choose the action you want to perform:"* ]]; then
19 echo "2"
20 break
21 fi
22done

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
92
10y
11EOF
12

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/bash
2
3# Step 1: Change directory
4cd /dir/agent || exit 1
5
6# Step 2: Run set_agent_mode command
7printf "\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 command
14 cmd := exec.Command("your_command")
15 cmd.Stdout = os.Stdout
16 cmd.Stderr = os.Stderr
17 stdin, err := cmd.StdinPipe()
18 if err != nil {
19 fmt.Println("Error creating stdin pipe:", err)
20 return
21 }
22
23 // Start the command
24 err = cmd.Start()
25 if err != nil {
26 fmt.Println("Error starting the command:", err)
27 return
28 }
29
30 // Simulate pressing Enter until prompt selection is displayed
31 simulateEnters(stdin)
32
33 // Select an option by pressing 1 or 2
34 selectOption(stdin, "1") // Replace with your desired option
35
36 // Wait for the command to finish
37 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 time
47 }
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 time
53}

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 $*
10do
11 echo $i
12 case $i in
13 -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" ]; then
18 echo "Unknown $2"
19 exit 1
20 fi
21 ;;
22 esac
23echo $restart
24done
25
26# Asks user for validation
27echo "Do you want to restart? [Y/N]"
28read answer
29
30# Validates if the answer is yes or not
31if [ "$answer" = "y" -o "$answer" = "Y" -o "$restart" = "y" ] ; then
32 restart_agent_command
33fi
34
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