Copyright © 2005 Warp Networks S.L., DBS Servicios Informáticos S.L.
Copyright © 2006-2007 Warp Networks S.L.
Tabla de contenidos
Lista de ejemplos
__n_regenConfig_stopService de ejemplologAdminDeferredEBox::ObjectsObserverEBox::NetworkObserversummarystatusSummaryEBox::NTPEBox::CGI::NTP::Index
statusSummary en
EBox::NTP/etc/ntp.conf/etc/ntp.conf_regenConfig_stopService.Tabla de contenidos
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.
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:
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.
Todos los módulos eBox están implementados en perl, el interfaz web corre sobre mod_perl por razones de rendimiento.
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.
El interfaz web es servido por apache, nosotros usamos habitualmente el paquete debian apache-perl.
eBox almacena su configuración mediante gconf2 y la librería de bindins de perl.
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.
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.
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.
Tabla de contenidos
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:
restartAllModulesLlama 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.
stopAllModulesLlama 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.
revokeAllModulesCancela los cambios de configuración que se han hecho en todos los módulos desde la última vez que la configuración se guardó.
saveAllModulesGuarda los cambios de la configuración en todos los módulos.
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.
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.
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).
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.
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).
Tabla de contenidos
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.
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.
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;
}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.
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_dirsDada una clave, devuelve todos los directorios de su interior.
all_dirs_baseDada una clave, devuelve todos los directorios de su interior, eliminando cualquier directorio que aparezca por delante.
all_entriesDada 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_baseDada 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_dirDada 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_existsDada una clave que referencia un
directorio, devuelve true
si existe.
get_boolDevuelve el valor de una clave de tipo booleano.
get_intDevuelve el valor de una clave de tipo entero.
get_listDevuelve un array conteniendo la lista referenciada por la clave.
get_stringDevuelve el valor de una clave de tipo cadena.
get_unique_idGeneral 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_dirDevuelve un hash conteniendo todas las entradas del directorio referenciado por la clave.
isReadOnlyDevuelve true si la
instancia actual de
EBox::GConfModule a la
que se está accediendo es de sólo-lectura.
makeBackupVuelca el contenido actual de la configuración a un fichero.
restoreBackupRecupera el último backup.
revokeConfigTodos los cambios hechos desde que se escribió o eliminó algo serán descartados.
set_boolAsigna un valor
booleano a una clave.
set_intAsigna un valor
entero a una
clave.
set_listAsigna una lista de
valores de un
tipo en un
valor.
set_stringAsigna 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);
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:
highestDevuelve la clave
order más alta de
todos los objetos.
lowestDevuelve la clave
order más baja de
todos los elementos.
nextnDado un número, devuelve la clave
order del siguiente
objeto.
prevnDado un número, devuelve la clave
order del objeto
anterior.
getDevuelve el identificador del objeto
cuya clave order es
igual a un número dado.
swapEncuentra los objetos cuyas claves
order coinciden con
los dos números dados e intercambia sus valores.
listDevuelve 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);
}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.
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
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');
}
}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();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>
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.
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.
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);
}
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.
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:
Añadir una marca de acción a la función, como:
sub addObject # (description)
{
#action: addObject
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.
Tabla de contenidos
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.
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.
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:
usesObjectRecibe 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.
freeObjectTambié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");
}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:
staticIfaceAddressChangedSe 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.
ifaceMethodChangedSe 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.
vifaceAddedSe 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.
vifaceDeleteSe 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.
freeIfaceLe 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.
freeVifaceIdé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");
}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:
preroutingLas 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.
postroutingLas 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.
forwardLas 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.
inputLas 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.
outputLas 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.
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.
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);
}
}Tabla de contenidos
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