Cómo manejar tareas complejas con fecha y hora en Bash

La mayoría de las veces, cuando ejecuta un script, se preocupa por sus resultados inmediatos. A veces, sin embargo, la tarea es compleja o debe completarse en algún momento, y hay muchas maneras de lograr este objetivo.

Al final de este artículo, debería poder hacer lo siguiente:

  • Formatee las fechas y utilícelas como condiciones para hacer que su programa espere antes de continuar con el siguiente paso.
  • Esperar un archivo sin saber cuanto tiempo con inotify instrumentos.
  • Ejecute su programa en un momento específico dependiendo de las condiciones usando atq.
  • Usar cron realizar una tarea más de una vez.
  • Realice muchas tareas en diferentes máquinas, algunas con relaciones complejas. Apache Airflow es una gran herramienta para este tipo de situaciones.

Puede encontrar el código para este artículo en mi repositorio GitHub.

Índice

Obtener la fecha en un script Bash

Suponga que desea una secuencia de comandos para descargar el conjunto de datos del estado de Connecticut cuando existen las siguientes condiciones:

  • Es de lunes a viernes (no se realizan actualizaciones de datos durante el fin de semana).
  • Son más de las 6 p. m. (hoy no hay actualizaciones).

ÑU /usr/bin/date admite banderas de formato especial con el signo +. Para ver la lista completa, simplemente escriba:

# /usr/bin/date --help

Vuelve a tu escenario. Puede obtener el día de la semana y la hora del día y hacer algunas comparaciones con un simple script:

#!/bin/bash
# Simple script that shows how to work with dates and times
# Jose Vicente Nunez Zuleta
#

test -x /usr/bin/date || exit 100

function is_week_day {
  local -i day_of_week
  day_of_week=$(/usr/bin/date +%u)|| exit 100
  # 1 = Monday .. 5 = Friday
  test "$day_of_week" -ge 1 -a "$day_of_week" -le 5 && return 0 || return 1
}

function too_early {
    local -i hour
    hour=$(/usr/bin/date +%H)|| exit 100
    test "$hour" -gt 18 && return 0|| return 1 
}

# No updates during the weekend, so don't bother (not an error)
is_week_day || exit 0

# Don't bother to check before 6:00 PM
too_early || exit 0

report_file="$HOME/covid19-vaccinations-town-age-grp.csv"
# COVID-19 Vaccinations by Town and Age Group
/usr/bin/curl 
    --silent 
    --location 
    --fail 
    --output "$report_file" 
    --url 'https://data.ct.gov/api/views/gngw-ukpw/rows.csv?accessType=DOWNLOAD'

echo "Downloaded: $report_file"

Si las condiciones son verdaderas, la salida se verá así:

./WorkingWithDateAndTime.sh 
Downloaded: /home/josevnz/covid19-vaccinations-town-age-grp.csv

Espere un archivo usando las herramientas de inotify

Aquí hay otro tipo de problema: espera un archivo llamado $HOME/lshw.json llegar. Una vez hecho esto, desea comenzar a procesarlo. Escribí este guión (versión 1) para manejar esta situación:

#!/bin/bash
# Wait for a file to arrive and once is there process it
# Author: Jose Vicente Nunez Zuleta
test -x /usr/bin/jq || exit 100
LSHW_FILE="$HOME/lshw.json"
# Enable the debug just to show what is going on...
trap "set +x" QUIT EXIT
set -x
while [ ! -f "$LSHW_FILE" ]; do
    sleep 30
done
/usr/bin/jq ".|.capabilities" "$LSHW_FILE"|| exit 100

Un proceso mágico genera el archivo mientras espera:

# sudo /usr/sbin/lshw -json > $HOME/lshw.json

Y esperas a que llegue el archivo:

 ./WaitForFile.sh 
+ '[' '!' -f /home/josevnz/lshw.json ']'
+ sleep 30
+ '[' '!' -f /home/josevnz/lshw.json ']'
+ /usr/bin/jq '.|.capabilities' /home/josevnz/lshw.json
{
  "smbios-3.2.1": "SMBIOS version 3.2.1",
  "dmi-3.2.1": "DMI version 3.2.1",
  "smp": "Symmetric Multi-Processing",
  "vsyscall32": "32-bit processes"
}
+ set +x

Hay algunos problemas con este enfoque:

  • Puedes esperar demasiado. Si el archivo llega un segundo después de que el proceso haya comenzado a dormir, espere 29 segundos.
  • Si el sistema duerme muy poco, desperdicia ciclos de CPU.
  • ¿Qué pasa si el archivo nunca llega? Puedes usar el timeout herramienta o lógica más compleja para manejar este escenario.

Otra alternativa es utilizar el API de notificación con inotify-herramientas y hacerlo mejor, versión 2 del guión usando esperar:

#!/bin/bash
# Wait for a file to arrive and once is there process it
# Author: Jose Vicente Nunez Zuleta
test -x /usr/bin/jq || exit 100
test -x /usr/bin/inotifywait|| exit 100
test -x /usr/bin/dirname|| exit 100
LSHW_FILE="$HOME/lshw.json"
while [ ! -f "$LSHW_FILE" ]; do
    test "$(/usr/bin/inotifywait --timeout 28800 --quiet --syslog --event close_write "$(/usr/bin/dirname "$LSHW_FILE")" --format '%w%f')" == "$LSHW_FILE" && break
done
/usr/bin/jq ".|.capabilities" "$LSHW_FILE"|| exit 100

Entonces, si aparece un archivo aleatorio en $HOME, esto no interrumpirá el ciclo de espera, pero si el archivo que está buscando aparece y está completamente escrito, saldrá del bucle:

#/usr/bin/touch $HOME/randomfilenobodycares.txt

#sudo /usr/sbin/lshw -json > $HOME/lshw.json

Tenga en cuenta el tiempo de espera en segundos (28.800 = 8 horas). inotifywait se cerrará después de eso si el archivo no está allí.

Hazlo una vez a mano, hazlo dos veces con cron

Volvamos al script anterior que descarga los datos de COVID-19. Si desea automatizarlo, puede hacerlo parte de un trabajo cron pero sin la lógica de la hora y el día de la semana.

Como recordatorio, este es el comando que desea ejecutar:

report_file="$HOME/covid19-vaccinations-town-age-grp.csv"
# COVID-19 Vaccinations by Town and Age Group
/usr/bin/curl 
    --silent 
    --location 
    --fail 
    --output "$report_file" 
    --url 'https://data.ct.gov/api/views/gngw-ukpw/rows.csv?accessType=DOWNLOAD'

Para ejecutarlo todos los días de la semana a las 6:00 p. m. y registrar la salida en un registro, use:

# minute (0-59),
#    hour (0-23),
#       day of the month (1-31),
#          month of the year (1-12),
#             day of the week (0-6, 0=Sunday),
#                command
0 18 * * 1-5 /usr/bin/curl --silent --location --fail --output "$HOME/covid19-vaccinations-town-age-grp.csv"  --url 'https://data.ct.gov/api/views/gngw-ukpw/rows.csv?accessType=DOWNLOAD' > $HOME/logs/covid19-vaccinations-town-age-grp.log

Cron también da la posibilidad de hacer cosas que normalmente solo puedes lograr con otras herramientas como unidades del sistema. Por ejemplo, suponga que desea descargar los detalles de vacunación tan pronto como se reinicie un servidor Linux (esto funcionó para mí en Fedora 29):

@reboot /usr/bin/curl --silent --location --fail --output "$HOME/covid19-vaccinations-town-age-grp.csv"  --url 'https://data.ct.gov/api/views/gngw-ukpw/rows.csv?accessType=DOWNLOAD' > $HOME/logs/covid19-vaccinations-town-age-grp.log

Entonces, ¿cómo editas y mantienes cron ¿obras? crontab -e proporciona un editor interactivo con algunas comprobaciones de sintaxis. prefiero usar el Módulo cron de Ansible para automatizar mi cron edición. Además, garantiza que pueda mantener mis trabajos en Git para una revisión e implementación adecuadas.

 

Finalmente, el cron la sintaxis es muy poderosa y esto puede conducir a una complejidad inesperada. Puedes usar herramientas como generador crontab para obtener la sintaxis correcta sin tener que pensar demasiado en el significado de cada campo.

Ahora, ¿qué sucede si necesita realizar algo pero no de inmediato? Generar un crontab para esto puede ser demasiado complicado, pero hay otras cosas que puede hacer.

Usar atq

La herramienta Unix atq es muy similar a cron, pero su belleza es que admite una sintaxis muy flexible y rica para programar tareas pendientes.

Imagine el siguiente ejemplo: tiene que ejecutar una ejecución en 50 minutos y desea permitir que su servidor descargue un archivo, y determina que está bien descargar el archivo en 60 minutos:

# cat aqt_job.txt
/usr/bin/curl --silent --location --fail --output "$HOME/covid19-vaccinations-town-age-grp.csv"  --url 'https://data.ct.gov/api/views/gngw-ukpw/rows.csv?accessType=DOWNLOAD'
at 'now + 1 hour' -f aqt_job.txt
warning: commands will be executed using /bin/sh
job 10 at Sat Aug 14 06:59:00 2021

Puede confirmar que su tarea se programó correctamente para ejecutarse (tenga en cuenta que el ID de la tarea es 10):

# atq
10    Sat Aug 14 06:59:00 2021 a josevnz

Y si cambias de opinión, puedes eliminarlo:

# atrm 10
# atq

Volver al guión original. Reescríbelo (versión 3) usar at en lugar de solo date para programar la descarga del archivo de datos:

#!/bin/bash
# Simple script that shows how to work with dates and times, and Unix 'at'

# Jose Vicente Nunez Zuleta
#
test -x /usr/bin/date || exit 100
test -x /usr/bin/at || exit 100

report_file="$HOME/covid19-vaccinations-town-age-grp.csv"
export report_file

function create_at_job_file {
    /usr/bin/cat<<AT_FILE>"$1"
    # COVID-19 Vaccinations by Town and Age Group
    /usr/bin/curl 
        --silent 
        --location 
        --fail 
        --output "$report_file" 
        --url 'https://data.ct.gov/api/views/gngw-ukpw/rows.csv?accessType=DOWNLOAD'
AT_FILE
}

function is_week_day {
  local -i day_of_week
  day_of_week=$(/usr/bin/date +%u)|| exit 100
  # 1 = Monday .. 5 = Friday
  test "$day_of_week" -ge 1 -a "$day_of_week" -le 5 && return 0 || return 1
}

function already_there {
    # My job is easy to spot as it has a unique footprint...
    for job_id in $(/usr/bin/atq| /usr/bin/cut -f1 -d' '| /usr/bin/tr -d 'a-zA-Z'); do
      if [ "$(/usr/bin/at -c "$job_id"| /usr/bin/grep -c 'COVID-19 Vaccinations by Town and Age Group')" -eq 1 ]; then
        echo "Hmmm, looks like job $job_id is already there. Not scheduling a new one. To cancel: '/usr/bin/atrm $job_id'"
        return 1
      fi
    done
    return 0
}

# No updates during the weekend, so don't bother (not an error)
is_week_day || exit 0

# Did we schedule this before?
already_there|| exit 100

ATQ_FILE=$(/usr/bin/mktemp)|| exit 100
export ATQ_FILE
trap '/bin/rm -f $ATQ_FILE' INT EXIT QUIT
echo "$ATQ_FILE"
create_at_job_file "$ATQ_FILE"|| exit 100
/usr/bin/at '6:00 PM' -f "$ATQ_FILE"

Entonces atq es un práctico planificador de "dispara y olvida" que también funciona con la carga de la máquina. Eres más que bienvenido a ir más allá y descubrir más detalles en en.

Administre múltiples dependencias de tareas que se ejecutan en múltiples hosts

Este último ejemplo tiene menos que ver con cron y Bash y más con la creación de una canalización compleja de tareas que pueden ejecutarse en diferentes máquinas y tener interdependencias.

Cron en particular, no es muy bueno organizando varias tareas que dependen unas de otras. Afortunadamente, herramientas sofisticadas como flujo de aire apache se puede utilizar para crear canalizaciones y flujos de trabajo complejos.

Aquí hay otro ejemplo: tengo varios repositorios de Git en mis máquinas en mi red doméstica. Quiero asegurarme de enviar automáticamente los cambios a algunos de estos repositorios, presionando algunos de estos cambios de forma remota si es necesario.

En Airflow, las tareas se definen mediante Python. Esto es excelente porque puede agregar sus propios módulos, la sintaxis es familiar y también puede mantener las definiciones de tareas bajo control de versión (como Git).

Entonces, ¿cómo es este nuevo trabajo? aquí está Gráfico Acíclico Dirigido (muy documentado en formato de marcado):

# pylint: disable=pointless-statement,line-too-long
"""
# Make git backups on different hosts in Nunez family servers
## Replacing the following cron jobs on dmaf5
----------------------------------------------------------------------
[email protected]
*/5 * * * * cd $HOME/Documents && /usr/bin/git add -A && /usr/bin/git commit -m "Automatic check-in" >/dev/null 2>&1
*/30 * * * * cd $HOME/Documents && /usr/bin/git push --mirror > $HOME/logs/codecommit-push.log 2>&1
----------------------------------------------------------------------
"""
from datetime import timedelta
from pathlib import Path
from os import path
from textwrap import dedent
from airflow import DAG
from airflow.providers.ssh.operators.ssh import SSHOperator
from airflow.operators.bash import BashOperator
from airflow.utils.dates import days_ago

default_args = {
    'owner': 'josevnz',
    'depends_on_past': False,
    'email': ['[email protected]'],
    'email_on_failure': True,
    'email_on_retry': False,
    'retries': 5,
    'retry_delay': timedelta(minutes=30),
    'queue': 'git_queue'
}

TUTORIAL_PATH = f'{path.join(Path.home(), "DatesAndComplexInBash")}'
DOCUMENTS_PATH = f'{path.join(Path.home(), "Documents")}'

with DAG(
    'git_tasks',
    default_args=default_args,
    description='Git checking/push/pull across Nunez family servers, during week days 6:00-19:00',
    schedule_interval="*/30 6-19 * * 1-5",
    start_date=days_ago(2),
    tags=['backup', 'git'],
    ) as git_backup_dag:
    git_backup_dag.doc_md = __doc__

    git_commit_documents = SSHOperator(
        task_id='git_commit_documents',
        depends_on_past=False,
        ssh_conn_id="ssh_josevnz_dmaf5",
        params={'documents': DOCUMENTS_PATH},
        command=dedent(
        """
        cd {{params.documents}} && 
        /usr/bin/git add --ignore-errors --all 
        &&
        /usr/bin/git commit --quiet --message 'Automatic Document check-in @ dmaf5 {{ task }}'
        """
        )
    )
    git_commit_documents.doc_md = dedent(
    """
    #### Jose git commit PRIVATE documents on dmaf5 machine
    * Add and commit with a default message.
    * Templated using Jinja2 and f-strings
    """
    )

    git_push_documents = SSHOperator(
        task_id='git_push_documents',
        depends_on_past=False,
        ssh_conn_id="ssh_josevnz_dmaf5",
        params={'documents': DOCUMENTS_PATH},
        command=dedent(
        """
        cd {{params.documents}} && 
        /usr/bin/git push
        """
        )
    )
    git_push_documents.doc_md = dedent(
    """
    #### Jose git push PRIVATE documents from dmaf5 machine into a private remote repository
    """
    )

    remote_repo_git_clone = BashOperator(
        task_id='remote_repo_git_clone',
        depends_on_past=False,
        params={'tutorial': TUTORIAL_PATH},
        bash_command=dedent(
        """
        cd {{params.tutorial}} 
        && 
        /usr/bin/git pull --quiet
        """
        )
    )
    remote_repo_git_clone.doc_md = dedent(
    """
    You need to clone the repository first:
    git clone --verbose [email protected]:josevnz/DatesAndComplexInBash.git
    Uses BashOperator as it runs on the same machine where Airflow runs.
    """
    )

    # Task relantionships
    # Git documents is a dependency for push documents
    git_commit_documents >> git_push_documents
    # No dependency except the day of the week and time
    remote_repo_git_clone

Las relaciones de estas tareas se pueden ver en la interfaz gráfica. En este caso, se muestra el modo de gráficos.

(José Vicente Núñez, CC BY-SA 4.0)

Hay mucho más para explorar con Airflow. Eres más que bienvenido a mira a tu alrededor para más detalles y para aprender más.

 

Concluir

Era mucho terreno para cubrir en un artículo. Administrar fechas y horas es complejo, pero existen muchas herramientas para ayudarlo con sus tareas de codificación. Estas son las cosas que aprendiste a hacer:

  • Opciones de formato en /usr/bin/date que se puede utilizar para modificar el comportamiento de los scripts.
  • Usar inotify-tools para escuchar de manera eficiente los eventos del sistema de archivos, como esperar a que se copie un archivo.
  • Automatice las tareas periódicas y repetitivas con cron y usos at cuando se necesita un poco más de flexibilidad.
  • Considere marcos más avanzados como Airflow cuando cron se queda corto.

Algunos de los ejemplos de script anteriores son complejos. Como de costumbre, verifique estáticamente sus scripts con Pylint o Bash SpellCheck para evitar dolores de cabeza.

Artículos de interés

Subir