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

martes, 5 de diciembre de 2017

Instalar un panel informativo en el IES.

A los centros eScholarium se enviaron unos paneles informativos para poner en sitios visibles y poder publicar contenidos de interés para la comunidad educativa. Por desgracia, mi centro no estaba entonces en ese programa y no nos llegó nada.

Pero por fortuna teníamos una antigua TV plana (en realidad relativamente plana, es de las primeras que salieron y tiene un grosor de mas de 10cm) que estaba ociosa después de instalar cañones de vídeo en casi todas las aulas, y pensamos en rescatarla para hacer nuestro propio panel informativo. Como no es una Smart TV, para darle la parte smart hace falta un elemento externo, que ha sido un sencillo PC con una modesta CPU Intel Atom (aunque bien podría servir un miniportátil de alumno, una Raspberry Pi, una Android TVBox o incluso un móvil antiguo o tablet con salida HDMI). Una vez montada quedará así:


Mi idea es instalar un Xubuntu pelado con un usuario local que haga autologin y abra un navegador que reproduzca el contenido de una página web a pantalla completa, en modo "kiosco". La página web en mi caso está en el mismo PC desde donde se reproduce todo, pero igualmente podría estar en un servidor del centro (lo cual sería imperativo en el caso de usar una SmartTV o un dispositivo Android).

Para ello tenemos un script panel.sh que hace:
#!/bin/bash
firefox http://localhost/intranet/panel.html?tipo=slide & # Tipo video, flow, slide, panel
sleep 10
xdotool search --sync --onlyvisible --class "Firefox" windowactivate key F11
Este script se ejecuta al iniciar sesión desde un fichero /etc/xdg/autostart/panel.desktop usual. El comando xdotool envia a Firefox una pulsación simulada de la tecla F11, con lo cual se pone a pantalla completa, sin decoración de barras de ningún tipo.

Lo que usamos es un fichero .html lo mas versátil posible, con la siguiente estructura:

  • En la parte izquierda tenemos una zona donde se proyecta la información, luego lo veremos pero la idea es poder proyectar presentaciones de Google Slides, vídeos y otras fuentes de información.
  • En la parte superior derecha pondremos un widget meteorológico, con la predicción del tiempo en Cáceres. El usado es el generado en la página tiempo.es
  • En la parte inferior derecha pondremos un reloj-calendario hecho en Javascript a partir de este ejemplo con algunos retoques estéticos y de idioma .
  • Debajo del reloj nos sobra un pequeño espacio y ponemos allí el logotipo del IES.

Como la idea es que la parte de proyección pueda mostrar varios contenido incluimos un parámetro "tipo" que pasaremos en la URL y que puede tomar 4 valores (en el script panel.sh anterior se ve como se pasa dicho parámetro al hacer la petición, por ejemplo: http://localhost/intranet/panel.html?tipo=slide):

  • video: para proyectar vídeos en formato mp4. Normalmente actividades del IES. Permite reproducir un array de vídeos en bucle continuo. Los vídeos tienen un tamaño de 1280x720 y un bitrate de aproximadamente 1000kbps, dando una calidad suficiente y sin necesitar mucha potencia para reproducirlos. Si no lo tenemos en ese formato lo podremos convertir con la utilidad avconv (avconv -i input.mp4 -s 1280x720 output.mp4).
  • flow: para proyectar vídeos en el viejuno formato flv, usa el widget flowplayer, basado en Flash. Tienen menor calidad y gasta mas recursos de cpu que los mp4, pero ya tenía contenidos previos en dicho formato de otros años y no quería perder la posibilidad de proyectarlos.
  • slide: para proyectar presentaciones de Google Slides compartidas de forma pública por su creador (explicado en Embed a document, spreadsheet, or presentation).
  • panel: para mostrar el contenido de la URL http://panelinformativo.educarex.es, que es el sistema de gestión de los paneles oficiales de la Consejería. Los usuarios autorizados de cada centro pueden entrar a través de http://panelinformativo.educarex.es/login y dar de alta noticias y/o mensajes (con fechas configurables de activación y caducidad), que serán mostradas en la URL antes citada de forma automática en formato "carrusel" (filtrando lo mostrado en centro en función de la IP peticionaria, claro está). Este es el sistema que tienen los paneles enviados por la Consejería .

Cada vez que queremos cambiar los vídeos o la presentación debemos editar el fichero panel.html. Ya sé que eso es hard coding pero no me he puesto a automatizar la configuración mediante ficheros externos. Dentro del código HTML que pongo a continuación marco en rojo las partes que habría que cambiar para adaptar a las circunstancias de cada cual. También he cambiado la IP del pc donde están los contenidos web poniendo 172.X.Y.Z.

La página está calculada para una resolución de 1360x768, siendo todas las anchuras expresadas en pixeles para cuadrar perfectamente. La parte del vídeo/presentación tiene un ancho de 1000px aproximadamente y la de los widgets unos 330px. En caso de usar otras resoluciones habría que ir probando hasta que cuadrase.

Empezamos viendo la parte HEAD de panel.html. En ella definimos los CSS de los widget tiempo y reloj, así como todo el javascript necesario:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html style="height: 100%;">
<head>
  
  <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
  <title>panel.html</title>

  <!-- ======================================================================================================================================================= -->
  <!--Importante: poner la IP fija definitiva. Si no se pone, no funciona. Nada de localhost.-->
  <!-- ======================================================================================================================================================= -->

  <!-- ======================================================================================================================================================= -->
  <!-- Antiguo Widget Flowplayer basado en flash para videos .flv --->
  <!-- ======================================================================================================================================================= -->
  <script src="http://172.X.Y.Z/intranet/javascript/flowplayer-3.2.8.min.js"></script>
  <script src="http://172.X.Y.Z/intranet/javascript/jquery.js" type="text/javascript" charset="utf-8"></script>
  <script src="http://172.X.Y.Z/intranet/javascript/flowplayer.playlist-3.2.8.min.js" type="text/javascript" charset="utf-8"></script>

  <style type="text/css">
        a.player {
            display: block;
            width: 560px;
            height: 420px;
            text-align: center;
            text-decoration: none;
            cursor: pointer;
            background: #333333;
        }

        a.player img {
            margin-top: 110px;
            border: 0;
            opacity: 0.8;
            filter: alpha(opacity=80);
        }

        a.player img:hover {
            opacity: 1;
            filter: alpha(opacity=100);
        }


        a.player {
            margin-top: 0px;
            width: 500px;
        }
  </style>  

  <!-- ======================================================================================================================================================= -->
  <!-- CSS Reloj -->
  <!-- ======================================================================================================================================================= -->

  <style type="text/css">
 body{
   font:bold 12px Arial, Helvetica, sans-serif;
   margin:0;
   padding:0;
   color:#bbbbbb; 
 }

 a { 
  text-decoration:none; 
  color:#00c6ff;
 }

 h1 {
  font: 4em normal Arial, Helvetica, sans-serif;
  padding: 20px; margin: 0;
  text-align:center;
 }

 h1 small{
  font: 0.2em normal  Arial, Helvetica, sans-serif;
  text-transform:uppercase; letter-spacing: 0.2em; line-height: 5em;
  display: block;
 }

 h2 {
     font-weight:700;
     color:#bbb;
     font-size:50px;
 }

 h2, p {
  margin-bottom:10px;
 }

 @font-face {
     font-family: 'BebasNeueRegular';
     src: url('fonts/BebasNeue-webfont.eot');
     src: url('fonts/BebasNeue-webfont.eot?#iefix') format('embedded-opentype'),
   url('fonts/BebasNeue-webfont.woff') format('woff'),
   url('fonts/BebasNeue-webfont.ttf') format('truetype'),
   url('fonts/BebasNeue-webfont.svg#BebasNeueRegular') format('svg');
     font-weight: normal;
     font-style: normal;

 }

 .container {width: 300px; margin: 0 auto; overflow: hidden;}

 .clock {width:280px; margin:0 auto; padding:5px; border:1px solid #333; color:#fff; }

 #Date { font-family: Arial, 'BebasNeueRegular', Helvetica, sans-serif; font-size:20px; text-align:center;  }

 ul { width:280px; margin:0 auto; padding:0px; list-style:none; text-align:center; }
 ul li { display:inline; font-size:50px; text-align:center; font-family: Arial, 'BebasNeueRegular', Helvetica, sans-serif; }

 #point { position:relative; -moz-animation:mymove 1s ease infinite; -webkit-animation:mymove 1s ease infinite; padding-left:10px; padding-right:10px; }

 @-webkit-keyframes mymove 
 {
 0% {opacity:1.0; text-shadow:0 0 20px #00c6ff;}
 50% {opacity:0; text-shadow:none; }
 100% {opacity:1.0; text-shadow:0 0 20px #00c6ff; } 
 }


 @-moz-keyframes mymove 
 {
 0% {opacity:1.0; text-shadow:0 0 20px #00c6ff;}
 50% {opacity:0; text-shadow:none; }
 100% {opacity:1.0; text-shadow:0 0 20px #00c6ff; } 
 }

  </style>
  <script type="text/javascript" src="http://code.jquery.com/jquery-1.6.4.min.js"></script>
  <script type="text/javascript">
 $(document).ready(function() {
 // Create two variable with the names of the months and days in an array
 var monthNames = [ "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre" ]; 
 var dayNames= ["Domingo","Lunes","Martes","Miércoles","Jueves","Viernes","Sábado"];

 // Create a newDate() object
 var newDate = new Date();
 // Extract the current date from Date object
 newDate.setDate(newDate.getDate());
 // Output the day, date, month and year    
 $('#Date').html(dayNames[newDate.getDay()] + " " + newDate.getDate() + ' ' + monthNames[newDate.getMonth()] + ' ' + newDate.getFullYear());

 setInterval( function() {
  // Create a newDate() object and extract the seconds of the current time on the visitor's
  var seconds = new Date().getSeconds();
  // Add a leading zero to seconds value
  $("#sec").html(( seconds < 10 ? "0" : "" ) + seconds);
  },1000);
 
 setInterval( function() {
  // Create a newDate() object and extract the minutes of the current time on the visitor's
  var minutes = new Date().getMinutes();
  // Add a leading zero to the minutes value
  $("#min").html(( minutes < 10 ? "0" : "" ) + minutes);
     },1000);
 
 setInterval( function() {
  // Create a newDate() object and extract the hours of the current time on the visitor's
  var hours = new Date().getHours();
  // Add a leading zero to the hours value
  $("#hours").html(( hours < 10 ? "0" : "" ) + hours);
     }, 1000);
 
 }); 
  </script>

  <!-- ======================================================================================================================================================= -->
  <!-- Codigo Javascript para poner carrusel de Videos modernos: mp4 y webm. -->
  <!-- ======================================================================================================================================================= -->
  <script type="text/javascript">

      var videoSources = ["videos/terror.mp4"]; //Array con la lista de videos a reproducir
      var currentIndex = 0;

      //listener function changes src
      function myNewSrc() {
           var myVideo = document.getElementsByTagName('video')[0];
           myVideo.src = videoSources[currentIndex];
           myVideo.load();
      }
      // add a listener function to the ended event
      function myAddListener(){
           var myVideo = document.getElementsByTagName('video')[0];
           currentIndex = (currentIndex+1) % videoSources.length;
           myVideo.src = videoSources[currentIndex];
           myVideo.addEventListener('ended', myNewSrc, false);
      }
  </script>


  <!-- ======================================================================================================================================================= -->
  <!-- Codigo Javascript para ver que tipo de objeto se muestra (video normal, presentacion google o video flv en función del parametro get de la url tipo=XXX -->
  <!-- ======================================================================================================================================================= -->

  <script type="text/javascript">

      // analiza el queryString buscando el parametro tipo=XXX en la URL para decidir que mostrar.
      function muestraDiv() {

        var params = {};
        params.tipo="video"; // video, slide, flow, panel, ... Por defecto, video
        if (location.search) {
            var parts = location.search.substring(1).split('&');

            for (var i = 0; i < parts.length; i++) {
                var nv = parts[i].split('=');
                if (!nv[0]) continue;
                params[nv[0]] = nv[1] || true;
            }
        }
        divVideo=document.getElementById("div-video");
        divSlide=document.getElementById("div-slide");
        divFlow=document.getElementById("div-flow");
        divPanel=document.getElementById("div-panel");
        switch (params.tipo) {
                  case "video": 
                        purgaElemento(divSlide);
                        purgaElemento(divFlow);
                        purgaElemento(divPanel);
                        divVideo.style.display = 'block';     
                        myNewSrc(); //Carga el primer video
                        break;
                  case "slide":
                        divSlide.style.display = 'block';  
                        purgaElemento(divFlow);
                        purgaElemento(divVideo);
                        purgaElemento(divPanel);
                        break;
                  case "flow":
                        divFlow.style.display = 'block';  
                        purgaElemento(divSlide);
                        purgaElemento(divVideo);
                        purgaElemento(divPanel);
                        break;
                  case "panel":
                        divPanel.style.display = 'block';  
                        purgaElemento(divSlide);
                        purgaElemento(divVideo);
                        purgaElemento(divFlow);
                        break;
        }
      }
      
      function purgaElemento(oDiv) {
            oDiv.style.display = 'none';     
            oDiv.outerHTML = "";
            delete oDiv;
      }

  </script>
    
</head>
En la parte BODY definimos una tabla para formatear las partes la página (lo siento, aprendí HTML en los años 90 y sigo formateando con tablas en lugar de con DIV+CSS) y llamamos a la funcion javascript muestraDiv, que analiza el parámetro tipo y decide cual de los cuatro div (div-video, div-slide, div-flow o div-panel) mostrar, ocultando y destruyendo los otros tres.

Para el reloj y el widget tiempo pongo el código html que se generaba en las respectivas páginas de donde los he sacado:

<body style="height: 90%;" bgcolor="#000815"  onload="muestraDiv();">  <!--  muestraDiv-> decide que div aparece -->
   <table width="100%" style="height: 100%;" >
        <tr valign="middle">
           <td rowspan="2">

                <!-- ======================================================================================================================================================= -->
                <!-- UN DIV PARA CADA TIPO DE CONTENIDO MOSTRADO. SOLO UNO ES VISIBLE, LOS OTROS SE OCULTAN Y DESTRUYEN                                                      -->
                <!-- ======================================================================================================================================================= -->
               
                <!-- ======================================================================================================================================================= -->
                <!-- VIDEO MP4/WEBM                                                                                                                                          -->
                <!-- ======================================================================================================================================================= -->
                <div id="div-video" style="display: none">
                        <video width="990" poster="logo.png" height="740" autoplay preload="auto" controls id="miVideo" onended="myAddListener()" >
                          <source src="" type="video/mp4" />
                          Your browser does not support the video tag.
                        </video>
                </div>
                <!-- ======================================================================================================================================================= -->
                <!-- SLIDE                                                                                                                                                   -->
                <!-- ======================================================================================================================================================= -->
                <div id="div-slide" style="display: block"> <!-- si lo intentamos ocultar da error en la carga inicial -->
                        <div style="width:1000px;height:720px;overflow:hidden;" >  <!-- esto es para ocultar la barra de controles -->
                                  <iframe src="https://docs.google.com/presentation/d/e/2PACX-1vQ4WA8UkpxrJeeDdQOI-wIQ7S9kEpNoHZYpXxUvorCir-gTVoo2fWVyLN8VShiDivdbvtHZy4w1VRD1/embed?start=true&loop=true&delayms=5000" 
                                        frameborder="0" width="1000" height="750" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true">
                                </iframe>
                        </div>
                </div>
                <!-- ======================================================================================================================================================= -->
                <!-- PANEL                                                                                                                                                   -->
                <!-- ======================================================================================================================================================= -->
                <div id="div-panel" style="display: none">
                        <div style="width:1000px;height:750px;overflow:hidden;" >  <!-- esto es para ocultar la barra de controles -->
                                  <iframe src="http://panelinformativo.educarex.es" 
                                        frameborder="0" width="1000" height="750" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true">
                                </iframe>
                        </div>
                </div>
                <!-- ======================================================================================================================================================= -->
                <!-- VIDEO FLASH                                                                                                                                             -->
                <!-- ======================================================================================================================================================= -->
                <div id="div-flow" style="display: none">
                        <script>
                                $(function() {
                                    // set up player without "internal" playlists
                                    $f("player", "http://172.X.Y.Z/intranet/javascript/flowplayer-3.2.9.swf", {
                                        clip: { baseUrl: 'http://172.X.Y.Z/intranet/videos/antiguos' }
                                    // use playlist plugin. again loop is true
                                    }).playlist("#myplaylist", {loop:true});
                        });
                        </script>
                        <div style="float:left" id="myplaylist">
                             <a href="LipDub2016.flv"></a>
                             <a href="video-estetica.flv"></a>
                        </div>
                        <!-- player container without nested content -->
                        <a class="player" id="player"  style="width:1000px;height:750px;float:left"  href="videos/antiguos/bachillerato.flv"></a>
                </div>
                <!-- ======================================================================================================================================================= -->
                <!-- FIN DIVs                                                                                                                                                -->
                <!-- ======================================================================================================================================================= -->
           </td>
           <td width="330px" height="200px">        
             <!-- ======================================================================================================================================================= -->
             <!-- WIDGET TIEMPO                                                                                                                                           -->
             <!-- ======================================================================================================================================================= -->
             <div style="height:390px; width:330px; overflow-x:hidden; " valign="center">                               
                   <div id="tiempo_033974258b5fb50f89ccd5071c8f4f8c"> 
                   <div></div>
                   <div> 
                       <img src="//www.tiempo.es/build/img/logo/tiempo133.png" width="80" height="18" alt="tiempo.es"> </div> 
                       <script type="text/javascript" src="//www.tiempo.es/widload/es/ver/320/340/010/es0ca0037/033974258b5fb50f89ccd5071c8f4f8c.js">
                       </script> 
                   </div>
             </div>
           </td>
        </tr>
        <tr valign="top" align="middle">
           <td>
              <!-- ======================================================================================================================================================= -->
              <!-- WIDGET RELOJ                                                                                                                                           -->
              <!-- ======================================================================================================================================================= -->
              <div class="container">
                <div class="clock">
                <div id="Date"></div>
                        <ul>
                            <li id="hours"> </li>
                            <li id="point">:</li>
                            <li id="min"> </li>
                            <li id="point">:</li>
                            <li id="sec"> </li>
                        </ul>
                </div>
              </div>
              <br/>
              <img src="logo.png" width="290px" height="210px"/>
           </td>
        </tr>
   </table>

</body></html>
El código html completo de panel.html y varias cosas más podemos descargarlo de aquí. No olvidemos adaptar las IP y los nombres de los vídeos, ficheros de imagen, enlaces a presentaciones y el widget del tiempo a nuestro caso particular.

El resultado final que aparece en la pantalla es:



Bueno, pues con todo esto ya tenemos nuestra pantalla hecha con materiales de derribo.

viernes, 1 de diciembre de 2017

Desactivando la red wifi de 5Ghz en DLink DIR-860L con dd-wrt

Nuestros puntos de acceso DLink traen de serie 2 wifis: la de 2.4Ghz y la de 5Ghz, cada una con SSID y gestión de contraseñas independiente. La mayoría de los portátiles de los que disponemos en los centros solo ven la red de 2.4Ghz y los programas de gestión (SiaticControl y ControlWifi) que tenemos solo manejan esa frecuencia.

Como consecuencia de ello la red de 5Ghz estará ociosa a no ser que la configuremos a mano via ssh o interfaz web. Como algunos de nosotros queremos dejarla apagada en tanto en cuanto no la necesitemos estuve investigando como hacerlo entrando por ssh al DLink.

Lo que hacemos cambiar el SSID y contraseña que trae por defecto dicha red e indicar que el comando "ifconfig ba0 down" se ejecute en el arranque del sistema dd-wrt. El interface "ba0" es el correspondiente a la red de 5Ghz (en el caso de la de 2.4Ghz es "ra0"):
nvram set wl1_ssid=NUEVO_SSID_5GHZ
nvram set wl1_wpa_psk=NUEVA_PASSWORD_5GHZ
nvram set rc_startup="ifconfig ba0 down"
nvram commit
reboot
Y con esto nos despedimos hasta una nueva pildorita sobre nuestros DLink.

martes, 28 de noviembre de 2017

Reinicio periódico en DLink DIR-860L con dd-wrt

Es muy conveniente reiniciar algunos sistemas de forma periódica. En el caso de nuestros puntos de acceso DLink DIR-860L es casi obligatorio ya que cuanto mas tiempo pasan en funcionamiento mas empiezan a fallar ciertos aspectos, lo cual nos obliga al final a hacer un reset manual.

Mis compañeros Paco y Noemí me descubrieron que el propio dd-wrt incorpora un mecanismo de reinicio periódico, sin necesidad de configurar a mano crontab. Aunque se puede definir usando el interface web, buscando la opción "Administracion->Keep alive" entre sus pestañas nosotros lo vamos a hacer por comando. Consiste en entrar por ssh y ejecutar en cada punto de acceso los siguientes comandos:
nvram set schedule_enable=1
nvram set schedule_hour_time=2
nvram set schedule_weekdays=*
nvram set schedule_hours=7
nvram set schedule_minutes=0
nvram set schedule_time=3600
nvram set ntp_enable=1
nvram set ntp_server=..ip servidor centro..
nvram set ntp_mode=auto
nvram commit
reboot
Comentemos:
  • schedule_enable: puesto a 1 habilita el reinicio programado.
  • schedule_hour_time: puesto a 1 activa el reinicio cada X segundos, puesto a 2 el reinicio a dias/horas determinados.
  • schedule_weekdays, schedule_hours, schedule_minutes: días y hora donde se realiza el reinicio. En mi caso, todos los dias a las 7:00 de la mañana.
  • schedule_time: segundos entre reinicio si schedule_hour_time=1.
  • ntp_enable: puesto a 1 habilita la sincronización de hora por ntp.
  • ntp_mode: con valor auto se realiza una sincronización automática.
  • ntp_server: IP del servidor ntp de nuestra red. Normalmente es el servidor principal del centro.
El servidor ntp se configura para que el punto de acceso se ponga en hora y haga los reinicios en el momento correcto, ya que si no se piensa que estamos en 1970 y a una hora intempestiva del día. Una vez arrancado el punto de acceso tarda un rato en sincronizar la hora, parece ser que no le corre prisa eso, pero si somos pacientes al final coge la hora y fecha correcta.

Gracias a estos reinicios nuestros puntos de acceso serán mucho mas estables y se portarán mejor. Nada como un buen reinicio para limpiar un sistema.

martes, 14 de noviembre de 2017

Conectar por ssh/vnc al PC del profesor tras el punto de acceso DLink DIR-860L

En esta entrada anterior vimos como hacer NAT en el aula usando un punto de acceso DLink DIR-860L, de manera que los PC de alumnos y profesor quedaban en red privada dentro del rango 192.168.0.X.

El punto de acceso tenía dos direcciones: la 172.X.Y.Z (que sería la pública del centro) y la 192.168.0.254 (dentro de la red privada del aula), haciendo NAT entre ellas. Esto aísla por completo el aula de la red del centro pudiendo asegurar que el DLink realiza el NAT de una forma bastante más eficiente que los equipos HP que nos trajeron a los infolab.

El problema derivado es que no podemos conectar por ssh ni por vnc con el ordenador del profesor, que queda detrás del DLink. Esto causa bastante trastorno ya que es muy normal conectar para realizar diversas tareas de forma remota. Existe una vía indirecta para conectar por ssh, entrando primero en el DLink y luego desde alli haciendo ssh root@192.168.0.100, pero aparte de ser lento no podemos usar ssh -X para abrir aplicaciones gráficas sobre la conexión.

La solución está en configurar el port forwarding en el DLink para redigir determinadas conexiones a 172.X.Y.Z para que acaben en el 192.168.0.100 de la red privada. Es el famoso "abrir puertos" que hemos hecho todos en el router de casa para usar torrent o juegos.

El método paso a paso para abrir puertos en el entorno web de DD-WRT de nuestro punto de acceso DLink está descrito en este vídeo.

Para redirigir ssh he optado conectar el puerto 23 de la IP externa con el puerto 192.168.0.100:22, mediante la opción NAT-Qos -> Port Forwarding:


Uso el puerto 23 porque el puerto 22 está reservado para el servicio sshd del punto de acceso y no quiero perder la posibilidad de hacer ssh al DLink. Una vez aplicados los cambios, si desde cualquier punto de la red del centro hacemos:
$ ssh -X -p 23 root@172.X.Y.Z
conectaremos con el PC del profesor, saltando de forma transparente el DLink. Al usar "-X" tenemos la posibilidad de abrir aplicaciones gráficas.

Para redirigir vnc he usado la opción NAT-Qos -> Port Range Forwarding:


Pongo el rango de puertos 5900-5910 ya que vnc va usándolos de forma secuencial si hay varias sesiones vnc abiertas. En este caso el mapeo de los puertos es directo: el 5900 de fuera se conecta con el 5900 de dentro. Teniendo arrancado el servidor vnc en el PC del profesor (en mi caso lo arranco a mano cuando lo necesito, entrando antes por ssh) desde fuera haremos:
$ vinagre 172.X.Y.Z 
y entramos en el pc interno al aula.

Con esto tendremos acceso a esos PC igual al que tenemos a cualquier otro PC de profesor del centro.

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á.