"Después del juego es antes del juego"
Sepp Herberger

miércoles, 19 de junio de 2019

Captura remota de pantalla en las sesiones de los alumnos.

Una profesora me pidió este curso escribir un programa para realizar capturas de pantalla de los escritorios de los alumnos de forma periódica. El objetivo era tener en su carpeta personal una colección con las capturas de pantalla realizadas cada X minutos de los PC de todos los alumnos del aula, para controlar a posteriori el trabajo que habían estado haciendo durante la clase.

Capturar la pantalla desde un script no es complicado, aunque si quieres hacerlo de forma remota y traerlo al ordenador del profesor hay que afinar un poco la cosa.

Como paso imprescindible e inicial es obligatorio que entre el ordenador del profesor y los de los alumnos exista una relación de confianza ssh que nos permita conectar desde la cuenta de root del PC del profesor sin necesidad de introducir contraseña. Tras generar y copiar las claves, en /root/.ssh/id_rsa y /root/.ssh/id_rsa.pub del profesor tendremos las claves pública y privada, mientras que en /root/.ssh/... de los alumnos tendremos la clave privada.

Una vez resuelto el problema de la comunicación entre profesor y alumnos, tenemos que pensar en los posibles escenarios que nos encontramos en nuestras aulas:

  • Aula aislada: profesor y alumnos están una red privada con rango de direcciones propio, mediante una VLAN creada en el switch. En mi caso con los alumnos tienen direccionamamiento 192.168.0.200-253, sin importar si se usan portátiles o equipos de sobremesa.
  • Aula integrada: profesor y alumnos no están aislados, si no que usan direcciones públicas de la red del centro. Hay que encontrar una manera de que el PC del profesor sepa a priori cuales son las IP de los PC de alumnos que dependen de él. Esta manera consiste en definir en el fichero /etc/escuela2.0 (el que usamos para configuraciones de nuestras máquinas) una variable llamada IES_IP_ALUMNOS, en la cual indicamos en rango de IP donde están los PC de los alumnos, por ejemplo: 172.23.241.70-85. El valor tomado por defecto es 192.168.0.200-253
  • Thinclients: en este caso también es un aula aislada, con direcciones del rango 192.168.0.200 en adelante, pero con una particularidad consistente en que con los thinclients las sesiones de los alumnos se ejecutan en el PC del profesor, en una sesión privada en la que la captura de pantalla se hace de una forma diferente. Para saber si el aula tiene thinclients usaremos la variable IES_ISLTSP de /etc/escuela2.0

Aparte de eso, la aplicación debe tener un interface gráfico sencillo para el usuario. Por esta razón he optado por hacer la aplicación de captura usando python e interface gráfico Qt con PyQt5. Para ello uso Qt Designer para diseñar el interface gráfico y Geany para escribir el programa.

Una vez implementado, los ficheros necesarios son distribuidos por la siguiente tarea puppet:
# cat xubuntu18_photomaton/manifests/init.pp
import "/etc/puppet/defines/*.pp"
class xubuntu18_photomaton {

  file {"/usr/local/bin/photomaton.py":
        owner=>root, group=>root, mode=>755,
        source=>"puppet:///modules/xubuntu18_photomaton/photomaton.py",
  }


  file {"/usr/local/bin/photomaton_ui.py":
        owner=>root, group=>root, mode=>755,
        source=>"puppet:///modules/xubuntu18_photomaton/photomaton_ui.py",
  }

  file {"/usr/local/bin/photomaton.jpg":        #/usr/share/icons/photomaton.jpg
        owner=>root, group=>root, mode=>644,
        source=>"puppet:///modules/xubuntu18_photomaton/photomaton.jpg",
  }


  file {"/usr/share/applications/Photomaton.desktop" :
          owner => root , group =>root, mode => 644 ,
          source => "puppet:///modules/xubuntu18_photomaton/Photomaton.desktop",
          notify => Exec['update-desktop'],
  }
 
  exec { "update-desktop":
          path => "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
          command => "/usr/bin/update-desktop-database",
          refreshonly => true,
          require => [File["/usr/local/bin/photomaton.jpg"],File["/usr/share/applications/Photomaton.desktop"]],
  }

  line { sudoers:
            file => "/etc/sudoers",
            line => "%teachers ALL = (ALL) NOPASSWD: /usr/local/bin/photomaton.py",
            ensure => present
  }

  package { 'python3-paramiko': ensure => 'installed' }
  package { 'python3-nmap': ensure => 'installed' }
  package { 'python3-configobj':   ensure => 'installed' }  
  package { 'python3-psutil': ensure => 'installed' }
  package { 'python3-pyqt5': ensure => 'installed' }
}
Como vemos la tarea instala varios paquetes necesarios, copia los ficheros y añade una linea a sudoers para permitir a los miembros del grupo teachers ejecutar el programa con permisos de root (necesario para acceder a los pc de los alumnos).

Vamos primero el código del programa principal:
# cat xubuntu18_photomaton/files/photomaton.py
#!/usr/bin/python3
from photomaton_ui import *
import sys
import socket
import subprocess
import os, errno
from datetime import datetime
import paramiko
Para acabar un pequeño detalle: en las au
import time
from configobj import ConfigObj
import psutil
import nmap

class MainWindow(QtWidgets.QMainWindow, Ui_Photomaton):
   def __init__(self, *args, **kwargs):
       QtWidgets.QMainWindow.__init__(self, *args, **kwargs)
       self.setupUi(self)
       #Conectamos los botones con los eventos
       self.iniciarCaptura.clicked.connect(self.capturar)
       self.visualizarCapturas.clicked.connect(self.visualizar)
       self.finalizar.clicked.connect(self.finalizarApp)
       #Definimos el entorno, creando el directorio destino y leyendo escuela2.0
       hostname = socket.gethostname()
       #Como se llama usando sudo, el usuario original está en SUDO_USER

       try:
           self.user=os.environ["SUDO_USER"]       
       except Exception as e:
           print("No eres usuario sudo. Abortando")
           #self.user="root"
           sys.exit(0)  

       home = os.path.expanduser("~"+self.user) #Directorio $HOME del usuario
       self.pathcapturas=home+"/capturas/"+hostname
       config = ConfigObj("/etc/escuela2.0")
       self.uso=config.get('USO') #infolab/siatic/...
       self.isltsp=config.get('IES_ISLTSP') #true si tiene thinclients
       self.ip_alumnos=config.get('IES_IP_ALUMNOS') # Rango de ip de los alumnos, para aulas de workstations.    
       self.capturando=False         
       if self.ip_alumnos is None or self.ip_alumnos == "" :
          self.ip_alumnos="192.168.0.200-253" # Rango por defecto en aulas con vlan
       try:
          comando="su "+self.user+" -c 'mkdir -p "+self.pathcapturas+"'"
          os.system(comando)
       except FileExistsError:
          # directory already exists
          pass     
         
   def finalizarApp(self):
       if self.capturando == False:
          sys.exit(0)
       else:
          self.capturando=False
    
   
   def visualizar(self):                            
       comando="su "+self.user+" -c 'nohup thunar "+self.pathcapturas+" &'" 
       os.system(comando)   
   
   def capturar_ip(self, ip):          
       
       s = socket.socket()       #Quizá esto se podria quiar, el ssh_client.connnect ya tiene timeout
       s.settimeout(30)
       address = ip
       port = 22         
       try:
           #Intenta conectar por ssh a ver si la IP acepta peticiones ssh
           s.connect((address, port))  #Si falla se va a la excepcion
           s.close()    
           #Conectamos por ssh y ejecutamos el comando de captura.
           ssh_client = paramiko.SSHClient()
           ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
           ssh_client.connect(ip, 22, 'root', key_filename="/root/.ssh/id_rsa" , timeout=30) 
           fecha=datetime.now().strftime("%Y-%m-%d-%H:%M:%S")
           sleeptime = 0.001
           comando='rm -f /tmp/captura.jpg; pc=$(hostname); user=$(who | grep "(:0)" | head -1 | cut -f1 -d" "); test -n "$user" && (su $user -c "DISPLAY=:0 import -window root /tmp/captura.jpg"; chmod 777 /tmp/captura.jpg; echo -n $pc/$user)'
           #print("Ejecutando "+comando)
           ssh_transp = ssh_client.get_transport()
           chan = ssh_transp.open_session()
           
           chan.setblocking(0)
           outdata=""
           
           chan.exec_command(comando)
           while True:  # Hay que esperar a que acabe el comando
               while chan.recv_ready():
                  outdata += chan.recv(1000).decode('ascii')
               if chan.exit_status_ready():  # If completed
                  break
               time.sleep(sleeptime)
           
           #Como salida se devuelve "pc/usuario"
           signatura=outdata           
           #Aprovechando la conexion ssh se usa sftp para recuperar el fichero /tmp/captura.jpg y guardarlo en la ruta de destino
           sftp = ssh_client.open_sftp()
           destino=self.pathcapturas+"/"+signatura         
           try:
             comando='su '+self.user+' -c "mkdir -p '+destino+'"'
             os.system(comando)              
           except FileExistsError:         
             pass  
           fichero=destino+"/"+fecha+".jpg"           
           sftp.get("/tmp/captura.jpg", "/tmp/captura-local.jpg") #Traemos el fichero a local
           comando='su '+self.user+' -c "cp /tmp/captura-local.jpg '+fichero+'"'
           #print("Ejecutando "+comando)
           os.system(comando)           
           
           # Cerramos todo lo abierto
           ssh_transp.close()
           sftp.close()
           ssh_client.close()           
       except Exception as e:
           print("Error "+str(e))
   
   def capturar_thinclients(self):
   
       #Obtenemos la lista de usuarios conectados en el servidor de aula, filtramos por aquellos que están conectados con la ip 192.X.Y.Z (thinclients)
       for usuario in psutil.users():
           if usuario.host[:4] == "192." :
               try:
                   #Sacamos el home del usuario con getent
                   comando="getent passwd "+usuario.name
                   process = subprocess.Popen(comando.split(), stdout=subprocess.PIPE) 
                   output, error = process.communicate()
                   home_user=output.decode("ascii").split(":")[5]
                   
                   #Conectamos con el thinclient para sacar el hostname
                   ssh_client = paramiko.SSHClient()
                   ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                   ssh_client.connect(usuario.host, 22, 'root', key_filename="/root/.ssh/id_rsa", timeout=30)               
                   fecha=datetime.now().strftime("%Y-%m-%d-%H:%M:%S")
                   sleeptime = 0.001
                   comando='cat /etc/hostname | tr -d "\n"'
                   ssh_transp = ssh_client.get_transport()
                   chan = ssh_transp.open_session()
                   # chan.settimeout(3 * 60 * 60)
                   chan.setblocking(0)
                   outdata=""
                   chan.exec_command(comando)
                   while True:  # Hay que esperar a que acabe el comando
                      while chan.recv_ready():
                         outdata += chan.recv(1000).decode('ascii')
                      if chan.exit_status_ready():  # If completed
                         break
                      time.sleep(sleeptime)
                   host=outdata    
                   # Cerramos todo lo abierto
                   ssh_transp.close()
                   ssh_client.close()                  
                   
                   #Ejecutamos el comando de captura de la sesión del usuario. Se usa os.system porque para comandos complejos es mejor que subprocess.popen
                   comando='su '+usuario.name+' -c  "DISPLAY='+usuario.host+':7  XAUTHORITY='+home_user+'/.Xauthority import -window root /tmp/capture.jpg"'
                   os.system(comando)
                   
                   #Copiamos la captura a la carpeta destino  
                   destino=self.pathcapturas+"/"+host+"/"+usuario.name
                   try:
                      comando='su '+self.user+' -c "mkdir -p '+destino+'"'
                      os.system(comando)
                   except FileExistsError:         
                      pass  
                   fichero=destino+"/"+fecha+".jpg"      
                   comando='su '+self.user+' -c "cp /tmp/capture.jpg '+fichero+'"'
                   os.system(comando)
                   comando='rm -f /tmp/capture.jpg'
                   os.system(comando)
           
               except Exception as e:       
                   pass
                   
   def capturar(self):       
    
       try:
            segundos=int(self.intervaloCaptura.text())*60
       except ValueError:
            segundos=0
        
       self.capturando=True     
       self.iniciarCaptura.setText("Capturando...")
       self.finalizar.setText("Parar captura")
       self.iniciarCaptura.setEnabled(False)
       while self.capturando == True: 
            self.iniciarCaptura.setText("Capturando...")
            self.iniciarCaptura.setEnabled(False)
            self.do_captura()
            if segundos == 0:            
               break
            else: #Espera activa hasta la próxima captura
               for i in range(segundos):
                  time.sleep(1)
                  app.processEvents()
                  if self.capturando==False: break                
       self.iniciarCaptura.setEnabled(True)
       self.iniciarCaptura.setText("Iniciar Captura")
       self.finalizar.setText("Finalizar")
       self.capturando=False
       

   def do_captura(self):
       app.processEvents()
       #print("Inicio captura")
       if self.isltsp == "true":
           self.capturar_thinclients()
       else:          
           nm = nmap.PortScanner()
           nm.scan(self.ip_alumnos, '22')
           for ip in  nm.all_hosts():# Equipos conectados, up.     
               #print("Procesando "+ip)
               if nm[ip]['tcp'][22]['state']=="open": 
                  #print("Puerto ssh abierto") 
                  self.capturar_ip(ip)               
               app.processEvents()   # Para procesar clicks de raton durante el bucle.                     
       app.processEvents()
       #print("Fin captura")
    

if __name__ == "__main__":
   app = QtWidgets.QApplication([])
   window = MainWindow()
   window.show()
   app.exec_()
Como se puede apreciar es un código python que utiliza clases de conexión ssh para ejecutar en remoto los comandos bash de captura de sesión y traer los ficheros con la captura al home del usuario en el PC del profesor.

El fichero photomaton.jpg contendría la imagen:


Seguimos con el fichero .desktop, el acceso directo para poner en el menú principal y en el escritorio. Nótese el uso de "gksu" para ejecutar como root el programa photomaton.py.
# cat xubuntu18_photomaton/files/Photomaton.desktop                                                   

[Desktop Entry]
Version=1.0
Encoding=UTF-8
Name=Photomaton
Type=Application
Exec=gksu /usr/local/bin/photomaton.py
TryExec=
Icon=/usr/local/bin/photomaton.jpg
X-GNOME-DocPath=
Terminal=false
Name[es_ES]=Photomaton
GenericName[es_ES]=
Comment[es_ES]=
GenericName=
Comment=
Continuamos con el fichero .ui en formato XML con el interface de usuario creado por Qt Designer:
# cat xubuntu18_photomaton/files/photomaton.ui                                                         
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Photomaton</class>
 <widget class="QDialog" name="Photomaton">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>508</width>
    <height>212</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Photomaton</string>
  </property>
  <widget class="QPushButton" name="iniciarCaptura">
   <property name="geometry">
    <rect>
     <x>290</x>
     <y>70</y>
     <width>191</width>
     <height>28</height>
    </rect>
   </property>
   <property name="text">
    <string>Iniciar Captura</string>
   </property>
  </widget>
  <widget class="QPushButton" name="visualizarCapturas">
   <property name="geometry">
    <rect>
     <x>290</x>
     <y>110</y>
     <width>191</width>
     <height>28</height>
    </rect>
   </property>
   <property name="text">
    <string>Visualizar Capturas</string>
   </property>
  </widget>
  <widget class="QPushButton" name="finalizar">
   <property name="geometry">
    <rect>
     <x>290</x>
     <y>150</y>
     <width>191</width>
     <height>28</height>
    </rect>
   </property>
   <property name="text">
    <string>Finalizar</string>
   </property>
  </widget>
  <widget class="QLabel" name="label">
   <property name="geometry">
    <rect>
     <x>290</x>
     <y>30</y>
     <width>151</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>Intervalo capturas (min):</string>
   </property>
  </widget>
  <widget class="QLineEdit" name="intervaloCaptura">
   <property name="geometry">
    <rect>
     <x>440</x>
     <y>20</y>
     <width>41</width>
     <height>28</height>
    </rect>
   </property>
   <property name="inputMask">
    <string>09</string>
   </property>
   <property name="text">
    <string>00</string>
   </property>
   <property name="maxLength">
    <number>2</number>
   </property>
  </widget>
  <widget class="QLabel" name="label_2">
   <property name="geometry">
    <rect>
     <x>10</x>
     <y>20</y>
     <width>261</width>
     <height>181</height>
    </rect>
   </property>
   <property name="text">
    <string/>
   </property>
   <property name="pixmap">
    <pixmap>/usr/local/bin/photomaton.jpg</pixmap>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>
Y seguimos con el fichero photomaton_ui.py, generado a partir del fichero photomaton.ui anterior:
# cat xubuntu18_photomaton/files/photomaton_ui.py 
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'photomaton.ui'
#
# Created by: PyQt5 UI code generator 5.10.1
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Photomaton(object):
    def setupUi(self, Photomaton):
        Photomaton.setObjectName("Photomaton")
        Photomaton.resize(508, 212)
        self.iniciarCaptura = QtWidgets.QPushButton(Photomaton)
        self.iniciarCaptura.setGeometry(QtCore.QRect(290, 70, 191, 28))
        self.iniciarCaptura.setObjectName("iniciarCaptura")
        self.visualizarCapturas = QtWidgets.QPushButton(Photomaton)
        self.visualizarCapturas.setGeometry(QtCore.QRect(290, 110, 191, 28))
        self.visualizarCapturas.setObjectName("visualizarCapturas")
        self.finalizar = QtWidgets.QPushButton(Photomaton)
        self.finalizar.setGeometry(QtCore.QRect(290, 150, 191, 28))
        self.finalizar.setObjectName("finalizar")
        self.label = QtWidgets.QLabel(Photomaton)
        self.label.setGeometry(QtCore.QRect(290, 30, 151, 16))
        self.label.setObjectName("label")
        self.intervaloCaptura = QtWidgets.QLineEdit(Photomaton)
        self.intervaloCaptura.setGeometry(QtCore.QRect(440, 20, 41, 28))
        self.intervaloCaptura.setMaxLength(2)
        self.intervaloCaptura.setObjectName("intervaloCaptura")
        self.label_2 = QtWidgets.QLabel(Photomaton)
        self.label_2.setGeometry(QtCore.QRect(10, 20, 261, 181))
        self.label_2.setText("")
        self.label_2.setPixmap(QtGui.QPixmap("/usr/local/bin/photomaton.jpg"))
        self.label_2.setObjectName("label_2")

        self.retranslateUi(Photomaton)
        QtCore.QMetaObject.connectSlotsByName(Photomaton)

    def retranslateUi(self, Photomaton):
        _translate = QtCore.QCoreApplication.translate
        Photomaton.setWindowTitle(_translate("Photomaton", "Photomaton"))
        self.iniciarCaptura.setText(_translate("Photomaton", "Iniciar Captura"))
        self.visualizarCapturas.setText(_translate("Photomaton", "Visualizar Capturas"))
        self.finalizar.setText(_translate("Photomaton", "Finalizar"))
        self.label.setText(_translate("Photomaton", "Intervalo capturas (min):"))
        self.intervaloCaptura.setInputMask(_translate("Photomaton", "09"))
        self.intervaloCaptura.setText(_translate("Photomaton", "00"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Photomaton = QtWidgets.QDialog()
    ui = Ui_Photomaton()
    ui.setupUi(Photomaton)
    Photomaton.show()
    sys.exit(app.exec_())
Un vez distribuidos los ficheros con la tarea puppet el modo de funcionamiento es lanzarlo en el ordenador del profesor y ejecutarlo para conectar a los equipos de alumnos detectados como encendidos y realizar las capturas. La pantalla es:


En "Intervalo de capturas (min)" indicamos cada cuanto tiempo queremos hacer las capturas de pantalla. Si dejamos 0 solo se realiza una captura en el momento en que damos al botón "Iniciar Captura".


Mientras que está realizando las capturas el botón "Iniciar Captura" se desactiva. El proceso de captura para pulsando el botón "Parar Captura". A veces tarda un rato en responder ya que no se puede interrumpir mientras está ejecutando la conexión con uno de los clientes.

El botón "Visualizar Capturas" nos lleva al directorio donde se realizan las capturas para que veamos las imágenes. Las capturas se estructuran en directorios separados según el patrón: ~/Capturas/$pc_profesor/$pc_alumno/$login_alumno/*.jpg


Los nombres de ficheros jpg nos indican la fecha y hora de captura:

Y ya está. Con este sistema el profesor puede realizar un seguimiento desatendido completo de la actividad de los alumnos. No solo eso: nosotros también desde nuestro PC podremos realizar seguimiento de ordenadores de uso público del centro.


Ahora que con la magnífica serie Chernobyl (quizá desvirtuada con algunos elementos de ficción y drama innecesarios, en detrimento de otros más interesantes como la construcción del sarcófago y los biorobots) se ha reavivado el tema, dejo varios enlaces sobre accidentes nucleares bastante silenciados:

  • Three Mile Island: o como "refrigerar" un reactor con vapor de agua en lugar de con agua líquida durante horas hasta fundir parcialmente el núcleo y no darse cuenta.
  • Vandellós I: o cómo quedarte a dos grados de explotar, mientras que el director de la central se iba a por tabaco en helicóptero a Barcelona y los trabajadores conseguían controlar la temperatura in extremis con el único turbosoplante que no se había parado por inundación.
  • Lucens: o cómo los suizos se lo intentan montar por su cuenta y funden un reactor el día de su puesta en funcionamiento. La única idea buena: como por si las moscas lo hicieron debajo de una montaña, solo se contaminó ésta.

Por supuesto, recomiendo todos los artículos de Yuri sobre Chernobyl:


Y no podía faltar:



miércoles, 12 de junio de 2019

Conexión automática a red Wifi

Ahora que llegan las vacaciones viene bien dar un repaso a todos los portátiles de los alumnos: ejecutar puppet, pkgsync, limpiar los usuarios cacheados,... No solo eso, también durante el curso de vez en cuando nos toca actuar sobre todos los portátiles de un armario para aplicar alguna configuración que nos han pedido o que soluciona un bug pendiente.

Hasta ahora para tener conectividad tras encender los portátiles había 2 alternativas:
  1. Conectarlos por red cableada a las tomas del suelo o un switch del aula.
  2. Iniciar sesión en cada uno de ellos y conectarlos manualmente a la red Wifi del aula.
Evidentemente ambas opciones son bastante reiterativas y aburridas. Lo ideal sería que para estos casos los portátiles se conectasen de forma autónoma a una red wifi y nosotros pudiesemos manejarlos de forma masiva con tmux-cssh.

Y esa es la idea de este post: basándome en un script de conexión que usa en su centro nuestro compañero Esteban lo he adaptado para que se conecte de forma autónoma a una red wifi concreta predefinida tan pronto como la detecte.

El código del script es (nota: he corregido algunas erratas en el script el 8/10/2019):
# cat /usr/local/bin/conecta-default-wifi
#!/bin/bash

idioma=$LC_ALL
export LC_ALL=C

wifi_maestra="WIFI_AULAS"
wifi_key="a12345678"
nombre_conexion="ies"

activado=$(nmcli radio wifi)
if [ "$activado" != "enabled" ]
then
    nmcli radio wifi on #activamos la wifi si no lo esta
fi
conectado=$(nmcli dev status | grep wifi |  grep -w -e 'connected' -e "conectado" | awk ' {print $4}')
if  [ "$conectado" != "$nombre_conexion" ]  # Si no estamos conectados a la wifi maestra...
then
      definida=$(nmcli con | grep wifi | grep -w "$nombre_conexion") #Si ya se definió la conexión en network manager la quitamos, para crearla de nuevo cuando nos conectemos
      if [ -n "$definida" ]
      then
          nmcli con delete "$nombre_conexion"conecta-default-wifi
      fi

      detectada=$(nmcli -f SSID,BSSID,SIGNAL,SECURITY dev wifi list | grep -w "$wifi_maestra")
      if [ -n "$detectada" ]  # Si la wifi maestra está al alcance....
      then
            #Si la wifi maestra está al alcance y no estamos en ella, conectamos automaticamente.
            nmcli dev wifi con "$wifi_maestra" password "$wifi_key" name "$nombre_conexion"
            #Si ya existiese la conession podriamos hacer:  nmcli con up $ssidconfigurado
      fi
fi

export LC_ALL=$idioma

exit 0
Que distribuyo con estas reglas puppet:
file {"/usr/local/bin/conecta-default-wifi":
        owner=>root, group=>root, mode=>755,
        source=>"puppet:///modules/xubuntu18_portatil_ajustes/conecta-default-wifi",
}

cron { "cron_conecta_default_wifi":
           command => "/usr/local/bin/conecta-default-wifi",
           user => root,
           minute => "*/3",
           ensure => present
}
Por tanto tenemos un script y una tarea cron que lo llama cada 3 minutos. El script hace lo siguiente:

  1. Configuramos parámetros para una red wifi con SSID "WIFI_AULAS", una clave fija y un nombre de conexión "ies" para el Network Manager.
  2. Si la tarjeta wifi está apagada en Network Manager, se enciende.
  3. Si la wifi está conectada a un punto wifi, mira si el nombre de la SSID es "WIFI_AULAS". En caso afirmativo, finalizamos el script y no continuamos en los pasos siguientes.
  4. Si existe alguna conexión en Network Manager con el nombre "ies", la borramos. Esto es simplemente para hacer limpieza si ya nos habíamos conectado antes.
  5. Escaneamos las redes wifi circundantes en busca de "WIFI_AULAS". Si la encontramos a nuestro alcance nos conectamos a ella creando una conexión "ies" en Network Manager.
  6. Todo esto se repite cada 3 minutos, de tal forma que tan pronto aparezca en el espectro radioeléctrico la wifi "WIFI_AULAS" el portátil se conectará a ella, desconectando de cualquier otra red wifi en la que estuviera enlazado y permaneciendo conectado mientras esté a su alcance.

Con esto el trabajo queda reducido a llegar al aula con un punto wifi (vale cualquier punto de acceso portátil, incluido un router casero de ADSL) configurado con WIFI_AULAS y la clave indicada, enchufarlo a un punto de red cableado, sacar los portátiles del armario y encenderlos.

En pocos minutos todos estarán conectados a la wifi sin ninguna actuación por nuestra parte y con tmux-cssh o ssh podremos controlarlos. Incluso si tenemos puppet y pgksync en modo automático se ejecutarán sin que tengamos que hacer nada. El punto de acceso solo lo usaremos cuando queramos trabajar de esta manera. El resto del tiempo estará apagado y guardado.


Debido a que te encuentras que la gente mas insospechada se niega a creer la gesta del Apolo XI, ahí va el documental definitivo:



jueves, 6 de junio de 2019

CloudReady: convirtiendo los portátiles en ChromeBooks

Tras la llegada de los portátiles Techcomputer TC11 he liberado un par de docenas de antiguos miniportátiles MSI L1350D y APD P10ALD. Son aparatos bastante antiguos y justitos (ambos con Intel Atom CPU N455 y 2GB de RAM) que ya daban problemas de usabilidad con Ubuntu 18.

Como he hecho en otros equipos obsoletos con buen resultado, probé a instalar FreeBSD/TrueOS pero la verdad es que no he notado mejora significativa. Pienso que la causa pudiera ser el limitado soporte a la tarjeta gráfica Intel que traen.

Luego probé con varias versiones de Android-x86 con idéntico resultado. Estaba a punto de probar Manjaro pero recordé que tenía pendiente testear una distribución que convierten un portátil en un ChromeBook, que es un sistema operativo ligero cuyo escritorio es un navegador Chrome y por tanto solo permite ejecutar dicho software y los plugins que instalemos en él. En algunos centros educativos están empezando a llegar este tipo de equipos. En otros países se usa masivamente en entornos educativos.

De esta manera tenemos un ordenador en el que hacemos login con las credenciales de Google y que solo permite trabajar con el navegador, en la nube (con Google Docs, Google Drive y las extensiones que instalemos de Chrome Web Store).

Por supuesto, el sistema Chrome OS es propietario de Google y no es fácil instalarlo en un equipo que no sea un ChromeBook certificado. Afortunadamente han salido clones y alternativas que remedan bastante bien el espíritu de Chrome OS. Aunque hay varios donde elegir me he decantado para probar por CloudReady en su versión Home (traducido: la gratuita). Tiene también buena pinta este desarrollo chino, ahora que Trump está en guerra comercial con China.

1. Descarga.

Lo descargamos de la página de Neverware. En caso de duda se descarga la imagen en un .zip para OSX. Lo descomprimimos y dentro hay un fichero .bin con la imagen de disco de arranque. La grabamos a un pendrive USB con:
sudo dd if=cloudready.bin of=/dev/sdX bs=4M
Luego reiniciamos el pc y arrancamos con el pendrive. La instalación comenzará.

2. Instalación.

Durante la instalación se borrará el disco completo, no es sencilla una instalación dual. Lo cierto es que CloudReady construye un sistema infernal basado en mas de 20 particiones, la causa al parecer es algo que viene de serie en Chromium OS. Esta estructura no es algo normal en Linux, pero si lo he visto muchas veces en Android.

La primera pantalla de instalación, con la bienvenida y la elección de idioma:


Luego nos pide una cuenta de gmail.com. Esta cuenta será la propietaria ("owner") del sistema instalado y nunca podrá borrarse. Por ello lo mejor es usar en la instalación una cuenta gmail.com genérica que nos permitirá entrar luego el pc. Una vez instalado todo el sistema, cualquier persona que tenga una cuenta de Gmail podrá iniciar sesión libremente sin problema (o usar una cuenta temporal de invitado, si no tiene/quiere usar su cuenta de gmail).


Evidentemente, para que esta cuenta pueda ser validada es necesario que el ordenador esté conectado a Internet, ya sea por cable o wifi. Esto es un imperativo siempre presente un Chromebook: un sistema diseñado para trabajar en la nube necesita Internet constante.

Por último nos pide el idioma de instalacion:


3. Configuración y uso.

Una vez instalado, el sistema reinicia y aparece la pantalla de login con varias posibilidades:


  • Iniciar sesión con la cuenta del propietario.
  • Iniciar sesión con una cuenta válida de Gmail, la cual quedará cacheada para futuros logins offline.
  • Iniciar sesión con una cuenta de invitado, que al cerrar sesión no quedará rastro en el disco duro local de su actividad.
  • Borrar una cuenta de las creadas, excepto la de invitado y propietario.
  • Conectar a una red wifi, necesario para poder iniciar sesión con cuentas de gmail no cacheadas.

Esta es la configuración desde la pantalla de login:


Y esta una pantalla con ya dos cuentas de gmail cacheadas:


Una vez iniciada sesión carga el Google Chrome como única aplicación:


Este sería menú de opciones de la derecha de la barra de tareas:


Y este el menú de la izquierda de la barra:


Si entramos en la configuración vemos las preferencias usuales del Google Chrome, mas varias opciones extra referidas al escritorio de CloudReady:


Comprobando el rendimiento podemos afirmar que no va nada mal. Al tener un Linux subyacente con drivers actualizados y un escritorio con Chrome y sus extensiones de la Chrome Web Store todo va bastante ligerito teniendo en cuenta la poca potencia de esta máquina.

4. Configuración final e imagen Clonezilla para clonar.

Debajo de esto hay un Linux, por lo que podemos acceder a un terminal para configurar otros aspectos. Con la combinación de teclas CTRL-Alt-T entramos en el terminal, llamado "crosh". Una vez dentro con el comando "shell" entramos en una shell parecida a bash.

Las credenciales por defecto para trabajar son chronos/chronos. Si queremos cambiar la contraseña los pasos son:
# sudo mount -o rw,remount /
# sudo passwd chronos
# sudo mount -o ro,remount /
Y después reiniciamos.

Otra cosa que me resulta útil es desactivar la suspensión. Los pasos, tras entrar en el terminal:
# shell
# sudo su
# disable_verity
Reiniciamos la máquina y repetimos los anteriores comandos una vez. Luego hacemos
# mount -o rw,remount /
# cat /sys/class/dmi/id/product_name >> /usr/share/power_manager/suspend_prevention_models
# restart ui
Aquí tenemos bastante soporte y trucos de configuración adicionales.

Una vez tenemos configurado es el momento de hacer una imagen con clonezilla para instalarlo en otros equipos. La creación de la imagen se inicia normalmente, pero al llear a la partición sda16 siempre aborta con este mensaje:
Starting to clone device (/dev/sda16) to image (-)
Reading Super Block
memory needed: 28369449 bytes
bitmap 7397925 bytes, blocks 2*10485760 bytes, checksum 4 bytes
Calculating bitmap... Please wait... 
extfsclone.c: bitmap free count err, partclone get free:57578015 but extfs get 57553763.
Please run fsck to check and repair the file system
Failed to use partclone program to save or restore an image!
Tras muchas pruebas solo puedo afirmar que CloudReady al apaar el sistema siempre deja la partición sda16 en ese estado, provocando un error al crear la imagen. La solución es arrancar clonezilla y, antes de empezar a hacer nada, irnos a un terminal y teclear:
# sudo fsck -a /dev/sda16
Después de esto ya podemos realizar la imagen sin problema. La restauración funciona correctamente.


Esta semana los camaradas chinos nos han sorprendido lanzando un cohete Larga Marcha 11 desde una plataforma marítima.




Espectacular. Con este sistema resulta mucho mas barato y eficiente el envío de satélites.

miércoles, 5 de junio de 2019

Generación del musthave mínimo para pkgsync

Ya vimos en su día como configurar un musthave lo mas pequeño posible. Como ahora estoy configurando los portátiles y algunos equipos nuevos, si quiero dejarlos preparados para pkgsync en primer lugar tengo que generar un musthave a partir de los paquetes instalados y luego purgarlo de paquetes dependientes para quedarlo con el menor tamaño posible. Por ello he unificado ambos procesos en un único script:
# cat genera-musthave-minimo.sh
#!/bin/bash

FICHERO=$HOME/musthave.$HOSTNAME

echo "Generando lista de paquetes instalados en $FICHERO..."
aptitude show "?installed ?not(?priority(required)) ?not(?essential) ?not(?automatic)" | grep -e ^Package -e ^Paquete | cut -f 2 -d " " | sort > $FICHERO
echo "Obteniendo lista de paquetes multiarch..."
aptitude show "?installed ?multiarch(same)" | grep -e ^Package -e ^Paquete | cut -f 2 -d " " > /tmp/same
sort -u -o /tmp/same /tmp/same

echo "Procesando lista de paquetes multiarch..."
for paquete in `cat /tmp/same`
do
        sed -i 's|$paquete||' $FICHERO
        dpkg -l|grep "$paquete:i386" 1>/dev/null && echo "$paquete:i386" >> $FICHERO
        dpkg -l|grep "$paquete:amd64" 1>/dev/null && echo "$paquete:amd64" >> $FICHERO
done
sort -u -o $FICHERO $FICHERO 

#Ahora vamos a purgar los paquetes dependientes y dejar solo los paquetes "primigenios"
if [ ! -f /usr/bin/apt-rdepends ]
then
    echo "Instalando apt-rdepends"
    apt-get install apt-rdepends
fi

cp /dev/null /tmp/musthave.new
cp /dev/null /tmp/musthave.deps

paquetes_explicitos=$(aptitude search '~i !~M' -F '%p' --disable-columns | sort -u)
for i in $paquetes_explicitos
do
   echo -n "Paquete $i"
   if grep $i $FICHERO > /dev/null
   then
        echo " ->instalado por musthave"
        echo $i >> /tmp/musthave.new        
   else
      if grep $i /etc/pkgsync/mayhave > /dev/null
      then
          echo "  ->instalado por mayhave"
      else
          echo "  ->instalado por musthave.d"
      fi
   fi
done
sort -u -o /tmp/musthave.new /tmp/musthave.new

num=1
total=$(wc -l  /tmp/musthave.new)
for i in $(cat /tmp/musthave.new)
do
   echo "Dependencias de $i: $num/$total"
   ((num++))
   apt-rdepends $i |grep Depends: | awk ' { print $2 } ' | sed "/^${i}$/d" >> /tmp/musthave.deps 
done
sort -u -o /tmp/musthave.deps /tmp/musthave.deps
comm -23 /tmp/musthave.new  /tmp/musthave.deps  > $FICHERO.final

echo "Proceso concluido, musthave en $FICHERO.final"

exit 0
Lanzamos el script y tras un intenso proceso que debemos dejar acabar con paciencia, en el fichero "musthave.$HOSTNAME.final" nos quedan los paquetes básicos para que funcione el pkgsync, que suele quedar en un 15% del musthave que obtenemos con el script original. Una vez generado este musthave en un equipo de pruebas lo podemos copiar a mano o por puppet al resto de equipos similares en su ubicación definitiva dentro de /etc/pkgsync/musthave o /etc/pkgsync/musthave.d/*.


Pedazo artículo el de Daniel Marín sobre el Apolo X, ahora que estamos en el 50 aniversario. Seguro que nos tiene preparada una maravilla sobre el Apolo XI en unas semanas.


miércoles, 29 de mayo de 2019

Conectando por ssh a un Clonezilla Live

A veces nos vendría bien conectarnos a la la sesión clonezilla que está ejecutando un compañero o nosotros mismos en otra ubicación, para así poder hacer la clonación desde nuestro puesto de trabajo o ver y depurar las causas de algún error.

Esto no es tan trivial como en un Linux corriente ya que clonezilla arranca como una distribución live con el servicio ssh apagado. Vamos a ver como habilitar el sshd y conectar a la red, permitiendo así la conexión remota desde otra ubicación.

Una vez arrancado clonezilla en el equipo, nos vamos a la shell (ya sea desde el menú o con CTRL-ALT-Fx) y activamos el servicio ssh con:
# sudo service ssh start
A continuación hay que conectar a la red el equipo, para ello nos aseguramos de que tiene un cable ethernet conectado y hacemos:
# sudo ocs-live-netcfg
Que mostrará el asistente de conexión a red. Lo habitual es elegir "dhcp" y apuntar la IP que se recibe.

Una vez hecho esto, el pc con el clonezilla queda a la espera, y desde la otra ubicación podemos conectarnos con:
# ssh user@ip.del.clonezilla
La contraseña por defecto es "live". Una vez conectados en remoto tenemos una shell desde donde realizar las tareas que deseemos sobre la máquina remota, incluyendo lanzar el asistente de clonezilla:
# sudo clonezilla
Si además estamos conectando a una ubicación remota donde hay un compañero, puedes sernos usar el comando "screen" para compartir la sesión y que ambos podamos ver a la vez lo que se hace en una pantalla común. Estos dos enlaces nos muestran como hacerlo:



Este es el tren de 60 satélites Starlink de Elon Musk surcando el cielo. Se pueden observar con unos prismáticos si estamos en una zona de baja contaminación lumínica.

Con este enlace podemos ver en tiempo real por donde va el tren de satélites y nos permite saber cuando pasará por nuestra vertical.

miércoles, 22 de mayo de 2019

TVHeadend: configuración y uso (II)

Seguimos con el Tvheadend. Después de instalarlo, configurarlo y conectarlo a varios streams de IPTV para jugar con ellos, en este post vamos a ver como configurarlo con una tarjeta de TV física.

1. Hardware.

Para ello usaremos una tarjeta TDT PCI. En su día ya estuvimos lidiando con otra parecida. Identifiquemos la tarjeta:
# lspci -nnvv

04:01.0 Multimedia controller [0480]: Philips Semiconductors SAA7134/SAA7135HL Video Broadcast Decoder [1131:7134] (rev 01)
 Subsystem: Creatix Polymedia GmbH Medion 7134 [16be:0003]
 Control: I/O- Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx-
 Status: Cap+ 66MHz- UDF- FastB2B+ ParErr- DEVSEL=medium >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
 Latency: 64 (21000ns min, 8000ns max)
 Interrupt: pin A routed to IRQ 16
 Region 0: Memory at febffc00 (32-bit, non-prefetchable) [size=1K]
 Capabilities: [40] Power Management version 1
  Flags: PMEClk- DSI- D1+ D2+ AuxCurrent=0mA PME(D0-,D1-,D2-,D3hot-,D3cold-)
  Status: D0 NoSoftRst- PME-Enable- DSel=0 DScale=1 PME-
 Kernel driver in use: saa7134
 Kernel modules: saa7134
La carga del driver la he configurado así:
# cat /etc/modprobe.d/saa7134.conf 
options saa7134 card=12 alsa=1
install saa7134 /sbin/modprobe --ignore-install saa7134; /sbin/modprobe saa7134-dvb ; /sbin/modprobe tuner ; /sbin/modprobe tda9887
En el arranque, el syslog recoge estos mensajes de detección:
# dmesg |grep saa
[   13.906748] saa7134: saa7130/34: v4l2 driver version 0, 2, 17 loaded
[   13.906963] saa7134: saa7134[0]: found at 0000:04:01.0, rev: 1, irq: 16, latency: 64, mmio: 0xfebffc00
[   13.906971] saa7134: saa7134[0]: subsystem: 16be:0003, board: Medion 7134 [card=12,insmod option]
[   13.906999] saa7134: saa7134[0]: board init: gpio is 0
[   14.076684] saa7134: i2c eeprom 00: be 16 03 00 54 20 1c 00 43 43 a9 1c 55 d2 b2 92
[   14.076688] saa7134: i2c eeprom 10: 00 ff 86 0f ff 20 ff 00 01 50 32 79 01 3c ca 50
[   14.076690] saa7134: i2c eeprom 20: 01 40 01 02 02 03 01 00 06 ff 00 1f 02 51 96 2b
[   14.076692] saa7134: i2c eeprom 30: a7 58 7a 1f 03 8e 84 5e da 7a 04 b3 05 87 b2 3c
[   14.076694] saa7134: i2c eeprom 40: ff 1d 00 c2 86 10 01 01 00 00 fd 79 44 9f c2 8f
[   14.076696] saa7134: i2c eeprom 50: ff ff ff ff ff ff 06 06 0f 00 0f 00 0f 00 0f 00
[   14.076698] saa7134: i2c eeprom 60: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
[   14.076701] saa7134: i2c eeprom 70: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
[   14.076703] saa7134: i2c eeprom 80: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
[   14.076705] saa7134: i2c eeprom 90: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
[   14.076707] saa7134: i2c eeprom a0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
[   14.076709] saa7134: i2c eeprom b0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
[   14.076711] saa7134: i2c eeprom c0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
[   14.076714] saa7134: i2c eeprom d0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
[   14.076716] saa7134: i2c eeprom e0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
[   14.076718] saa7134: i2c eeprom f0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
[   14.106696] saa7134: saa7134[0] Board has DVB-T
[   14.106700] saa7134: saa7134[0] Tuner type is 63
[   14.513476] saa7134: saa7134[0]: registered device video0 [v4l2]
[   14.513564] saa7134: saa7134[0]: registered device vbi0
[   14.513639] saa7134: saa7134[0]: registered device radio0
[   14.609062] saa7134_dvb: dvb_init() allocating 1 frontend
[   14.706705] DVB: registering new adapter (saa7134[0])
[   14.706713] saa7134 0000:04:01.0: DVB: registering adapter 0 frontend 0 (Philips TDA10046H DVB-T)...
[   17.115786] saa7134_alsa: saa7134 ALSA driver for DMA sound loaded
[   17.115822] saa7134_alsa: saa7134[0]/alsa: saa7134[0] at 0xfebffc00 irq 16 registered as card -1
[   30.560105] saa7134 0000:04:01.0: DVB: adapter 0 frontend 0 frequency 0 out of range (51000000..858000000)
Y puesto que el driver está bien configurado, crea los dispositivos adecuados en /dev:
# ls -l /dev/dvb/adapter0/*
crw-rw----+ 1 root video 212, 4 may 15 16:49 /dev/dvb/adapter0/demux0
crw-rw----+ 1 root video 212, 5 may 15 16:49 /dev/dvb/adapter0/dvr0
crw-rw----+ 1 root video 212, 3 may 15 16:49 /dev/dvb/adapter0/frontend0
crw-rw----+ 1 root video 212, 7 may 15 16:49 /dev/dvb/adapter0/net0

2. Canales TDT.

Cada zona de España tiene sus propios canales TDT en función del repetidor. Por tanto, según sea nuestra ubicación geográfica debemos conocer que frecuencias se utilizan y qué canales hay disponibles dentro de cada frecuencia. Esto se puede consultar en la página http://www.tdt1.com, donde aparece la información tal que así:


Para no tener que meter las frecuencias a mano, todos los programas de manejo de la TDT (vlc, kaffeine, Tvheadend, ...) cuentan con algún script o fichero de configuración donde vienen las frecuencias de nuestra zona. En el caso de Tvheadend la ubicación de estos ficheros es:
# ls /usr/share/tvheadend/data/dvb-scan/dvb-t/es-*
/usr/share/tvheadend/data/dvb-scan/dvb-t/es-Alava
/usr/share/tvheadend/data/dvb-scan/dvb-t/es-Albacete
/usr/share/tvheadend/data/dvb-scan/dvb-t/es-Alfabia
/usr/share/tvheadend/data/dvb-scan/dvb-t/es-Alicante
...
Como a veces el gobierno reubica las frecuencias con el fin de hacer sitio para la telefonía móvil puede suceder que estos ficheros anteriores estén obsoletos. Afortunadamente hay gente que pone en github un repositorio con ficheros actualizados. Por tanto no está mal comparar el fichero correspondiente a nuestra zona con el del repositorio y actualizarlo si fuese necesario.

3. Configuración de Tvheadend.

Una vez pinchada la tarjeta y arrancado el PC, esta aparece en la configuración de Tvheadend al entrar por su interface web:


A partir de aquí, la secuencia de pasos es bastante parecida a lo hecho en el post anterior:

  • Creamos una red para la TDT: Configuration->DVB Inputs->Networks->Add Network->DVB-T Network (el nombre técnico real de la TDT es "DVB-T"):


    En el formulario seleccionamos "Enabled", damos nombre a la red ("TDT"), y en "Pre-defined muxes" elegimos el fichero de configuración de frecuencias TDT correspondiente a nuestra zona (la que vimos en el apartado previo - en mi caso es "es-Carceres". No es un error: hace años alguien lo escribió mal y la errata se ha perpetuado):


    Al dar a "Create" se crea la red y los muxes asociados a las frecuencias definidas en el fichero de configuración asociado.
  • Ahora hay que vincular la tarjeta de TV y la red, ya que si no Tvheadend no sabe dónde sintonizar las frecuencias definidas para los muxes recién creados. Para ello vamos a Configuration->DVB Inputs->TV Adapters y pinchamos sobre la tarjeta de TV. Aparecerá un formulario para la configuración de la misma. Allí nos aseguramos de que "Enabled" y "Over-the-air EPG" estén activados. En el desplegable "Networks" seleccionamos "TDT", que es la red creada en el paso anterior. Al dar al botón "Save" se guardan y aplican los cambios.

  • Muxes y servicios: ahora los muxes encuentran un "origen" de señal y empiezan a sintonizar los canales. En Configuration->DVB Inputs->Muxes nos aparece la lista de muxes, uno asociado a cada frecuencia (expresada en MHz):


    Dentro de cada mux/frecuencia hay varios canales multiplexados. Esos canales deberían aparecer en la pestaña Configuration->DVB Inputs->Services:


    Nota: en el caso de que no aparezcan los servicios de forma automática podemos volver a la pestaña Muxes y darle al reproductor de cada mux (botón de la columna más a la izquierda que muestra la etiqueta "Play this stream"), eso descarga un fichero con un stream que se abrirá en vlc y mostrará uno de los canales del mux. Al hacer esto se fuerza la detección de los canales y la creación de los servicios correspondientes.

  • Mapeo de los servicios en canales de Tvheadend: una vez tenemos cada uno de los canales de TDT sintonizados metido como servicio en Tvheadend es el momento de decidir que canal queremos que se converta en un canal de Tvheadend.

    Recordemos que Tvheadend distingue entre servicio (un stream de vídeo entrante) y canal (canal de Tvheadend que aparecerá en el interface web, las aplicaciones cliente android y sobre el que podremos programar grabaciones). De esta manera podemos ignorar los canales entrantes que no nos interesen y configurar solo unos cuantos de ellos de cara al usuario final.

    Esto lo hacemos en Configuration->DVB Inputs->Services->Map Selected, que permite mapear los servicios que queramos en canales:


    Una vez mapeado, si nos vamos a Channel EPG->Channel podremos ver el canal recién creado:

    Con esto dicho canal ya estará disponible en los clientes Tvheadend y sobre el podremos programar grabaciones y timeshifts. A partir de aquí sólo tendremos que repetir esto con todos los canales que nos interese tener.

4. Enlaces.

Para escribir este post he tenido que buscar la información en varias fuentes muy interesantes donde se hablan tanto de sintonización TDT como satélite, así que pongo los enlaces usados por si quisieramos ampliar:

Veamos esta foto:


La sonda lunar del Estado Nación Judío de Israel se hace un selfie un ratito antes de estrellarse violentamente contra la Luna, en lugar de aterrizar suavemente. Quizá lo confundió con la franja de Gaza.

jueves, 9 de mayo de 2019

Configuración de pórtatiles TechComputer TC11

Nos han llegado unos portátiles TechComputer TC11, unos clónicos que internamente se identifican (usando dmidecode podemos verlo) como P325J. Traen una imagen de Xubuntu 18 para miniportátiles casi totalmente configurada. Anotaré en esta entrada las modificaciones que vaya haciendo.

Lo primero es definir en /etc/escuela2.0 las variables que identifican al nuevo modelo de equipo, de cara a aplicar reglas puppet:
# cat /etc/escuela2.0
SISTEMA=ubuntu1804
USO=portatiles
HARDWARE=TC11
USUARIO=alumno

Para los portátiles tengo un módulo puppet "xubuntu18_portatil_ajustes", en la cual hay un case por $hardware:

...
case $hardware {
    "TC11": {
       file {"/etc/X11/xorg.conf.d":
            owner => root , group => root , mode => 644 , 
            ensure => directory,
       }

       file {"/etc/X11/xorg.conf.d/10-xorg-tc11.conf":
            owner => root , group => root , mode => 644 , 
            source => "puppet:///modules/xubuntu18_portatil_ajustes/10-xorg-tc11.conf",
       }
       #Cambia resolucion del grub para que en consola aparezca bien el contenido.
       file { "/etc/default/grub":
            source => "puppet:///modules/xubuntu18_portatil_ajustes/grub.default.TC11",
            owner => root , group => root , mode => 644 , 
       }
       exec { "update-grub2":
            subscribe => File ["/etc/default/grub"],
            refreshonly => true
       }
       #Certificado ldap
       file { "/etc/ldap/ssl/ldap-server-pubkey.pem":
            owner => root , group => root , mode => 644 , 
            source => "puppet:///modules/xubuntu18_portatil_ajustes/ldap-server-pubkey.pem",
       }
    }
...

Lo que hacemos es:
  1. Añadir un /etc/X11/xorg.conf.d/10-xorg-tc11.conf con una configuración personalizada de Xorg. Por defecto estos portátiles vienen con una resolución altísima que deja la letra muy pequeña. El fichero 10-xorg-tc11.conf para una resolución de 1360x768 (se pueden probar otras resoluciones posibles usando el comando xrandr en un terminal) sería:
    Section "Device"
            ### Available Driver options are:-
            ### Values: <i>: integer, <f>: float, <bool>: "True"/"False",
            ### <string>: "String", <freq>: "<f> Hz/kHz/MHz",
            ### <percent>: "<f>%"
            ### [arg]: arg optional
            #Option "ColorKey" "integer"
            #Option "DRI" "string"
            #Option "CacheLines" "integer"
            #Option "DDC" "boolean"
            #Option "Dac6Bit" "boolean"
            #Option "XvMCSurfaces" "integer"
            #Option "Accel" "boolean"
            #Option "Present" "boolean"
            #Option "AccelMethod" "string"
            #Option "TearFree" "boolean"
            #Option "ReprobeOutputs" "boolean"
            #Option "VideoKey" "integer"
            #Option "XvPreferOverlay" "boolean"
            #Option "Backlight" "string"
            #Option "CustomEDID" "string"
            #Option "FallbackDebug" "boolean"
            #Option "DebugFlushBatches" "boolean"
            #Option "DebugFlushCaches" "boolean"
            #Option "DebugWait" "boolean"
            #Option "HWRotation" "boolean"
            #Option "VSync" "boolean"
            #Option "PageFlip" "boolean"
            #Option "SwapbuffersWait" "boolean"
            #Option "TripleBuffer" "boolean"
            #Option "Tiling" "boolean"
            #Option "LinearFramebuffer" "boolean"
            #Option "RelaxedFencing" "boolean"
            #Option "XvMC" "boolean"
            #Option "Throttle" "boolean"
            #Option "HotPlug" "boolean"
            #Option "Virtualheads" "integer"
            #Option "ZaphodHeads" "string"
            #Option "ZaphodHeads" "LVDS1,VGA1"
            #Option "ZaphodHeads" "0,2:HDMI1,DP2"
            #Option "Position" "0 0"
            #Option "Position" "1024 0"
            #Option "RightOf" "Laptop FoodBar Internal Display"
            #Option "monitor-LVDS" "Laptop FooBar Internal Display"
            #Option "monitor-VGA" "Some Random CRT"
            Identifier  "Card0"
            Driver      "intel"
            BusID       "PCI:0:2:0"
    EndSection
    
    Section "Screen"
        Identifier "Screen0"
        Device     "Card0"
        Monitor    "Monitor0"
        SubSection             "Display"
                Depth               24
                Modes              "1360x768" #Choose the resolution
        EndSubSection
    EndSection
    

  2. De igual manera, el grub aparece con una letra minúscula, para solucionar esto debemos meter un nuevo fichero /etc/default/grub y regenerarlo:
    # If you change this file, run 'update-grub' afterwards to update
    # /boot/grub/grub.cfg.
    # For full documentation of the options in this file, see:
    #   info -f grub -n 'Simple configuration'
    
    GRUB_DEFAULT=0
    GRUB_HIDDEN_TIMEOUT=0
    GRUB_HIDDEN_TIMEOUT_QUIET=true
    GRUB_TIMEOUT=10
    GRUB_TIMEOUT_STYLE=menu
    GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
    GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
    GRUB_CMDLINE_LINUX="ipv6.disable=1"
    
    # Uncomment to enable BadRAM filtering, modify to suit your needs
    # This works with Linux (no patch required) and with any kernel that obtains
    # the memory map information from GRUB (GNU Mach, kernel of FreeBSD ...)
    #GRUB_BADRAM="0x01234567,0xfefefefe,0x89abcdef,0xefefefef"
    
    # Uncomment to disable graphical terminal (grub-pc only)
    #GRUB_TERMINAL=console
    
    # The resolution used on graphical terminal
    # note that you can use only modes which your graphic card supports via VBE
    # you can see them in real GRUB with the command `vbeinfo'
    GRUB_GFXMODE=800x600
    
    # Uncomment if you don't want GRUB to pass "root=UUID=xxx" parameter to Linux
    #GRUB_DISABLE_LINUX_UUID=true
    
    # Uncomment to disable generation of recovery mode menu entries
    #GRUB_DISABLE_RECOVERY="true"
    
    # Uncomment to get a beep at grub start
    #GRUB_INIT_TUNE="480 440 1"
    GRUB_DISABLE_OS_PROBER=true
    

  3. Los portátiles no traen /etc/ldap/ssl/ldap-server-pubkey.pem, por lo que si usamos Secure LDAP la autenticación nos fallará. Metemos la clave (cogida de otra máquina que si la traiga) por si nos hace falta.
Seguiremos añadiendo cosas en este mismo post según vayan apareciendo problemas a resolver.


Addenda 17/mayo/2019:

  • Algunos portátiles una vez arrancados no cogen red y dan continuos errores en consola de RTC. Eso se debe a que tienen mal la fecha y debemos pulsar ESC o Supr en el inicio para entrar en la BIOS y meter la fecha correcta manualmente.
  • En algún portátil nos ha sucedido que en el arranque no carga el grub, sino que se muestra:
    Que es una shell de EFI. Eso sucede porque en la BIOS no está configurado bien el orden de arranque de dispositivos, el cual debe ser:

Addenda 20/mayo/2019:
Cuando vamos a consola con CTRL-ALT-F1 todo se ve con letra minúscula, tal como se veía el GRUB inicialmente. La modificación hecha antes para arreglarlo en el GRUB no funciona una vez se ha cargado el sistema.

La solución es hacer un script que cargue un font que sea mas visible. El script se llamará /usr/bin/local/fontset:
#!/bin/sh
setfont /usr/share/consolefonts/Uni3-TerminusBold32x16.psf.gz
Después hacemos ejecutable dicho script y lo invocamos en el inicio de sesión de root, añadiendo al final de /root/.profile:
test "$TERM" == "linux" && /usr/local/bin/fontset
Otra solución que he probado es quedar /etc/default/console-setup así:
# CONFIGURATION FILE FOR SETUPCON
# Consult the console-setup(5) manual page.
ACTIVE_CONSOLES="/dev/tty[1-6]"
CHARMAP="UTF-8"
CODESET="Lat15"
FONTFACE="Terminus"
FONTSIZE="16x32"
VIDEOMODE=
FONT="Lat15-Terminus32x16.psf.gz"
Y ejecutar después:
# cp /usr/share/consolefonts/Lat15-Terminus32x16.psf.gz /etc/console-setup/
# update-initramfs -u
# reboot
Esto hace que el cambio de tamaño de font sea ya desde la petición de login...pero en la consola 2 y sucesivas (CTRL-ALT-F2, F3,..). En la primera consola (CTRL-ALT-F1) no comprendo por qué motivo no se cambia.

Si en lugar de estas 3 últimas líneas hacemos "setupcon" se consigue un resultado similar, pero en las pruebas me he encontrado con que no siempre funciona.


Addenda 22/mayo/2019:

Hemos tenido otro problema con el touchpad, que de manera aleatoria deja de funcionar cuando llevamos un rato trabajando, obligándonos a conectar un ratón USB. Se detecta como un dispositivo SYNA3602:00 0911:5288 Touchpad (podemos verlo con "cat /proc/bus/input/devices") y es manejado por el módulo i2c_hid. Si buscamos en Internet vemos que tiene un pequeño historial de problemas.

La solución propuesta por nuestros compañeros de servicios centrales es actualizar el kernel y sus módulos a la versión 5.0.0-15, lo cual podemos hacer con unas reglas puppet:
package { 'linux-image-5.0.0-15-generic' :
    ensure => "installed" ,
}
package { 'linux-modules-5.0.0-15-generic' :
    ensure => "installed" ,
}
package { 'linux-modules-extra-5.0.0-15-generic' :
    ensure => "installed" ,
}
Y después de esto no vuelve a aparecer el problema del touchpad.


Addenda 23/mayo/2019:

Seguimos con cosas para corregir o mejorar que van descubriendo los compañeros.

En el escritorio de los portátiles parece el disco "sda2" accesible. Esta partición es donde se guarda clonezilla y lo mas adecuado es ocultarla con este fichero de configuración:
# cat /etc/udev/rules.d/99-hide-disks.rules
KERNEL=="sda2", ENV{UDISKS_IGNORE}="1"
Si queremos acceder desde los portátiles a la carpeta compartida /home/instituto hay un pequeño problema con los permisos, que se corrige:
file { "/etc/auto.instituto":
    ensure => file,
    owner => root, group => root, mode => 644,
}

Me despido compartiendo un maravilloso bar que encontré en Bulgaria:


Un buen sitio para un sysadmin y su cerveza.

Listar clientes wifi conectados a un punto de acceso OpenWRT/LEDE

Cuanto tenemos un punto de acceso creado desde un dispositivo con OpenWRT o LEDE en el centro educativo puede suceder que se sature porque hay muchos clientes conectados. Es muy útil saber en cualquier momento cuantos clientes tiene asociados nuestro punto, por si fuera necesario hacer una limpieza.

Esto lo haríamos con el comando:
# iw dev wlan0 station dump | grep Station
o bien con:
# iwinfo wlan0 assoclist
Siendo wlan0 la tarjeta wifi sobre la que se monta el punto de acceso. Una vez averiguadas las MACs podemos hacer limpieza expulsando clientes con:
# ubus call hostapd.wlan0 del_client "{'addr':'00:11:22:33:44:55', 'reason':5, 'deauth':false, 'ban_time':0}"

En el último mes Space X nos ha alegrado con el lanzamiento y aterrizaje exítoso de un Falcon Heavy con carga útil (un satélite Arabsat). El despegue:


El aterrizaje de los dos boosters, siempre me queda estremecido esta imagen:


Y el aterrizaje de la etapa central en la barcaza "Of course, I still love you":


Por desgracia, la etapa central cayó al mar por el fuerte oleaje al volver a tierra, ya que falló el anclaje. No volverá a suceder, seguro.

El alucinante vídeo con todo el aterrizaje:

jueves, 25 de abril de 2019

Monitorizando los puestos del aula con monit (II)

Seguimos desde aquí. Vamos a ampliar añadiendo controles basándonos en esta entrada de mi compañero Esteban:
  • Temperatura de la CPU
  • Temperatura de la placa base
  • Temperatura del disco duro
  • Temperatura de la GPU (tarjeta gráfica)
  • Velocidad ventiladores
  • Estado de las fuentes de alimentación redundantes en el servidor del centro
Todo esto lo haremos usando los comandos sensors, nvidia-smi, hddtemp y hplog.

El comando sensors es el que mas información da, pero el problema es que es muy dependiente del hardware y con él no obtenemos una salida homogénea con los números bien colocaditos, sino distintos formatos de presentación a partir de los cuales extraemos los datos. A eso hay que añadir que no siempre tenemos accesibles todos los valores de temperaturas y/o ventiladores debido a que a veces los comandos sensors y hddtemp no son compatibles con el hardware subyacente.

Con esto así he adaptado los scripts para que sean los mas completos posibles, pero no es descartable irlos ampliando conforme vayamos recibiendo nuevos equipos o actualizaciones que hagan que sensorsse comunique mejor con el hardware.

El fichero de monitorización para todas la máquinas sería (esto se añadiría a la tarea puppet vista en el post anterior):
# cat /etc/monit/conf.d/monitrc.temperatura
check program mbtemperatura with path "/usr/local/bin/mbtemp.sh"
   if status > 60 for 3 cycles then alert
   group temperature

check program cputemperature  with path "/usr/local/bin/cputemp.sh"
   if status > 65 for 3 cycles then alert
   group temperature

check program hddtemperature with path "/usr/local/bin/hddtemp.sh"
   if status > 50 for 3 cycles then alert
   group temperature

check program vgatemperature with path "/usr/local/bin/vgatemp.sh"
   if status > 98 for 3 cycles then alert
   group temperature

check program fanspeed with path "/usr/local/bin/fanspeed.sh"
   if status = 1 for 3 cycles then alert
   group temperature
El script que comprueba la velocidad de los ventiladores (solo funciona en las placas Asus de los servidores LTSP que nos envió Telefónica) sería:
# cat /usr/local/bin/fanspeed.sh
#!/bin/bash
hardware=$(facter hardware)
FANSPEED=0 #
case $hardware in
   "telefonica")
         FANSPEED1=$(sensors |grep "^CPU FAN Speed" | awk '{print $4}')
         FANSPEED2=$(sensors |grep "^CHASSIS2 FAN Speed" | awk '{print $4}')
         test $FANSPEED1 -lt 600 -o $FANSPEED2 -lt 600 && FANSPEED=1       #Si alguno de los ventiladores gira por debajo de los 600RPM se avisa de ello
         ;;
esac
#echo $FANSPEED # for debug only
exit $FANSPEED
Para estos PC, que tienen una placa Asus P5Q DELUXE, la salida de sensors es muy completa:
# sensors
coretemp-isa-0000
Adapter: ISA adapter
Core 0:       +43.0°C  (high = +82.0°C, crit = +100.0°C)
Core 1:       +43.0°C  (high = +82.0°C, crit = +100.0°C)
Core 2:       +43.0°C  (high = +82.0°C, crit = +100.0°C)
Core 3:       +40.0°C  (high = +82.0°C, crit = +100.0°C)

radeon-pci-0100
Adapter: PCI adapter
temp1:        +56.0°C  

atk0110-acpi-0
Adapter: ACPI interface
Vcore Voltage:       +1.13 V  (min =  +0.80 V, max =  +1.60 V)
 +3.3 Voltage:       +3.28 V  (min =  +2.97 V, max =  +3.63 V)
 +5 Voltage:         +5.14 V  (min =  +4.50 V, max =  +5.50 V)
 +12 Voltage:       +12.21 V  (min = +10.20 V, max = +13.80 V)
CPU FAN Speed:      2556 RPM  (min =  600 RPM, max = 7200 RPM)
CHASSIS1 FAN Speed:    0 RPM  (min =  600 RPM, max = 7200 RPM)
CHASSIS2 FAN Speed: 3515 RPM  (min =  600 RPM, max = 7200 RPM)
CHASSIS3 FAN Speed:    0 RPM  (min =  600 RPM, max = 7200 RPM)
POWER FAN Speed:       0 RPM  (min =  600 RPM, max = 7200 RPM)
CPU Temperature:     +27.0°C  (high = +60.0°C, crit = +95.0°C)
MB Temperature:      +45.0°C  (high = +45.0°C, crit = +95.0°C)
Por desgracia, en el resto de máquinas que tengo la información es mucho mas limitada.

El script que comprueba la temperatura de la VGA utiliza varios filtros y comandos para extraer a los datos. Su código es:
# cat /usr/local/bin/vgatemp.sh
#!/bin/bash

#ati radeon y nouveau se comprueban con "sensors"
#nvidia se comprueba con nvidia-smi
#intel no se puede monitorizar, no he encontrado herramienta
 
VGATEMP=0
if sensors | grep -q radeon-pci
then
  VGATEMP=$(sensors | sed -n '/radeon-pci/,/^$/p' | grep "^temp" | tr -d '+°C' | awk '{printf("%d\n",$2 + 0.5)}' | sort -nr | head -1)
else
   if sensors | grep -q nouveau-pci
   then
     VGATEMP=$(sensors | sed -n '/nouveau-pci/,/^$/p' | grep "^temp" | tr -d '+°C' | awk '{printf("%d\n",$2 + 0.5)}' | sort -nr | head -1)
   else
      if test -f /usr/bin/nvidia-smi
      then
        VGATEMP=$(nvidia-smi -q | grep "GPU Current Temp" | awk '{printf("%d\n",$5)}')
      fi
   fi

fi

#echo $VGATEMP # for debug only
exit $VGATEMP
El script que comprueba la temperatura de la placa base, según el hardware que usemos, sería:
# cat /usr/local/bin/mbtemp.sh
#!/bin/bash
hardware=$(facter hardware)
MBTEMP=0
case $hardware in
   "telefonica") MBTEMP=$(sensors |grep "^MB Temperature:"| awk '{printf $3}'  | tr -d '+°C'  | awk '{printf("%d\n",$1 + 0.5)}')
           ;;
   "siatic") MBTEMP=$(sensors | sed -n '/acpitz-virtual-0/,/^$/p' | grep "^temp" | tr -d '+°C' | awk '{printf("%d\n",$2 + 0.5)}' | sort -nr | head -1)
           ;;
   "infolab" ) MBTEMP=$(sensors | sed -n '/pch_skylake-virtual-0/,/^$/p' | grep "^temp" | tr -d '+°C' | awk '{printf("%d\n",$2 + 0.5)}' | sort -nr | head -1)
           ;;

   * ) MBTEMP=$(sensors | sed -n '/acpitz-virtual-0/,/^$/p' | grep "^temp" | tr -d '+°C' | awk '{printf("%d\n",$2 + 0.5)}' | sort -nr | head -1)

        ;;
esac
#echo $MBTEMP # for debug only
exit $MBTEMP
El script que obtiene la temperatura de todos los discos duros y devuelve la mayor entre ellas es:
# cat /usr/local/bin/hddtemp.sh
#!/bin/bash

HDDTEMP=0
DISCOS=$(lsblk  -l | grep disk | cut -f1 -d" ")
for i in $DISCOS
do
  TEMP=$(hddtemp  /dev/$i 2> /dev/null | tr -d "°C " | cut -d":" -f3)
  test -z "$TEMP" && TEMP="0"
  test "$TEMP" -gt "$HDDTEMP" && HDDTEMP="$TEMP"
done

#echo $HDDTEMP # for debug only
exit $HDDTEMP
El script que comprueba la temperatura de la CPU o sus cores y devuelve la mayor:
# cat /usr/local/bin/cputemp.sh
#!/bin/bash
hardware=$(facter hardware)
CPUTEMP=0
case $hardware in
   "telefonica") CPUTEMP=$(sensors |grep "^CPU Temperature"| awk '{printf $3}'  | tr -d '+°C'  | awk '{printf("%d\n",$1 + 0.5)}')
           ;;
   "siatic" | "infolab" ) CPUTEMP=$(sensors |grep "^Package id"| awk '{printf $4}'  | tr -d '+°C' | awk '{printf("%d\n",$1 + 0.5)}')
           ;;
   * )  CPUTEMP=$(sensors |grep "^Core "| tr -d '+°C' |  awk '{printf("%d\n",$3 + 0.5)}'  | sort -nr | head -1) #El mayor de los cores
        ;;
esac
#echo $CPUTEMP # for debug only
exit $CPUTEMP
En el caso del servidor del centro tenemos un HP Proliant que gracias al paquete hp-health nos permite acceder a más valores interesantes, como el estado de las dos fuentes de alimentación redundantes que tiene.
# cat /etc/monit/conf.d/monitrc.temperatura
check program mbtemperatura with path "/usr/local/bin/mbtemp.sh"
   if status > 60 then alert
   group temperature

check program cputemperature  with path "/usr/local/bin/cputemp.sh"
   if status > 60 then alert
   group temperature

check program hddtemperature with path "/usr/local/bin/hddtemp.sh"
   if status > 50 then alert
   group temperature

check program fanspeed with path "/usr/local/bin/fanspeed.sh"
   if status = 1 then alert
   group temperature

check program powersupply with path "/usr/local/bin/powersupply.sh"
   if status = 1 then alert
   group temperature
El script de chequeo de ambas fuentes de alimentación sería:
# cat /usr/local/bin/powersupply.sh
#!/bin/bash
fans=$(hplog -p | grep Normal | wc -l)
if [ $fans -ne 2 ] #Verificamos que ambas fuentes estén en estado "normal"
then
   salida=1
else
   salida=0
fi
#echo $salida # for debug only
exit $salida
El script de chequeo de los ventiladores (para el HP Proliant se usa hplog en lugar de sensors) sería:
# cat /usr/local/bin/fanspeed.sh
#!/bin/bash
fans=$(hplog -f | grep Normal | wc -l)
if [ $fans -ne 2 ] #Verificamos que ambos ventiladores estén en estado "normal"
then
   salida=1
else
   salida=0
fi
#echo $salida # for debug only
exit $salida
Nota: los valores de temperatura en monitrc.temperatura los he ido afinando con diversas pruebas a lo largo de 10 días, ya que no es raro que haya máximos puntuales sobre todo cuando se reproducen vídeos. Cada uno debe ir probando y cambiando valores hasta dar con el equilibrio óptimo que no te fría a mensajes y a la vez te alerte cuando algo empiece a ir mal.

Bueno, pues hasta aquí hemos llegado. Si nos salen nuevas cosas que comprobar haremos una tercera parte.

domingo, 14 de abril de 2019

Monitorizando los puestos del aula con monit (I)


Mi compañero Esteban ha publicado en su blog varios artículos muy interesantes sobre monit. Resumidamente: monit es un servicio que se encarga de monitorizar el sistema y asegurarse de que otros servicios funcionan y/o informarnos ante determinados eventos.

Aparte de usarlo en los servidores del centro también viene bien tenerlo en funcionamiento en los ordenadores de los profesores dentro del aula, ya que se ocupan de varias tareas críticas dentro del contexto de sus funciones y es recomendable tener asegurada y controlada su disponibilidad.

1. Configuración de alertas mediante correo.

Para que monit nos notifique cosas la opción más sencilla es que nos envíe correos electrónicos a través de una cuenta de Gmail. En mi caso ya tengo en el centro un servidor de correo postfix que redirige sus correos a través de la cuenta de Gmail, tal como conté en al apartado 2 de la entrada sobre la Gestión de Avisos del SAI usando nut, por lo que lo mas correcto es que los distintos servicios monit que hay por los pc del centro envíen sus alertas al servidor postfix y este a su vez lo reenvíe a través Gmail.

Para que el servidor postfix admita correos de cualquier PC de nuestra red hay que editar su fichero /etc/postfix/main.cf y añadir:
..
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 172.19.196.0/24
..
smtpd_client_restrictions = permit_mynetworks, reject
Siendo 172.19.196.0 mi red local y no olvidando reiniciar el servicio postfix después.

A continuación, en cada /etc/monit/monitrc añadimos:
.....
set mailserver 172.19.196.X port 25

set alert mi.correo@gmail.com not on {instance}

set mail-format { from: mi.correos@gmail.com
subject: $HOST - $SERVICE $EVENT a $DATE
message: Monit $ACTION $SERVICE a $DATE en $HOST, Descripcion: $DESCRIPTION.
}
Siendo 172.19.196.X el servidor donde está ejecutándose el servicio postfix en mi red y mi.correo@gmail.com la cuenta de correo Gmail usada para notificar las alertas. El "not on {instance}" es un filtro para que no nos envien correos de alerta cada vez que el servicio monit se enciende o se apaga, ya que son bastante molestos.

2. Monitorizando servicios básicos.

En los puestos de los profesores tengo como mínimo 2 servicios críticos: dhcp (para dar IP a los portátiles, thinclients o worskstations de alumnos) y tftp (para la imagen de arranque de thinclients).
# cat /etc/monit/conf.d/monit.dhcp
check process dhcpd matching "dhcpd"
  start program "/etc/init.d/isc-dhcp-server start"
  stop  program "/etc/init.d/isc-dhcp-server stop"
Y:
# cat /etc/monit/conf.d/monit.tftp
check process tftp matching "/usr/sbin/in.tftpd"
  start program "/etc/init.d/tftpd-hpa restart"  # El restart funciona mejor, porque a veces si no se hace stop luego no arranca.
  stop  program "/etc/init.d/tftpd-hpa stop"
De igual manera podemos ir añadiendo ficheros para monitorizar cualquier servicio que juzguemos imprescindible.

3. Monitorizando los puntos wifi.

Otro recurso crítico en las aulas son los puntos wifi para permitir la conexión de portátiles y dispositivos móviles. Por regla general están en la IP 192.168.0.1 de la red interna del aula:
# cat /etc/monit/conf.d/monit.puntowifi
check host punto-wifi  with address 192.168.0.1
      if failed icmp type echo count 5 with timeout 30 seconds then alert
De esta manera si hay algún problema con el punto wifi (bloqueo o desconexión de cables) nos llegará una alerta por correo.

4. Monitorizando la conexión de las pizarras digitales.

Esta es más divertida: las pizarras digitales se conectan por USB y a veces por accidente o por diabluras de los alumnos se desconecta el cable que las une al PC.
# cat /etc/monit/conf.d/monit.pizarra
check program pizarra with path /usr/local/bin/check_pizarra
    if status != 0 then alert
Evidentemente, para este caso no hay orden específica de monit, lo que tenemos es un script que busca la pizarra entre los dispositivos USB y devuelve 1 si no la encuentra.
# cat /usr/local/bin/check_pizarra
#!/bin/bash
# Devuelve 1 para avisar de desconexión, 0 si esá conectada o bien ya se avisó.

pizarra=$(lsusb -d 0b8c: ; lsusb -d 13ff:) # 0b8c:0069  0b8c:0001  13ff:0008 - Pizarras SmartBoard 480/640 y Galneo
if test -z "$pizarra"
then
     if ! test -f /var/cache/no-pizarra
     then
         retorno=1
         touch /var/cache/no-pizarra
     else
         retorno=0 # Ya se avisó, no necesario volver a hacerlo
     fi
else
      retorno=0
      rm -f /var/cache/no-pizarra  # Reconectada, se borra el testigo de aviso.
fi
exit $retorno
De esta manera se avisa de forma inmediata de cualquier desconexión, quedando registrado en el correo el momento en que sucedió por si necesitamos hacer indagaciones posteriores.

5. Monitorizando teclados y ratones.

Al igual que las pizarras digitales, los teclados y ratones en los puestos del profesor son causa de algún que otro problema. Ya traté este tema en otro artículo, pero ahora lo enfocaré para que la detección se haga desde monit:
# cat /etc/monit/conf.d/monit.kbmouse
check program teclado_raton with path /usr/local/bin/check_kb_mouse
    if status != 0 then alert
Siendo el script:
# cat /usr/local/bin/check_kb_mouse
#!/bin/bash

FICHERO=/var/cache/kbvigila

#En teclados Sweex USB, aparece mouse en devices, aun cuando no haya ratón 
#conectado. Con el filtro grep -v kbd eliminamos esas lineas, para que solo
#coja las corresponden a un ratón real.
raton=$(cat /proc/bus/input/devices  | grep -i mouse | grep -v kbd | grep Handlers| wc -l)

#En teclados RML, la cadena es "keykoard" en lugar de "keyboard"
teclado=$(cat /proc/bus/input/devices  | grep -i key[bk]oard | wc -l)

pizarra=$(lsusb -d 13ff:0008) # Pizarra Galneo
if test -n "$pizarra"
then
   #Las pizarras Galneo se identifican como ratón, asi que hay que decrementar el contador
   ((raton--))
fi

test $raton -eq 0  && hayraton="1" ||  hayraton="2"
test $teclado -eq 0 &&  hayteclado="1" || hayteclado="2"

#En hayXXX: Valor "1": no detectado. 
#           Valor "2": detectado. 

#Recupera el estado anterior de teclado y ratón.
if [ -f $FICHERO ]
then
   km_anterior=$(cat $FICHERO)
else
   km_anterior=""
fi
#Estado actual
km_actual="${hayteclado}${hayraton}"

#Si el estado teclado/raton ha cambiado respecto al estado anterior, devuelve 1
#para que haya una alerta
if  [ "$km_anterior" != "$km_actual" ]
then
   retorno=1
   #Guarda el estado actual para la siguiente comprobación.
   echo "${hayteclado}${hayraton}" > $FICHERO   
else
   retorno=0
fi
exit $retorno
Simplemente se comprueba el estado de conexión de teclado/ratón y se compara con el resultado de la comprobación anterior. Si ha variado algo se lanza un aviso.

6. Tarea puppet.

Todo esto lo distribuyo mediante una tarea puppet que aplicaría a las distintos pc de profesor que quiero monitorizar, siendo el init.pp de la tarea:
class  xubuntu18_monit {

        package { "monit": ensure =>installed }

        service { 'monit':
                ensure  => 'running',
                enable  => true,
                require => Package['monit'],
        }

        file { "monitrc":
                path => "/etc/monit/monitrc",
                owner => root, group => root, mode => 600,
                source => "puppet:///modules/xubuntu18_monit/monitrc",
                ensure => file,
                notify  => Service['monit'],  # Reinicia el servicio si el fichero cambia
                require => Package['monit'],
        }

        case $ies_isltsp {
                "si","yes","true" : {  #En los pc con thinclients hay serficio tftp
                        file { "monitrc-tftp":
                                path => "/etc/monit/conf.d/monitrc.tftp",
                                owner => root, group => root, mode => 600,
                                source => "puppet:///modules/xubuntu18_monit/monitrc.tftp",
                                ensure => file, notify  => Service['monit'], require => Package['monit'],
                        }
                }
                default: { 
                }
        }


        file { "monitrc-wifiap":
                path => "/etc/monit/conf.d/monitrc.wifiap",
                owner => root, group => root, mode => 600,
                source => "puppet:///modules/xubuntu18_monit/monitrc.wifiap",
                ensure => file, notify  => Service['monit'], require => Package['monit'],
        }

        file { "monitrc-pizarra":
                path => "/etc/monit/conf.d/monitrc.pizarra",
                owner => root, group => root, mode => 600,
                source => "puppet:///modules/xubuntu18_monit/monitrc.pizarra",
                ensure => file, notify  => Service['monit'], require => Package['monit'],
        }

        file { "pizarra":
                path => "/usr/local/bin/check_pizarra",
                owner => root, group => root, mode => 755,
                source => "puppet:///modules/xubuntu18_monit/check_pizarra",
        }

        file { "kb_mouse":
                path => "/usr/local/bin/check_kb_mouse",
                owner => root, group => root, mode => 755,
                source => "puppet:///modules/xubuntu18_monit/check_kb_mouse",
        }

}
7. Continuará...

Una vez montado el sistema lo seguiré ampliando con más scripts de monitorización de temperaturas de CPU, placa y GPU, espacio en disco, uso de CPU, estado de las fuentes de alimentación, etc. Lo iremos viendo en artículos sucesivos.


Llevamos unas semanas frenéticas para los espaciotrastornados. Podría hablar de:
  • Increíbles fotos de agujeros negros.


  • El éxito completo de la Falcon Heavy de Space con los tres cohetes reutilizados aterrizando suavemente en sus plataformas.


    ¡Eres grande, Elon Musk!

  • Próxima C: otro candidato a planeta rocoso a 4 años luz de nosotros.

Pero no, la imagen es un eclipse de sol desde la superficie Marte, al pasar las lunas Fobos/Deimos delante del astro rey. ¿Quién tomó esta maravillosa imagen? Mi admirada Opportunity:


Impresionante.