Introducción

El siguiente proyecto trata de dar respuesta al problema de dejar trabajando el ordenador, recogiendo datos o haciendo una simulación, para volver a los varios días y encontrar que se ha colgado.

El tutorial recoge la primera solución que se nos ha ocurrido en inmediatamente la hemos subido. Falta aún mucho trabajo, tanto con los programas como con el tutorial, por lo que es de esperar alguna actualización.

TKINTER BÁSICO

Autor: Alberto Peiró

Labels: Python 3, sockets, threads, tkinter, systemd

WATCHDOG

Introducción

Supongamos que nos encontramos en la situación en que queremos saber si un programa en Python ha dejado de funcionar, ya sea porque se ha colgado el programa o el ordenador. Si tenemos la suerte de estar en una red local podemos implementar una solución en Python en que el otro ordenador de la misma red nos manda un aviso por correo electrónico cuando detecte que la aplicación haya dejado de funcionar o el ordenador se ha colgado.

La idea es muy sencilla (figura 1). Desde la aplicación que queremos vigilar se mandan mensajes periódicamente al watchdog que se encuentra en otro ordenaador. En el momento que dejan de llegar estos mensajes el watchdog envía un correo de alerta.

Esquema
Figura 1: Esquema.

FASE 1. Envío y recepción de mensajes

Vamos a ir construyendo el programa de una forma incremental partiendo de una forma simple para ir mejorandola poco a poco hasta la versión final. Primero vamos a crear dos programas en el que uno envía un pequeño mensaje al otro. Todo ello dentro de la misma máquina.

Utilizamos comunicación por sockets, exactamente igual que la que aparece como ejemplo en la documentación de sockets en Python.org. En nuestro caso construiremos dos programas, el programa watchdog.py que va a estar la espera de una conexión, por tanto está haciendo una función de servidor, y radiofaro.py que es el cliente que se conecta, envía un mensaje y termina la conexión.

Nota

Como construimos bucles infinitos deberemos de finalizar los programas con ctrl-c.

# watchdog.py
# socket server

import socket

socketHost = ''    # interfaces de la maquina en la que corre
socketPort = 50091 # puerto de escucha

sockobj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # objeto TCP
sockobj.bind((socketHost, socketPort)) # liga el objeto al puerto y al host
sockobj.listen(5)
print ('watchdog socket creado')

while True:
    connection, address = sockobj.accept() # espera a que un cliente se conecte
    while True:
        data = connection.recv(1024)
        if not data: break
        print(data)
        connection.sendall(data)
    connection.close()
# radiofaro.py
# socket client

import socket
import time

serverHost = 'localhost' # a quien conectarnos
serverPort = 50091

i = 9
while i > 0:
   sockobj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # objeto TCP
   sockobj.connect((serverHost, serverPort)) # abre una conexion

   sockobj.send(str(i).encode('ascii'))
   data = sockobj.recv(1024)
   sockobj.close()
   print(data)
   i -= 1
   time.sleep(1)

FASE 2: El temporizador.

La idea es la siguiente:

Vamos a usar la clase Timer del módulo threading. Cuando llegue un 'on' al vigilante watchdog crearemos un objeto Timer que lanzamos inmediatamente con objeto_timer.start() con lo que inicia en paralelo una cuenta atrás que si no es detenido antes con objeto_timer.cancel() ejecutará la función que le proporcionamos como argumento, que será la de mandar una alerta.

# watchdog.py
# socket server

import socket
import threading

socketHost = ''    # interfaces de la maquina en la que corre
socketPort = 50091 # puerto de escucha

t_alert = 5 # segundos
def alert():
    print('Alert!!!')

sockobj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # objeto TCP
sockobj.bind((socketHost, socketPort)) # liga el objeto al puerto y al host
sockobj.listen(5)
print ('watchdog socket creado')

while True:
    connection, address = sockobj.accept() # espera a que un cliente se conecte
    while True:
        data = connection.recv(1024)
        if not data: break
        print(data)
        if data == b'on': 
            try: # para no tener que revisar si existe threadobj
                threadobj.cancel()
                print('canceling')
            except NameError: # ocurre si threadjob no ha sido inicializado
                pass
            threadobj = threading.Timer(t_alert, alert)
            threadobj.start()
        if data == b'off':
            threadobj.cancel()
        connection.sendall(data)

    connection.close()

Para probar si funciona, al menos el 'on', cambiamos la siguiente línea que envía el mensaje en radiofaro.py.

   sockobj.send(b'on')

Debería aparecer la alerta solo una vez al final de todos los 'on' que le han llegado.

FASE 3: Envíar un correo electrónico de aviso.

Por tocar todos los temas vamos a construir un módulo con el nombre alert_email. Para mandar el mensaje usaremos la biblioteca smtlib de Python. No tiene más complicación que crear el mensaje con sus campos de origen, destino, asunto y cuerpo de texto. Proporcionar el usuario y la clave. Y usar las funciones de la biblioteca para mandar el correo.

Podéis testear el programa corriendo directamente el módulo que debería enviar un correo de alerta a la cuenta que especifiquéis.

# Modulo alert_email

import smtplib

def send_alert_email(
    from_addr = 'direccion_origen@gmail.com',
    to_addr = 'direccion_destino@gmail.com',
    subject = 'Alerta',
    texto = 'Se ha producido un fallo'):
    ''' Envia un correo de alerta desde una cuenta de gmail'''

    #  Une los elementos de la lista separandolos con
    # un carriage return y un line feed
    msg = '\r\n'.join([
        'From: ' + from_addr,
        'To: ' + to_addr,
        'Subject: Alert',
        '',
         texto])


    user_name = 'user_name@gmail.com' # direccion de nuestra cuenta
    password = '12345678'

    server = smtplib.SMTP('smtp.gmail.com:587') # crea un objeto
    server.starttls()                           # seguridad de la conexion
    server.login(user_name, password)           # login
    server.sendmail(from_addr, to_addr, msg)
    server.quit()

if __name__ == '__main__':
    send_alert_email()

FASE 4: Modelo para usar en la aplicación a vigilar.

Ahora vamos a crear un aplicación gráfica que simulará ser la aplicación que queremos vigilar. Siguiendo es esquema inicial esta lanzará un hilo, en concreto un Timer, que tras el tiempo estipulado envía un mensaje al watchdog a la vez que programa el lanzamiento de un nuevo Timer para mantener un tren de pulsos.

También se ha añadido el tratamiento de algunas exepciones como la de socket.timeout para no quedarnos atascados esperando una respuesta y poder continuar el programa.

Por hacerlo más bonito, y seguramente útil, creamos un widget para iniciar y parar el radio faro que puede utilizarse individualmente para vigilar si el ordenador se cuelga.

# radiofaro_gui.py
#

import socket
import time
import threading
import queue
from tkinter import *

wdHost='localhost' # ip del host donde el watchdog se encuentra
wdPort=50091
intervalo = 1 # segundos

mutex = threading.Lock()
dataqueue = queue.Queue()

def send_to_watchdog(serverHost, serverPort, msg):
    ''' Funcion que manda un mensaje al watchdog'''
    try:
        sockobj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sockobj.settimeout(5) # tiempo de espera max.
        sockobj.connect((serverHost, serverPort))
        sockobj.send(msg)
        data = sockobj.recv(1024)
        dataqueue.put(data)
    except ConnectionRefusedError: # no conecta al watchdog
        dataqueue.put('error conex')
    except ConnectionResetError:
        dataqueue.put('error conex')
    except socket.timeout: # no hay respuesta del watchdog.
        dataqueue.put('error conex')
    finally:
        sockobj.close()

def temporizador(intervalo=intervalo, msg=b'on'):
    ''' Programa una nueva llamada a si mismo con un Timer'''
    mutex.acquire()
    global actual_thread
    send_to_watchdog(wdHost, wdPort, msg)
    thread = threading.Timer(intervalo, temporizador ) # lanza otro temporizador
    thread.start()
    actual_thread = thread
    mutex.release()

def stop_temporizador():
    ''' Finaliza el ultimo Timer del temporizador'''
    mutex.acquire()
    try:
        actual_thread.cancel()
    except NameError: # no se ha lanzado ningun temporizador
        pass
    send_to_watchdog(wdHost, wdPort, b'off')
    mutex.release()

# --------------- funciones  ---------------

def launch_timer():
    try: # cuando construyamos metodos guardaremos el estado
        if not actual_thread.isAlive():
            threading.Timer(intervalo, temporizador).start()
    except NameError:
        threading.Timer(intervalo, temporizador).start()

def launch_stop_timer():
    threading.Thread(target = stop_temporizador).start()


# --------------- funciones GUI ---------------

def make_watchdog_widget(parent=None):
        global label_watchdog
        botonera = Frame(parent)
        Button(botonera, text='ON', command=launch_timer).pack(side=LEFT)
        Button(botonera, text='OFF', command=launch_stop_timer).pack(side=LEFT)
        botonera.pack()
        label_watchdog = Label(parent, text='watchdog')
        label_watchdog.pack()

def repeater(t_refresco=1000):
    if dataqueue.empty(): label_watchdog.config(text='----')
    while not dataqueue.empty():
        try:
            data = dataqueue.get(block=False)
        except queue.Empty:
            pass
        else:
            label_watchdog.config(text=data)
    winroot.after(t_refresco, repeater)

# ------------ programa principal ---------------

winroot = Tk()
make_watchdog_widget(winroot)
winroot.after(1000, repeater)
mainloop()

# ----- finalizar los hilos de ejecucion ------
threading.Thread(target = stop_temporizador).start()

FASE 5: Desplegamos el sistema en la red local.

Hasta ahora hemos hecho todo en un mismo ordenador. Llegó el momento de instalar el watchdog en el otro ordenador del la red local. Supongamos que es un Raspberry Pi al que nos conectamos via ssh.

Antes de pasar los programas a la raspberry vamos a escribir un fichero de configuración para lanzar un servicio con systemd. Brevemente, lo que va a hacer es lanzar el programa watchdog.py cuando el sistema se inicie pero despúes de levantar la red. Además con el parámetro Restart=on-abort si el demonio (watchdog.py) cae este se reinicia.

[Unit]
  Description=Python server TCP/IP watchdog
  After=syslog.target network.target
[Service]
   ExecStart=/home/alberto/.py/watchdog.py
   Restart=on-abort
[Install]
WantedBy=multi-user.target)
fichero: py_watchdog.service

Los pasos a realizar:

Supongamos que la dirección de la raspberry es 192.168.0.20, que hay un usuario llamado alberto y una carpeta /home/alberto/.py. Sin olvidar que debemos tener Python 3 instalado.

Copiamos los programas Python watchdog.py, email_alert y py_watchdog.service en una carpeta de la raspberry. Tomad nota de donde se encuentran para actualizar el valor ExecStart en el fichero de configuración en py_watchdog.service.

$ scp watchdog.py email_alert.py py_watchdog.service alberto@192.168.0.20:/home/alberto/.py/
alberto@192.168.0.20's password: 
email_alert.py                                100%  844     0.8KB/s   00:00    
watchdog.py                                   100% 1259     1.2KB/s   00:00    
py_watchdog.service                           100%  197     0.2KB/s   00:00 

Nos conectamos via ssh y damos permiso de ejecución a watchdog.py

$ ssh usuario@192.168.0.20
alberto@192.168.0.20's password: 
$ cd .py/
$ chmod 744 watchdog.py

Nota

Al hacer que archivo Python sea ejecutable hay que decirle que intérprete debe usar. Añadimos la siguiente línea:

#!/usr/bin/env python3
# watchdog.py
# ........
..........

No olvidéis comprobar que ExecStart en py_watchdog.service apunte al fichero watchdog.py.

Como usuario root o con sudo copiamos el archivo de configuración py_watchdog.service en la carpeta /etc/systemd/system/

$ su
Password:
# cp py_watchdog.service /etc/systemd/system/

Podemos probar a lanzar el servicio para esta sesión. También con la instrucción sudo o como usuario root.

# systemctl start py_watchdog.service
# systemctl status py_watchdog.service

Si todo va bien habilitamos que la unidad se inicie en el arranque. Si hay que modificar el fichero de configuración hay que usar systemctl reload py_watchdog.service para que tengan efectos los cambios.

# systemctl enable py_watchdog.service

Nota

Está planeado un tutorial de systemd. Por ahora cuatro instrucciones importantes:

Cambiamos la dirección ip en radio_faro.py por la dirección del Raspberry Pi y a funcionar.

wdHost='192.168.0.20' # ip del host donde el watchdog se encuentra

FASE 6: Futuro, hacer más amigable el funcionamiento y utilizar programación orientada a objetos.

En todos los programas solo hemos usado lo que sería programación con procedimientos. El siguiente paso sería cambiar el modelo de programación a uno orientado a objetos que siempre sería más reutilizabe. Ese será el siguiente paso que pospondremos a una revisión de este tutorial.

De momento os dejo los archivos para que no tengáis que teclear.

Radiofaro Host 1
Watchdog Host 2