eBox developers guide

Javier Uruen

Ricardo Muñoz

Jorge Arcas

Isaac Clerencia

Guillermo Ontañón

Javier Amor

Enrique J. Hernández

This work is licensed under the Creative Commons Attribution License. To view a copy of this license, visit http://creativecommons.org/licenses/by/2.0/ or send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA.


Tabla de contenidos

1. Introducción
1.1. ¿A quién va dirigido?
1.2. ¿Qué es eBox?
1.3. Terminología
1.4. Como funciona un módulo
2. API Básica
2.1. EBox::Global
2.2. Excepciones
2.3. Validación de datos
2.4. i18n
2.4.1. i18n para desarrolladores
2.4.2. i18n para traductores
3. Backend del módulo
3.1. El módulo base
3.1.1. Constructor del módulo
3.2. Comandos de root
3.3. EBox::GConfModule
3.4. Ordenando cosas
3.5. Controlando un demonio
3.5.1. Generación del fichero de configuración
3.5.2. Controlando la ejecución
3.6. Esquemas de GConf
3.6.1. Esquema de GConf mínimo
3.6.2. Valores por defecto
3.6.3. Dependencias de módulos
3.7. Copias de seguridad
3.8. Registro administrativo
4. Interactuando con otros componentes
4.1. Introducción
4.1.1. Observers
4.2. Módulo objects
4.3. Módulo network
4.4. Módulo firewall
4.5. Definiendo nuevos observers
5. Frontend web
5.1. CGIs
5.2. Plantillas mason
5.2.1. Elementos estándar de la GUI
5.3. El menú
5.4. Resumen
6. Módulos basados en LDAP
6.1. Introducción
6.2. Obtención de la configuración
6.3. Accediendo a LDAP
6.4. Implementando tu módulo basado en LDAP
6.5. Añadiendo tu funcionalidad al interfaz de usuario
6.6. Incluyendo tus esquemas LDAP
6.7. Añadiendo tus lista de control de acceso
7. Creando un módulo simple
7.1. Estudio del servicio y evaluación de características
7.2. Crear un nuevo módulo a partir de la plantilla de módulo.
7.3. Definir e implementar la API
7.4. Creando CGIs y plantillas
7.5. Mostrando el módulo en el menú y la página Resumen
7.6. Generando los ficheros de configuración y controlando el demonio.
7.7. Estableciendo reglas a medida al firewall
7.8. Finalizando

Lista de ejemplos

2.1. Creando una instancia de un módulo de sólo-lectura
2.2. Funciones para instanciar más de un módulo
2.3. Lanzando una excepción interna genérica
2.4. Capturando una excepción
2.5. Usando funciones de validación de datos
2.6. Utilizando las funciones de validación de datos con gestión automática de errores
2.7. Usando la función __n
3.1. Sencillo constructor de un módulo
3.2. Using EBox::Sudo::root()
3.3. Asignando un valor de tipo cadena
3.4. Asignando una lista de cadenas
3.5. Recogiendo y usando un identificador único
3.6. Ordenando las reglas del firewall
3.7. Generando un fichero de configuración
3.8. Ejemplo de implementación de _regenConfig
3.9. Implementación de _stopService de ejemplo
3.10. Esquema de GConf mínimo
3.11. Creando la instancia de un módulo
3.12. Configurando un valor por defecto en los esquemas de GConf
3.13. Configurando dependencias entre módulos
3.14. Sobreescribiendo funciones para las copias de seguridad
3.15. Sobreescribiendo funciones para las copias de seguridad extendidas
3.16. Incluyendo el paquete LogAdmin
3.17. Creando un objeto con soporte a registro administrativo
3.18. Añadiendo acciones para el registro
3.19. Llamando al método logAdminDeferred
4.1. Implementación de la clase EBox::ObjectsObserver
4.2. Implementando un EBox::NetworkObserver
4.3. Creando reglas de firewall a medida
4.4. Definiendo un observer que pueda ser implementado por otros módulos
4.5. Llamando a los módulos con un observer
5.1. CGI Hola mundo
5.2. Configurando la plantilla mason para un CGI
5.3. Enviando argumentos a la plantilla de mason
5.4. Estableciendo el atributo 'msg'
5.5. Utilizando parámetros de la petición HTTP
5.6. Redirigiendo una petición a un CGI distinto
5.7. Configurando el dominio gettext en un CGI
5.8. Declarando argumentos en una plantilla de mason
5.9. Sección init en las plantillas de mason
5.10. Incluyendo código perl en una plantilla de mason
5.11. Imprimiendo cadenas en una plantilla de mason
5.12. Esquema html para mostrarle información al usuario
5.13. Tabla de listado de datos
5.14. Formulario de entrada de datos
5.15. Añadiendo carpetas y elementos al menú
5.16. Implementación de ejemplo de summary
5.17. Ejemplo de implementación de statusSummary
6.1. Implementando el método _groupAddOns
6.2. Implementando el método _includeLDAPSchemas
7.1. Constructor de EBox::NTP
7.2. Activación del servicio de sincronización
7.3. Obtención el estado del servicio NTP
7.4. Establecer el estado de la sincronización externa
7.5. Obtener el estado de la sincronización externa
7.6. Establecer servidores ntp externos
7.7. Obtener la lista de servidores ntp externos
7.8. Establecer una nueva fecha y hora en el sistema
7.9. Establecer una nueva zona horaria
7.10. Reiniciar módulos y servicios del sistema
7.11. Constructor del CGI EBox::CGI::NTP::Index
7.12. Envío del estado del servicio a la plantilla mason
7.13. CGI que habilita o deshabilita el servicio NTP
7.14. Plantilla mason para habilitar el servicio NTP
7.15. Código de plantilla mason de selección de plantillas
7.16. Añadiendo entradas al menú eBox
7.17. statusSummary en EBox::NTP
7.18. Generar el fichero de configuración /etc/ntp.conf
7.19. Plantilla mason para generar el fichero /etc/ntp.conf
7.20. Método _regenConfig
7.21. Método para la gestión del demonio
7.22. Comandos para arrancar y parar el demonio ntp.
7.23. Método _stopService.
7.24. Método para saber si el servicio está ejecutándose.
7.25. Configuración del firewall

Capítulo 1. Introducción

1.1. ¿A quién va dirigido?

Esta guía está escrita para desarrolladores interesados en programar nuevos módulos con nuevas funcionalidades para eBox, y para aquellos que necesitan cambiar o extender la framework base eBox.

eBox está escrita en perl y está mayoritariamente orientada a objetos. Se asume que los lectores saben realizar programas orientados a objetos en perl. Muchas de las características del lenguaje o de las librerias usadas en eBox han sido comentadas pero no es nuestra intención el explicarlas profundamente.

Experiencia escribiendo aplicaciones orientadas a objetos y el uso de patrones de diseño en cualquier lenguaje te será de ayuda, pero deberías tener una buena base de los conceptos básicos de la programación orientada a objetos.

Nos hemos esforzado en esta guía en ofrecer ejemplos de todos los aspectos del desarrollo de eBox. Muchos de ellos vienen directamente de módulos existentes y funcionales. Además, en Capítulo 7 se desarrolla paso a paso un módulo completo. Es un módulo real asi que el capítulo debería cubrir todos los aspectos de un módulo completo.

1.2. ¿Qué es eBox?

eBox es una plataforma para el desarrollo y despliegue de servicios relacionados con la seguridad y trabajo en grupo para una red local. Es configurable a través de un interfaz web que integra todos los servicios de una manera consistente y fácil de usar. El objetivo es que pueda ser usada por personas no expertas.

eBox está orientada a instalarse sobre una máquina dedicada, todas las tareas de configuración son realizadas a traés de la interface web de eBox. Esto significa que la configuración de los servicios subyacentes es unidireccional: los módulos eBox generan ficheros de configuración, en algunos casos sobreescriben ficheros del sistema (aunque esto tiende a ser evitado en la medida de lo posible) y cambios a mano sobre esos ficheros no son detectados por eBox. Esto simplifica la implementación y uso del paquete pero tiene la desventaja de que los desarrolladores deben tener cuidado si usan su propio sistema para pruebas.

El diseño de eBox es modular, los nuevos módulos que proporcionan nuevos servicios y funcionalidades pueden ser desarrollados independientemente del paquete base. eBox simplifica el despliegue de nuevos módulos y la actualización de los ya existentes mediante un módulo de gestón de software, el cual es también a su vez independiente del paquete base eBox.

El sistema está basado en Linux y ha sido desarrollado sobre Debian, como hay algún "debianismo" sobre algún módulo, no se ofrece soporte sobre otras distribuciones Linux. Portar eBox a otras distribuciones Linux debería ser sencillo sobre otros sistemas operativos Unix como OpenBSD puede tomar un poco mas de trabajo pero debería seguir siendo factible.

eBox está basada en unos pocos paquetes de software, los cuales son usados para diferentes propósitos:

Linux 2.6

eBox hace uso de algunas características ofrecidas por el kernel, algunas de ellas están disponibles sólo en la serie 2.6. Entre esas caraterísticas están netfilter (para el firewall), 802.1q (para VLANs) e ipsec.

perl y mod_perl

Todos los módulos eBox están implementados en perl, el interfaz web corre sobre mod_perl por razones de rendimiento.

mason

mason es un sistema de plantillas para perl, es usado para generar HTML para la interfaz de usuario basada en web y para generar ficheros de configuración.

apache

El interfaz web es servido por apache, nosotros usamos habitualmente el paquete debian apache-perl.

gconf

eBox almacena su configuración mediante gconf2 y la librería de bindins de perl.

sudo

Apache se ejecuta como usuario no privilegiado, todos los módulos eBox usan sudo para ejecutar comandos que necesitan ser ejecutados como root.

El paquete base de eBox ofrece una framework de desarrollo para nuevos módulos. Usando esta framework los módulos obtienen automáticamente características como backups de configuración y el descarte de los cambios efectuados en la configuración antes de se guardados. Todas las características disponibles para los desarrolladores de módulos serán explicados en detalle en los próximos capítulos.

1.3. Terminología

Vamos a usar unas pocas convenciones a lo largo de esta guía sobre varios conceptos:

Booleanos. Cuando necesitemos hablar sobre un valor booleano pasado a un método o retornado por el, nos referiremos al valor true como true y al valor false como undef or false.

Módulos perl y módulos eBox. Puede haber cierta confusión en ciertas partes de la guía sobre el término module, puede referirse a un módulo eBox o a un módulo perl. Un módulo eBox es un set completo de clases (y módulos perl) y otros ficheros que ofrecen funcionalidades por si mismos para eBox y pueden ser instalados o desinstalados independientemente del resto de eBox. Un módulo perl es básicamente un fichero fuente perl.

Cuando no hay posibilidad de confusión, simplemente escribiremos módulo, de otro modo escribiremos explícitamente módulo perl ya que este término será el menos usado.

1.4. Como funciona un módulo

Un módulo eBox típico maneja la configuración de un demonio, pudiendo integrarse también con otros módulos de eBox. El desarrollador será quien decida la forma en la que el usuario podrá configurar el demonio, que no será necesariamente una relación directa entre las del módulo y las que admita el demonio. El desarrollador podrá elegir el valor, por defecto, más adecuado para la mayoría de las opciones y esconderlas de cara al usuario, mostrándole sólo las que se consideren realmente importantes. De igual forma, el cambio de una opción por parte del usuario mediante el interfaz web, puede causar cambios de configuración en varios parámetros de configuración del demonio, o en varios módulos de eBox. El principal objetivo es mantener un interfaz de usuario sencillo, facil de usar e integrado lo más posible, mientras se ofrece un conjunto de parámetros de configuración lo más rico posible.

Sin embargo, puede haber módulos que no manejen la configuración de un servicio de red. Un ejemplo de un módulo de este tipo es el módulo de "sysinfo" del sistema base, se encarga de recoger la información del sistema a ser mostrada en la página de Resumen y ofrece unas entradas de menú para características que no pertenecen a ningún módulo en particular. La clase padre del módulo define varios métodos abstractos que los módulos reales pueden dejar sin implementar, así un módulo puede simplemente mantener información en la página de Resumen, añadir nuevas entradas de menú, manejar un servicio de red o todo lo anterior.

El caso normal, y mas interesante, es el del módulo descrito en el primer parrafo. Este módulo tiene tres partes.

  • Una que define e implementa una API que permitirá a la GUI, a otros módulos o scripts en perl configurar el demonio que van a manejar.

  • La segunda es la GUI, la cual integra una serie de CGIs que muestran la configuración en ése momento al usuario y le permiten cambiarla, estos CGIs usan la API definida anteriormente para obtener la información de la configuración y realizar cambios sobre ella.

  • La tercera parte del módulo es mas pequeña normalmente, traduce toda la información sobre configuración almacenada en GConf en reglas del firewall, ficheros de configuración y comandos que hacen que el servicio de red se comporte como el usuario espera. También se hace cargo del inicio, parada o reinicio del sistema cuando sea necesario.

Esta separación de la GUI y el backend abre la posibilidad de de cambiar la configuración por otras vías. Una de estas otras vías es a través de scripts, esto es muy útil cuando se realizan paquetes para una distribución, el mantenedor del paquete puede escribir un script sencillo para importar la configuración actual del sistema a eBox, o establecer unos valores por defecto. Otro uso de la API la puede realizar otro módulo diferente, por ejemplo el módulo del firewall es uno de los casos mas habituales de este uso, casi todos los módulos necesitan "decirle" al firewall que abra algunos puertos para ellos. En el futuro puede que se desarrolle un wrapper sobre estas APIs para publicarlas a través de web-services, esto podría hacer a la configuración de eBox programable sobre la red.

Tenemos una API que ofrece ciertas características configurables de un servicio, y una GUI que permite al usuario manipular la configuración. La tercera parte del módulo es normalmente mas pequeña, traduce toda la informacón acerca de la configuracón almacenada en gconf en reglas para el firewall, ficheros de configuración y comandos que hacen que el servicio de red se comporte como el usuario espera. También se encarga del lanzamiento, parada y relanzamiento del servicio cuando sea necesario.

Además de estas tres partes, el módulo tiene otras funcionalidades menores, como su parte de información en la página de Resumen de la interfaz web, las entradas de menú, la declaración de dependencias, backups de las partes de la configuración no almacenadas en gconf, etc...

Esto es todo lo que hay, crear un módulo es tan simple como seguir los siguientes pasos:

  • Decidir qué demonio va a manejar tu módulo y aprender cómo trabaja y cómo configurarlo.

  • Planear que opciones vas a ofrecer a los usuarios a través de la interface web, y cómo pueden interactuar con otros módulos eBox.

  • Definir e implementar la API que va a permitir a la GUI manipular las opciones necesarias de configuración. La clase principal de tu módulo debe heredar de EBox::GConfModule, esta clase envuelve la API de gconf e implementa algunas características útiles que todos los módulos eBox necesitan tener de manera transparente.

  • Crear los CGIs y plantillas HTML que permitirán al usuario interactuar con el módulo. Los CGIs deben heredar de la clase EBox::CGI::Base la cual ofrece, de nuevo, algunas características a todas sus clases hijas de manera transparente.

  • Escribir el código necesario para hacer que el demonio funcione, posiblemente generar algun fichero de configuración y establecer alguna regla de firewall usando la clase EBox::Firewall. Los ficheros de configuración son generados de manera trivial con mason, que es el sistema de plantillas usado también para generar las páginas HTML para la GUI.

El tamaño y complejidad de un módulo depende directamente de la complejidad del servicio involucrado y de la cantidad de parámetros configurables sobre el servicio que ofrecemos al usuario. El trabajo necesario para hacer un módulo eBox pequeño es mínimo, tomando como ejemplo el módulo DNSCache, sus CGIs tienen 49 lineas de código y el módulo en sí 134.

La estructura de directorios de un módulo eBox puede parecer un poco compleja a primera vista. Un módulo eBox normal debería tener el siguiente aspecto:

AUTHORS     configure.ac  INSTALL     Makefile.am  README   stubs/
autogen.sh  COPYING       NEWS        schemas/     tools/   ChangeLog
debian/     m4/           po/         src/         www/
		

Los directorios más importantes son src/, schemas/, www/ y stubs/.

El directorio src/ contiene el código fuente del módulo. Dentro de él encontramos dos subdirectorios: EBox y templates. En el directorio EBox se encuentran los ficheros fuente Perl, incluyendo el subdirectorio CGI con la interfaz web del módulo. El directorio templates se utiliza para almacenar las plantillas Mason, que serán utilizadas para generar la salida HTML.

El directorio schemas/ contiene esquemas gconf, utilizados para definir las opciones de configuración de los módulos y opcionalmente proporcionar valores por defecto para algunas de ellas.

El directorio www/ contiene imágenes y hojas de estilo CSS que son utilizadas por la interfaz web.

En stubs/ se almacenan las plantillas Mason utilizadas para generar los ficheros de configuración para los servicios del módulo.

Capítulo 2. API Básica

2.1. EBox::Global

La clase EBox::Global ofrece varias funciones para la administración de módulos. Las funciones utilizadas más comúnmente son las que hacen referencia a la instanciación de módulos. EBox::Global funciona como una factoría de módulos, obtienes una instancia suya usando la función estática getInstance y después usar esa instancia para crear módulos. La factoría puede tener dos formas, una de sólo lectura y otra de lectura-escritura.

Llamar a getInstance sin argumentos producirá una factoría de lectura-escritura, la cual crea módulos que le permiten hacer llamadas para cambiar la configuración. Un detalle muy importante de los módulos lectura-escritura es que devuelven la información de su última configuración, incluso si no se ha guardado todavía (lo que significa que podría ser revocada más tarde por el usuario). Esta idea es importante, ya que la configuración que nos ofrece el módulo lectura-escritura no debe ser tratada como definitiva, a no ser que el módulo no tenga cambios esperando a ser guardados. Puede ver si un módulo tiene cambios sin guardar llamando a la función modIsChanged en la clase EBox::Global.

La idea subyacente en el comportamiento de los módulos lectura-escritura es tener un front-end para construir una configuración. Por ejemplo, si el usuario crea un nuevo objeto de red en el módulo objetos, el nuevo objeto mostrará instantáneamente la configuración del firewall y se podrá crear nuevas reglas del firewall que lo usen. Después de realizar todos los cambios deseados, el usuario guarda la configuración. Si decide cancelar los cambios que acaba de hacer, tanto el nuevo objeto como las reglas del firewall serán borradas.

Hay una situación en la que no desea recoger la información que no ha sido guardada todavía. Ésta es cuando se está generando el fichero de configuración de un demonio, configurando las reglas del firewall, configurando la dirección de un interfaz de red, o cualquier otra actividad que necesita la configuración real y definitiva. Esta situación se encuentra en los scripts del sistema (el script de arranque, trabajos de cron, etc). En estos casos necesita lo que se denominan módulos de sólo-lectura, que sólo ofrecen la información almacenada y no permiten hacer llamadas a funciones que cambiarían la configuración del módulo. Para obtener módulos de sólo-lectura necesita crear una instancia de EBox::Global configurando su parámetro readonly como true. Las instancias del módulo devueltas por una factoría creada de esta forma serán de sólo-lectura. Este script muestra como obtener una instancia del módulo squid y le pide que se reinicie:

Ejemplo 2.1. Creando una instancia de un módulo de sólo-lectura

#!/usr/bin/perl

use strict;
use warnings;

use EBox;
use EBox::Global;
use Error qw(:try);

EBox::init();

my $global = EBox::Global->getInstance(1);
my $squid = $global->modInstance('squid');

try {
	$squid->restartService();
} catch EBox::Exceptions::Base with {
	print STDERR "El módulo Squid falló al reiniciarse.\n";
};

Hay dos funciones que hacen más sencillo realizar dos tareas muy comunes: obtener una instancia de cada módulo y obtener una instancia de cada módulo que implemente alguna clase abstracta. Éstas son modInstances y modInstancesOfType. Usarlas es muy sencillo:

Ejemplo 2.2. Funciones para instanciar más de un módulo

my $global = EBox::Global->getInstance(1);

# se reinician todos los módulos
foreach my $mod (@{$global->modInstances()}) {
	$mod->restartService();
}

# se reinician todos los módulos que implementen la clase NetworkObserver
foreach my $mod (@{$global->modInstancesOfType('EBox::NetworkObserver')}) {
	$mod->restartService();
}

Reiniciar todos los módulos es todavía más sencillo que eso. EBox::Global nos provee de la función restartAllModules justamente para eso. Éste es uno de los métodos que realiza una operación en todos los módulos instalados:

restartAllModules

Llama a la función restartService de todos los módulos. Reinicia todos los servicios manejados por eBox. Los ficheros de configuración son regenerados cada vez que un servicio es reiniciado.

stopAllModules

Llama a la función stopService en todos los módulos, esto incluye la interfaz web de administración, por lo que hay que ser cauteloso.

revokeAllModules

Cancela los cambios de configuración que se han hecho en todos los módulos desde la última vez que la configuración se guardó.

saveAllModules

Guarda los cambios de la configuración en todos los módulos.

2.2. Excepciones

El manejo de todos los errores en eBox está implementado con excepciones del estilo de Java. Éstas están provistas por el módulo de perl Error. Puede leer la documentación de perl para ese módulo para obtener una descripción detallada de cómo utilizar las excepciones en perl. Las excepciones de eBox están definidas en el espacio de nombres EBox::Exceptions.

eBox provee una jerarquía de excepciones de la cual puede elegir la más apropiada para cada situación. Todas las excepciones de eBox heredan de una de estas dos:

  • EBox::Exceptions::External

  • EBox::Exceptions::Internal

La diferencia entre estos dos tipos es que el usuario verá cuando una excepción no ha sido capturada. La plataforma del interfaz de usuario mostrará el mensaje contenido en la excepción sin alterarlo si la excepción es una de tipo externo. Debería usar excepciones externas para los errores introducidos por el usuario, como un error de sintaxis en una dirección IP dada por el interfaz de usuario al backend del módulo.

Si la excepción es interna se registrará el error y se asumirá que algo falló internamente en eBox (un bug) o en el sistema en general. Cuando una excepción interna se captura por GUI el usuario verá un mensaje de error genérico. Cualquier excepción, interna o externa, puede ser utilizada internamente para otros propósitos, siempre que sea capturada antes de que recorra todo el camino hasta la GUI.

Si no puede encontrar una excepción para ciertas condiciones de error, tiene dos opciones: crear una nueva clase de excepción, heredándola de EBox::Exceptions::External o EBox::Exceptions::Internal, o usar únicamente una de estas dos clases como excepciones genéricas:

Ejemplo 2.3. Lanzando una excepción interna genérica

if ($foo_condition) {
	throw EBox::Exceptions::Internal('Algo ha pasado!!');
}

Como puede ver, lanzar excepciones sigue una sintaxis muy similar a Java. Capturarlas también es muy similar:

Ejemplo 2.4. Capturando una excepción

use Error qw(:try);

sub foo
{
	my $bar = shift;

	try {
		$bar->doSomething();
	} catch EBox::Exceptions::Base with {
		# no hace nada, sólo se ignora el error
	};
}

Puede hacer lo que quiera dentro de la sección “catch”. Puede escribir varias secciones “catch” si necesita hacer diferentes cosas para diferentes tipos de excepción. Las secciones “otherwise” y “finally” también están permitidas. Para una explicación detallada sobre éstas, únicamente ejecute perldoc Error. Un detalle muy importante es no olvidar el punto y coma después de la última sección (el “catch” en el ejemplo). No hacerlo puede producir resultados inesperados, esto se debe a la magia negra que es utilizada en la implementación del try-catch en perl.

2.3. Validación de datos

Es muy importante que cada funcionalidad ofrecida por cada módulo valide sus datos de entrada correctamente. Todos los argumentos necesitan ser comprobados. Si esto no se hace, un módulo podría guardar valores sintácticamente incorrectos en su configuración, y el servicio subyacente se comportaría de una forma impredecible con el fichero de configuración generado por el módulo. La validación de datos también ayuda a encontrar errores en la GUI, y cualquier otro código que utilice el módulo.

El módulo de perl EBox::Validate provee de una serie de funciones para validar diferentes tipos de datos. Todas estas funciones funcionan de la misma manera, su primer argumento es el valor que se va a comprobar. Devuelven true si el valor es correcto, undef si no lo es:

Ejemplo 2.5. Usando funciones de validación de datos

use EBox::Validate qw(:all);

my $ip = '192.168.0.1';

unless (checkIP($ip))
	print STDERR "$ip no es válida.\n";
}

Lo que hay que hacer normalmente si un argumento es incorrecto es lanzar una excepción. Y la mayor parte del tiempo el valor incorrecto será uno suministrado por el usuario. Por esta razón, todas las funciones en EBox::Validate nos suministran un mecanismo sencillo para lanzar excepciones del tipo EBox::External. Todo lo que es necesario hacer es pasarles un argumento más, con un nombre o descripción para el valor que está intentando comprobar. Si pasa ese argumento y la validación falla, se lanzará una excepción con un mensaje indicando el nombre del campo incorrecto:

Ejemplo 2.6. Utilizando las funciones de validación de datos con gestión automática de errores

checkIP($ip, 'dirección IP ');

Si no quiere que se lance una excepción, o si quiere que se lance un tipo distinto, no pase el último argumento y gestione el error dentro de su código.

2.4. i18n

eBox utiliza “gettext”, la plataforma de internacionalización (i18n) del proyecto GNU, para permitir a los colaboradores traducir fácilmente el interfaz gráfico de eBox a su propio idioma.

Esta sección describe la i18n de eBox tanto desde el punto de vista del desarrollador (reglas que deben seguirse para desarrollar un módulo traducible) como del traductor (cómo realizar una traducción de eBox a su idioma).

2.4.1. i18n para desarrolladores

Cada módulo de eBox tiene su propio dominio de texto para las cadenas que contiene, con un nombre construido a partir del nombre del propio módulo. Los módulos deben indicar tanto en el constructor del módulo como en cada uno de los CGI's el dominio de texto al que pertenecen, tal y como se indica en Sección 3.1.1 y Ejemplo 5.7.

Todos los ficheros que contengan cadenas traducibles, incluyendo las plantillas de Mason, deben incluir módulo perl EBox::Gettext y cualquier cadena que deba poder ser traducible debe ser marcada como tal, utilizando la función __ tal y como se indica en el siguiente ejemplo:

print __("Hello world");
			

Esto hará que el sistema de compilación de eBox sea capaz de detectar las cadenas traducibles y recopilarlas para su posterior traducción.

Si la cadena debe incluir alguna variable no hay que concatenarla

print __("Edit ") . $group . __(" members"); #incorrecto
			

ya que esto hace imposible una traducción correcta de la frase completa. La opción correcta es utilizar la función __x, una variante de la función __ que permite incluir variables:

print __x("Edit {group} members", group => $group); #correcto
			

de tal modo que el traductor puede incluir la variable group en el lugar apropiado de la frase.

Si quieres marcar una cadena como traducible pero decidiendo no traducirla, deberías usar la función __n. Un ejemplo de uso es el siguiente:

Ejemplo 2.7. Usando la función __n

my $options = [__n('Foo'), __n('Bar'), __n('Foobaz') ]; # As many as you want
print __($options[$index]);
			  

El ejemplo mostrado antes da la oportunidad al traductor de traducir únicamente la opción elegida dejando a las demás posibles opciones (normalmente un alto número) sin una traducción.

Si se siguen correctamente estas reglas, el sistema de compilación de eBox generará automáticamente los ficheros PO de los módulos conteniendo todas las cadenas incluidas en los mismos. Estos ficheros podrán ser traducidos comodamente por los traductores, como se explica en la siguiente subsección.

2.4.2. i18n para traductores

Al utilizar GNU gettext como plataforma de i18n, eBox utiliza el formato más utilizado para la traducción, los ficheros PO. Cada módulo de eBox cuenta con un fichero PO para cada uno de los idiomas disponibles. Un traductor sólo tiene que traducir las cadenas catalogadas en ese fichero para traducir completamente un módulo. En el caso de que se desee empezar la traducción para un nuevo idioma sólo es necesario solicitar un fichero PO para el nuevo idioma.

Un fichero PO para un determinado idioma contiene cadenas de texto en el idioma original (inglés) junto a su traducción. Cada cadena puede encontrarse en tres posibles estados: traducida (si existe una traducción para la cadena), difusa o borrosa (si existe una traducción para la cadena, pero la cadena ha cambiado ligeramente) o sin traducir:

# cadena traducida
msgid "Name"
msgstr "Nombre"

# cadena borrosa
#, fuzzy
msgid "Interfaces"
msgstr "Interface"

#cadena sin traducir
msgid "External"
msgstr ""
			

Si la cadena a traducir necesita incluir el valor de una variable al ser presentada en la interfaz de usuario, encontrará en el fichero PO una entrada similar a ésta:

msgid "Edit {group} members"
			

En este caso simplemente se debe reescribir la frase recolocando el nombre de la variable (entre llaves) en el lugar necesario, pero sin ser traducido. La traducción correcta de la cadena en este caso sería:

msgstr "Editar miembros de {group}"
			

Aunque es posible realizar la traducción del fichero PO utilizando cualquier editor capaz de editar ficheros utilizando codificación UTF-8, es muy recomendable usar alguna de las herramientas creadas específicamente para la traducción de ficheros PO, como por ejemplo KBabel (*nix) o poEdit (multiplataforma).

Capítulo 3. Backend del módulo

La plataforma eBox ofrece unos módulos con ciertas funcionalidades que están disponibles a través de una API orientada a objetos en su mayor parte. No todo está orientado a objetos, interfaces procedurales se han usado cuando tenía sentido hacerlo.

Algunas de las características de la API funcionan a través de herencia, éstas normalmente ofrecen un medio para que el módulo implemente las funcionalidades estándar como los menús, la página de estado, el guardado de la configuración o su revocación, etc. Algunas de estas características necesitan ser implementadas por el módulo mientras que otras son completas en la plataforma, y el módulo puede extenderlas o sobreescribirlas.

3.1. El módulo base

Todos los módulos de eBox heredan de la clase EBox::Module. Esta clase define funciones abstractas que los módulos sobreescribirán para implementar una cierta funcionalidad. Estas funciones son llamados por la plataforma cuando son necesarios.

Además de estas funciones abstractas, la clase implementa unos pocas funciones con algunas funcionalidades básicas. Estas últimas, siguen normalmente el método que define una plantilla de un patrón de diseño, es decir, realizan algunas operaciones pero delegan parte de ellas en alguna función abstracta que deberá ser implementada por la clase hija.

Finalmente, hay unas pocas funciones que implementan algunas operaciones comunes que son utilizadas por la mayoría de los módulos, están escritas para que sean llamadas por las clases hijas cuando se les necesita. Están situadas en la clase EBox::Module únicamente por conveniencia.

3.1.1. Constructor del módulo

Todas las instancias del módulo son creadas y cacheadas por la clase Ebox::Global. Los constructores de un módulo no deben ser llamados directamente excepto por la clase EBox::Global. Por esta razón, se llaman _create en vez de new.

El primer guión bajo en el nombre de la función es una convención de nombres. Las funciones con un nombre así están pensadas para ser usados de forma privada por su propia clase y por sus clases ascendentes. No deberían ser llamadas directamente por cualquier otra clase que no tenga una relación de este tipo. En el caso del constructor del módulo, la única clase que debería llamarlo es Ebox::Global.

La función _create en EBox::Module recibe dos argumentos desde sus clases hijas. name es el nombre del módulo y es obligatorio; domaines el dominio gettext para el módulo, es opcional, y “ebox” es su valor por defecto.

Ejemplo 3.1. Sencillo constructor de un módulo

sub _create
{
	my $class = shift;
	my $self = $class->SUPER::_create(name => 'dhcp',
					domain => 'ebox-dhcp',
					@_);
	bless ($self, $class);
	return $self;
}

3.2. Comandos de root

El servidor apache bajo el que funciona eBox y cualquier otro script de perl que utilice el API de eBox, utilizan un id de usuario dedicado. El usuario es normalmente llamado “ebox”. Los módulos de eBox necesitan ejecutar ciertos comandos y escribir ciertos ficheros con privilegios de root, esto se hace usando sudo.

Se puede llamar a cualquier comando utilizando la función root() en el paquete perl EBox::Sudo. Si el comando falla, root() lanza una excepción de tipo EBox::Exceptions::Sudo::Command, asegúrese de que sea capturada si es correcto que el comando falle o si desea informar al usuario de una forma distinta. En cualquier caso se pueden usar los métodos output, error y exitValue para obtener más información sobre el fallo de comando. En los raros casos en lo que el propio programa sudo falla la excepción levantada es del tipo EBox::Exceptions::Sudo::Wrapper y los últimos métodos no estarán disponibles.

Ejemplo 3.2. Using EBox::Sudo::root()

try {
  EBox::Sudo::root("/usr/bin/eject -t");
}
catch EBox::Exceptions::Sudo::Wrapper with {
  throw EBox::Exceptions::Internal("sudo program failed");
}
catch EBox::Exceptions::Sudo::Command with {
  my ($ex) = @_;
  EBox::debug("/usr/bin/eject failed. Stderr was " . (join "\n", @{ $ex->error() })) ;
  throw EBox::Exceptions::External("Cannot close CD-ROM tray. Please close it manually" );
}

EBox::Sudo provee funciones de interfaz con comandos frecuentemente ejecutados con privilegios de superusuario. stat propociona interfaz con el programa stat, resulta util para obtener informacion sobre cualquier archivo. Si necesitas una infromacion mas concisa sobre el archivo puedes usar fileTest para realizar comprobaciones simples sobre archivos, como hace el programa /usr/bin/test.

3.3. EBox::GConfModule

eBox utiliza GConf para almacenar su configuración. La plataforma de desarrollo ofrece un recubrimiento sobre los bindings originales de perl. GConf ofrece una sencilla API para almacenar y consultar configuración con valores clasificados por tipos y organizada jerárquicamente. También nos permite definir esquemas limitados para algunas claves de configuración, configurando sus tipos y valores por defecto.

La plataforma de desarrollo define un recubrimiento sobre el API de GConf más algunas funcionalidades añadidas. El recubrimiento es implementado como una clase hija de Ebox::Module, así que todos los módulos que quieren usar GConf heredan de EBox::GConfModule. Sus hijos automáticamente adquieren estas características:

  • Copias de seguridad automáticos: La primera vez que la clave de un valor es modificada, se hace una copia de seguridad del árbol completo de configuración para ese módulo. Cuando la configuración se guarda, la copia de seguridad es automáticamente eliminada. Cuando los cambios en la configuración son descartados, la copia de seguridad es automáticamente recuperada. EBox::GConfModule implementa las funciones makeBackup, restoreBackup y revokeConfig definidas en la clase EBox::Module, por lo que las clases hijas de EBox::GConfModule no necesitan implementar estas funciones a no ser que se necesite algún requisito en especial o almacenar parte de su configuración fuera de GConf.

  • Manejo de errores: Los errores de GConf son manejados por la clase recubridora. Ella los traduce a excepciones internas.

  • Instancias de sólo-lectura y sólo-escritura: Cuando el módulo es instanciado en modo sólo-lectura, la clase recubridora utiliza una copia de la configuración para evitar ver que hay cambios sin guardar. También previene de llamadas a métodos que escriban en GConf.

  • Comprobación de los límites del espacio de nombre: El recubrimiento comprueba todas las claves de GConf utilizadas por el módulo, para ver si están en su espacio de nombres. Esto asegura que las claves para un módulo sólo son leídas o escritas directamente por el propio módulo (siempre que todos los módulos utilicen esta clase recubridora).

  • Rutas relativas: las funciones en la clase recubridora pueden recibir para las claves de gconf tanto rutas absolutas como relativas. La raíz del espacio de nombres de un módulo cambia dependiendo del tipo de instancia (sólo-lectura o lectura-escritura) y el tipo de clave (normal o de estado) a la cual se acceda. Por esta razón lo mejor es utilizar rutas relativas cuando se llamen a las funciones de la clase recubridora, ya que ellas traducen la ruta relativa en una ruta absoluta automáticamente.

  • Estado del espacio de nombres: hay cierta información que no es suministrada por el usuario y que necesita ser escrita en cualquier momento, incluso en las instancias de sólo-lectura. Este tipo de información no está sujeta a guardar o cancelar la configuración. Es la información de estado, como los nombres de los servidores o la dirección IP dada por el sistema por un servidor DHCP externo. Un espacio de nombres aparte está disponible para este tipo de información, y de nuevo es automáticamente apartado de las operaciones de copia de seguridad, guardado y cancelación mencionadas anteriormente. Las funciones que suelen acceder a este espacio de nombres son idénticas a las normales, sólo que tienen el prefijo st_ en sus nombres.

  • Recuperación recursiva de directorios: hay dos funciones en EBox::GConfModule que permiten fácilmente recuperar la estructura completa de directorios. hash_from_dir toma un directorio como argumento y devuelve un hash con todas las claves que están por debajo suyo. array_from_dir toma un directorio como argumento y devuelve un array de hashes tal y como devuelve hash_from_dir para cada uno de sus subdirectorios.

Esta es la lista de las funciones más importantes en EBox::GConfModule:

all_dirs

Dada una clave, devuelve todos los directorios de su interior.

all_dirs_base

Dada una clave, devuelve todos los directorios de su interior, eliminando cualquier directorio que aparezca por delante.

all_entries

Dada una clave, devuelve todas las entradas de su interior. Estas entradas son todas aquellas claves que no son directorios, por lo que contendrán un valor.

all_entries_base

Dada una clave, devuelve todas las de su interior, eliminando cualquier directorio que aparezca por delante. Las entradas son todas aquellas claves que no son directorios, por lo que contendrán un valor.

array_from_dir

Dada una clave, devuelve un array utilizando una referencia a un hash para contener en cada elemento los directorios bajo su clave. También, el hash contiene la clave _dir que dice cual es el nombre del directorio.

dir_exists

Dada una clave que referencia un directorio, devuelve true si existe.

get_bool

Devuelve el valor de una clave de tipo booleano.

get_int

Devuelve el valor de una clave de tipo entero.

get_list

Devuelve un array conteniendo la lista referenciada por la clave.

get_string

Devuelve el valor de una clave de tipo cadena.

get_unique_id

General un identificado único aleatorio con un prefix delante en la raíz del nombre del espacio del módulo. Si directory es pasado, será añadido a la ruta. Notar que esto no crea la entrada, únicamente devuelve un identificador único, así que es tarea del programador crear la entrada correctamente.

hash_from_dir

Devuelve un hash conteniendo todas las entradas del directorio referenciado por la clave.

isReadOnly

Devuelve true si la instancia actual de EBox::GConfModule a la que se está accediendo es de sólo-lectura.

makeBackup

Vuelca el contenido actual de la configuración a un fichero.

restoreBackup

Recupera el último backup.

revokeConfig

Todos los cambios hechos desde que se escribió o eliminó algo serán descartados.

set_bool

Asigna un valor booleano a una clave.

set_int

Asigna un valor entero a una clave.

set_list

Asigna una lista de valores de un tipo en un valor.

set_string

Asigna un valor de tipo cadena en la clave.

Veamos algunos ejemplos de las funciones anteriormente descritas.

Ejemplo 3.3. Asignando un valor de tipo cadena

 $self->set_string("printers/x4235/name", "fooprinter"); 

Ejemplo 3.4. Asignando una lista de cadenas

$self->set_list('foo/foolist', 'string', ['foo', 'bar']);

Ejemplo 3.5. Recogiendo y usando un identificador único

my $id = $self->get_unique_id('p', 'printers');
$self->set_string("printers/$id/name", $name);
$self->set_bool("printers/$id/configured", undef);
		

3.4. Ordenando cosas

La necesidad de mantener cierta información ordenada es muy normal en los módulos eBox. También es común ofrecer funciones de reordenación. Un ejemplo claro son las reglas del firewall, las cuales necesitan ser aplicadas en un cierto orden, y el usuario ha de poderles cambiar su orden. La clase EBox::Order resuelve justamente este problema.

La idea es dar un directorio a cada objeto que se quiere mantener ordenado. Siguiendo con el ejemplo del firewall, podríamos tener el directorio rules/ y, por debajo, un directorio por regla. Cada regla tendría un identificador único, y eso sería el nombre de su directorio bajo rules/. Una regla con el id r3561 se almacenaría en rules/r3561, y debajo de ese directorio se almacenarían las claves con cada uno de los parámetros de la regla. Ésta es la forma más natural de organizar elementos como reglas de firewall, y es con una organización de este tipo para lo que está diseñado EBox::Order para trabajar.

El mecanismo de ordenación añade un campo a los objetos para ser ordenados. Lógicamente, se le ha denominado order. Para usar el API de ordenación ha de crear una instancia de EBox::Order. Su constructor toma como argumentos: la instancia del módulo al que pertenecen los objetos que van a ser ordenados y el directorio base donde los objetos serán almacenados.

EBox::Order implementa estas operaciones:

highest

Devuelve la clave order más alta de todos los objetos.

lowest

Devuelve la clave order más baja de todos los elementos.

nextn

Dado un número, devuelve la clave order del siguiente objeto.

prevn

Dado un número, devuelve la clave order del objeto anterior.

get

Devuelve el identificador del objeto cuya clave order es igual a un número dado.

swap

Encuentra los objetos cuyas claves order coinciden con los dos números dados e intercambia sus valores.

list

Devuelve una referencia a un array que contenga los identificadores de todos los objetos, ordenados del más bajo al más alto.

Ejemplo 3.6. Ordenando las reglas del firewall

Vamos a ver como utiliza EBox::Order el módulo firewall para mantener sus reglas de forwarding ordenadas. La función _fwdRulesOrder devuelve la instancia de EBox::Order de las reglas del firewall:

sub _fwdRulesOrder
{
	my $self = shift;
	return new EBox::Order($self, "fwdrules");
}

fwdrules es el directorio que contiene todas las reglas. Otra función auxiliar privada es _fwdRuleNumber, la cual devuelve el número de orden para el identificador de una regla dada:

sub _fwdRuleNumber # (rule)
{
	my ($self, $rule) = @_;
	return $self->get_int("fwdrules/$rule/order");
}

Las nuevas reglas son añadidas al final de la lista, por lo que vamos a buscar el número de orden más alto y le añadiremos una nueva. Este código es parte de la función addFwdRule:

my $order = $self->_lastFwdRule() + 1;

$self->set_string("fwdrules/$id/name", $id);
$self->set_string("fwdrules/$id/action", $action);
$self->set_bool("fwdrules/$id/active", 1);
$self->set_int("fwdrules/$id/order", $order);

_lastFwdRule es una función recubridora trivial que devuelve el número de orden más alto:

sub _lastFwdRule
{
	my $self = shift;
	my $order = $self->_fwdRulesOrder();
	defined($order) or return 0;
	return $order->highest;
}

Finalmente hay dos funciones que admiten la reordenación de las reglas, éstas son FwdRuleUp y FwdRuleDown (mostramos sólo la primera dado que son prácticamente idénticas):

sub FwdRuleUp # (rule)
{
	my ($self, $rule) = @_;
	my $order = $self->_fwdRulesOrder();
	defined($order) or return;
	my $num = $self->_fwdRuleNumber($rule);
	if ($num == 0) {
		return;
	}
	my $prev = $order->prevn($num);
	$order->swap($num, $prev);
}

3.5. Controlando un demonio

Además de tener un API que permite leer y cambiar la configuración de un servicio dado, el backend de un módulo está a cargo de hacer que el servicio funcione. Típicamente, el servicio será algún tipo de demonio que ofrezca algunas funcionalidades a través de la red después de leer su fichero de configuración. Por tanto, el módulo creado necesitará generar el fichero de configuración y parar/arrancar/reiniciar el demonio las veces que sea necesario.

3.5.1. Generación del fichero de configuración

La forma más sencilla de generar el fichero de configuración es usar el sistema de plantillas mason el cual es usado también en el front-end web. El uso de las plantillas de mason está documentado en Sección 5.2 así que no lo repetiremos aquí.

Las plantillas de mason para los ficheros de configuración son instaladas en el directorio stubs baje el directorio compartido para eBox. En el árbol de directorios del código fuente, cada módulo también tiene normalmente un directorio llamado stubs. El fichero Makefile.am del directorio stubs para el módulo dnscache tiene esta forma:

Stubdir = @STUBSPATH@/dns-cache

nobase_Stub_DATA = named.conf.mas named.conf.options.mas \
	named.conf.local.mas

EXTRA_DIST = $(nobase_Stub_DATA)

MAINTAINERCLEANFILES = Makefile.in

La macro de autoconf, llamada ebox.m4, incluida automáticamente exporta la ruta del directorio stubs como STUBPATH, por lo que sólo se necesita crear un directorio para el módulo en dicha ruta y colocar las plantilla de mason en su interior.

Hay un método en EBox::Module que ofrece ayuda con los permisos de los ficheros y otros detalles. Se llama writeConfFile y necesita tres argumentos:

  • La ruta del fichero de configuración que va a ser generado.

  • La ruta de la plantilla de mason relativa al directorio stubs.

  • Una referencia a los argumentos que se le quieren pasar a la plantilla de mason.

writeConfFilegenerará el fichero de configuración en un directorio temporal y después lo copiará encima del fichero que exista en el directorio deseado, manteniendo sus propietarios y permisos originales.

Ejemplo 3.7. Generando un fichero de configuración

Este es el código que genera el fichero de configuración en el módulo NTP:


my $self = shift;
my @array = ();
my @servers = $self->servers;
my $synch = 'no';
my $active = 'no';

($self->synchronized) and $synch = 'yes';
($self->service) and $active = 'yes';

push(@array, 'active'   => $active);
push(@array, 'synchronized'  => $synch);
push(@array, 'servers'  => \@servers);

$self->writeConfFile(NTPCONFFILE, "ntp/ntp.conf.mas", \@array);

				

Y esta es la plantilla que genera el fichero ntp.conf:


<%args>
	$active
	$synchronized
	@servers
</%args>	
# /etc/ntp.conf, configuration for ntpd
# Generated by EBox

driftfile /var/lib/ntp/ntp.drift
statsdir /var/log/ntpstats/

% if ($synchronized eq 'yes') {
%	if ($servers[0]) {
server <% $servers[0] %>
%	}
%	if ($servers[1]) {
server <% $servers[1] %>
%	}
%	if ($servers[2]) {
server <% $servers[2] %>
%	}
% }
% if ($active eq 'yes') {
server 127.127.1.0
% }
fudge 127.127.1.0 stratum 13

restrict default kod notrap nomodify nopeer noquery

restrict 127.0.0.1 nomodify

				

3.5.2. Controlando la ejecución

La primera cosa que necesita saber es cuando iniciar y para el demonio que se está controlando. Los servicios son arrancados llamando a las funciones restartService o save en la instancia de un módulo, sin embargo estos métodos son implementados por EBox::Module y no deberían ser reimplementados normalmente, siendo que controlan los registros del sistema y/o guardan los cambios de configuración. Ambas funciones llaman a una función abstracta cuando necesitan iniciar/reiniciar el servicio. Este método es _regenConfig, y es el que necesita implementar.

_regenConfig debería generar los ficheros de configuración para el demonio e iniciarlo o reiniciarlo. Un ejemplo de implementación de este módulo se encuentra en Ejemplo 3.8. Si necesita saber cuando la llamada es realizada por el inicio/reinicio del servicio o por la petición de guardar los cambios en la configuración, puede comprobar los parámetros pasados a _regenConfig. Cuando se llama porque la configuración fue guardada (save()) el argumento llamado save será puesto a 1. En la mayoría de situaciones no será esto necesario, ya que se puede saber fácilmente si el demonio está ejecutándose, y sólo es útil para casos especiales como el módulo network.

Cuando se llama a _regenConfig, probablemente necesite saber cuando iniciar o reiniciar el demonio, porque elegir la operación incorrecta podría producir un error. Para realizar tal decisión necesita saber si el demonio se encuentra funcionando en ese momento. EBox::Module tiene dos funciones para hacer esta tarea más sencilla. Si sabe cual es el ID del proceso, puede usar pidRunning. Recibe el ID de un proceso como argumento. Si sólo sabe el nombre del fichero donde el demonio almacenó su ID de proceso, querrá llamar a la función pidFileRunning, el cual toma el nombre de un fichero, comprueba el ID del proceso y llama a pidRunning. Ambas funciones devuelven true si el proceso está funcionando y false si no lo está.

Ejemplo 3.8. Ejemplo de implementación de _regenConfig

sub _regenConfig
{
	my $self = shift;
	$self->_setBindConf;
	$self->_doDaemon();
}

sub _doDaemon
{
	my $self = shift;

	if ($self->service and $self->pidFileRunning(PIDFILE)) {
		$self->_daemon('reload');
	} elsif ($self->service) {
		$self->_daemon('start');
	} elsif ($self->pidFileRunning(PIDFILE)) {
		$self->_daemon('stop');
	}
}

sub _daemon # (action)
{
	my ($self, $action) = @_;
	my $command = BIND9INIT . " " . $action . " 2>&1";

	if ( $action eq 'start') {
		root($command);
	} elsif ( $action eq 'stop') {
		root($command);
	} elsif ( $action eq 'reload') {
		root($command);
	} else {
		throw EBox::Exceptions::Internal(
			"Bad argument: $action");
	}
}

Parar el servicio es similar. Se puede comprobar si está ejecutándose, y si es así, entonces lanzar el comando que lo pare. Como con restartService, EBox::Module tiene una implementación básica de la función stopService, la cual llama a una función abstracta cuando es momento de parar el servicio. La función abstracta, _stopService es la que se necesita implementar.

Ejemplo 3.9. Implementación de _stopService de ejemplo

sub _stopService
{
	my $self = shift;

	if ($self->pidFileRunning(PIDFILE)) {
		$self->_daemon('stop');
	}
}

3.6. Esquemas de GConf

3.6.1. Esquema de GConf mínimo

Un esquema de GConf es una clave especial de GConf que configura el tipo y el valor por defecto de alguna clave de GConf. Son almacenados en la base de datos de GConf bajo el directorio /schemas.

Necesitará crear un esquema de GConf para el módulo. Esto es necesario, al menos para integrar el módulo con la plataforma. Por medio de este esquema, el módulo podrá ser instanciado. Por tanto, la primera cosas que se deberá hacer es asociar el nombre dado para el módulo y su módulo de perl. Veamos el ejemplo para clarificar un poco más esta idea:

Digamos que tenemos un módulo llamado foobar y la implementación para este módulo recae en EBox::Foobar. El esquema de GConf sería algo así:

Ejemplo 3.10. Esquema de GConf mínimo

<gconfschemafile>
<schemalist>
	<schema>
		<key>/schemas/ebox/modules/global/modules/foobar/class</key>
		<applyto>/ebox/modules/global/modules/foobar/class</applyto>
		<owner>ebox</owner>
		<type>string</type>
		<default>EBox::Foobar</default>
		<locale name="C"/>
	</schema>
</schemalist>
</gconfschemafile>

Una vez que ha hecho esto, cada vez que se instancie el módulo foobar utilizando el método modInstance de EBox::Global, sabrá que tiene que cargar e instanciar la clase EBox::Foobar.

Ejemplo 3.11. Creando la instancia de un módulo

my $foobar = EBox::Global->modInstance('foobar');
$foobar->some_method();

3.6.2. Valores por defecto

Los esquemas de GConf son útiles para estableces valores por defecto. Estos valores se usarán por el sistema cuando el usuario no ha dado un valor a la clave correspondiente.

Hay un escenario obvio en el cual puede estar interesado en utilizar estas características. Este es cuando su módulo es instalado por primera vez. Podría ser útil configurar unos valores por defecto en la configuración inicial.

Vamos a ilustrar esto con un pequeño ejemplo. Imagine que está desarrollando un módulo para manejar un proxy HTTP. Uno de los parámetros configurables es el puerto por donde escucha. Desea que el usuario tenga la capacidad de cambiarlo, pero también desea que tenga como valor inicial configurado, el puerto 3128. El esquema de GConf es el sitio ideal para hacerlo. Para este ejemplo, usaremos la clave /schemas/ebox/modules/proxy/port.

Ejemplo 3.12. Configurando un valor por defecto en los esquemas de GConf

<schema>
	<key>/schemas/ebox/modules/proxy/port</key>
	<applyto>/ebox/modules/proxy/port</applyto>
	<owner>ebox</owner>
	<type>int</type>
	<default>3128</default>
	<locale name="C"/>
</schema>

3.6.3. Dependencias de módulos

Es cuestión de la configuración de su módulo que pueda depender de otros. Si esto ocurre, estará interesado en que le sea notificado cada vez que hay un cambio en cualquiera de los módulos de los que depende.

Continuando con el ejemplo utilizado anteriormente, considere que desea configurar el proxy para que escuche únicamente en los interfaces internos. El módulo network se el único que maneja los interfaces de red. Así que en el momento de generar la configuración del proxy, se le pedirá al módulo network los interfaces de red configurados como internos. Esto levanta una duda obvia, el módulo debería regenerar su configuración cuando el interfaz interno cambia, esto es, cuando la configuración del módulo network cambie.

Para expresar esta relación de dependencia haremos uso del esquema de gconf del módulo de esta forma:

Ejemplo 3.13. Configurando dependencias entre módulos

<schema>
	<key>/schemas/ebox/modules/global/modules/proxy/depends</key>
	<applyto>/ebox/modules/global/modules/proxy/depends</applyto>
	<owner>ebox</owner>
	<type>list</type>
	<list_type>string</list_type>
	<default>[network]</default>
	<locale name="C"/>
</schema>

El trozo de código le dice al módulo global que proxy depende de network. De esta forma, el módulo global lanzará la función _regenConfig de EBox::Proxy cuando la configuración del módulo de red cambie.

3.7. Copias de seguridad

Como señalábamos con anterioridad, todos los cambios hechos en las claves almacenadas en gconf son automáticamente guardados la primera vez que se efectúa una operación de escritura o borrado. En consecuencia, todos esos cambios pueden ser recuperados automáticamente si el usuario desea descartar o recuperarlos de un fichero. Por supuesto esto ocurre siempre que se usen las funciones provistas a tal efecto por EBox::GConfModule.

Si su módulo tiene parte o la totalidad de su configuración independiente de GConf, deberá implementar los métodos dumpConfig y restoreConfig.

Adicionalmente, si aparte de la configuración desea copiar otros tipo de datos asociados al módulo que no forman parte de su configuración, como por ejemplo ficheros de usuarios, deberá también implementar los métodos extendedBackup y extendedRestore. Recuerde que este tipo de información sólo será guardada y restaurada si el usuario lo solicita explícitamente.

El módulo usersandgroups almacena su configuración (usuarios, grupos, contraseñas, ..) en el servidor de LDAP. Por este motivo deberemos guardar y recuperar posteriormente la información guardada en openldap para poder realizar copias de seguridad de este módulo correctamente. Veamos como podemos hacerlo:

Ejemplo 3.14. Sobreescribiendo funciones para las copias de seguridad

sub dumpConfig
{
  my ($self, $dir) = @_;
 
  $self->{ldap}->dumpLdapData($dir);
}


sub restoreConfig
{
  my ($self, $dir) = @_;

  $self->{ldap}->loadLdapData($dir);
}

El módulo ebox-logs es el encargado de la administración de los registros de eBox. Deseamos tener la posibilidad de hacer copia de seguridad de sus contenidos. Como dichos contenidos no forman parte de la configuración del módulo usaremos extendedBackup y extendedRestore. Los datos no pertenecientes a la configuración sólo se almacenarán en la copia de seguridad si el usuario así lo elige.

A continuación, mostramos como ebox-logs implementa estas características:

Ejemplo 3.15. Sobreescribiendo funciones para las copias de seguridad extendidas

sub extendedBackup
{
  my ($self, %params) = @_;
  my $dir    = $params{dir};
  
  my $dbengine = EBox::DBEngineFactory::DBEngine();
  my $dumpFile = "$dir/eboxlogs.dump";

  $dbengine->dumpDB($dumpFile);
}


sub extendedRestore
{
  my ($self, %params) = @_;
  my $dir    = $params{dir};

  my $dbengine = EBox::DBEngineFactory::DBEngine();
  my $dumpFile = "$dir/eboxlogs.dump";

  $dbengine->restoreDB($dumpFile);
}
         	

3.8. Registro administrativo

eBox puede registrar cada cambio realizado a través de interfaz administrativo, lo que es útil para saber cuando se ha cambiado o quién lo ha hecho. Cada módulo eBox debería dar parte de cada mensaje de registro para dar un soporte completo al registro administrativo.

Para conseguirlo, debes incluir un nuevo paquete del núcleo eBox EBox::LogAdmin en tu módulo.

Ejemplo 3.16. Incluyendo el paquete LogAdmin

use EBox::LogAdmin qw ( :all );

Se usa un nuevo atributo del módulo, title, debes añadirlo a la llamada de creación del módulo. Por ejemplo:

Ejemplo 3.17. Creando un objeto con soporte a registro administrativo

my $self = $class->SUPER::_create(name => 'objects',
                    title => __n('Objects'),
                    domain => 'ebox-objects',
                    @_);

Además, se necesita una tabla hash en el constructor con las actiones que tu módulo reportará con una formato de mensaje bonito. Siguiendo el mismo ejemplo en el módulo EBox::Objects:

Ejemplo 3.18. Añadiendo acciones para el registro


$self->{'actions'} = {};
$self->{'actions'}->{'addObject'} = __n('Added object {object}');
$self->{'actions'}->{'addToObject'} =
        __n('Added {name} ({ip}/{mask} [{mac}]) to object {object}');
$self->{'actions'}->{'removeObject'} = __n('Removed object {object}');

	    

Para cada acción que quieras registrar, que debería ser todas que puedan hacerse desde el interfaz web, deberás seguir estos pasos:

  1. Añadir una marca de acción a la función, como:

    sub addObject # (description)
    {
        #action: addObject
    	      
  2. Llama los métodos logAdminDeferred or logAdminNow. logAdminDeferred pone la acción en la base de datos con el campo confirmado a falso, y debería ser llamado para acciones que no tengan lugar hasta que los cambios sean guardados, esto es, todo que se guarda en gconf. Cuando salves los cambios, las acciones se marcan como confirmadas, si reviertes los cambios estas acciones se borran.

    logAdminNow pone la acción en la base de datos con el campo confirmado a verdadero, y debería ser usado por acción que tengan efecto inmediato como cambiar la contraseña o añadir un usuario.

    Ambas funciones tiene la misma firma:

    logAdminDeferred(module,action,params)
    logAdminNow(module,action,params)
    	      

    Deberías llamarlo sólo cuando estés seguro que la acción ha tenido lugar, por ejemplo, para addObject.

    Ejemplo 3.19. Llamando al método logAdminDeferred

    $self->set_string("$id/description", $desc);
    logAdminDeferred('objects',"addObject","object=$desc");
    return $id;
                    

    Como se muestra, la base de datos guarda addObject, object=objname", el código mostrado crea la cadena que debería mostrarse y traducirse al lenguaje concreto.

Capítulo 4. Interactuando con otros componentes

4.1. Introducción

Hay tres razones diferentes por las cuales tu módulo puede necesitar interactuar con otros módulos:

  • El caso más simple es sólo la necesidad de cambiar alguna configuración de otro módulo utilizando su API. Un ejemplo de esto son todos los módulos que ofrecen servicios de red. Todos ellos necesitan decirle al firewall en que puerto van a estar escuchando, para que pueda mostrar todos los servicios en la configuración del interfaz, dejando al usuario que elija quien hace uso de cada uno. Este caso es trivial, sólo cree una instancia del módulo que quiera llamar y use su API para lo que necesite.

  • El segundo caso son las dependencias. Una dependencia se observa cuando un módulo mantiene una referencia a partes de la configuración manejada por otros módulos y necesita reiniciarse cuando los otros módulos cambian la configuración. Un ejemplo de este caso es el firewall, te deja definir reglas por cada objeto de red definido en el modulo objects. Sin embargo si crea reglas para un objeto, y entonces usa el interfaz de usuario de objects para añadir nuevos miembros a ese objeto, el firewall necesitaría reiniciarse también, para que esas reglas creadas se apliquen también al nuevo objeto. Esta situación es muy sencilla de manejar, el firewall declara una dependencia en el módulo network en su esquema de GConf y entonces la plataforma automáticamente reinicia el módulo firewall cuando el módulo objects es reiniciado. La declaración de dependencias está explicada en Sección 3.6

  • Finalmente, la situación más compleja es cuando alguna información de la configuración del módulo en desarrollo depende de la configuración de otros. En el ejemplo del firewall, ¿que pasaría si se borrase el objeto para el cual se han definido ciertas reglas? Este tipo de situación requiere cooperación entre ambos módulos para que puedan ofrecer un comportamiento consistente al usuario. Esto es sobre lo que trata todo este capítulo. Las siguientes secciones describirán como los módulos objects, firewall y network resuelvan este problema (estos módulos son los, normalmente, más necesitados por otros), y mostrará como resolver este problema en nuestros propios desarrollos de módulos.

4.1.1. Observers

Suponga que está escribiendo un módulo DHCP y desea configurar el servicio DHCP con una base distinta por cada interfaz de red. Sin embargo, no a todos los interfaces se les debe permitir tener un demonio DHCP corriendo en ellos. Si el usuario configura el interfaz eth1 como cliente DHCP, entonces no tiene sentido dejarle configurar un servidor DHCP en dicho interfaz. Así que, se decide que las páginas de configuración de DHCP sólo mostrarán los interfaces de red que tengan configurada una IP estática. Eso está bien, pero ¿que pasaría si el usuario configura el servicio DHCP en un interfaz estático y más tarde intenta cambiar el interfaz a DHCP o sin nada?

Aquellos módulos que pudieran informar a otros módulos sobre ciertos eventos definen clases abstractas observer, las cuales otros módulos pueden extender si ellos necesitan ser notificados cuando ocurran tales eventos. El módulo network provee de la clase EBox::NetworkObserver para este propósito. Esta clase tiene varias funciones abstractas, por lo que el módulo podría implementar cualquiera de estas funciones y ser notificado cuando un evento relevante sucediera.

4.2. Módulo objects

El módulo objects mantiene una lista de objetos de red creada por el usuario. Estos objetos pueden contener cualquier número de direcciones IP y bloques CIDR.

Su interfaz es muy simple. La clase EBox::ObjectObserver tiene dos funciones abstractas que deben ser implementadas por los módulos que hacen uso del módulo objects:

usesObject

Recibe el nombre de un objeto como argumento y devuelve true si el módulo está haciendo uso del objeto dado en su configuración, y false en caso contrario.

freeObject

También recibe el nombre de un objeto como argumento. Este método le dice al módulo que el usuario ha pedido la eliminación del objeto dado (incluso si estaba siendo usado por los otros módulos). El módulo debería borrar todas las referencias al objeto en su configuración cuando se llame a freeObject.

Como ejemplo, aquí está la implementación de ambas funciones en el módulo firewall:

Ejemplo 4.1. Implementación de la clase EBox::ObjectsObserver

sub usesObject # (object) 
{
	my ($self, $object) = @_;
	defined($object) or return undef;
	($object ne "") or return undef;
	return $self->dir_exists("objects/$object");
}

sub freeObject # (object) 
{
	my ($self, $object) = @_;
	defined($object) or return;
	($object ne "") or return;
	$self->delete_dir("objects/$object");
}

4.3. Módulo network

El caso de network es más complejo. Hay varios cambios potenciales en la configuración de la red que otros módulos podrían necesitar conocer. EBox::NetworkObserver tiene cuatro funciones que el módulo network llama cuando se hacen distintos tipos de cambios, y dos funciones que avisan a los módulos para que eliminen las referencias a ciertos interfaces de red de sus configuraciones:

staticIfaceAddressChanged

Se llama cuando la dirección de un interfaz estático de red va a cambiarse. Este método recibe las direcciones antigua y nueva y sus respectivas máscaras como argumentos. La devolución de un valor true significa que la configuración de este módulo pasaría a ser inconsistente si tal cambio se hiciese. En ese caso el módulo de red no haría el cambio, pero en vez de eso, avisaría al usuario.

ifaceMethodChanged

Se le llama cuando la forma de configurar un interfaz va a cambiar. Tanto la forma anterior como la nueva, son pasadas como argumentos para esta función. Son cadenas: static, dhcp, trunk o notset. Como en la función anterior, la devolución de un valor true prevendrá de que el cambio se produzca.

vifaceAdded

Se llama cuando un nuevo interfaz virtual se va a crear. Sus argumentos son el interfaz real al cual se le va a añadir, el nombre del nuevo interfaz, su dirección IP y su máscara de red. Funciona de la misma forma: devolverá true si la creación del interfaz virtual es incompatible con la configuración actual del módulo.

vifaceDelete

Se le llama cuando un interfaz virtual va a ser eliminado. Sus argumentos son los nombres del interfaz real y el virtual. Funciona de la misma forma que para las tres funciones que acabamos de describir.

freeIface

Le pide eliminar todas las referencias a un interfaz de red, de la configuración. Es normalmente llamada después de devolver true en uno de las funciones arriba descritas y el usuario insistió en hacer el cambio que estaba intentando hacer.

freeViface

Idéntica situación que en freeIface, pero para los interfaces virtuales.

Ejemplo 4.2 muestra la implementación de las funciones EBox::NetworkObserver en el módulo firewall.

Ejemplo 4.2. Implementando un EBox::NetworkObserver

sub ifaceMethodChanged # (iface, oldmethod, newmethod)
{
	 my ($self, $iface, $oldm, $newm) = @_;

	 ($newm eq 'static') and return undef;
	 ($newm eq 'dhcp') and return undef;

	 return $self->usesIface($iface);
}

sub vifaceDelete # (iface, viface)
{
	 my ($self, $iface, $viface) = @_;
	 return $self->usesIface("$iface:$viface");
}

sub usesIface # (iface)
{
	 my ($self, $iface) = @_;
	 my @reds = $self->all_dirs("redirections");
	 foreach (@reds) {
		  if ($self->get_string("$_/iface") eq $iface) {
			   return 1;
		  }
	 }
	 return undef;
}

sub freeIface # (iface)
{
	 my ($self, $iface) = @_;
	 $self->removePortRedirectionOnIface($iface);
}

sub freeViface # (iface, viface)
{
	 my ($self, $iface, $viface) = @_;
	 $self->removePortRedirectionOnIface("$iface:$viface");
}

4.4. Módulo firewall

Los módulos firewall necesitan permitir que otros módulos inserten reglas NAT y de filtrado a medida. Tiene un API limitado que permite a los módulos modificar las reglas de filtrado. Por ejemplo, si un módulo necesita conectar a servidores HTTP de internet, llamaría a addOutputRule y el firewall abriría los puertos necesarios.

Sin embargo, este mecanismo no es muy flexible desde que hay módulos con necesidades muy específicas. El módulo squid necesita configurar varias reglas NAT y de filtrado para su funcionar transparentemente.

La clase EBox::FirewallObserver fue creada para estos casos más complejos. Cuando el firewall se reinicia pide a todos los módulos que heredan de EBox::FirewallObserver para ver si necesitan insertar sus propias reglas. Los módulos pueden insertar cualquier regla que quieran, con la sintaxis de iptables. La única cosa que no pueden controlar es donde exactamente son colocadas esas reglas. Los módulos de firewall tienen varios enganches a través de las distintas cadenas y tablas donde ponen estas reglas a medida.

EBox::FirewallObserver define dos funciones:

  • firewallHelper

  • usesPort

firewallHelper debería devolver undef (el comportamiento por defecto) o un objeto de tipo EBox::FirewallHelper. Esta clase define varias funciones que deberían devolver reglas que el firewall insertaría en cada uno de los enganches definidos para este propósito.

La sintaxis de las reglas provistas por los módulos es simple: sólo utilizan la misma sintaxis que se usaría en la línea de comandos de iptables, pero excluyendo la cadena, tabla y el comando iptables al principio:

Estas son las funciones definidas en EBox::FirewallHelper, cada uno de ellos para cada tipo distinto de reglas, todos ellos devuelven una referencia a un array que contienen las reglas:

prerouting

Las reglas devueltas por esta función son añadidas a la cadena PREROUTING en la tabla nat. Puedes usarlas para hacer NAT en la dirección de destino de los paquetes.

postrouting

Las reglas devueltas por esta función son añadidas a la cadena POSTROUTING> en la tabla nat. Puedes usarla para hacer NAT en la dirección de origen de los paquetes.

forward

Las reglas devueltas por esta función se añadirán a la cadena FORWARD en la tabla filter. Puedes usarlas para filtrar paquetes a través del firewall.

input

Las reglas devueltas por esta función son añadidas a la cadena INPUT en la tabla filter. Puedes usarlas para filtrar paquetes dirigidos directamente al firewall.

output

Las reglas devueltas por este método son añadidas a la cadena OUTPUT en la tabla filter. Puedes usarlos para filtrar paquetes originados en el propio firewall.

Aviso

Debe ser cauteloso con las reglas del firewall que se escriban, ya que pueden abrir importantes agujeros de seguridad en eBox.

Si necesita más información sobre como iptables y Netfilter trabajan, consulte el Howto de NAT y el Howto de Packet filtering en el sitio web de Netfilter.Ejemplo 4.3 muestra la implementación de la función output en EBox::FirewallHelper definida en el módulo squid.

Ejemplo 4.3. Creando reglas de firewall a medida

sub output
{
        my $self = shift;
        my $sq = EBox::Global->modInstance('squid');
        my @rules = ();
        push(@rules, "-m state --state NEW -p tcp --dport 80 -j ACCEPT");
        push(@rules, "-m state --state NEW -p tcp --dport 443 -j ACCEPT");
        return \@rules;
}

usesPort acepta tres argumentos: protocol, port y network interface. Esta función se usa para preguntar a los módulos si utilizan un puerto tcp o udp concreto. Le permite al firewall conocer si es una buena idea permitir o no la creación de la redirección de un puerto. Devuelve true si el módulo utiliza el puerto dado y undef en otro caso.

4.5. Definiendo nuevos observers

Supongamos que estamos implementando un módulo mail con soporte para dominios virtuales, y queremos permitir a otros módulos saber cuando se borra un dominio virtual. Podríamos definir una clase abstracta llamada EBox::MailObserver, como esta:

Ejemplo 4.4. Definiendo un observer que pueda ser implementado por otros módulos

package EBox::MailObserver;

use strict;
use warnings;

sub new
{
	my $class = shift;
	my $self = {};
	bless($self, $class);
	return $self;
}

# Cuando un dominio virtual se borra, se llama a este método.
sub virtualDomainDeleted # (domainName)
{}

Cuando ocurre un evento importante, el módulo mail llamaría a la función virtualDomainDeleted en todos los módulos que hubiesen extendido la clase EBox::MailObserver:

Ejemplo 4.5. Llamando a los módulos con un observer

sub removeVirtualDomain # (domainName)
{
	my ($self, $domain) = @_;

	#
	# do stuff
	#

	my $global = EBox::Global->getInstance();
	my @modules = @{$global->modInstancesOfType('EBox::MailObserver')};
	foreach my $mod (@modules) {
		$mod->virtualDomainDeleted($domain);
	}
}

Capítulo 5. Frontend web

5.1. CGIs

El interfaz de administración de eBox está basado en web, utilizando mod_perl por encima de apache. La información es recogida desde los backends de los distintos módulos por scripts CGI y es mostrada a través del sistema de plantillas mason.

La separación por barras de las URLs están asociados con nombres de clases de perl. La URL Network/Index es traducida al nombre de la clase EBox::CGI::Network::Index. De hecho, la raíz del espacio de nombres es /ebox/, por lo que la URL actual sería /ebox/Network/Index, pero esa parte es automáticamente manejada por la plataforma. Así que si se necesita escribir un CGI para mostrar algún dato o dejarle al usuario configurar algo, se debe escribir la clase en el espacio de nombres EBox::CGI y todas las peticiones coincidentes serán enviadas a ésta.

Hay muchas funcionalidades comunes compartidas por la mayoría de CGIs en eBox: manejo de errores, validación de datos de entrada, llamada a las plantillas de mason, muestra de mensajes y avisos, impresión del menú, etc.

Por esta razón hay una clase padre de la cual todos los CGI deberían heredar. Esta clase implementa todas aquellas funciones que que casi no intervienen con las clases hijas, esto es el por qué tantos CGI acaban siendo de unas 20 líneas de código. Toda esta funcionalidad ahora se divide en dos clases, porque hay una pequeña parte específica a la parte del cliente de eBox. Esta parte es mantenida por el cliente, y es la clase de la que se debe heredar:EBox::CGI::ClientBase.

La otra clase, EBox::CGI::Base, tiene la mayor parte del código, y reside en libebox por lo que puede ser rechazada por el lado del servidor. EBox::CGI::ClientBase hereda de EBox::CGI::Base, por lo que se puede olvidar de manera segura que son dos clases y únicamente recordar la primera.

Heredar de EBox::CGI::ClientBase mostrará una página con la barra de titulo normal y el menú, pero con el cuerpo vacío. Si se pasa el argumento title al constructor de la clase padre, se mostrará también.

Ejemplo 5.1. CGI Hola mundo

package EBox::CGI::Hello::World;

use strict;
use warnings;

use base 'EBox::CGI::ClientBase';

use EBox::Gettext;

sub new 
{
	my $class = shift;
	my $self = $class->SUPER::new('title' => __('Hola Mundo!'), @_);
	bless($self, $class);
	return $self;
}

En la gran mayoría de casos, sólo será necesario implementar la función abstracta _process en las clases para conseguir el resultado deseado. Es llamada por la clase padre, la cual realiza distintas acciones dependiendo del estado del objeto después de la llamada a _process. Hay ciertos atributos en la clase que pueden cambiarse para modificar el comportamiento de la clase padre. La propia función _process normalmente haría alguna de estas dos cosas (quizá ambas):

  • Obtener información del backend del módulo y dárselo a una plantilla de mason que la mostraría.

  • Leer los campos de entrada de la petición HTTP y usarlos para realizar alguna acción en el backend del módulo.

La próxima cosa que hay que saber es como hacer que un CGI utilice una plantilla de mason para mostrar información. Es realmente sencillo. Se puede pasar únicamente un argumento template al constructor del padre, o configurar el atributo template en cualquier momento antes del return de su función _process. Este código muestra ambas aproximaciones:

Ejemplo 5.2. Configurando la plantilla mason para un CGI

# Configurando la plantilla en el constructor
sub new 
{
	my $class = shift;
	my $self = $class->SUPER::new('title' => __('Hola Mundo!'), 
					'template' => 'hello/world.mas',
					@_);
	bless($self, $class);
	return $self;
}

# Setting the template in _process
sub _process
{
	my $self = shift;
	$self->setTemplate('hello/world.mas');
}

En segundo lugar, necesitamos poder pasar alguna información a la plantilla mason. Esto se hace utilizando el atributo params. Debería ser una referencia a un array que contenga todos los parámetros y sus nombres. Esto es la función _process para la página de configuración general (la que contiene el puerto, contraseña e idioma):

Ejemplo 5.3. Enviando argumentos a la plantilla de mason

sub _process
{
	my $self = shift;

	my $global = EBox::Global->getInstance();
	my $apache = $global->modInstance('apache');

	my @array = ();
	push(@array, 'port' => $apache->port());
	push(@array, 'lang' => EBox::locale());

	$self->{params} = \@array;
}

Eso está así para utilizar plantillas desde los CGIs. Los detalles acerca de la escritura están descritos en Sección 5.2.

Hay otro mecanismo para mostrar los datos al usuario. Puede mostrar mensajes configurando los atributos msg y error para mostrar cualquier texto que se quiera. Ambos tipos de mensajes se muestran en una caja en la parte superior de la página. Necesita tener algo de cuidado con el atributo error, siendo que serán sobreescritas si una excepción es capturada por la clase padre.

Ejemplo 5.4. Estableciendo el atributo 'msg'

sub _writeBackupToDiscAction
{
  my ($self) = @_;
 
  my $backup = new EBox::Backup;
  my $id     = $self->param('id');
  
  $backup->writeBackupToDisc($id);

  $self->setMsg(__('Backup was written to CD/DVD disk'));
}

Leyendo los argumentos enviados en la petición HTTP es muy sencillo. La clase padre utiliza el módulo de perl CGI para acceder a esta información, y provee de un recubridor sobre esta función param, que hace algunas comprobaciones en la entrada de datos. Esta es la función _process en el CGI que cambia el puerto TCP del interfaz web:

Ejemplo 5.5. Utilizando parámetros de la petición HTTP

sub _process
{
	my $self = shift;

	my $global = EBox::Global->getInstance();
	my $apache = $global->modInstance('apache');

	$apache->setPort($self->param('port'));
}

Es tan simple porque todas las comprobaciones sobre los datos son realizados por el backend (utilizando el módulo de perl EBox::Validate) y por la clase padre. Otra razón para conseguir la simplicidad del código de arriba es que el manejo de errores es automático.

Mientras se hacen todas esas cosas, las excepciones se pueden lanzar. A no ser que haya una buena razón para capturar una excepción (p.ej. espera el error y no quiere que se muestre como un error) probablemente será suficiente que las excepciones sean lanzadas y escribir el código como si no existieran realmente. El comportamiento estándar es que las excepciones que hereden de EBox::Exceptions::Internal causen un mensaje de error genérico, mandando al usuario a consultar los registros si necesitase ver los detalles. Las subclases de EBox::Exceptions::External son mostradas textualmente, el uso más común para estas excepciones es cuando los datos introducidos por el usuario se validan.

La última parte que necesita aprender es como redirigir las peticiones a otros CGIs. Hay pocas razones por las cuales se requiere hacer eso, por ejemplo, escribir un CGI que tome unos datos del usuario desde el navegador, cambie algo en el backend del módulo y redirija la petición al CGI que muestra los datos. El CGI mostrado en Ejemplo 5.5 no llama a ninguna plantilla de mason, únicamente cambia el puerto de apache y no hace nada más. Queremos que el usuario vea la misma página que veía antes de cambiar el puerto, con el nuevo valor. Ejemplo 5.6 muestra el constructor para ese CGI:

Ejemplo 5.6. Redirigiendo una petición a un CGI distinto

sub new
	my $class = shift;
	my $self = $class->SUPER::new(@_);
	bless($self, $class);
	$self->setRedirect("EBox/General");
	return $self;
}

Se puede ver que el constructor configura el atributo redirect con el valor de la URL de la página a la que queremos ser redirigidos. Eso es todo lo que hace falta hacer. A redirect se le da un valor y la clase padre hará una redirección HTTP después de que haya acabado la función _process.

Una redirección HTTP hace que el navegador lance una nueva petición HTTP, por lo que toda la información de estado de la antigua petición se pierde. Habrá casos en los que querremos mantener la información para el nuevo CGI, y para ello tenemos dos formas de hacerlo. Podría hacerse pasando los parámetros GET en la URL de la redirección, pero es mucho más sencillo hacerlo en una redirección interna, sin que pase a través del navegador.

Si necesita mantener la información, como un mensaje o una advertencia la usuario, puede utilizar el atributo chain. Funciona exactamente de la misma forma que redirect pero en vez de enviar una respuesta HTTP al navegador, la clase padre procesa la URL, instancia el CGI coincidente, copia todos los datos en él y lo ejecuta. Los mensajes y errores son copiados automáticamente, los parámetros en la petición HTTP no, ya que un error causado por uno de ellos se podría propagar al siguiente CGI.

Si necesita mantener los parámetros HTTP puede utilizar la función keepParam de la clase padre. Toma el nombre del parámetro como un argumento y lo añade a la lista de parámetros que se le copiarán al nuevo CGI si se usa el atributo chain.

Los errores son un caso especial. Cuando aparece un error, no se desea hacer redirecciones, ya que el mensaje de error se perdería. Si aparece un error y redirect tiene un valor, entonces ese valor se utiliza como si fuera chain. Sin embargo, algunas veces se desea encadenarlo a un CGI aparte si hay un error, por ejemplo si la causa del error es la ausencia de un parámetro de entrada necesario para mostrar la página. Si se da ese caso, se puede utilizar el atributo errorchain, el cual tiene una prioridad más alta que chain y redirect si hay un error.

Finalmente, el último detalle que hace falta conocer acerca de los CGIs es como hacer que funcione correctamente la internacionalización, tanto en los CGIs como en las plantillas de mason. Siendo que el módulo no tendrá el mismo dominio gettext en la plataforma eBox, la plataforma necesita conocer el dominio para poder configurarlo antes de llamar al CGI y a la plantilla. Es tan simple como pasarle como argumento al constructor del padre. El constructor "Hola mundo" en Ejemplo 5.1 sería así:

Ejemplo 5.7. Configurando el dominio gettext en un CGI

sub new 
{
	my $class = shift;
	my $self = $class->SUPER::new('title' => __('Hola mundo!'),
					'domain' => 'ebox-hello',
					@_);
	bless($self, $class);
	return $self;
}

Si el título se puede traducir, debería configurarlo de nuevo en la función _process, co