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

viernes, 15 de junio de 2018

Ubuntu 2018: postconfiguracion de thinclients en sistemas LTSP

Ya estamos preparando el salto a Ubuntu 18, motivo por el cual estoy atareado como gato en matanza y escribo poco por aquí. En nuestros planes esta seguir con las aulas de thinclient usando como clientes ordenadores de hace 13 años.

Me llena de orgullo y satisfacción verlos ejecutando Libreoffice 6 y Firefox 60 con fluidez mientras los miniportátiles comprados años después echan el bofe abriendo páginas de diarios regionales. Ahí va un video un Fujitsu P300 con 1Gb de RAM, un procesador Pentium IV y una tarjeta gráfica que da vergüenza ajena:



Debido a problemas con la tarjeta de red de los Fujitsu P300 hemos mantenido congelado el kernel de la serie 3.X con un driver retocado que soporta el encendido remoto mediante WOL. Mi compañero Francis se ha ocupado de integrar el entorno de soporte de los thinclients en Ubuntu 18 y ha estimado con muy buen criterio que no es necesario actualizar la versión de la imagen y es mejor quedarla en Ubuntu 14.

A fin de cuentas el único propósito existencial de la imagen de thinclients es levantar una conexión gráfica remota con el servidor de aula, que si corre Ubuntu 18, por lo que una vez inicia sesión el alumno todo funciona en ese entorno y el Ubuntu 14 queda oculto entre bambalinas.

Después de configurar el entorno LTSP como mandan los cánones siempre tenemos unos cuantos problemillas de ajustes que de una vez para otra se repiten y hay que tirar de nuestra memoria para solucionarlos. Estos cambios ya están integrados en la imagen que nos van a distribuir de Ubuntu 18 para LTSP, pero ahora voy a dejarlo anotado como chuleta para cuando pasemos a Ubuntu 22 y nuestros Fujitsu P300 sigan dando el callo junto con la Curiosity en Marte.

  • resolv.conf: los thinclients en ejecución deben tener el resolv.conf de nuestro centro, para poder conectar con otras máquinas si es necesario. Esto se hace poniendo el nameserver/domain del resolv.conf que queremos en la ruta /opt/ltsp/i386/etc/resolvconf/resolv.conf.d/original y regenerando la imagen del cliente con ltsp-update-image.
  • avahi: los clientes se anuncian a si mismos en la red del aula con el nombre "$HOSTNAME.local". De esta forma es fácil identificarlos y acceder a ellos sin averiguar su IP. Para que se anuncien es necesario que avahi esté activado en la imagen de los thinclients, pero por defecto este servicio está parado debido a que está en la lista negra de /usr/share/ltsp/init-ltsp.d/50-rm-system-services. Si quitamos avahi-daemon de ese fichero y regeneramos la imagen del cliente ya funcionará.
  • autenticación en thinclients: cuando hacemos alguna variación en el LTSP (cambio de nombre, clonado, etc) los alumnos no pueden iniciar sesión en los thinclients, dando error de credenciales. En apariencia todo esta bien (configuracion de pam, nsswitch, etc). La causa real es que entre servidor y thinclients se hace una conexión SSH y hay que regenerar esas claves usando "ltsp-update-sshkeys" y reconstruyendo luego la imagen.

Cuando haga la instalación del entorno LTSP en las Siatic subiré una guía. Nos vemos.

jueves, 14 de junio de 2018

Android: entrar y manejar el recovery de un tablet sin usar las teclas de volumen.

Me ha llegado una tablet Android baratuna moribunda de Hogarium que se queda en la pantalla de inicio sin cargar el sistema. Lo normal en esos casos es arrancar pulsando la teclas Power+Vol Down o Up, con lo que entramos en el recoverý de Android y allí hacemos un Factory Reset. Pero en esta tablet se resistía: después de dejarme los dedos cadavéricos apretando no había manera de entrar en el menú de recovery.

Bueno, no hay nada que no se pueda hacer con una buena línea de comandos. Así que enchufé la tablet a mi Manjaro Linux y tecleé:
# adb devices
....
# adb shell
Bien... me dejaba conectar con el adb del Android y entrar en el shell para ejecutar comandos. La tablet no estaba tan muerta como parecía. Ahora pongo:
 # adb reboot recovery
Y reiniciamos el sistema entrando en el recovery.


Ahora solo tengo que moverme con las teclas de volumen y hacer un "Wipe data/factory reset" y "Wipe cache" para restaurar la tablet a fábrica y ver si funciona. Horror, compruebo que siguen sin funcionar la teclas de volumen. En este punto solo hay dos explicaciones: o esas teclas están averiadas (lo cual, como a Rajoy, no me consta) o bien el firmware de la tablet es tan cutre que no trae el driver para manejarlas hasta que no se ha cargado el sistema.

¿Qué hago?: al conector USB micro del tablet (el típico de carga de los móviles) conecto un cable OTG


y a éste un teclado USB. Entro de nuevo en el recovery et voilá... puedo moverme por el menú con las teclas subir/bajar y ejecutar las acciones con Enter. Ya si puedo hacer un Factory Reset y de la Caché. Minutos después tenemos la tablet arrancada ejecutando el asistente de configuración de Android. Otro aparato salvado de la quema.

Por cierto, una vez arrancado el Android completo las teclas de volumen si funcionan :-D.

miércoles, 16 de mayo de 2018

Captura de la pantalla de los alumnos (II)


En el anterior artículo vimos como capturar una imagen de las escritorios de los thinclients en un servidor de aula.

En el presente veremos como capturar las pantallas de pc normales (portátiles o pc de sobremesa). Aunque visualmente no hay diferencia, el reto es ahora es distinto ya que en este escenario los escritorios a capturar corren en pc remotos cuyas sesiones se ejecutan físicamente fuera del ordenador del profesor. En los thinclients todo eran tareas realizadas dentro del propio servidor de aula.

Para esta entrada asumimos que todos los pc de alumnos están dentro de una VLAN privada del aula, con direccionamiento 192.168.0.X.

La idea del proceso es:

  • En los PC de los alumnos hay un script que al ejecutarse hace una captura del escritorio y manda la pantalla al PC del profesor por algún medio.
  • En el PC del profesor hay un script que rastrea el aula en busca de PC de alumnos encendidos y ejecuta remotamente el script anterior en paralelo sin que haya petición de contraseña.
  • Todas las imágenes recopiladas se reunen en un único directorio y se abren para que el profesor pueda verlas.

En primer lugar, ¿cómo hacemos que el desde el pc del profesor se pueda ejecutar algo en los pc de los alumnos sin pedir contraseña?... pues con una relación de confianza. Esto puede automatizarse para que una vez generada la clave pública se distribuya mediante puppet a todos los pc de alumnos usando el recurso ssh_authorized_key.

En segundo lugar, ¿cómo hacemos para ejecutar un comando a la vez en varias máquinas desde el pc del profesor?...pues hay múltiples alternativas. En nuestros centros tenemos la suite "utilaula", desarrollada por nuestro compañero Ricardo, que incluye el script ejecutaenhosts:
ejecutaenhosts -p comando
y que ejecuta el comando indicado en paralelo en todos los pc de alumnos que encuentra dentro del aula en el espacio 192.168.0.X. Al final de esta entrada muestro una versión reducida de este script, pero cada cual puede usar la técnica que desee de las enumeradas en el enlace anterior.

En tercer lugar ¿cómo enviamos la imagen capturada al pc del profesor?. Evalué varias alternativas: copiarlas por scp desde el pc del profesor, enviarlas por tftp desde los pc de los alumnos, enviarlas por netcat ...al final la más simple que encontré es usar una carpeta samba para que los clientes dejen los ficheros y después recogerlos el profesor. Es una técnica que ya tengo montada en mi IES para otros scripts y siempre me ha funcionado bastante bien.

1. Script de captura en los pc de los alumnos.

Vamos primero a ver el script de captura que pondremos en todos los pc de los alumnos:
# cat /usr/bin/captura-escritorio
#!/bin/bash

directorio=$1
pc=$(hostname)
user=$(who | grep "(:0)" | head -1 | cut -f1 -d" ")
if [ -n "$user" ]
then
          su $user -c "DISPLAY=:0 import -window root /tmp/$user-$pc.jpg"
          cd /tmp
          smbclient -N //172.X.Y.Z/almacen -c "cd $directorio; put $user-$pc.jpg"
fi
exit 0 

Comentemos:
  • Toma un parámetro de entrada que será el directorio donde se dejará la imagen capturada del escritorio usando smbclient para subir el fichero a la carpeta remota.
  • La captura se hace de una forma similar a como se hacia en la entrada pasada, con su $user -c "DISPLAY=:0 import -window root /tmp/$user-$pc.jpg".
  • Envía la captura a una carpeta compartida por samba llamada "almacen" en el servidor 172.X.Y.Z, tal como describimos aquí.

2. Scripts en el pc del profesor.

A continuación vemos el programa que ordena la ejecución del script anterior en todos los pc de alumnos, que irá en sudoers ya que se ejecutará como root (esto también sucedió en la parte I de esta serie de entradas).
# cat /etc/sudoers
....
....
%teachers ALL = (ALL) NOPASSWD: /usr/bin/captura-escritorio-alumnos
....
....

El script que lanza la ejecución remota:
# cat /usr/bin/captura-escritorio-alumnos
#!/bin/bash
directorio=$1
/sbin/ejecutaenhosts -p "/usr/bin/captura-escritorio $directorio" 
exit 0

Comentemos:
  • De nuevo toma un parámetro de entrada que será el directorio dentro de "almacen" donde los clientes dejarán la imagen de captura.
  • Usa ejecutaenhosts para lanzar el comando en paralelo en todos los alumnos conectados dentro de la red privada del aula. Como dijimos antes, ejecutaenhosts es un script nuestro de los centros, pero se puede usar cualquier otro método.
Por último, el script que ejecutará el profesor y que lanza todo el proceso:
# cat /usr/bin/captura-escritorio-todos
#!/bin/bash

directorio=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1)
smbclient -N //172.X.Y.Z/almacen -c "mkdir $directorio"

pc=$(hostname)
fecha=$(date +%Y-%m-%d)
hora=$(date +%H:%M:%S)
destino="$HOME/capturas/$pc/$fecha/$hora"
rm -rf "$destino"
mkdir -p "$destino"

sudo /usr/bin/captura-escritorio-alumnos $directorio

cd "$destino"
smbclient -N //172.X.Y.Z/almacen -c "cd $id; prompt off; mget *; del *;cd ..;rd $id"

thunar "$destino" &

exit 0

Comentemos:
  • El nombre del directorio donde guardar las imágenes dentro de //172.X.Y.Z/almacen lo formamos con 12 caracteres aleatorios y lo creamos remotamente.
  • Creamos una carpeta local en el home del profesor con el pc, la fecha y la hora de la captura.
  • Llamamos al script de captura mediante sudo. Cuando acaba recogemos todas las capturas de "almacen", las metemos en la carpeta local, borramos la carpeta remota y abrimos "thunar" para mostrarlas al profesor
Como vemos, el resultado final es el mismo que en la entrada anterior, solo que la forma de conseguirlo es bastante distinta dada la diferente arquitectura de funcionamiento del aula.

Se aconseja poner al profesor un fichero .desktop en el escritorio para ejecutar "captura-escritorio-todos".

3. Ejecución en paralelo.

Pongo una versión light de ejecutaenhosts para que lo adaptes a tus necesidades si no lo tienes. Necesitamos que, tal como hemos comentado, la clave pública ssh haya sido repartida previamente a los alumnos para evitar que se pida la contraseña con cada ssh.
# cat /sbin/ejecutaenhosts
#!/bin/bash

if [ "$1" \= "-p" ] ; then 
   enparalelo="yes"
   shift
fi
#Busca los hosts conectados en la vlan en el rango 200-253
listahosts=$(nmap -oG - --system-dns  -sP 192.168.0.200-253 | grep -v '^#' | tr -d '()' | sed 's/.local//' | awk '{print $3 ";" $2}' | sort)
if [ -z "$listahosts" ]; then
  echo "No se ha encontrado ninguna maquina conectada"
  exit 0
fi

for hostip in $listahosts 
do
  host=$(echo $hostip | cut -d";" -f1)
  ip=$(echo $hostip | cut -d";" -f2)
  
  ssh -o StrictHostKeyChecking=no -o ConnectTimeout=9 -o ServerAliveCountMax=2 -o ServerAliveInterval=15 root@$ip $* > /dev/null 2>&1 &
done

echo -n "Esperando a que terminen los procesos "
for job in $(jobs -p)
do
     echo -n "."
     wait $job
done
echo -e "\n"
echo "Hecho!"
exit 0

Y con esto ya estamos, solo nos queda un caso más para tener cerrada la serie.

Nos vamos con la música a otra parte:


sábado, 12 de mayo de 2018

Monitorizando nuestro SAI con nut (Parte 4)

Hace ya tiempo que no dedico una entrada al SAI, pero como se me estropeó el que tenía y recibí uno de reemplazo, al conectarlo y monitorizarlo de nuevo he aprovechado para probar y cerrar temas pendientes. Las entradas precedentes eran:


En el apartado 5 de la última entrada dejaba abierto el problema del apagado del servidor cuando está funcionando con baterías, un tema peliagudo que no tenía muy probado por falta de tiempo y oportunidad. No es plan desconectar el SAI de la corriente y ver que pasa en horario de producción. Ahora si he probado con calma y estas son las conclusiones.

1. Ficheros de configuración básica.

Empecemos repasando como tenemos los ficheros básicos en /etc/nut:

/etc/nut/hosts.conf
MONITOR salicru@localhost "UPS Virgen de Guadalupe"
/etc/nut/nut.conf
MODE=standalone
/etc/nut/ups.conf
maxretry = 3
[salicru]
driver = blazer_usb
port = auto
subdriver = cypress
protocol = mustek
desc = "SAI Salicru"
vendorid = 0665
productid = 5161
bus = "004" # adaptalo a tu caso 
/etc/nut/upsd.conf
LISTEN 127.0.0.1 3493
#IP del servidor donde esta conectado el SAI con el cable USB.
LISTEN 172.20.123.2 3493
/etc/nut/upsd.users
[admin_sai]
password = 13578axd
upsmon master
/etc/nut/upsset.conf
deny from all
allow from 172.20.123.0  
/etc/nut/upsmon.conf
MONITOR salicru@localhost 1 admin_sai 13578axd master
RUN_AS_USER nut
MINSUPPLIES 1
SHUTDOWNCMD "/root/scripts/apagar_servidores.sh"
POLLFREQ 5
POLLFREQALERT 5
HOSTSYNC 15
DEADTIME 15
POWERDOWNFLAG /etc/killpower
RBWARNTIME 43200
NOCOMMWARNTIME 300
FINALDELAY 5
NOTIFYCMD "/sbin/upssched"
NOTIFYMSG ONLINE "UPS: Normal state"
NOTIFYMSG ONBATT "UPS: On battery"
NOTIFYMSG LOWBATT "UPS: Battery low"
NOTIFYMSG FSD "UPS: Starting shutdown"
NOTIFYMSG COMMOK "UPS: Communication restored"
NOTIFYMSG COMMBAD "UPS: Communication lose"
NOTIFYMSG SHUTDOWN "UPS: Shutting down"
NOTIFYMSG REPLBATT "UPS: Replace battery"
NOTIFYMSG NOCOMM      "UPS %s is unavailable"
NOTIFYMSG NOPARENT    "upsmon parent process died - shutdown impossible"
NOTIFYFLAG ONLINE SYSLOG+WALL+EXEC
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC
NOTIFYFLAG LOWBATT SYSLOG+WALL+EXEC
NOTIFYFLAG FSD SYSLOG+WALL+EXEC
NOTIFYFLAG COMMOK SYSLOG+WALL+EXEC
NOTIFYFLAG COMMBAD SYSLOG+WALL+EXEC
NOTIFYFLAG SHUTDOWN SYSLOG+WALL+EXEC
NOTIFYFLAG REPLBATT SYSLOG+WALL+EXEC
NOTIFYFLAG NOCOMM     SYSLOG+WALL+EXEC
NOTIFYFLAG NOPARENT   SYSLOG+WALL+EXEC
/etc/nut/upssched.conf:
CMDSCRIPT /usr/bin/upssched-cmd
PIPEFN /tmp/upssched.pipe
LOCKFN /tmp/upssched.lock
AT ONBATT * START-TIMER ups-on-battery 15
AT ONLINE * CANCEL-TIMER ups-on-battery
AT ONLINE * EXECUTE ups-back-on-line
AT REPLBATT * EXECUTE ups-change-battery  
AT LOWBATT * EXECUTE ups-low-battery  
AT FSD * EXECUTE ups-fsd
AT SHUTDOWN * EXECUTE ups-shutdown
AT COMMOK * EXECUTE ups-comunication-ok 
AT COMMBAD * EXECUTE ups-comunication-bad
Of course, en todo lo anterior pondremos el usuario que nos apetezca (en lugar de admin_sai), contraseña y direccionamiento IP correspondiente. Los que he puesto yo son de pega.

2. Proceso de apagado desde nut.

Con calma he leído todo lo que he encontrado sobre el proceso de eventos que genera NUT cuando el SAI está funcionando con batería y empieza a descargarse. Repasamos la secuencia de eventos:

  • El SAI está operando con corriente y con los 2 leds de la derecha del frontal encendidos. Se va la corriente eléctrica y se activa la batería del SAI. De los 6 leds que tiene se encienden en verde los 5 de la derecha, a modo de indicador de carga máxima y comienza a sonar un beep cada pocos segundos. Se irán apagando conforme se descargue la bateria y el beep sonará cada vez con mayor cadencia.
  • Si miramos el estado del SAI con "upsc salicru ups.status" vemos que está en "OB", On Battery. El demonio ups-monitor ha enviado el evento ONBATT para que lo procese el ups-scheduler.
  • Pasa el tiempo y la batería se va descargando. Con "upsc salicru battery.charge" podemos ver el % de carga.
  • Algunos SAI tienen un parámetro battery.charge.low que permite definir el umbral de carga mínima de la batería. Ese umbral se puede redefinir poniendo "override.battery.charge.low = 25" en /etc/nut/ups.conf, pero nuestro Salicru no lo tiene y el umbral está fijado en el 10%. Podemos poner dicho parámetro, reiniciar el demonio y ver con "upsc salicru" que battery.charge.low = 25, pero a la hora de la verdad dicho valor es ignorado y todo se comporta como ese 10% estuviese escrito en piedra.
  • En general: la forma de determinar si la batería está baja depende el SAI/driver concreto y se calcula en función de battery.charge/battery.charge.low y battery.runtime/battery.runtime.low.
  • Pasado el umbral de batería baja se genera un evento LOWBATT para que lo procese el ups-scheduler. Aquí empieza la fiesta.
  • Si nuestro nut tiene esclavos (como vimos en la entrada anterior del blog), se les notifica un evento FSD (forced shutdown). A su vez, cada esclavo:
    • Genera un evento SHUTDOWN.
    • Espera FINALDELAY segundos(tipicamente 5).
    • Ejecuta su propio SHUTDOWNCMD.
    • Desconecta del upsd maestro.
  • El nut maestro, si ha enviado el FSD a los esclavos espera HOSTSYNC segundos (típicamente 15) para darles tiempo a desconectar. Pasado esto se envía un FSD a si mísmo, lo cual:
    • Genera un evento SHUTDOWN.
    • Espera FINALDELAY segundos(tipicamente 5).
    • Crea el fichero POWERDOWNFLAG - usualmente /etc/killpower.
    • Ejecuta su propio SHUTDOWNCMD.
  • A diferencia del resto de código, el comando SHUTDOWNCMD se ejecuta con permisos de root. Esto es debido a que siempre hay en marcha dos instancias de upsmon, una con el usuario "nut" y otra con el usuario "root". Está última está casi siempre ociosa y solo existe para ejecutar lo que diga el parámetro SHUTDOWNCMD definido en upsmon.conf.
Esta es la teoría, haciendo una prueba en entorno real vamos a describir lo que pasa en la práctica:
  • Tengo conectado al SAI una fuente del servidor principal, un servidor auxiliar y un monitor. Desconecto el SAI de la corriente.
  • El parámetro ups.status se pone en OB. battery.charge baja rápidamente en menos de un minuto del 100% al 85-88%. Esto pinta mal.
  • Se estabiliza y empieza a bajar despacio, con los leds frontales apagándose conforme avanza la carga. Este es el ritmo:

    HORA CARGA STATUS EVENTO
    16:40 100% OL
    16:41 100% OB ONBATT
    16:42 88% OB
    17:09 69% OB
    17:24 65% OB
    17:42 29% OB
    17:45:11 15% OB
    17:45:25 12% OB
    17:45:51 10% OB LB FSD LOWBAT, FSD
    17:45:56 4% OB LB FSD EJECUCIÓN SHUTDOWNCMD
    17:46:11 0% OB LB FSD
    17:46:26 42% OL FSD VUELVE LA CORRIENTE
    ONLINE

    Para hacer el cuadrante anterior he capturado los datos con este script:
    #!/bin/bash
    
    while true
    do
       hora=$(date)
       carga=$(upsc salicru battery.charge 2> /dev/null)
       estado=$(upsc salicru ups.status  2> /dev/null)
       voltio=$(upsc salicru battery.voltage  2> /dev/null)
       echo "$hora -> ${carga}% $estado $voltio" >> /root/sai-status.txt
       echo "$hora -> ${carga}% $estado $voltio"
       sleep 15
    done
    
  • Conclusiones:
    • Hay una descarga rapidísima al comienzo, de un 12% en cuestión de segundos.
    • Luego va bajando con suavidad durante una hora. A los 30 minutos está al 70%. A los 60 llega al 15%.
    • Cuando llega al 15% empieza a bajar de nuevo en caída libre y en 2 minutos llega al 0%.
    • Al bajar del 10% se generan los eventos LOWBATT y FSD. Pocos segundos después se llama a SHUTDOWNCMD con credenciales de root.
    • Con la batería al 0% no se corta la corriente, pero no he querido tentar cuanto aguanta así hasta el apagado físico real.
    • Al conectar de nuevo el SAI a la corriente vuelve a cargar y el estado OB se transforma en OL, pero se queda activado el flag FSD ya que no ha llegado a suceder el apagado del servidor.

En resumen: el SAI aguanta 60 minutos o más con 2 servidores y un monitor, pero tiene una curva de descarga bastante acelerada al comienzo y al final. Desde que se genera el evento LOWBATT hasta que se llega al 0% de carga pasan segundos, lo que queda muy poco tiempo de maniobra si queremos apagar varias máquinas.

Una segunda cosa a que destacar que el SAI queda en estado "FSD OL" al volver la corriente en el último momento. Para quitar el flag FSD sin reiniciar el servidor hay que reiniciar el demonio nut-server, pero al hacer eso algunas veces (no siempre) se genera un evento FSD y se ejecuta un SHUTDOWCMD. Esto puede provocar que si no tenemos precauciones con lo que se hace en SHUTDOWNCMD se apague el servidor estando el SAI ya con corriente externa.

En general, siempre que llegamos al estado FSD (incluso manualmente haciendo "/sbin/upsmon -c fsd") tarde o temprano se ejecutará SHUTDOWNCMD, por lo que es algo que habrá que tener en cuenta.

En el siguiente apartado veremos como he abordado las cuestiones planteadas.

3. Configuración del apagado.

Ya que los pocos segundos que transcurren desde que se genera el LOWBATT+FSD hasta que la batería llega al 0% no me inspiran ninguna confianza lo que voy a hacer es que cuando se lleven 30 minutos de batería (momento en que está está sobre el 70%) se apague el servidor de forma segura.

Podría forzar más tiempo, pero si en media hora no ha vuelto la corriente parece que la cosa va para largo y... ¿para que esperar lo inevitable?

Modificamos /etc/nut/upssched.conf para incluir un nuevo timer de 1800 segundos:
CMDSCRIPT /usr/bin/upssched-cmd
PIPEFN /tmp/upssched.pipe
LOCKFN /tmp/upssched.lock
AT ONBATT * START-TIMER  ups-on-battery-shutdown 1800
AT ONLINE * CANCEL-TIMER  ups-on-battery-shutdown
AT ONBATT * START-TIMER ups-on-battery 15
AT ONLINE * CANCEL-TIMER ups-on-battery
AT ONLINE * EXECUTE ups-back-on-line
AT REPLBATT * EXECUTE ups-change-battery  
AT LOWBATT * EXECUTE ups-low-battery  
AT FSD * EXECUTE ups-fsd
AT SHUTDOWN * EXECUTE ups-shutdown
AT COMMOK * EXECUTE ups-comunication-ok 
AT COMMBAD * EXECUTE ups-comunication-bad
Los eventos son gestionados con /usr/bin/upssched-cmd:
#! /bin/bash
#
# This script should be called by upssched via the CMDSCRIPT directive.
# 
# Here is a quick example to show how to handle a bunch of possible
# timer names with the help of the case structure.
#
# This script may be replaced with another program without harm.
#
# The first argument passed to your CMDSCRIPT is the name of the timer
# from your AT lines.

function mailSend() {

   echo "$2" | mail -s "$1" -a "From: Avisos.ies "  tu.correo@gmail.com

}

MESSAGE_MAIL=""
EVENT_TYPE=$1
APAGADO=0
FECHA=$(date)

case "$EVENT_TYPE" in
    "ups-on-battery-shutdown")
      #Saltado evento AT ONBATT * START-TIMER  ups-on-battery-shutdown 1800: llevamos 30 minutos con bateria. Apagamos.
      MESSAGE_MAIL="UPS on battery: shutdown now"
      APAGADO=1
    ;;
    "ups-on-battery")
      MESSAGE_MAIL="UPS on battery: warning"
    ;;
    "ups-comunication-bad")
      MESSAGE_MAIL="Communications with UPS lost"
    ;;
    "ups-change-battery")
      MESSAGE_MAIL="UPS battery needs to be replaced"
    ;;
    "ups-back-on-line")
      MESSAGE_MAIL="UPS on line power"
    ;;
    "ups-low-battery")
      MESSAGE_MAIL="UPS battery is low"
    ;;
    "ups-comunication-ok")
      MESSAGE_MAIL="Communications with UPS established"
    ;;
    "ups-shutdown")  # Llega tras el FSD
      MESSAGE_MAIL="FSD: shutdown message"
    ;;
    *)
      MESSAGE_MAIL="Aviso tipo: $EVENT_TYPE"
    ;;
esac

mailSend  "$FECHA => $MESSAGE_MAIL"  "[UPS] Event $EVENT_TYPE de SAI Virgen de Guadalupe:"
echo "$FECHA => SAI: $MESSAGE_MAIL" >> /var/log/sai.log  # /var/log/sai.log permisos 664 y root:nut

if [ $APAGADO = "1" ]
then
    sleep 30
    /sbin/upsmon -c fsd #  Esto genera un evento ups-fsd y un evento ups-shutdown, que a su vez llama a SHUTDOWNCMD como root.
fi
La mayoría de los eventos mandan un correo y escriben en el log /var/log/sai.log. Tan sólo es difrente el salto del timer ups-on-battery-shutdown lanza una orden "/sbin/upsmon -c fsd", que genera a su vez un evento FSD y posteriormente dispara la ejecución de SHUTDOWNCMD, que apunta a /root/scripts/apagar_servidores.sh:
#!/bin/bash

function mailSend() {

   echo "$2" | mail -s "$1" -a "From: Avisos.ies "  tu.correo@gmail.com

}

FECHA=$(date)
estado=$(upsc salicru ups.status 2> /dev/null)
en_bateria=$(echo $estado | tr ' ' '\n' | grep "OB")

if [ -z $en_bateria ]  #Si en up.status no parece OB, no estamos en bateria. Es un SHUTDOWNCMD innecesario
then
   MESSAGE_MAIL="Evento SHUTDOWNCMD pero estado $estado. No apagamos el servidor"
   mailSend  "$FECHA => $MESSAGE_MAIL"  "[UPS] Evento SHUTDOWNCMD"
   echo "$FECHA => SAI: $MESSAGE_MAIL" >> /var/log/sai.log  # /var/log/sai.log permisos 664 y root:nut
else
   MESSAGE_MAIL="Apagado servidor por evento SHUTDOWNCMD"
   mailSend  "$FECHA => $MESSAGE_MAIL"  "[UPS] Evento SHUTDOWNCMD"
   echo "$FECHA => SAI: $MESSAGE_MAIL" >> /var/log/sai.log  # /var/log/sai.log permisos 664 y root:nut
   ssh root@servidor-auxiliar "/sbin/shutdown -h +0"
   sleep 10
   /sbin/shutdown -h +0
fi
exit 0
El script anterior recordemos que se ejecuta como root.

Primero comprueba que el SAI sigue en estado OB (en bateria). Si no es así, manda un mensaje diciendo que aunque se haya recibido un FSD-SHUTDOWN no se apagará nada. Esto previene que si tenemos el flag FSD activado estando OL (ON LINE, con corriente) se apague el servidor accidentalmente al reiniciar el demonio nut-server.

Si el SAI está en estado OB se mandan mensajes, se apaga un servidor auxiliar (con el que tenemos una relación de confianza para ejecutar comandos por ssh en él) y finalmente se apaga el servidor que ejecuta nut. Todo esto se hace a la media hora de estar funcionando con el SAI, por lo que da tiempo a cerrar todo tranquilamente ya que queda mas de la mitad de la batería.

4. Fuentes.

Acabo haciendo honores a las fuentes que he seguido para redactar esta entrada:

Y con esto creo que quedo cerrado del todo el tema de nut mientras que no suceda nada excepcional.

Nos leemos pronto.

lunes, 23 de abril de 2018

Sistema de gestión de turnos y colas (II)

Se acerca la temporada de colas en el IES y desempolvamos nuestro sistema para dar gestionar las colas de espera para hacer algunas reformitas.

En su día creamos 3 scripts para incrementar, decrementar y poner a cero el contador de turnos. Hoy empezamos añadiendo un script nuevo que nos permitirá dar un valor concreto, que entrará como parámetro, al marcador:
# cat pon_turno.sh 
#!/bin/bash

num=$1
test "$num" == "" && num="00"

servidor="172.X.Y.Z"
topic="numero"

if num=$(printf "%02d" $num 2> /dev/null)
then  #Si $num no es un numero, falla el comando anterior
  mosquitto_pub -t $topic -h $servidor -m "A-$num" -r
fi
Esto será útil si hay cualquier fallo que descontrole el marcador y necesitemos ir hasta un número directamente.

Para continuar, se me planteó la siguiente necesidad: controlar los turnos desde una aplicación normal y corriente como alternativa al método de la entrada anterior del blog (la cual usaba un ratón y triggerhappy como sistema para incrementar/decrementar el control del marcador de turnos). Podría escribir una aplicación de escritorio en Python y GTK, ya que con bash sería mas complicado, pero al final me he ido a lo mas sencillo y he optado por usar una aplicación web con Javascript+Mosquitto, usando la misma tecnología con que hice la pantalla que muestra el sistema de turnos del post anterior.

El aspecto de la aplicación será:


Al pulsar "Definir Turno" nos permitirán introducir un turno nuevo directamente:


Todo esto se implementa con una página HTML con mucho CSS y Javascript, ahí va:
# cat /var/www/turnos/GestionTurnos.html 
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
  <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sistema de Gestión de Turnos</title>

    <style type="text/css">

        html, body {
             margin: 0px;
             height: 100%;
             background-color: #000099;
        }
        @font-face{
             font-family: 'Antonio';
             src: url('Antonio-Regular.ttf') format('truetype');
        }
        #Pagina {
             font-family: 'Antonio', Arial, sans-serif;
             margin: 0px;
             height: 100%;
        }
        #cabecera {
             font-style: normal;
             font-weight: bold;
             background-color: #000099;
             font-size:7vw;
             width: 100%;
             color: white;
             text-align: center;
             background-image: url(img/logo_guadalupe.png);
             background-position: left center;
             background-repeat: no-repeat;
        }
        #cuerpo {
             color: yellow;
             text-align: center;
             background-color: black;
             font-size:22vw;
             font-weight: bold;
             width: 100%;
             height: 60%;
        }
        #pie {
             font-size:7vw;
             background-color: #000099;
             font-weight: bold;
             width: 100%;
             height: 15%;
             color: white;
             font-style: normal;
             text-align: center;
        }

 .botonAvanza {
  -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
  -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
  box-shadow:inset 0px -3px 7px 0px #29bbff;
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
  background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
  background-color:#2dabf9;
  -moz-border-radius:12px;
  -webkit-border-radius:12px;
  border-radius:12px;
  border:1px solid #0b0e07;
  display:inline-block;
  cursor:pointer;
  color:#ffffff;
  font-family:Arial;
  font-size:28px;
  padding:22px 48px;
  text-decoration:none;
  text-shadow:0px 1px 0px #263666;
                vertical-align:middle;
 }
 .botonAvanza:hover {
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0688fa), color-stop(1, #2dabf9));
  background:-moz-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-webkit-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-o-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-ms-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:linear-gradient(to bottom, #0688fa 5%, #2dabf9 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0688fa', endColorstr='#2dabf9',GradientType=0);
  background-color:#0688fa;
 }
 .botonAvanza:active {
  position:relative;
  top:1px;
 }

 .botonRetrocede {
  -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
  -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
  box-shadow:inset 0px -3px 7px 0px #29bbff;
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
  background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
  background-color:#2dabf9;
  -moz-border-radius:12px;
  -webkit-border-radius:12px;
  border-radius:12px;
  border:1px solid #0b0e07;
  display:inline-block;
  cursor:pointer;
  color:#ffffff;
  font-family:Arial;
  font-size:28px;
  padding:22px 48px;
  text-decoration:none;
  text-shadow:0px 1px 0px #263666;
                vertical-align:middle;

 }
 .botonRetrocede:hover {
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0688fa), color-stop(1, #2dabf9));
  background:-moz-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-webkit-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-o-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-ms-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:linear-gradient(to bottom, #0688fa 5%, #2dabf9 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0688fa', endColorstr='#2dabf9',GradientType=0);
  background-color:#0688fa;
 }
 .botonRetrocede:active {
  position:relative;
  top:1px;
 }

       .botonSet {
  -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
  -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
  box-shadow:inset 0px -3px 7px 0px #29bbff;
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
  background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
  background-color:#2dabf9;
  -moz-border-radius:12px;
  -webkit-border-radius:12px;
  border-radius:12px;
  border:1px solid #0b0e07;
  display:inline-block;
  cursor:pointer;
  color:#ffffff;
  font-family:Arial;
  font-size:28px;
  padding:22px 48px;
  text-decoration:none;
  text-shadow:0px 1px 0px #263666;
                vertical-align:middle;

 }
 .botonSet:hover {
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0688fa), color-stop(1, #2dabf9));
  background:-moz-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-webkit-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-o-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-ms-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:linear-gradient(to bottom, #0688fa 5%, #2dabf9 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0688fa', endColorstr='#2dabf9',GradientType=0);
  background-color:#0688fa;
 }
 .botonSet:active {
  position:relative;
  top:1px;
 }

        .inputturno {
                -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
                -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
                box-shadow:inset 0px -3px 7px 0px #29bbff;
                background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
                background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
                filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
                background-color:#2dabf9;
                -moz-border-radius:12px;
                -webkit-border-radius:12px;
                border-radius:12px;
                border:1px solid #0b0e07;
                display:inline-block;
                cursor:pointer;
                color:#ffffff;
                font-family:Arial;
                font-size:28px;
                padding:22px 48px;
                text-decoration:none;
                text-shadow:0px 1px 0px #263666;
                vertical-align:middle;
                width: 30px;
        }

       .labelturno {
                display:inline-block;
                cursor:pointer;
                color:#ffffff;
                font-family:Arial;
                font-size:28px;
                padding:22px 2px;
                text-decoration:none;
                text-shadow:0px 1px 0px #263666;
                vertical-align:middle;
        }

    
    </style>

    <script src="http://tercero/turnos/mqttws31.js" type="text/javascript"></script>
    <script src="http://tercero/turnos/jquery.min.js" type="text/javascript"></script>
    <script src="http://tercero/turnos/config.js" type="text/javascript"></script>

    <script type="text/javascript">

    var valor;
    var mqtt;
    var reconnectTimeout = 2000;
    function MQTTconnect() {
 if (typeof path == "undefined") {
  path = '/mqtt';
 }
 mqtt = new Paho.MQTT.Client(
   host,
   port,
   path,
   "web_" + parseInt(Math.random() * 100, 10)
 );
        var options = {
            timeout: 3,
            useSSL: useTLS,
            cleanSession: cleansession,
            onSuccess: onConnect,
//            mqttVersion: 3, 
            onFailure: function (message) {
                $('#status').val("Connection failed: " + message.errorMessage + "Retrying");
                setTimeout(MQTTconnect, reconnectTimeout);
            }
        };

        mqtt.onConnectionLost = onConnectionLost;
        mqtt.onMessageArrived = onMessageArrived;

        if (username != null) {
            options.userName = username;
            options.password = password;
        }

        console.log("Host="+ host + ", port=" + port + ", path=" + path + " TLS = " + useTLS + " username=" + username + " password=" + password);
        mqtt.connect(options);
    }

    function onConnect() {
        $('#status').val('Connected to ' + host + ':' + port + path);
        // Connection succeeded; subscribe to our topic
        mqtt.subscribe(topic, {qos: 0});
        $('#topic').val(topic);
    }

    function onConnectionLost(response) {
        setTimeout(MQTTconnect, reconnectTimeout);
        $('#status').val("connection lost: " + responseObject.errorMessage + ". Reconnecting");

    };

    function onMessageArrived(message) {

        var topic = message.destinationName;
        var payload = message.payloadString;
        valor=payload;
        $('#cuerpo').html(payload);
    };


    $(document).ready(function() {
        MQTTconnect();
        var caja=$('#id_turno');
        caja.hide();
        var label=$('#id_label');
        label.hide();
    });

    function subirTurno () {

         var numero=valor.substring(2);
         if (numero==99) numero=0;
         else numero++;
          
         topicMessage = new Paho.MQTT.Message("A-"+("00" + numero).slice (-2));  //Para añadir padZero a la izquierda del numero
         topicMessage.destinationName = "numero";
         topicMessage.retained = true;
         mqtt.send(topicMessage);

    }

    function bajarTurno () {

         var numero=valor.substring(2);
         if (numero==0) numero=0;
         else numero--;

         topicMessage = new Paho.MQTT.Message("A-"+("00" + numero).slice (-2));  //Para añadir padZero a la izquierda del numero
         topicMessage.destinationName = "numero";
         topicMessage.retained = true;
         mqtt.send(topicMessage);

    }


   function verCaja () {

        var caja=$('#id_turno');
        caja.show();
        var numero=valor.substring(2);
        caja.val(numero); 
        var label=$('#id_label');
        label.show();
        var boton=$('#id_boton');
        boton.hide();

   }


   function ponerTurno () {

        var caja=$('#id_turno');
        var numero=caja.val();

        if ( !! numero && ! isNaN(numero) ) {            // !!numero-> string no vacio.
            caja.hide();
            var label=$('#id_label');
            label.hide();
            var boton=$('#id_boton');
            boton.show();muchomucho
            if ( numero<0 || numero > 99) numero=valor.substring(2);
            topicMessage = new Paho.MQTT.Message("A-"+("00" + numero).slice (-2));  //Para añadir padZero a la izquierda del numero
            topicMessage.destinationName = "numero";
            topicMessage.retained = true;
            mqtt.send(topicMessage);
        }

   }

    </script>
</head>
<body>
  <div id="Pagina">
    <input type='text' id='topic' disabled hidden/>
    <input type='text' id='status' size="80" disabled hidden/>
    <div id="cabecera">IES VIRGEN DE GUADALUPE</div>
    <div id="cuerpo">TURNO</div>
    <div id="pie">
       <a href="#" onclick="subirTurno();" class="botonAvanza">Avanza Turno</a>
       <a href="#" onclick="bajarTurno();" class="botonRetrocede">Retrocede Turno</a>
       <a href="#" onclick="verCaja();" class="botonSet" id="id_boton">Definir Turno</a>
       <label class="labelturno" id="id_label">Introduzca nuevo turno ==></label>
       <input id="id_turno" class="inputturno" onclick="this.select()" onKeyDown="if (event.keyCode==13) ponerTurno();" value="" />
    </div>
  </div>
</body>
</html>
Todos los ficheros necesarios ya los enlacé en entrada anterior del blog. Simplemente habría que añadir GestionTurnos.html en el mismo sitio web donde pusimos SuTurno.html

Al estar basada en Mosquitto la página web GestionTurnos.html se actualizará en tiempo real y si está abierta en varias ubicaciones (por ejemplo todos los PC de administración) cualquier cambio se propagará a todas los demás de forma inmediata.

Una vez la tenemos funcionando se me ocurre dar una vuelta de tuerca más y convertir la aplicación web en una aplicación independiente de escritorio. De esta manera correrá en una ventana autónoma del navegador que podrá minimizarse y maximizarse a voluntad y será mas sencilla de usar. Hay muchas herramientas para realizar esta conversión web->escritorio, pero me ha gustado por su sencillez Nativifier. Lo que haremos será:
# cat /etc/apt/sources.list.d/nodesource.list 
deb https://deb.nodesource.com/node_0.10 trusty main
deb-src https://deb.nodesource.com/node_0.10 trusty main
# apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80   1655A0AB68576280
# apt-get update
# apt-get install nodejs
# npm install nativefier -g
# nativefier -n "Sistema de Gestión de Turnos"  http://172.X.Y.Z/turnos/GestionTurnos.html
Esto crea un directorio con un fichero ejecutable estándar de Linux que al lanzarlo abre la aplicación Web de Gestión de Turnos como si fuera una aplicación de escritorio. Bastará con poner un acceso directo en el escritorio del usuario y quedará todo simple y accesible.

Si queremos que la aplicación sea usada por todos los usuarios de la máquina lo mas sencillo será llevarla a /opt y dar permisos 777 a todo el directorio donde está.

Bueno, pues ya solo nos falta hacer algún día una aplicación para el móvil...

viernes, 20 de abril de 2018

RPG Maker MV en miniportátiles MSI con Ubuntu

Un docente de mi centro está trabajando en un proyecto para realizar aplicaciones educativas en forma de videojuegos de rol usando la herramienta RPG Maker MV, que permite generar un videojuego ejecutable en Linux, Windows, OSX, Android e iPhone.

El programa final es una aplicación portable que se lanza desde un ejecutable ./Game contenido en el directorio principal y que contiene un navegador Chrome/Chromium empotrado, en el cual se ejecuta la aplicación como si fuese de escritorio. Como lenguaje de programación se usa JavaScript y para la parte gráfica usa WebGL mediante PixiJS. El resultado es un videojuego de rol con gráficos en 2D del estilo de los venerables Zelda de los primeros tiempos. El contenido de la carpeta de la versión Linux tiene este aspecto:


Estamos en fase de pruebas del juego en las máquinas de los centros y un compañero nos avisó de que en los miniportátiles MSI L1350D la ventana se quedaba en negro y no mostraba nada. Lanzando el ejecutable Game desde terminal y revisando el montón de mensajes de log se aprecia el error:
GL_INVALID_OPERATION : glConsumeTextureCHROMIUM: invalid mailbox
Buscando un poco por Internet determino que es un error de WebGL en Chromium. No me extraña, ya había tenido antes problemas con páginas que usan WebGL y los navegadores. Normalmente son derivados de bugs o incompatibilidades en el driver de la tarjeta VGA. En el caso de los MSI tenemos una vieja:
00:02.0 VGA compatible controller: Intel Corporation Atom Processor D4xx/D5xx/N4xx/N5xx Integrated Graphics Controller, (8086:a011)
Una prueba rápida: si accedo al portátil con "ssh -X" y ejecuto el juego, si que se ve bien. Eso confirma que es un problema de las X. Los drivers están actualizados, así que por ahí hay poco que rascar. Una solución extrema es desactivar el DRI en un xorg.conf a medida o bien no cargar el módulo "glx". El problema es que esto afectaría al rendimiento de todas las X, no solo del juego, penalizando por tanto el uso normal del escritorio.

La opción mas deseable es arrancar el Chrome/Chromium con WebGL desactivado (usando el parámetro --disable-webgl) y ver si así funciona. El problema es que este Chromium está embebido en el juego...¿cómo manipular su arranque?. Pues investigando un rato veo que en el mismo directorio donde está ./Game hay un fichero llamado package.json con este contenido:
{
    "name": "KADOKAWA/RPGMV",
    "main": "www/index.html",
    "js-flags": "--expose-gc",
    "chromium-args": "--disable-setuid-sandbox",
    "window": {
        "title": "",
        "toolbar": false,
        "width": 816,
        "height": 624,
        "icon": "www/icon/icon.png"
    }
}
Cáspita, parece el fichero de configuración de arranque del Chromium. Si añado lo marcado en negrita:
{
    "name": "KADOKAWA/RPGMV",
    "main": "www/index.html",
    "js-flags": "--expose-gc",
    "chromium-args": "--disable-setuid-sandbox  --disable-webgl",
    "window": {
        "title": "",
        "toolbar": false,
        "width": 816,
        "height": 624,
        "icon": "www/icon/icon.png"
    }
}
Lo arrancamos... et voilá:


Todo funcionando. A otra cosa, mariposa.



lunes, 16 de abril de 2018

Captura de la pantalla de los alumnos (I)

Lamento este tiempo de silencio, he estado muy ocupado haciendo un Máster en Estupefacción.

Siempre es muy útil para el profesor controlar que uso están haciendo los alumnos con sus ordenadores en el aula. Para esto hemos usado hasta ahora Aulalinex o epoptes. El problema es que ambos son sencillos de usar para observar que hace un alumno concreto, pero no lo son para un grupo de ellos o todos a la vez. Además, desde hace un tiempo nuestro Aulalinex no acaba de funcionar al 100% en Ubuntu.

Un profesor me planteó la posibilidad de hacer una "foto" colectiva de todas las pantallas, que dejase las imágenes con las capturas de los escritorios en un directorio concreto para su consulta posterior. El problema es interesante, pero nos encontramos que aunque desde la óptica del profesor todo son aulas con ordenadores de alumnos, desde un punto de vista interno tenemos al menos 3 escenarios:

  • Aulas con thinclients, donde los ordenadores de los alumnos ejecutan su escritorio y aplicaciones dentro del ordenador del profesor. Sus pc son terminales tontos que apenas hacen nada que no sea conectarse remotamente y abrir sus sesiones en el pc del profesor a través de la red interna 192.168.0.X del cada aula.
  • Aulas con ordenadores independientes para alumnos dentro de una VLAN privada, con direcciones 192.168.0.X. Estos ordenadores pueden ser portátiles o sobremesa (aulas "infolab"), realmente son el mismo caso.
  • Aulas con ordenadores independientes para alumnos que no están aislados, sino que están en la red general del centro y tienen IPs 172.X.Y.Z.

Internamente, el manejo de esta casuística es muy diferente. Por ello iremos por partes y solucionaremos primero el caso de las aulas con thinclients, que es seguramente el más sencillo. En este escenario todas las sesiones están dentro del propio pc del profesor de tal manera que no hace falta ninguna conexión remota: todo queda en casa.

Lo primero es averiguar si hay alumnos en el aula conectados desde los thinclient. Eso se consigue lanzando el comando "who" en el pc del profesor:
cbayont04@a23-pro:~$ who
sjimenezt09 pts/5        2018-04-12 08:43 (a23-o03.local)
cbayont04    :0           2018-04-12 08:33 (:0)
sruizb13    pts/1        2018-04-12 08:43 (a23-o06.local)
........
Aquí se ve que hay un profesor en la consola (:0) y dos alumnos conectados remotamente desde los thinclients a23-o03 y a23-o06. El código para verificarlo será:
thinclients=$(who |  grep ".local)$")  #Si hay logeados usuarios con  el prefijo .local) al final estamos en un aula de thinclients y esos son los alumnos
if [ -z "$thinclients" ]
then 
   echo "No detecto thinclients. Abortando.
   exit 1
else #Son thinclients
   ...
Luego preparamos un directorio para grabar las capturas de la pantalla (en negrita):
pc=$(hostname)
fecha=$(date +%Y-%m-%d)
hora=$(date +%H:%M:%S)
destino="$HOME/capturas/$pc/$fecha/$hora"
rm -rf "$destino"
mkdir -p "$destino"
cd "$destino"
A continuación se identifican las IP de los pc encendidos mediante un nmap en el rango 192.168.0.200-253 (el usado en mi caso para los thinclients) e iteramos sobre ellas:
encendidos=$(nmap -oG - --system-dns  -sP 192.168.0.200-253 | grep -v '^#' | tr -d '()' | sed 's/.local//' | awk '{print $2}' | sort)
for ip in $encendidos
do
Seguidamente obtenemos el nombre del thinclient a partir de la IP (usando avahi-resolve-address ya que los thinclients no están dados de alta en nuestro servidor DNS y la resolución de nombres la realizan ellos mismos mediante el demonio avahi) y el nombre del alumno que ha hecho login en dicho thinclient.
pc=$(avahi-resolve-address $ip | cut -f2)  #El delimitador por defecto es  
if [ -n "$pc" ]    #Si resolvemos el nombre, esta operativo
then
  user=$(who | grep -i $pc | cut -d" " -f1)
  if [ -n "$user" ]  #Si tiene user logueado
  then
Por último ya solo queda capturar el escritorio del thinclient y copiarlo a la carpeta $destino. Para ello debemos averiguar la ruta del $HOME del alumno (con "getent passwd ..") y tomar la identidad de dicho usuario (con "su $user") para realizar la captura de su escritorio completo con:
home_user=$(getent passwd "$user" | cut -d: -f6)
su $user -c  "DISPLAY=$ip:7  XAUTHORITY=$home_user/.Xauthority import -window root /tmp/$user-$pc.jpg"
cp /tmp/$user-$pc.jpg $destino/$user-$pc.jpg
El comando anterior:
DISPLAY=$ip:7  XAUTHORITY=$home_user/.Xauthority import -window root /tmp/$user-$pc.jpg
digamos que lo he encontrado a base de prueba-error.

Para poder hacer la captura del escritorio del thinclient conectado remotamente hay que definir las variables DISPLAY y XAUTHORITY correctas para acceder a dicho escritorio con la credencial del alumno y tener los permisos adecuados. El acceso a las sesiones X desde otra sesión X es algo que está siempre bastante protegido para evitar espionaje entre usuarios y es complicado incluso para el usuario root. Finalmente la foto se guarda en /tmp/$user-$pc.jpg y de ahí la llevamos luego a $destino/$user-$pc.jpg.

Una vez recopiladas las capturas de todas las IP abrimos la carpeta $destino con thunar y así el profesor puede ver todas las imágenes cómodamente.

El script completo es:
# cat /usr/bin/captura-escritorio-todos
#!/bin/bash

thinclients=$(who |  grep ".local)$")  #Si hay logeados usuarios con  el prefijo .local) al final estamos en un aula de thinclients y esos son los alumnos

if [ -z "$thinclients" ]
then 
   echo "No detecto thinclients. Abortando."
   exit 1
else #Son thinclients
  pc=$(hostname)
  fecha=$(date +%Y-%m-%d)
  hora=$(date +%H:%M:%S)
  destino="$HOME/capturas/$pc/$fecha/$hora"
  rm -rf "$destino"
  mkdir -p "$destino"
  cd "$destino"

  #En los thinclients el DNS ip<-->nombre se publica desde cada puesto mediante avahi
  encendidos=$(nmap -oG - --system-dns  -sP 192.168.0.200-253 | grep -v '^#' | tr -d '()' | sed 's/.local//' | awk '{print $2}' | sort)
  for ip in $encendidos
  do
     pc=$(avahi-resolve-address $ip | cut -f2)  #El delimitador por defecto es  , por eso no uso -d
     if [ -n "$pc" ]    #Si resolvemos el nombre, esta operativo
     then
         user=$(who | grep -i $pc | cut -d" " -f1)
         if [ -n "$user" ]  #Si tiene user logueado
         then
             sudo /usr/bin/captura-escritorio-thinclient $user $ip $pc
             cp /tmp/$user-$pc.jpg $destino/$user-$pc.jpg
         fi
     fi
  done
  thunar "$destino"
fi

exit 0
El otro script usado es:
# cat /usr/bin/captura-escritorio-thinclient
#!/bin/bash

user=$1; ip=$2; pc=$3
home_user=$(getent passwd "$user" | cut -d: -f6)
su $user -c  "DISPLAY=$ip:7  XAUTHORITY=$home_user/.Xauthority import -window root /tmp/$user-$pc.jpg"
   
exit 0
Para que el el script llamado por el usuario normal pueda hacer el "su $user ..." necesitamos que esa parte se ejecute con permisos de root, que es el único usuario que puede adoptar la identidad de otro sin saber su contraseña. Por eso llevamos esa parte a otro script que ejecutaremos con "sudo". Evidentemente también hay que modificar /etc/sudoers para permitir su ejecución con sudo a todos los usuarios regulares del grupo teachers (profesores del centro).
# cat /etc/sudoers
....
....
%teachers ALL = (ALL) NOPASSWD: /usr/bin/captura-escritorio-thinclient
....
....
Al finalizar la captura de todos los escritorios de alumnos se abre la carpeta $destino con todas las imágenes de las capturas de escritorio para que el profesor pueda revisarlas tranquilamente con el visor de fotografías de nuestro sistema operativo:


Bueno, pues está resuelto el caso mas simple. Quedan dos pendientes para continuar la serie y esta vez no vamos a esperar tanto para continuar.