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

Mostrando entradas con la etiqueta alumnos. Mostrar todas las entradas
Mostrando entradas con la etiqueta alumnos. Mostrar todas las entradas

miércoles, 19 de junio de 2019

Captura remota de pantalla en las sesiones de los alumnos.

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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

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

El fichero photomaton.jpg contendría la imagen:


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

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

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

from PyQt5 import QtCore, QtGui, QtWidgets

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

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

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


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


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


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

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


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

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


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

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

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


Y no podía faltar:



martes, 7 de noviembre de 2017

Personalizando la pantalla de login de los portátiles de alumnos.

Como ya dijimos en la anterior entrada lo mas extendido es asignar cada portátil a un mismo alumno durante todo el curso.

Para poner las cosas sencillas ponemos una etiqueta adhesiva en la tapa o en el inferior con los datos del portátil y del alumno, pero esta etiqueta se puede quitar/caer y, por otro lado, no es infrecuente que algunos alumnos estén en la luna de Valencia y acaben confundiendo sus portátiles.

Una buena solución es que en la pantalla de login salga algo que identifique al alumno justo antes de poner sus credenciales: una foto, un texto con su nombre, etc. De esta manera se minimiza la posibilidad de coger otro portátil de forma accidental ya que se darán cuenta al meter las credenciales. Mi compañero Manu hizo la aplicación autolabel, que toma los datos del árbol ldap (en su árbol ldap hay campos que asocian portátil y alumno) y modifica el greeter del gdm3 (fichero /etc/gdm3/greeter.gsettings) para mostrar en él la foto y nombre del alumno.

Con xubuntu nos hemos cambiado a lightdm que desgraciadamente no tiene la versatilidad de gdm3 para poner imágenes y textos en la pantalla de login. Pero que no cunda el pánico: siempre podemos coger la imagen de fondo del greeter y manipularla con la utilidad convert para añadir la foto y nombre del alumno.

Para ello tengo un script que obtiene el login del alumno asociado al portátil, coge su foto y otros campos (tal como ya hicimos en la entrada anterior) y modifica nuestro fondo estándar de lightdm, almacenado en "/usr/share/xfce4/backdrops/fondo_1a.jpg".
# cat cambia_background_greeter.sh 

#!/bin/bash
#Curso 17-18-v1.
nombre_pc=$(/bin/hostname)
ldaphost=ldap
base=dc=instituto,dc=extremadura,dc=es
userbase=ou=People,$base
interno="cn=interno,dc=instituto,dc=extremadura,dc=es"
pwd_interno="mipwdinterno"
tmp="/tmp"
fondo_original="/usr/share/xfce4/backdrops/fondo_1a.jpg"
fondo_bak="/usr/share/xfce4/backdrops/fondo_1a_bak.jpg"
fichero="/root/scripts/asignaciones.txt"

test -e $fondo_bak || cp -f $fondo_original $fondo_bak

linea=$(/bin/grep -i -e "^${nombre_pc};" $fichero)
if [ -z $linea ] # no encontrado nombre.
then
  echo "ERROR: No encuentro portatil. Abortado"
  exit 1
fi

#Llegados aquí, en $linea tenemos nombre-pc;mac;usuario;password;grupo
usuario=$(echo $linea | /usr/bin/cut -d";" -f3)
pass=$(echo $linea | /usr/bin/cut -d";" -f4)
grupo=$(echo $linea | /usr/bin/cut -d";" -f5)
nombre=$(ldapsearch -w $pwd_interno  -D $interno -xLLL -t -h $ldaphost -b $userbase "(&(objectClass=posixAccount)(uid=$usuario))" | grep "^cn: ")
nombre=${nombre:4} # quitamos "cn: "

rm -f $tmp/ldapsearch-jpegPhoto-*
ldapsearch -w $pwd_interno  -D $interno -xLLL -T $tmp -t -h $ldaphost -b $userbase "(&(objectClass=posixAccount)(uid=$usuario))" jpegPhoto
if [ -e $tmp/ldapsearch-jpegPhoto-* ]; then
  mv -f $tmp/ldapsearch-jpegPhoto-* $tmp/.face 2> /dev/null
else
  cp -f /root/scripts/anonymous.jpg $tmp/.face 2> /dev/null # Foto genérica para alumnos sin foto.
fi
convert $fondo_bak  \( $tmp/.face -resize 200x200 \) -geometry +1700+50 -composite -matte $tmp/output.jpg
comando="text 20,300 \"Alumno: $usuario\"; text 20,340 \"${nombre}\"; text 20,380 \"Grupo: ${grupo}\""
convert $tmp/output.jpg  -pointsize 30 -fill yellow -gravity northeast -draw "$comando" $fondo_original
rm $tmp/.face
rm $tmp/output.jpg

exit 0
Recordemos que en pwd_interno debemos poner la contraseña que hayamos puesto a nuestro usuario "interno" de ldap. Todo esto se distribuye y ejecuta una vez más desde una tarea puppet:

class xubuntu_portatil {
  .....
  .....
  .....
  #Etiquetado fondo lightdm con datos alumno

  file {"/root/scripts/cambia_background_greeter.sh":
        owner=>root, group=>root, mode=>755,
        source=>"puppet:///modules/xubuntu_portatil/cambia_background_greeter.sh",
        notify => Exec["etiqueta-lightdm"],
  }

  file {"/root/scripts/anonymous.jpg":
        owner=>root, group=>root, mode=>755,
        source=>"puppet:///modules/xubuntu_portatil/anonymous.jpg",
        notify => Exec["etiqueta-lightdm"],
  }

  exec { "etiqueta-lightdm":
                        path => "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        command => "/root/scripts/cambia_background_greeter.sh",
                        require => [File["/root/scripts/cambia_background_greeter.sh"],File["/root/scripts/anonymous.jpg"]],
                        refreshonly => true,
  }

  .....
  .....
  .....
}
El fichero anonymous.jpg es cualquier fichero con una foto genérica que queramos poner como imagen por defecto. Yo tengo una imagen tipo:


Tras ejecutarse el script al actualizar por puppet la pantalla de login del portátil quedaría tal que así:


El próximo año bastaría con cambiar el fichero asignaciones.txt distribuido en la tarea anterior y modificar el script un poco para que se baje y ejecute de nuevo.

Y con esto creo que acabo por ahora la serie de preparación de portátiles de alumnos.

Por último, no puedo dejar esto sin conmemorar que hoy es el centenario de la Revolución Rusa y nada mejor que una buena película sobre la gloriosa gesta del primer paseo espacial de Alexey Leonov para homenajearlo:


domingo, 5 de noviembre de 2017

Automatizando la configuración de portátiles de alumnos al inicio de cada curso.


Lo más común en nuestros centros es que los portátiles estén asignados durante todo el curso a un mismo alumno, que es responsable de su buen uso. Eso hace que cada año al comenzar el curso haya que realizar un proceso manual que comprende al menos los siguientes pasos:
  1. Cargar la imagen del sistema operativo en el portátil si fuere necesario, usando Clonezilla o DRBL.
  2. Limpiar datos del usuario anterior y actualizar el portátil.
  3. Copiar la configuración de la red wifi del aula donde va el portátil en el caso de que el portátil sólo vaya a usarse en una única estancia y con contraseña fija.
  4. Actualizar el nombre del portátil. Aquí tenemos 2 opciones: que el nombre coincida con el login del alumno o que el nombre del portátil sea independiente del mismo y permanezca invariable de un año a otro. Cada centro decide como gestionar la nomenclatura según su criterio.
  5. Cachear las credenciales del alumno que va a usar el portátil, para permitir que inicie sesión sin tener conexión a la red del centro.
El paso 1 ya hemos visto hasta la saciedad que puede hacerse de forma bastante automática y concurrente usando DRBL o Clonezilla.

El paso 2 ya contamos como realizarlo en la anterior entrada.

El paso 3 es sencillo: basta con copiar un fichero con la configuración a /etc/NetworkManager/system-connections/ de cada portátil. Ese fichero se saca conectando a la wifi con un portátil de forma manual y luego recuperando ese fichero de configuración para distribuirlo a los demás mediante puppet.

Veremos a continuación como automatizar lo más posible los pasos 4 y 5. Para ello necesitamos una manera de relacionar cada alumno con el portátil que tiene asignado y determinar el nombre del mismo, así como obtener mas información que pueda ser útil, como la contraseña que tendrá el usuario o el grupo al que pertenece.

Lo ideal sería tenerlo todo en una base de datos centralizada (ya sea una BBDD tradicional o en nuestro ldap) y consultar via un servicio web o similar, pero de momento no dispongo de dicho artefacto. Así que me he ido a lo más cutre: tengo un fichero de texto en formato .cvs con todos los datos que necesito. Dicho fichero lo copio via puppet y lo proceso en cada portátil. El formato del fichero es:
# cat asignaciones.txt 
porthp-o11;22:30:43:11:34:3A;aiglesiasz21;12433133;ESO3oA
porthp-o02;22:30:43:11:37:3C;rbarradop11;32212244;ESO3oB
......
Es un fichero con 5 columnas separadas por punto y coma: nombre del portátil, MAC de alguna tarjeta de red del portátil (normalmente ethX), nombre del alumno que lo tiene asignado, contraseña por defecto del alumno y grupo al que pertenece el mismo.

Al ser un .cvs es sencillo abrirlo en una aplicación de hoja de cálculo y rellenarlo de forma adecuada. Los datos de las MAC los podemos obtener de los facter almacenados en servidor:/var/lib/puppet/yaml/facts. Los datos de cada usuario se obtienen del fichero Alumnos.xml que descargamos desde Rayuela.

Este es el script que uso:
# cat cachea-credenciales.sh 
#!/bin/bash

fichero="/root/scripts/asignaciones.txt"  # fichero con las asignaciones de portatiles, con lineas csv en formato nombre-pc;mac;usuario;password;grupo. La mac puede ser "-"
                            # si el pc tiene nombre y se encuentra en el fichero, la mac es despreciada. En ese caso, el pc mantiene su nombre.
temporal="/tmp/macs_pc"
nombre_pc=$(/bin/hostname)
ldaphost=ldap
base=dc=instituto,dc=extremadura,dc=es
userbase=ou=People,$base
interno="cn=interno,dc=instituto,dc=extremadura,dc=es"
pwd_interno="mi_contrasena"

#Buscamos pc por nombre en fichero de asignaciones. 
#Nota: si queremos cambiar el nombre del pc en funcion de la mac habria que buscar aquí por mac primero y luego ajustar los nombres.
#Eso no lo hacemos, pero queda dicho.

linea=$(/bin/grep -i -e "^${nombre_pc};" $fichero)
if [ -z $linea ] # no encontrado nombre, buscamos por macs
then
  /sbin/ifconfig -a | /bin/grep HWa | tr -s ' ' | cut -f5 -d' ' | tr '[a-z]' '[A-Z]' | sort -u > $temporal # se cogen todas macs.
  macs_pc=$(/bin/cat $temporal)
  linea=$(/bin/grep -i -f $temporal $fichero)
  if [ -z $linea ]
  then 
     echo "ERROR: No encuentro las macs $macs_pc. Abortado"
     exit 1
  else
     #mac encontrada, hay que dar nombre al pc
     nombre_pc=$(echo $linea | /usr/bin/cut -d";" -f1)

     echo "127.0.0.1       localhost
127.0.1.1       $nombre_pc

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters" > /etc/hosts
     echo "$nombre_pc" >  /etc/hostname
     hostname -F /etc/hostname
  fi
fi

#Llegados aquí, en $linea tenemos nombre-pc;mac;usuario;password;grupo

usuario=$(echo $linea | /usr/bin/cut -d";" -f3)
pass=$(echo $linea | /usr/bin/cut -d";" -f4)
grupo=$(echo $linea | /usr/bin/cut -d";" -f5)

#Chequeamos credenciales por defecto del alumno  intentando acceder al campo privado employeeNumber
eNumber=$(ldapsearch  -w $pass -D "uid=$usuario,$userbase" -xLLL -t -h $ldaphost -b $userbase "(&(objectClass=posixAccount)(uid=$usuario))" employeeNumber | grep "^employeeNumber" | cut -d" " -f2)
if [ "$eNumber" != "" ]
then
   #Cachea credencial por defecto alumno
   /usr/sbin/cc_test -store any $usuario $pass 
   /usr/sbin/nss_updatedb ldap
else
   echo "WARNING: El alumno ha cambiado la contraseña, no se cachea."
fi

#Buscamos el homeDirectory del alumno en ldap
homeDirectory=$(ldapsearch -x -h $ldaphost -b $userbase "uid=$usuario" | grep "^homeDirectory" | cut -d" " -f2)
if [ "$homeDirectory" != "" ]
then
   #Si no existe se crea
   test -d $homeDirectory || /sbin/mkhomedir_helper $usuario
   #Y se baja la foto para guardarla en .face
   rm -f $homeDirectory/ldapsearch-jpegPhoto-*
   ldapsearch -w $pwd_interno  -D $interno -xLLL -T $homeDirectory -t -h $ldaphost -b $userbase "(&(objectClass=posixAccount)(uid=$usuario))" jpegPhoto
   if [ -e $homeDirectory/ldapsearch-jpegPhoto-* ]; then
          mv -f $homeDirectory/ldapsearch-jpegPhoto-* $homeDirectory/.face 2> /dev/null
          chown root  $homeDirectory/.face 2> /dev/null
          chmod 444 $homeDirectory/.face 2> /dev/null
   fi
fi

exit 0
Básicamente hace lo siguiente:
  • Obtenemos el nombre del portatil y lo buscamos en el fichero.
  • Si no lo encontramos, obtenemos su MAC y la buscamos. Si aparece la MAC, cogemos el nombre asociado y se lo damos al portátil.
  • Si no encontramos coincidencia en nombre ni MAC se acaba el script.
  • Cogemos el nombre de alumno, su clave y el grupo al que pertenece.
  • Verificamos contra ldap si el alumno mantiene esa clave y no la ha cambiado. Si es así la cacheamos.
  • Conseguimos en ldap su homeDirectory y lo creamos de forma local en el portátil usando mkhomedir_helper.
  • Por último, obtenemos de ldap su foto y la copiamos en el fichero .face de su home local. Es conveniente tenerla para algunas aplicaciones.
  • Para obtener la foto usamos el usuario "interno" que tiene definido nuestro arbol ldap. Este usuario tiene una clave en principio desconocida, pero la podemos cambiar nosotros mismos usando phpldapadmin y luego la ponemos a mano en el script, en la variable "pwd_interno".
A modo de sugerencia, otras cosas que no hace el script pero que sería interesante realizar:
  • Comprobar primero la MAC y ver si el nombre en el portátil coincide con el nombre en el fichero, renombrando en ese caso el portátil al del fichero.
  • Usar el grupo para otras tareas útiles, por ejemplo determinar y autoconfigurar la red wifi a la que se conectará.
Por último, distribuimos ambos ficheros desde nuestra tarea puppet favorita:
class xubuntu_portatil {
  ....
  ....
  file {"/root/scripts/asignaciones.txt":
        owner=>root, group=>root, mode=>500,
        source=>"puppet:///modules/xubuntu_portatil/asignaciones.txt",
  }


  file {"/root/scripts/cachea-credenciales.sh":
        owner=>root, group=>root, mode=>755,
        source=>"puppet:///modules/xubuntu_portatil/cachea-credenciales.sh",
  }
  ....
  ....
}
Podríamos automatizar también su ejecución, como lo tenemos hecho en el script de limpieza de credenciales, pero de momento prefiero lanzarla a mano en cada portátil mientras no adquiere mas rodaje. Ya habrá tiempo de hacerlo de forma desatendida en un futuro.

Todo esto es una mezcolanza de ideas tomadas en los últimos años de scripts que han hecho varios compañeros y han compartido conmigo. ¡Gracias Nando, Manu, Antonio Abasolo y seguramente alguno más!

martes, 31 de octubre de 2017

Limpieza de portátiles de alumnos a principio de curso.

A principio de curso siempre tenemos que preparar los portátiles asignados a los alumnos, empezando por hacer una limpieza general de los mismos. Con los años hemos ido automatizando esto lo más posible para evitar tener que repetir una serie de tareas portátil a portátil.

Aparte de ejecutar puppet y pkgsync para actualizar convenientemente los portátiles, es recomendable borrar las credenciales cacheadas de antiguos usuarios, sus homes y las conexiones wifi creadas en el pasado por los alumnos. Este sería el script a ejecutar al inicio de curso:
# cat limpia_credenciales.sh

#!/bin/bash
#Curso 17-18. Version v1.

#Limpia credenciales cacheadas
for i in $(cc_dump | awk ' {print $3} '); do  cc_test -update any $i -  ; done

#Limpia conexiones wifi antiguas
rm /etc/NetworkManager/system-connections/*

#Limpia homes antiguos
rm -rf /home/alumnos/*

exit 0
Para ejecutarlo la forma mas sencilla es usar unas reglas puppet que aplicaríamos a los portátiles. En mi caso tengo un módulo puppet "omnibús" llamado xubuntu_portatil donde tengo varias pequeñas tareas de este tipo.

El esquema es el típico de estos casos: si no existe o ha cambiado, copiamos el script al portátil y lo ejecutamos.
class xubuntu_portatil {
  
  .......
  .......
  #Limpieza de credenciales a inicio de curso. El script se ejecuta una vez. Si queremos que se ejecute de nuevo
  #hay que cambiar el contenido (p.ej. la versión)

  file {"/root/scripts":
     ensure => directory,
     before => File["/root/scripts/limpia-credenciales.sh"],
  }

  file {"/root/scripts/limpia-credenciales.sh":
        owner=>root, group=>root, mode=>755,
        source=>"puppet:///modules/xubuntu_portatil/limpia-credenciales.sh",
        notify => Exec["limpia-credenciales"],
  }

  exec { "limpia-credenciales":
                        path => "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                        command => "/root/scripts/limpia-credenciales.sh",
                        require => File["/root/scripts/limpia-credenciales.sh"],
                        refreshonly => true,
  }
  .......
  .......

}
Cada vez que queramos hacer limpieza basta con cambiar algo (basta, por ejemplo, con cambiar la fecha o nº de versión de v1 a v2) en el script almacenado en el directorio "files" de nuestro módulo xubuntu_portatil y se copiará de nuevo y ejecutará.