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

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.

domingo, 18 de marzo de 2018

Ocultando el código de un script bash

A veces necesitamos un script bash que puedan ejecutar los usuarios (por ejemplo, mediante un acceso directo en un fichero .desktop), pero dentro de él hay contraseñas o algún fragmento código que no nos interesa hacer público.

Por poner un ejemplo práctico, imaginemos un script que levanta/apaga a voluntad la red wifi de 5ghz de nuestros puntos de acceso DLink, que en mi caso mantengo apagada por defecto.

El script es sencillo:
# cat wifi5ghz.sh
#!/bin/bash
case "$1" in
  "on")   comando="ifconfig ba0 up"
          ;;
  "off")  comando="ifconfig ba0 down"
          ;;
  *)      echo "Error en parámetros"
          exit 1
          ;;
esac
sshpass -p "tuyM59b" ssh -o StrictHostKeyChecking=no root@192.168.0.1 "$comando"
exit 0
Usamos sshpass para entrar por ssh y ejecutar el comandod de apagado/encendido sin que se pida contraseña al usuario. El problema que tenemos es que si distribuimos el script tal que así, cualquier usuario puede ver la contraseña de administración del router DLink (la que he puesto es figurada) y eso no es tolerable.

Entre las diversas soluciones a este problema una consiste en hacer el script ilegible ofuscando su contenido. De las herramientas y trucos que hay para eso el mas potente que he encontrado es usando la aplicación "shc", que compila un script generando un fichero ejecutable ilegible. Nosotros nos quedaremos con el código fuente del script y a los usuarios les distribuiremos el ejecutable. Es raro esto de compilar scripts, pero para la situación descrita viene de perillas.

Una vez instalado compilaremos el script haciendo:
# shc -r -f wifi5ghz.sh -o wifi5ghz
Esto nos creará un fichero ejecutable wifi5ghz que hará lo mismo que wifi5ghz.sh sin que sea visible su código (si el usuario hace un cat sobre él verá un galimatías ASCII). El parámetro "-r" es importante porque permite que ese ejecutable pueda ejecutarse en otras máquinas distintas a aquella dónde se compila el script (si no se pone el ejecutable deja de funcionar a los pocos días).

Como apunte adicional, si consultamos los parámetros de shc veremos que tiene cosas bastante interesantes, como por ejemplo la posibilidad de hacer que el ejecutable caduque en una fecha predeterminada.

Hasta pronto.

domingo, 4 de marzo de 2018

Ejecutar comandos de consola de forma remota en Windows.

Como buen fanático del terminal, siempre prefiero hacer cosas de administración de sistemas tecleando antes que con ratón. En Unix esto es lo más habitual, pero en Windows no lo ponen tan fácil.

En cualquier caso siempre viene bien tener la opción de conectar con una consola remota de Windows y escribir los comandos en ella, sin necesidad de desplazarnos físicamente a la máquina ni de iniciar una sesión remota completa con rdesktop. En principio con un servidor OpenSSH en el equipo destino es suficiente. Un servidor ssh es un software que en Linux viene de serie mientras que en Windows, entre varias opciones, podemos poner una versión GPL liberada por Microsoft.

Pero no, vamos a ver una forma alternativa de ejecutar comandos de consola de forma remota en Windows que no necesita instalar nada especial en el PC destino. Contemplaremos 2 métodos: lanzar la ejecución de los comandos desde un equipo con Windows y hacerlo desde un equipo con Linux.

1. Consideraciones previas.

Para hacer que nuestros Windows estén dispuestos a aceptar órdenes remotas hay que prepararlos antes tocando varias configuraciones:

1.1. Abrir el puerto 445.

Para comunicarnos y dar órdenes al Windows remoto necesitamos comunicar con él por su puerto 445, denominado "microsoft-ds", que sirve para un montón de cosas incluida la entrada de virus ;-).

Como este puerto está cerrado normalmente por el firewall de Windows debemos primeramente abrirlo para que acepte conexiones entrantes. Podemos hacerlo con el confuso interface gráfico de configuración del firewall y tardar varios minutos:



o bien abrir una consola de comandos en modo administrador y teclear:
netsh advfirewall firewall add rule dir=in action=allow protocol=TCP localport=445 name="Allow_TCP-445" 
Seguramente después haya que reiniciar el servicio de firewall para que se aplique de forma efectiva.

Supongo que con este ejemplo de método gráfico versus método de terminal queda claro por qué muchas veces es preferible usar la línea de comandos.

1.2. Modificar el registro para permitir el acceso a los "administrative shares"

Si el equipo remoto trabaja con Windows 10 hay que añadir una entrada al registro para permitir las conexiones entrantes que ejecutarán comandos, ya que por defecto UAC impide que usuarios administradores se conecten a los "administrative shares". De nuevo lo haremos desde consola:
reg add HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f
Si no creamos esta entrada de registro obtendremos errores de "Acceso denegado" al intentar ejecutar comandos remotos.

1.3. Solo para Linux: activar SMB v1.

Esto solo debe hacerse si el equipo desde el que lanzamos el comando es un Linux.

Debido a la aparición del ransomware WannaCry, que aprovechaba una vulnerabilidad en SMB llamada EternalBlue (con la que seguramente los servicios de seguridad de USA nos han estado espiando a todos durante años) hubo que improvisar soluciones transitorias . Una de ellas, rápida y sucia, era deshabilitar SMB1 en Windows y así impedir la entrada de WannaCry.

Como efecto colateral, el programa que usamos en Linux para mandar comandos al Windows deja de funcionar, ya que necesita que SMB1 esté activo. Por tanto, dependiendo que tengamos o no activo SMB1 en nuestro Windows podrá funcionar o no la ejecución remota. A día de hoy EternalBlue ya ha sido solucionado con los parches de Microsoft, por lo que SMB1 puede estar de nuevo activo.

Para ver su estado abrimos una consola de PowerShell y tecleamos "Get-SmbServerConfiguration"
PS C:\> Get-SmbServerConfiguration

AnnounceComment                 : 
AnnounceServer                  : False
AsynchronousCredits             : 64
AuditSmb1Access                 : False
AutoDisconnectTimeout           : 15
AutoShareServer                 : True
AutoShareWorkstation            : True
CachedOpenLimit                 : 10
DurableHandleV2TimeoutInSeconds : 180
EnableAuthenticateUserSharing   : False
EnableDownlevelTimewarp         : False
EnableForcedLogoff              : True
EnableLeasing                   : True
EnableMultiChannel              : True
EnableOplocks                   : True
EnableSecuritySignature         : False
EnableSMB1Protocol              : False       
EnableSMB2Protocol              : True
...
...
Ahí vemos EnableSMB1Protocol a False. También podemos verlo directamente con:
PS C:\> Get-WindowsOptionalFeature –Online –FeatureName SMB1Protocol
...
...
¿Cómo lo activamos? Con:
PS C:\> Enable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol
Nos pedirá reiniciar para que sea efectivo el cambio. Este Windows es maravilloso: activas un protocolo y tienes que reiniciar la máquina completa, ahí queda eso.

Como curiosidad aquí tenemos recopilados todos los comandos para ver/cambiar el estado de los servicios SMBx.

2. Desde un equipo con Windows.

Bueno, ya tenemos el Windows remoto receptivo, vamos ahora a ver como darle órdenes. Necesitamos instalar las pstools en el PC desde donde daremos órdenes. Ojo: esto se instala en el PC desde donde se ejecuta el comando, el PC destinatario no necesita que instalemos nada, el único requisito para él es lo indicado en el apartado 1.

No viene mal echar un vistazo a todas las utilidades, son una maravilla que conviene poner de serie en todos nuestros Windows. La que usaremos nosotros es psexec. Veamos como ejecutar un comando remoto desde el Windows donde tenemos instalado psexec contra otro Windows:
psexec \\a20-o14 "ipconfig"
Esto anterior se ejecutaría usando las credenciales del usuario actual en el PC remoto. Si queremos dar otras credenciales especificamos el usuario (que debe ser un administrador) y contraseña:
psexec -u administrador -p password \\a20-o14 "ipconfig"

PsExec v2.2 - Execute processes remotely
Copyright (C) 2001-2016 Mark Russinovich
Sysinternals - www.sysinternals.com

Starting PSEXESVC service on a20-o14...........
Configuración IP de Windows

Adaptador de Ethernet Ethernet:

Sufijo DNS específico para la conexión. . : vguadalupe
Vínculo: dirección IPv6 local. . . : fe80::f938:6376:3936:6a45%3
Dirección IPv4. . . . . . . . . . . . . . : 172.24.XXX.12
Máscara de subred . . . . . . . . . . . . : 255.255.255.0
Puerta de enlace predeterminada . . . . . : 172.24.XXX.2

Adaptador de túnel isatap.vguadalupe:

Estado de los medios. . . . . . . . . . . : medios desconectados
Sufijo DNS específico para la conexión. . : vguadalupe

Como se ve conectamos con el equipo a20-o14 y ejecutamos el comando "ipconfig" sobre él, dándonos la dirección IP y configuración de red el equipo. Con:
psexec \\a20-o14 "cmd"

PsExec v2.2 - Execute processes remotely
Copyright (C) 2001-2016 Mark Russinovich
Sysinternals - www.sysinternals.com

Starting PSEXESVC service on a20-o14...........

Microsoft Windows [Versión 6.3.9600]
(c) 2013 Microsoft Corporation. Todos los derechos reservados.

C:\Windows\system32>

Se nos abre una consola de comandos remota, que ejecutará en la otra máquina lo que tecleemos.

Si alguna de las dos órdenes anteriores produce errores del tipo:
Couldn't access a20-o14:
No se ha encontrado la ruta de acceso de la red.
Make sure that the default admin$ share is enabled on a20-o14.
En mi caso era síntoma de que el puerto 445 estaba cerrado. Si ese es tu caso, abrelo como hemos comentado en la parte 1 y ya debería funcionar. Si además dice:
PsExec v2.2 - Execute processes remotely
Copyright (C) 2001-2016 Mark Russinovich
Sysinternals - www.sysinternals.com
Couldn't access a20-o14:
Acceso denegado.
Entonces tendremos que añadir la entrada de registro descrita en la parte 1.2.

Mas cosas interesantes relacionadas con psexec:

  • Como ejecutar en varios PC a la vez el mismo script almacenado en un servidor:
    psexec \\computername1,computernamfe2,computernameN -d -u usuario -p password \\servidor\scripts\script.bat
    
    Esto es parecido al comando dsh de Linux.
  • Con el modificador -c podemos enviar desde el ordenador local un fichero .exe para ejecutarlo remotamente:
    psexec \\computername1,computernamfe2,computernameN -d -c programa.exe -u usuario -p password programa.exe
    
  • Para ver cuantas sesiones hay abiertas en una máquina remota ejecutamos el comando de Windows "query session":
    psexec \\a20-o19 query session
    
    NOMBRE DE SESIÓN  NOMBRE DE USUARIO     ID  ESTADO TIPO   DISPOSITIVO
    >services                                0  Desc
    console           GM_SEGUNDO             1  Activo
    rdp-tcp#1         Administrado           2  Activo
    rdp-tcp                               5536  Escuchar
    
    Vemos que en consola hay una sesión abierta con el usuario GM_SEGUNDO y en remote desktop hay otra abierta con el usuario Administrador (¿cómo consigo sesiones de escritorio de usuarios concurrentes sobre un mismo Windows?: instalando rdpwrapper).
  • Psexec está concebido para ejecutar comandos de consola, con entrada/salida en texto. Cualquier aplicación gráfica que lancemos aparecerá en el escritorio remoto. Por ejemplo:
    psexec \\a20-o19 -i 1 -d notepad
    
    Hará que al usuario GM_SEGUNDO del ejemplo anterior (el de la sesión con ID 1, de ahí el parámetro "-i 1". Este es el valor por defecto) se le abra el bloc de notas en la pantalla ante su total estupefacción. Esta es la teoría, pero en las pruebas prácticas que he hecho se abre una ventana con el contenido en negro y que no permite interactuar. Debe ser algún bug o feature de Windows 10.

    Si queremos lanzar aplicaciones gráficas en un Windows remoto y que aparezcan en nuestro escritorio local (sería una conexión Remota tipo VNC o Remote Desktop pero a nivel de aplicación en lugar de a nivel de escritorio completo) habría que usar SeamlessRDP, pero ese tema escapa al contenido de este artículo.

A modo de resumen: psexec es adecuado para ejecutar comandos de terminal de forma remota sin tener que iniciar sesión en los escritorios Windows. Como truco: cuando queremos ejecutar varios comandos la mejor solución es ponerlos dentro de un script .bat en una carpeta compartida por Samba accesible por todos los Windows y lanzar dicho script desde psexec.

3. Desde un equipo con Linux.

Bueno, ¿y que pasa si el equipo cliente es un Linux y desde él queremos ejecutar los comandos remotamente en nuestros Windows? ¿Hay algun psexec para Linux?

Pues si hay algo que se le parece: winexe, que sigue la idea de usar el puerto 445 para mandar ordenes a los Windows de forma remota. Es un proyecto algo abandonado y del cual no hay paquetes oficiales en .deb ni en .rpm (para Manjaro/Arch por supuesto que sí, pero es que ahí jugamos en otra liga). Además es fácil encontrar la versión 1.0, pero la que ahora funciona con Windows 10 es la 1.1.

Al final lo he encontrado aquí, con versiones .deb para Debian y Ubuntu. En mi caso he bajado el paquete winexe-static-1.1-0-jessie.deb para ejecutarlo desde mi servidor montado Debian Jessie.

El paquete se llama "static" porque tiene las librerías de Samba metidas dentro del ejecutable durante la compilación. La causa es que al cambiar la versión de Samba esto tiende a fallar, así que han optado por llevarlas al estilo años 70, embebidas en el ejecutable para evitar errores.

# dpkg -i winexe-static-1.1-0-jessie.deb
# winexe-static
winexe version 1.1
This program may be freely redistributed under the terms of the GNU GPLv3
Usage: winexe-static [OPTION]... //HOST COMMAND
Options:
  -h, --help                                  Display help message
  -V, --version                               Display version number
  -U, --user=[DOMAIN/]USERNAME[%PASSWORD]     Set the network username
  -A, --authentication-file=FILE              Get the credentials from a file
  -N, --no-pass                               Do not ask for a password
  -k, --kerberos=STRING                       Use Kerberos, -k [yes|no]
  -d, --debuglevel=DEBUGLEVEL                 Set debug level
  --uninstall                                 Uninstall winexe service after
                                              remote execution
  --reinstall                                 Reinstall winexe service before
                                              remote execution
  --system                                    Use SYSTEM account
  --profile                                   Load user profile
  --convert                                   Try to convert characters
                                              between local and remote
                                              code-pages
  --runas=[DOMAIN\]USERNAME%PASSWORD          Run as the given user (BEWARE:
                                              this password is sent in
                                              cleartext over the network!)
  --runas-file=FILE                           Run as user options defined in a
                                              file
  --interactive=0|1                           Desktop interaction: 0 -
                                              disallow, 1 - allow. If allow,
                                              also use the --system switch
                                              (Windows requirement). Vista
                                              does not support this option.
  --ostype=0|1|2                              OS type: 0 - 32-bit, 1 - 64-bit,
                                              2 - winexe will decide.
                                              Determines which version (32-bit
                                              or 64-bit) of service will be
                                              installed.
Como se ve los parámetros son muy distintos a los de psexec, ya que es una herramienta independiente. En este enlace se ve un buen repaso de todos ellos.

Veamos como ejecutar un comando:
# winexe-static --convert -U administrador%password //a20-o14 "ipconfig"
Error: error Unknown argument (get codepage)
CTRL: Probably old version of service, reinstalling.

Configuración IP de Windows

Adaptador de Ethernet Ethernet:

Sufijo DNS específico para la conexión. . : vguadalupe
Vínculo: dirección IPv6 local. . . : fe80::f938:6376:3936:6a45%3
Dirección IPv4. . . . . . . . . . . . . . : 172.19.XXX.100
Máscara de subred . . . . . . . . . . . . : 255.255.255.0
Puerta de enlace predeterminada . . . . . : 172.19.XXX.2

Adaptador de túnel isatap.vguadalupe:

Estado de los medios. . . . . . . . . . . : medios desconectados
Sufijo DNS específico para la conexión. . : vguadalupe
O abrir un interpréte de comandos remoto y escribir las órdenes allí:
# winexe-static  --convert -U administrador%password //a20-o04 "cmd"
Microsoft Windows [Versión 6.3.9600]
(c) 2013 Microsoft Corporation. Todos los derechos reservados.

C:\Windows\system32>cd \users
cd \users

C:\Users>dir
dir
El volumen de la unidad C no tiene etiqueta.
El número de serie del volumen es: B62C-6CE1

Directorio de C:\Users

16/06/2016  08:09    DIR          .
16/06/2016  08:09    DIR          ..
26/01/2017  12:45    DIR          Administrador
22/08/2013  16:36    DIR          Public
20/07/2017  05:43    DIR          UpdatusUser
28/07/2017  08:04    DIR          usuario
0 archivos              0 bytes
6 dirs  33.118.711.808 bytes libres

C:\Users>exit
exit
# 
Si en algún momento da este error:
# winexe-static -U administrador%password //a20-o04 "cmd"
ERROR: Failed to open connection - NT_STATUS_CONNECTION_RESET
La causa mas probable es que esté desactivado SMB1 en el PC remoto, lo cual se soluciona como vimos en el apartado 1.

Finalizamos con una recopilación de errores:
  • ERROR: Failed to open connection - NT_STATUS_LOGON_FAILURE : credenciales incorrectas.
  • ERROR: CreateService failed. NT_STATUS_ACCESS_DENIED : puerto 445 cerrado o registro no modificado según apartado 1.1 y 1.2.
  • ERROR: Failed to open connection - NT_STATUS_CONNECTION_RESET : servicio SMB1 deshabilitado. Habilitar según apartado 1.3 .

Y con esto ya tenemos expuestas todas las posibilidades para interactuar con nuestros Windows desde terminal.


Me despido con una imagen impactante que no tiene nada que ver con el blog, pero que está aquí al lado y no puedo dejar pasar:


Esto tiene 66.700 años según las últimas dataciones y es la prueba mas antigua de arte realizada por un homo sapiens de la especie vecina, la neanderthalensis. En esa época los homo sapiens sapiens estábamos empezando a salir de África por Oriente Próximo y andábamos ocupados en otras cosas. Y aquí, al lado de la Ribera del Marco ya había una especie que desarrollaba pensamiento simbólico en este planeta. Impresionante.

Es nuestro pequeño Gobekli Tepe, espero que sepamos cuidarlo.