Programando el cambio de IVA en un entorno no amigable

Como sabéis, mañana por la noche se cambia el IVA en este nuestro país. Todavía no tengo muy claro lo que eso significa para la sociedad, aunque puedo hacerme una idea de lo que pasará. Pero no estamos aquí para hablar de política, así que vamos a intentar que nos quede una amena entrada sobre cómo anticiparse y programar el cambio del IVA en un entorno no amigable.

¿Por qué lo del "entorno no amigable"?, obviamente por la época del año en la que estamos. Todo el departamento está dedicado en exclusiva a la bebida, al buen comer y a unas merecidas - y espero que fantásticas - vacaciones; con lo que he sido el suertudo que ha tenido que lidiar con el marrón. Obviamente, son tantos los sitios que hay que tocar y tantas las configuraciones descentralizadas que, si mis compañeros no me hubieran echado una mano antes de marcharse a broncear sus lánguidas pieles, estaríamos hablando de una catástrofe en toda regla. Pero han sido buenos.

El primer requisito que me he planteado al intentar sacar adelante semejante heroicidad ha sido el de no tener que venir el viernes a la hora bruja. Se supone que a los programadores les gusta programar, así que una tarea programada sería lo ideal digo yo.

Partiendo de que - de momento - no hay ningún tipo de repositorio en producción y los cambios son, básicamente, de 2 tipos en múltiples ficheros y bases de datos, se me han pasado varias posibles soluciones por la cabeza, de más remota a más factible (lo sé, soy un animal):

  • Un script PHP que se conecte por SSH (problema) al host que toque y haga los cambios en los PHP correspondientes, además de conectarse a los MYSQL y updatear varios registros.
  • Si tengo que conectar por SSH, casi mejor un script en python (Paramiko) que haga los cambios remotos en los archivos .php y en la base de datos.
  • Fabric es una gran herramienta para deploys que ejecutan tareas muy similares a las que me hacen falta, ¿por qué no hacerlo en un fabfile?.
  • A ver amigo, ¿por qué reinventar la rueda o perder el tiempo en curvas de aprendizaje?, un bash script es lo suyo.

Así que estaba bastante claro, además de sencillo, sería más divertido para un perfil de sysadmin como el mío hacer un script en bash. Con lo que, dándole un par de vueltas de tuerca más, querría que el mismo script me avisara por correo al empezar, al acabar y si se han hecho adecuadamente todos los cambios. Vayamos por partes.

Está claro que va a ser una tarea programada, así que voy a hacer 3 scripts (dos realmente), uno que compruebe los datos que hay que cambiar justo antes de que se haga el cambio, otro que ejecute todo lo necesario para cambiar, y un tercero que compruebe y reporte que todo está correcto:

  • informa_iva_antes.sh
  • cambia_iva.sh
  • informa_iva_despues.sh

Obviamente el informe es el mismo, con lo cual quedan dos scripts (informa_iva.sh y cambia_iva.sh). Y además voy a utilizar otros ficheros adicionales que ahora paso a describir:

  • send.py: Script en python + smtplib que envía un correo a través de Gmail (no quería usar el propio sendmail de la máquina, por si la cuenta que tengo asociada con el teléfono - Gmail, donde presumiblemente recibiré la notificación - clasifica como SPAM estos correos. Así que prefiero usar Gmail directamente). 
  • config.sh: El típico config donde se guardan variables comunes a los scripts que se van a ejecutar en cron (rutas, hosts, usuarios...).
  • log.txt: Log donde se guardará el informe a enviar por correo.

Ahora que ya tenemos la estructura montada, vamos a la chicha, si has llegado hasta aquí imagino que tendrás algo de curiosidad por ver el contenido de los scripts...

informa_iva.sh

#!/bin/bash

source config.sh

echo -e "## Host1 ##################" > log.txt
echo -e "" >> log.txt
echo -e "--- archivo1.php ---" >> log.txt
ssh ${HOST1} "egrep -rn \'IVA1\'\|\'IVA2\' ${RUTA1}archivo1.php" >> log.txt

echo -e "" >> log.txt
echo -e "--- mysql ---" >> log.txt
ssh ${HOST1} "echo \"SELECT * FROM ${MYSQL1_DB}.tax_rates WHERE tax_rates.tax_rates_id=2;\" | mysql -u${MYSQL1_USER} -p${MYSQL1_PASS} ${MYSQL1_DB}" >> log.txt

echo -e "## Host2 ##################" > log.txt
echo -e "" >> log.txt
echo -e "--- archivo2.php ---" >> log.txt
ssh ${HOST2} "egrep -rn \'IVA1\'\|\'IVA2\' ${RUTA2}archivo2.php" >> log.txt

echo -e "" >> log.txt
echo -e "--- mysql ---" >> log.txt
ssh ${HOST2} "echo \"SELECT * FROM ${MYSQL2_DB}.tax_rates WHERE tax_rates.tax_rates_id=2;\" | mysql -u${MYSQL2_USER} -p${MYSQL2_PASS} ${MYSQL2_DB}" >> log.txt

REPORTE=`cat log.txt`
send.py "micorreo@degmail.com" "IVA: Informe ANTES de cambiar" "${REPORTE}"

cambia_iva.sh

#!/bin/bash

source config.sh

echo -e "## Host1 ##################"
# archivo1.php
ssh ${HOST1} "sed -e \"s/'IVA', 18/'IVA', 21/g\" -i ${RUTA1}archivo1.php"
ssh ${HOST1} "sed -e \"s/'IVA2', 1.18/'IVA2', 1.21/g\" -i ${RUTA1}archivo1.php"

# mysql
ssh ${HOST1} "echo \"UPDATE ${MYSQL1_DB}.tax_rates SET tax_rate = '21.0000', tax_description = 'IVA 21%' WHERE tax_rates.tax_rates_id=2;\" | mysql -u${MYSQL1_USER} -p${MYSQL1_PASS} ${MYSQL1_DB}"

echo -e "## Host2 ##################"
# archivo2.php
ssh ${HOST2} "sed -e \"s/'IVA', 18/'IVA', 21/g\" -i ${RUTA2}archivo2.php"
ssh ${HOST2} "sed -e \"s/'IVA2', 1.18/'IVA2', 1.21/g\" -i ${RUTA2}archivo2.php"

# mysql
ssh ${HOST2} "echo \"UPDATE ${MYSQL2_DB}.tax_rates SET tax_rate = '21.0000', tax_description = 'IVA 21%' WHERE tax_rates.tax_rates_id=2;\" | mysql -u${MYSQL2_USER} -p${MYSQL2_PASS} ${MYSQL2_DB}"

send.py "micorreo@degmail.com" "IVA: Se ha cambiado el IVA" "Se ha cambiado el IVA, en breve se enviará un informe con los cambios"

Entiendo que el contenido de config.sh y send.py también es importante antes de poner las tareas en el cron:

config.sh

#!/bin/bash
## Host1 *************************#
HOST1="user@host1"
RUTA1="/var/www/web/"
MYSQL1_USER="user_host1"
MYSQL1_PASS="passhost1"
MYSQL1_DB="database_host1"

## Host2 *************************#
HOST2="user@host2"
RUTA2="/var/www/web2/"
MYSQL2_USER="user_host2"
MYSQL2_PASS="passhost2"
MYSQL2_DB="database_host2"

send.py

#!/usr/bin/python
import smtplib
import sys
from email.MIMEText import MIMEText

if len(sys.argv) != 4:
    print "Error, 3 argumentos requeridos, uso: ./send.py email@destinatario.com \"subject\" \"text\""
else:
    emisor = 'cuentadesdelaqueenviamos@gmail.com'
    receptor  = sys.argv[1]

    # Configuracion del mensaje
    mensaje = MIMEText(sys.argv[3])
    mensaje['From']=emisor
    mensaje['To']=receptor
    mensaje['Subject']=sys.argv[2]

    # Credentials (if needed)
    username = 'cuentadesdelaqueenviamos@gmail.com'
    password = 'passdelacuentadesdelaqueenviamos'

    # The actual mail send
    server = smtplib.SMTP('smtp.gmail.com:587')
    server.starttls()
    server.login(username,password)
    server.sendmail(emisor, receptor, mensaje.as_string())
    server.quit()

Pues ya tenemos los argumentos necesarios para que todo este batallón funcione, tan sólo falta ordenar el ataque cuando el enemigo esté más distraído, crontab -e:

50	23	31	8	*	/ruta/a/informa_iva.sh
59	23	31	8	*	/ruta/a/cambia_iva.sh
10	00	01	9	*	/ruta/a/informa_iva.sh

Obvio decir que otro requisito indispensable es crear una relación de confianza entre la máquina en la que se dejan configurados los scripts y los servidores remotos (ssh sin password). Seguro que me he montado una pirula bastante gorda con todo este tema, pero en las pruebas que he hecho creo haber conseguido lo que quería. De todas formas, ya os contaré mañana si ha servido de algo.

PD: Por cierto, si estás leyendo ésto y eres uno de mis compañeros, os he agregado a los informes así que mañana a eso de las 00:10 recibiréis un correo, (en menudo sitio aviso). Si no eres ninguno de mis compañeros, gracias por leer todo el tostón, espero tus sugerencias y comentarios ;).

PD2: Ojo con el cron y sus variables de entorno, nada de jugar con paths relativos, aseguraos de probar bien que todo funcione, aunque sea el día antes ;).

PD3: Comenta @minWi vía twitter que también existe la posibilidad de ejecutar un sólo cron con las tres llamadas concatenadas "/ruta/a/informa_iva.sh && /ruta/a/cambia_iva.sh && /ruta/a/informa_iva.sh". Buen apunte, ¡gracias!.

About the author

Óscar
has doubledaddy super powers, father of Hugo and Nico, husband of Marta, *nix user, Djangonaut and open source passionate.