Dec 072012
 

Os ahorro tener que leer todo el post para llegar a esta conclusión: El título es un poco sensacionalista, lo sé. Sigamos.

Uno de los aspectos más consolidados en lo que a la administración de sistemas se refiere es la monitorización. No hay infraestructura razonablemente seria que no tenga un ciento de monitores de carga de CPU, memoria consumida, tráfico de red, conexiones abiertas, …. Y junto a estos tenemos otros monitores, algo más avanzados, que por ejemplo revisan si una sesión POP3 se establece correctamente, usando para ello un usuario/contraseña preestablecidos; o que un una página PHP contiene cierto texto, para lo que se hacen X consultas, también predefinidas, en una base de datos.

Básicamente, esto es lo que hay; aquí nos quedamos la mayoría. Pero, ¿Es suficiente?

Sigamos con un ejemplo más concreto:

Vamos a suponer a partir de ahora que somos los responsables de los servidores IMAP de alguna de las empresas más grandes del sector del correo electrónico, y que por lo tanto tenemos millones de usuarios para los que poder acceder a su correo es, obviamente, fundamental.

Con el gráfico de conexiones establecidas en la mano, sabemos que el patrón más habitual es el siguiente:

Gráfico base

Vemos que tenemos pocas conexiones abiertas durante la madrugada, y que el número va subiendo en la medida en que empieza la jornada laboral, con un pequeño descenso en las horas habituales de comida. Para este ejemplo nos quedamos con esto, aunque en condiciones normales se deberían tener en cuenta fines de semana, vacaciones, ….

La línea roja indica el umbral de alerta del monitor. Ya sabéis: el móvil suena cada vez que la línea verde supere a la roja. Aunque lo normal es que también tuviéramos un umbral de aviso amarillo y otro para cuando las conexiones fueran demasiado bajas, por ahora nos sirve esta versión simplificada.

Y llega el día en el que las conexiones establecidas contra nuestros servidores muestran lo siguiente:

Gráfico con alertas

Qué fácil parece todo cuando vemos el gráfico! Lamentablemente, cuando somos responsables de unos cuantos cientos de gráficos, que por supuesto no estamos vigilando constantemente, solo vamos a tener constancia de ese pico de las 10 a.m., que además podría ser perfectamente un pequeño aumento puntual de carga inocuo para el servicio.

Por supuesto, ni nos hemos enterado del ataque por fuerza bruta de las 04:00 a.m. que probablemente haya conseguido “adivinar” decenas de contraseñas de usuarios, ya disponibles para el “spameo” generalizado; o de ese problema de media tarde, que quizá sí haya supuesto una perdida de servicio para muchos usuarios. Pero la cosa es todavía peor, porque resulta que nuestro sistema de validación de cuentas se “volvió loco” justo antes de esa bajada de tráfico y empezó a aceptar logins, independientemente de la validez de la contraseña.

¿Cuántos sistemas de monitorización habéis visto capaces de detectar estas situaciones? Llevadlo a otros entornos: ¿Cuantos sistemas de monitorización concéis capaces de detectar que una tienda online en realidad está cobrando 10 euros menos en algunos pedidos? ¿O un motor de búsqueda que da resultados erróneos periódicamente?

¿Qué hacemos entonces? ¿Tiramos todos los monitores que tenemos a la basura? Obviamenente, no. Es evidente que un servidor con una carga de CPU alta necesita atención.

Sin embargo, lo que sí debemos cambiar es el punto de vista sobre el que gira la monitorización, de tal manera que en lugar de orientarla hacia el aspecto estrictamente operacional, lo hagamos teniendo en cuenta el propio servicio que estamos vigilando, que no deja de ser, en definitiva, lo único que aporta valor. Dicho de otra forma, lo que importa no es que la máquina reviente, sino que el acceso de los clientes falle o sea más lento. Puede parecer un cambio sutil, pero no lo es.

Acercándonos otra vez a la faceta técnica, esto significa que deberíamos prestar más atención a los siguientes aspectos:

  1. La monitorización debe centrarse en las aplicaciones, y no en el hardware, la red o las máquinas. (Lo que veníamos diciendo sobre la percepción del servicio que tienen los usuarios).
  2. La monitorización puede ser importante como mecanismo para buscar mejoras y optimaciones para el servicio.
  3. Desde el punto de vista de los sistemas, esto significa que la monitorización debe interactuar mucho más con las aplicaciones.
  4. Parámetros como la latencia o el tiempo de respuesta de una aplicación deben cobrar más importancia.
  5. Detectar las anomalías debe ser uno de los objetivos a conseguir. Dicho de otra forma, si nuestro sistema gana una conexión nueva de media a la semana, y si este lunes tenemos 10, el que la semana siguiente veamos 20 debe alertarnos.
  6. No se puede monitorizar lo que no se puede medir.
  7. Por si no ha quedado claro, sólo se puede monitorizar lo que se puede medir.
  8. Revisar el estado de las aplicaciones hace que algunos de los chequeos “tradicionales” dejen de ser necesarios, con lo que se simplifica la monitorización.
  9. Los monitores tradicionales siguen siendo útiles para detectar un buen número de problemas, incluidos los relacionados con la escalabilidad de las plataformas.

Pero, ¿Cómo llevamos esto a la práctica?

En líneas generales, necesitamos trabajar mucho más contra los logs que generan los servicios. Si somos los desarrolladores de nuestras aplicaciones (una web por ejemplo), queda en nuestra mano definir y loguear la información que consideramos importante. Si, por el contrario, estamos usando una aplicación de un tercero (el servidor POP/IMAP Dovecot, por citar uno), nos resultará más difícil incluir un logueo específico, pero siempre podremos buscar la información que nos puede aportar visibilidad extra del entorno. En este caso concreto, por ejemplo, el número de intentos de conexión con credenciales inválidas o el ratio logins/logoouts son métricas que nos podrían ayudar en un momento dado, por citar dos.

En cuanto al software que podemos usar para la monitorización, tenemos decenas de buenas alternativas que podemos usar. Algunas son más visuales, otras están pensadas para entornos muy grandes, otras usan backends especializados (Cassandra por ejemplo), …. Queda a nuestra elección.

En lo que sí están de acuerdo la mayoría de aplicaciones es en la forma de detectar anomalías, sobre todo porque todas las que yo conozco se basan en este estupendo documento, que a la postre sirvió para la implementación en RRDTool. Siempre podéis diseñaros vuestro sistema, quizá usando R y su paquete forecast, pero esto está muy lejos del objetivo de este post.

En unos días describiré con más detalle una pequeña implementación de ejemplo sobre un servicio IMAP (Dovecot).

Sep 272012
 

Si hay algo sobre lo que no hubiese querido escribir nunca en este blog es sobre el enrutamiento básico en Linux. Hace 10 años quizá hubiese sido más interesante, pero no ahora. Aún así, en este mundo del botón y del siguiente siguiente no tengo nada claro que la gente sepa exactamente lo que hay debajo de un “route -n”, así que vamos a ello. Eso sí, para dummies. De hecho, me voy a pasar de básico, con lo que escribiré cosas que en condiciones normales merecerían una discusión. En fin.

Empezamos con el viejo comando “route -n”, tan simple como siempre:

# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.213.208.1    0.0.0.0         UG    0      0        0 eth0
10.213.208.0    0.0.0.0         255.255.240.0   U     0      0        0 eth0
192.168.10.0    0.0.0.0         255.255.255.0   U     0      0        0 eth1
192.168.11.0    0.0.0.0         255.255.255.0   U     0      0        0 eth1

Lo normal cuando usamos la combinación route/ifconfig en la mayoría de las distribuciones, una puerta de enlace, una red para llegar a esa puerta de enlace a través de un dispositivo, y luego, en este caso, nuestras redes locales.

¿No hay nada más?

Pues sí, hay mucho más, claro. Esto no es más que una mínima porción de la tabla de enrutamiento de vuestro kernel favorito. Vamos a verlo pasando a la utilidad “ip”, la pobre, que lleva tanto tiempo existiendo, pero que tan poco se usa (solo comparable al uso eterno de los aliases de IPs, pero esta es otra historia).

# ip route ls
default via 10.213.208.1 dev eth0 
10.213.208.0/20 dev eth0  proto kernel  scope link  src 10.213.218.162 
192.168.10.0/24 dev eth1  proto kernel  scope link  src 192.168.10.1 
192.168.11.0/24 dev eth1  proto kernel  scope link  src 192.168.11.1 

¿Todo este rollo para ver lo mismo con un formato diferente?
Sí, porque estamos viendo, una vez más, solo una parte de la tabla de enrutamiento. ¡Hola “ip rule”!

# ip rule ls
0:      from all lookup local 
32766:  from all lookup main 
32767:  from all lookup default 

Y ahora, sorpresa:

# ip route ls table default
_vacio_
# ip route ls table main
default via 10.213.208.1 dev eth0
10.213.208.0/20 dev eth0  proto kernel  scope link  src 10.213.218.162
192.168.10.0/24 dev eth1  proto kernel  scope link  src 192.168.10.1
192.168.11.0/24 dev eth1  proto kernel  scope link  src 192.168.11.1
# ip route ls table local
broadcast 10.213.208.0 dev eth0  proto kernel  scope link  src 10.213.218.162 
local 10.213.218.162 dev eth0  proto kernel  scope host  src 10.213.218.162
broadcast 10.213.223.255 dev eth0  proto kernel  scope link  src 10.213.218.162
broadcast 127.0.0.0 dev lo  proto kernel  scope link  src 127.0.0.1
local 127.0.0.0/8 dev lo  proto kernel  scope host  src 127.0.0.1
local 127.0.0.1 dev lo  proto kernel  scope host  src 127.0.0.1
broadcast 127.255.255.255 dev lo  proto kernel  scope link  src 127.0.0.1
broadcast 192.168.10.0 dev eth1  proto kernel  scope link  src 192.168.10.1
local 192.168.10.1 dev eth1  proto kernel  scope host  src 192.168.10.1
broadcast 192.168.10.255 dev eth1  proto kernel  scope link  src 192.168.10.1
broadcast 192.168.11.0 dev eth1  proto kernel  scope link  src 192.168.11.1
local 192.168.11.1 dev eth1  proto kernel  scope host  src 192.168.11.1
broadcast 192.168.11.255 dev eth1  proto kernel  scope link  src 192.168.11.1           

Vaya sorpresa… Lo que vemos con un “route -n” es en realidad la tabla “main”, que además, por ese 32766, parece tener menos prioridad que esa tabla “local” tan curiosa. Fácil de leer, ¿Verdad? Los broadcast los conocemos todos pero, ¿Y las rutas de tipo local? Sacado literalmente del manual (Sí, todo esto está en el manual de ip route!!!): “the destinations are assigned to this host. The packets are looped back and delivered locally”. Y con esto hemos terminado, solo nos queda saber que “proto kernel” es la configuración más normal si no usamos software de enrutamiento (quagga, por ejemplo), y que “scope link” es para rutas broadcast y unicast mientras que “scope host” es para las locales.

Revisad los manuales de “ip rule” e “ip route”, por favor, y entended cada entrada de estas tablas y reglas.

Ya que estamos, vamos a jugar un poco con todo esto que hemos visto, aunque seguimos en “modo sencillo” y no nos vamos a complicar demasiado. Solo unas ideas.

Nota: No hagáis caso a las IPs que uso a partir de aquí. Intentaré mantenerlas coherentes, pero me las estoy inventando sobre la marcha.

Tenemos reglas en “ip rule”, tenemos tablas de enrutamiento, … ¿Apostamos a que todo esto es modificable/configurable? ¡Por supuesto!

Vamos a vuestra distribución Debian favorita, y busquemos el fichero “/etc/iproute2/rt_tables”

# cat /etc/iproute2/rt_tables 
#
# reserved values
#
255     local
254     main
253     default
0       unspec
#
# local
#
#1      inr.ruhep

1001 proveedor1
1002 proveedor2

No, en vuestros ficheros no van a estar las líneas “proveedor1″ y “proveedor2″. Las he añadido yo, alegremente. Donde estoy escribiendo este post no hay múltiples lineas de acceso a Internet, pero me voy a inventar, como ejemplo, que en mi equipo hay dos ADSL de proveedores diferentes. Uno me ha asignado la IP 192.0.2.33/24, con puerta de enlace 192.0.2.1, y el segundo 198.51.100.33/24, con gateway 198.51.100.1.

Como ya sabemos todo sobre enrutamiento, queremos mandar a los comerciales a través del primer proveedor, y a los técnicos a través del segundo. Supongamos que los comerciales están todos en la subred “172.16.0.0/24″, y los técnicos en “172.16.1.0/24″.

¡Juguemos con ip rule!

# ip rule add from 172.16.0.0/24 table proveedor1
# ip rule add from 172.16.1.0/24 table proveedor2
# ip rule ls
0:      from all lookup local 
32764:  from 172.16.1.0/24 lookup proveedor2 
32765:  from 172.16.0.0/24 lookup proveedor1 
32766:  from all lookup main 
32767:  from all lookup default 

Efectivamente, hemos separado el tráfico en dos tablas, por ahora vacías. Es el turno de ip route:

# ip route add 172.16.0.0/16 dev eth0 src 172.16.0.1 table proveedor1
# ip route add 192.0.2.0/24 dev eth1 src 192.0.2.33 table proveedor1
# ip route add default via 192.0.2.1 table proveedor1

# ip route add 172.16.0.0/16 dev eth0 src 172.16.0.1 table proveedor2
# ip route add 198.51.100.0/24 dev eth2 src 198.51.100.33 table proveedor2
# ip route add default via 198.51.100.1 table proveedor2

¿Veis lo que hemos hecho? Hemos asignado dos puertas de enlace por defecto diferentes, en base al origen, por lo que todo lo que venga desde 172.16.0.0/24 irá por 192.0.2.1, y lo originado en 172.16.1.0/24 por 198.51.100.1. Por si fuera poco, el tráfico que no entre en ninguna de estas dos subredes accederá a la tabla main (from all lookup main), y con ello usará la puerta de enlace por defecto de toda la vida, esa que pensábamos que era única.

Y ya está, aquí lo dejo, aunque debo recordaros que esto no es una guía tipo copy/paste, ni remotamente.

Notas:

  • Como no podía ser de otra forma, el direccionamiento interno de este post no puede salir directamente a Internet. Hay que hacer SNAT (o el masquerade de toda la vida) en iptables. Lo normal es que conntrack haga magia y sepa dónde mandar vuestro tráfico, pero también es posible que se tenga que jugar más a fondo con ello.
  • Las entradas que añaden rutas a las tablas están simplificadas. Sería deseable completarlas, por ejemplo para evitar avisos de ICMP sobre mejores rutas si quisiésemos mandar tráfico entre redes. Lo dicho, no es más que un ejemplo.
  • En función de las configuraciones, interfaces, firewalls, … puede ser posible que se tenga que cambiar algún parámetro de kernel, como por ejemplo, lo relacionado con rp_filter (Google es vuestro amigo). Por supuesto, ni que decir ip_forward.
  • Una vez más, esto es un mínimo ejemplo de lo mucho que se puede hacer. Las reglas, sin ir más lejos, pueden definirse por IP origen, destino, o incluso por marca de firewall, con lo que las posibilidades son enormes.
  • Os recomiendo completar lo visto en este post con todo lo que hay alrededor del enrutamiento en un kernel normal, con soporte Netfilter. Pista.
Feb 252012
 

La verdad, a estas alturas todavía no conozco a demasiada gente a la que le guste documentar. Es más, cuanto más técnico es uno, menos gusta.

Cada uno tendrá sus motivos, pero en mi caso, saber que nadie lee los documentos internos de instalaciones, actualizaciones, etc, que he escrito no me termina de hacer gracia.

Veamos el entorno en el que suelen moverse este tipo de documentos:

  • Son manuales que describen la instalación de servidores y aplicaciones, y que suelen guardarse en formato doc, pdf o similares en sistemas de gestión documental.
  • Su lógica es que un nuevo empleado pueda revisar el documento y que le sirva como referencia de la infraestructura.
  • Describen la instalación, paso a paso, de un servidor, por ejemplo de correo electrónico, las aplicaciones que ejecuta, ficheros de configuración, ….
  • Pretenden estar siempre actualizadas.

¿Cuál es el problema?

Según lo veo yo, y al igual que pasa con tantos otros “deberes” del trabajo (partes de actividad, gestión de procesos, …), rompen completamente el flujo de trabajo del empleado: Para documentar, o para rellenar un parte de actividad, uno tiene que dejar lo que está haciendo, abrir libreoffice, buscar la plantilla, escribir texto, …. viene trabajo urgente, se aparca el documento, se retoma a los dos días, no se recuerda lo que se ha escrito, se repasa, … otra vez trabajo prioritario, se vuelve a dejar el documento, ….

Y con las actualizaciones es todavía peor. No sé yo si mucha gente actualiza sus documentos cuando cambia algo de sus servidores (algo presumible en sistemas “vivos”), pero supone volver a dejar la rutina del trabajo, abrir el documento, buscar lo que hay que cambiar, redactar las modificaciones, repasar el documento, corregirlo, ….

Los wikis, que también se usan a menudo, tampoco están exentos de estos problemas: No siempre es fácil ir al navegador, buscar un documento que se escribió hace tiempo, modificarlo, ver referencias, mantenerlo al día, ….

Y para colmo, ¿Necesita esta documentación un nuevo empleado? Debemos suponer que alguien que va a trabajar con servidores de correo electrónico sabe como se instalan y lo que significa cada opción de configuración ¿Verdad?.

Con estos antecedentes, aquí van los mandamientos de forondarena.net para el trabajo de documentación:

  1. Por supuesto, hay que documentar todo aquello que no sea interno: Manuales para clientes y proveedores, documentos específicos para grupos de soporte y atención al cliente, ….
  2. Volviendo al trabajo interno de un departamento, sólo se documenta la definición del producto, quizá en forma de plan de proyecto, con su alcance, su público objetivo, lo que se pretende conseguir con él, …. En fin, estas cosas. Se pueden añadir detalles técnicos, pero más estructurales, de infraestructura, entorno tecnológico, ….
  3. Sólo hay una forma de garantizar que una instalación va a estar siempre bien documentada: Integrando la documentación en la rutina del trabajo. Dicho de otra forma, y concretando con un ejemplo, usando Puppet o similares en las instalaciones (y actualizaciones), y aprovechando su capacidad para auto-documentarse. Si alguien toca algo a mano, Puppet lo sobrescribe. Documentación actualizada, siempre.
  4. Si por algún motivo no se puede usar un sistema de gestión centralizada, al menos debe usarse software de control de versiones, ya sea git, subversion, cvs, o el que sea.
  5. La documentación más concreta, más detallada, de ficheros de configuración y scripts, como por ejemplo el motivo por el que se bloqueó una red en la configuración de apache, o la incidencia por la que se decidió aumentar el tamaño máximo de un mensaje en Postfix, deben estar al alcance del técnico, a ser posible sin tener que mover la vista de la terminal.

Y es este último punto el que me interesa en este post.

No, escribir comentarios en los mismos ficheros de configuración no es una opción, salvo para cosas puntuales. Además, una cosa es que queramos poder acceder rápido a la documentación, y otra que no podamos, en algún momento, pasarla a otros formatos, como html, con enlaces entre documentos, o pdf.

Entonces,…. Pues sí, sorpresa, ¡man!, el de toda la vida, y algunas aplicaciones construidas a su alrededor permiten hacer estas cosas, y mucho más.

Escribiendo páginas de manual

No, no voy a hablar sobre groff, ni mandoc, ni nada similar. Vamos a simplificar esto, aunque no haya nada complicado en groff, que conste. El objetivo es escribir documentación rápido, y no caer en los mismos errores que intentamos evitar.

A partir de ahora, sólo voy a escribir un pequeño resumen de este post del excelente blog dailyjs. Muy recomendable.

Por un lado, necesitamos un formato sencillo en el que escribir la documentación, y por otro, algún tipo de aplicación con la que convertir este texto en páginas compatibles con man, html, pdf, o lo que necesitemos.

Entre las opciones para el formato, la elección es markdown (que por cierto es interpretado por Github), y en cuanto a la aplicación que permite su conversión a otros formatos, tenemos varias alternativas, como ronn, o su versión para node.js ronnjs; pero en este post de ejemplo vamos a usar mantastic.herokuapp.com, que es una aplicación de Heroku que hace todo el trabajo por nosotros.

Pasemos a un ejemplo:

Tenemos un servidor de correo Postfix, con su correspondiente main.cf. Este fichero se sincroniza a través de Puppet, y se ha ido modificando en función de bugs, mejoras, incidencias de clientes y similares a lo largo de los meses.

En markdown, podríamos escribir algo como esto (que por cierto, se explica solo):

main.cf(5) -- Configuración principal de Postfix
================================================

## SYNOPSIS

`main.cf` - Fichero de configuración principal de Postfix.

## DESCRIPTION

El fichero `main.cf` define gran parte de los cambios sobre la instalación base que se hacen a Postfix. Por lo tanto, cualquier cambio que se realice en este fichero debe ser probado en servidores de pre producción.


`main.cf` es gestionado por `puppet(1)`. Los cambios que se apliquen directamente sobre este fichero serán sobreescritos.


En la siguiente lista se encuentran las modificaciones más significativas que se han ido aplicando al fichero:

* `message_size_limit`=20000:
     Se aplicó este límite de 20000 bytes a la configuración bla bla bla.

* `dovecot_destination_recipient_limit`=1:
     Opción más segura para controlar las entregas locales bla bla bla. El transport dovecot se define en `master.cf(5)`

* `smtpd_client_restrictions`=check_client_access cdb:/etc/postfix/clientes.out:
     Se define el fichero `clientes.out(5)`, como parte del grupo de restricciones en fase cliente, para rechazar aquellos clientes que no cumplan las normas de uso de la plataforma.

## INCIDENCIAS

[1234](http://bugtrack.example.com/?id=1234) - Cliente x con problemas de envío.
[5678](http://bugtrack.example.com/?id=5678) - Cliente z que envía miles de mensajes spam.

## HISTORY

2012-02-20, Versión inicial.

## AUTHOR

2012, Forondarena.net postmaster

## COPYRIGHT

Forondarena.net 

## SEE ALSO

`puppet(1)`, `master.cf(5)`, `clientes.out(5)`

Si guardamos este fichero como “main.cf.5.md”, y lo pasamos por la aplicación de Heroku, obtendremos una página de manual perfectamente formateada:

curl -F page=@main.cf.5.md http://mantastic.herokuapp.com > main.cf.5

Si por el contrario optáis por usar una instalación local de ronn, o de ronnjs, se podría generar, además de lo anterior, una serie de páginas html enlazadas entre sí, tipo wiki, aunque para eso quizá habría que trabajar algo más el markdown.

Conclusión

Tal y como he venido diciendo, una buena documentación tiene tres niveles:

  1. Documentación general, de alto nivel, o externa. Puede ser un doc, pdf, etc.
  2. Documentación sobre instalaciones de máquinas, actualizaciones de software, etc. Muy dinámica y difícil de mantener si no se integra completamente en el día a día del trabajo. Los sistemas de configuración centralizados son la clave.
  3. Documentación detallada sobre scripts y ficheros concretos. No conozco muchos entornos en los que se tengan realmente bien documentados todos los scripts y ficheros de configuración. Algunos son capaces de incluir comentarios inline, pero esto no siempre es suficiente, y se corre el riesgo de terminar con ficheros de configuración demasiado “densos”. Sólo pueden documentarse correctamente a través de aplicaciones muy sencillas y directas.

Notas

El fichero en markdown y su correspondiente página de manual están disponibles en Github.

Una vez más, el impulso definitivo para publicar esto se debe al post de dailyjs que he referenciado anteriormente.

Dejo como ejercicio el uso de ronn o ronnjs en línea de comandos, la generación de páginas html, la gestión centralizada, o incluso montar un servidor de páginas de manual (pista: tcpserver/tcpclient, netcat, socat, …).

Nov 062011
 

Con la evolución de los sistemas actuales, cada vez más grandes, con más elementos interconectados, con más necesidades de comunicación; con el cloud computing, las nubes, nieblas y demás parafernalia, llevamos ya tiempo viendo como las diferentes implementaciones de Advanced Message Queuing Protocol, como RabbitMQ, van ganando más y más seguidores.

Desde el punto de vista de la administración de sistemas, entornos como Nova (parte de Openstack) hacen uso de este protocolo, y de RabbitMQ más concretamente, para la comunicación entre sus múltiples componentes.

Hace unos años, y en desacuerdo con la evolución de AMQP, la gente de Imatix (ojo, es una de las empresas responsables de su implementación original), decidió apartarse de este protocolo y desarrollar una pequeña librería, zeroMQ, con la que se pudieran escribir aplicaciones con capacidades de comunicación avanzadas, pero de una manera sencilla; de tal manera que en lugar de un elemento más bien central en la infraestructura (AMQP), se tuviera un modelo más distribuido, en el que la mayoría del trabajo se delegase a los extremos (las aplicaciones), y no a ese nodo central. Usando una analogía de un documento de introducción a zeroMQ, estamos hablando de pasar de subversion (AMQP) a git (zeroMQ).

Bien, ahora que ya tengo vuestro interés (¿Verdad?), vamos a olvidarnos de la literatura y a buscar formas de aprovechar lo que zeroMQ puede ofrecernos para el trabajo diario del administrador de sistemas. Recordemos que al hablar de este tipo de tecnologías nos referimos al paso de mensajes entre procesos, a su gestión, a su enrutamiento, …. Y quien dice mensajes, cómo no, puede estar diciendo logs.

Al hablar de logs, por otro lado, hablamos de syslog, de servidores remotos a los que enviar datos, de ficheros que abrir y procesar, de plantillas para bases de datos en las que escribir los logs directamente, y quizá de sistemas más avanzados, como flume. ¿Sería posible usar lo bueno que tienen estas aplicaciones, y mejorarlas con lo que ofrece zeroMQ, sin volvernos locos en el intento?

Algo parecido debieron pensar en Aggregate Knowledge. Sencillamente, han escrito un par de plugins (de entrada y de salida) para rsyslog, de tal manera que podamos usar mucho de lo que nos ofrece zeroMQ (centrándome en el plugin de salida hablamos de sockets pub, push y rep básicamente) desde aplicaciones externas.

Dicho de otra forma, en una frase, podremos conectarnos en cualquier momento al rsyslog de un servidor, y ver lo que está logueando desde una aplicación que nosotros mismos habremos escrito, ya sea con Python, Perl, C, node.js, PHP, … o con cualquiera de los muchos lenguajes para los que hay un “binding”. De esta manera, podremos procesar datos en tiempo real o analizar lo que está pasando en una máquina, sin tener que acceder a ella y sin tener que ejecutar ningún tipo de software adicional en el servidor.

Antes de seguir, si queréis probarlo vosotros mismos, aquí os he dejado un backport de rsyslog 5.8.5 con soporte para zeromq para Debian Squeeze, y otro .deb de la misma versión de rsyslog para Debian Testing. En cualquier caso, recordad que son paquetes que se han creado en el momento en que se ha escrito este post. Por lo tanto, y más para Debian Testing, es muy posible que las versiones hayan cambiado cuando instaléis los paquetes, así que cuidado, bajo vuestra responsabilidad.

Aclarado esto, sigamos con el ejemplo.

Recordad que zeroMQ es una librería, y que como tal puede usarse en cualquier aplicación, sin límites. Accederemos a los logs de rsyslog (me estoy centrando en el plugin de salida) a través del socket adecuado, y a partir de ahí es cosa nuestra. En todo caso, hagamos lo que hagamos, probablemente podremos catalogar nuestra aplicación en uno de estos dos grandes grupos (es una simplificación, obviamente):

  1. Monitorización: A menudo sólo queremos ver lo que está pasando en un servidor. No necesitamos procesar los mensajes, nos vale con hacer un seguimiento de la actividad de una máquina. Cuando se configura rsyslog para usar sockets tipo “pub”, por ejemplo, estamos creando una especie de feed de datos. Todos los clientes que se conecten a ese socket (probablemente de tipo “sub”) recibirán los mismos datos, como si fuera una emisión de radio. Además, no necesitamos guardar los mensajes si no hay nadie escuchando, podemos descartarlos.
  2. Tratamiento: Los sockets tipo “push” balancean la carga automáticamente entre todos los clientes conectados (probablemente de tipo “pull”). Esto es perfecto cuando queremos procesar, por ejemplo, los logs de correo de un servidor. En un primer momento podríamos empezar con un único cliente escuchando y procesando los datos del servidor. Si en algún momento esta aplicación no pudiera con el volumen de logs, sería suficiente con arrancar una segunda instancia, de tal manera que se dividiría el trabajo entre las dos, automáticamente, y siempre que fueran lo suficientemente inteligentes, claro.

Podemos complicarnos mucho más, claro, pero lo dejaremos así por ahora. Como he dicho anteriormente, probablemente haya simplificado demasiado todo lo que puede hacerse, pero creo que estos dos escenarios describen bien el potencial de la librería.

Vamos a ver con un par de ejemplos lo sencillo que es todo esto. Empezamos con nuestro feed de logs.

# cat /etc/rsyslog.conf
...
*.* :‌omzeromq:bind=tcp://*:5557,pattern=pub;RSYSLOG_TraditionalFileFormat
...
*.*		-/var/log/syslog
...

Los clientes tendrán que conectarse al puerto 5557. Además, como se muestra en este ejemplo, el que usemos zeroMQ no significa que no podamos escribirlos también en otros ficheros.

En Fedora 15, si quisieramos escribir una pequeña aplicación con Python 3.x, sería suficiente con lo siguiente:


# yum install python3-zmq

# cat prueba_pub_sub.py
import zmq

context = zmq.Context()
socket = context.socket(zmq.SUB)

print ("Conectando a servidores…")
socket.connect ("tcp://servidor1.forondarena.net:5557")
socket.connect ("tcp://servidor2.forondarena.net:5557")
print ("Conectados a dos servidores")

socket.setsockopt(zmq.SUBSCRIBE, b"")

while 1:
string = socket.recv()
print ("Recibimos datos: ",string.decode())

Y, básicamente, esto es todo lo necesario. Es más, en este caso, nos conectamos a dos servidores, “servidor1.forondarena.net” y “servidor2.forondarena.net”, con lo que recibiremos todos los mensajes que generen ambos. Con “setsockopt” estamos poniendo un filtro para los mensajes, que en este caso es una cadena vacía, así que no aplicaremos ninguno. Si abrimos 10 terminales y ejecutamos este python en cada una, veremos como se muestran los mismos mensajes en todas ellas.

Con los sockets push/pull la cosa es igual de sencilla:


# cat /etc/rsyslog.conf
...
*.* :‌omzeromq:bind=tcp://*:5557,pattern=push;RSYSLOG_TraditionalFileFormat
...
*.* -/var/log/syslog
...

La aplicación cliente tampoco tiene ningún misterio:


import zmq

context = zmq.Context()
socket = context.socket(zmq.PULL)

print ("Conectando a servidores…")
socket.connect ("tcp://servidor1.forondarena.net:5557")
print ("Conectados")

while 1:
string = socket.recv()
print ("Recibimos datos: ",string.decode())

En este caso, nos conectamos sólo a “servidor1″. La gran diferencia con el ejemplo anterior es que, si ejecutamos el script en dos terminales, veremos un mensaje en una, y el siguiente en otra (load balance). Este modelo es perfecto, por lo tanto, para los casos en los que las aplicaciones generan una única línea de log independiente por evento. En todo caso, como es nuestra aplicación la responsable del tratamiento de los mensajes, tenemos vía libre para hacer lo que queramos con los logs, ya sea insertándolos en MySQL una vez procesados, o en Hadoop, o en Cassandra, o en Redis, o en MongoDB, o en ….

En definitiva, creo que la unión de zeroMQ con rsyslog es una estupenda idea de la gente de Aggregate Knowledge. Además, el que zeroMQ pueda usarse casi con cualquier lenguaje de programación abre la ventana para que desarrollemos todo tipo de aplicaciones, ya sean en modo texto o gráficas, para consola o para interfaces web.

Notas

Este post es extremadamente superficial. AMQP, zeroMQ o rsyslog dan por sí mismos para páginas y páginas de blogs. Es más, ya las hay, y a montones. Aquí tenéis un puñado de referencias que os pueden servir para profundizar en el tema:

  • Introducción a zeroMQ. Estupenda introducción a zeroMQ. Empezad por aquí.
  • Guía de zeroMQ. Magnífica guía de zeroMQ, con ejemplos y propuestas de patrones.
  • Ejemplo de aplicación zeroMQ.
  • Otro ejemplo de uno de los patrones propuestos en la guía de zeroMQ, y unos benchmarks de Aggregate Knowledge.
  • Motivos por los que Imatix no está de acuerdo con la evolución de AMQP. Cuidado, es pelín denso.

No he querido incluir en el post los pasos para preparar los .deb de rsyslog. Si os interesa, dejad un comentario o un tweet y escribo algo al respecto.

Oct 202011
 

Otro mini-post que vuelve a salirse completamente de la idea general de este blog. Ya hay mucha documentación sobre PXE y sobre instalaciones automatizadas tanto de CentOS como de Debian, pero bueno, a ver si le es útil a alguien.

En este caso, vamos a montar un sencillo servidor PXE, que podremos usar para hacer instalaciones de CentOS y Debian. Bueno, en realidad, podemos instalar cualquier cosa, pero en mi caso no suelo necesitar nada más.

Todo esto se debe a que, para las pruebas que hago en mi laboratorio, el mínimo de máquinas virtuales que necesito no baja de… tres, y al final tiendo a perder mucho tiempo con las instalaciones, y poco con el trabajo “de verdad”. Por eso, nada mejor que PXE, kickstart y debian-installer para agilizar el proceso.

Ojo, que todo esto, aunque perfectamente válido, no es lo que yo usaría, a priori, en un entorno real. Hay mucho software que automatiza lo que vamos a hacer aquí, con un interfaz web, scripts para actualizar repositorios, …. Por eso, creo que aplicaciones como Cobbler (por citar una) deben ser la primera opción.

Aún así, para los que queréis aprender cómo se montan estos “inventos”, aquí van unas pinceladas.

El problema

Necesitamos una forma rápida de instalar servidores. En mi caso, máquinas virtuales. Aunque hoy en día la gente de VmWare, RedHat o Virsh+libvirt ofrecen alternativas para clonar instancias como churros, vamos a optar por una solución genérica y mucho más divertida.

Las distribuciones que vamos instalar con este sistema son CentOS y Debian. Para el caso de las basadas en kickstart, la cosa está muy clara, pero para Debian, muchos os preguntaréis ¿Por qué no FAI? Bien, la respuesta rápida, en mi caso, es que no quiero instalar un servidor NFS, y la última vez que miré era un requisito. Como decía, buscad la solución que más os guste.

Otro problema posterior es la configuración “fina” de las instancias. Eso no lo voy a documentar aquí, pero como pista: cfengine, puppet, ….

La solución

He dicho que iba a ir rápido, y ya me estoy enrollando…. Venga, lo primero que necesitamos, como no, es una BIOS que permita instalar los clientes vía PXE. Por supuesto, tanto Qemu/KVM como VmWare lo permiten, así que ni una palabra más al respecto.

En cuanto al software necesario para el servidor de instalaciones, necesitamos un DHCPD razonable (por ejemplo el del ISC), un TFTPD (por ejemplo atftpd) y un servidor web, que en mi caso va a ser apache. Situemos todo en contexto:

  1. La máquina virtual arranca y “habla” con DHCPD.
  2. Además de la información IP, se trasmite, vía DHCP+TFTP, pxelinux.0.
  3. En el caso de Debian, también se aprovecha para dar una pista sobre dónde encontrar el fichero para debian-installer.
  4. Cargamos el menú de arranque con TFTP.
  5. El sistema usa apache para obtener los ficheros ks, debian-installer y los rpm, deb y demás parafernalia.

Empezamos por lo fácil: El contenido web. Tan sencillo como copiar el contenido de los CDs de instalación de las distribuciones a una ruta del arbol web.

# ls /var/www/instalaciones/centos/6.0-x86_64/
CentOS_BuildTag  EULA  images    Packages                  repodata              RPM-GPG-KEY-CentOS-Debug-6     RPM-GPG-KEY-CentOS-Testing-6
EFI              GPL   isolinux  RELEASE-NOTES-en-US.html  RPM-GPG-KEY-CentOS-6  RPM-GPG-KEY-CentOS-Security-6  TRANS.TBL

# ls /var/www/instalaciones/debian/squeeze/
autorun.inf  dists  firmware  g2ldr.mbr  install.amd  md5sum.txt  pool         README.mirrors.html  README.source  setup.exe  win32-loader.ini
css          doc    g2ldr     install    isolinux     pics        README.html  README.mirrors.txt   README.txt     tools

Sigamos con la configuración para DHCP. Hemos dicho que, además de las obvias IPs, también van a ayudar a las Debian con la definición del fichero para debian-installer. Hay tres bloques de configuración que os quiero enseñar:

# less /etc/dhcp/dhcpd.conf
...
subnet 192.168.10.0 netmask 255.255.255.0 {
   option routers 192.168.10.1;
   option domain-name-servers 192.168.10.1;
   option domain-name "forondarena.net";
   option tftp-server-name "inst.forondarena.net";
   filename "/instalaciones/pxelinux.0";
}
host centos1 {
   hardware ethernet 52:54:00:30:38:c6;
   server-name "fn134.forondarena.net";
   fixed-address 192.168.10.134;
}
host debian1 {
   hardware ethernet 52:54:00:da:4a:92;
   server-name "fn135.forondarena.net";
   fixed-address 192.168.10.135;
   if substring (option vendor-class-identifier, 0, 3) = "d-i" {
     filename "http://inst.forondarena.net/instalaciones/squeeze_preseed_135.cfg";
        }
}
...

Como veis, para CentOS no trasmitimos nada relacionado con el fichero kickstart, pero para Debian sí. ¿Por qué? Pues para enriquecer un poco el post con algo diferente, la verdad. Hay más de una forma de hacerlo.

Con esto ya tenemos la primera fase del arranque. Lo siguiente: El “boot menu”. Y para esto necesitamos TFTP:

/srv/tftp/instalaciones/
├── pxelinux.0
├── pxelinux.cfg
│   ├── C0A80A86
│   └── C0A80A87
├── squeeze-x86_64
│   ├── initrd.gz
│   └── vmlinuz
├── centos-6.0-x86_64
│   ├── initrd.img
│   └── vmlinuz
├── msgs
│   ├── centos
│   │   ├── boot.msg
│   │   └── general.msg
│   └── debian
│       ├── f1.txt
│       └── f2.txt

La mayoría de estos ficheros se pueden bajar casi desde cualquier sitio:


http://ftp.cz.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot/debian-installer/amd64/

Como el contenido de “msg” es obvio, vamos a los fichero importantes, C0A80A86 y C0A80A87. ¿Qué clase de nombres son estos? Pues son las IPs, en hexadecimal, de nuestras dos máquinas virtuales:

$ IP_ADDR="192.168.10.134"; printf '%02X' ${IP_ADDR//./ }; echo
C0A80A86
$ IP_ADDR="192.168.10.135"; printf '%02X' ${IP_ADDR//./ }; echo
C0A80A87

El contenido, para CentOS, en C0A80A86:

timeout 100
prompt 1
display msgs/centos/boot.msg
F1 msgs/centos/boot.msg
F2 msgs/centos/general.msg

default Centos6

label Centos6
  menu label ^Centos 6 amd64
  kernel centos-6.0-x86_64/vmlinuz
  append initrd=centos-6.0-x86_64/initrd.img ramdisk_size=6878 ip=dhcp ks=http://192.168.10.40/instalaciones/ks_rh6_134.ks

Como veis, hemos especificado la ruta web para el fichero kickstart.

Para Debian, en C0A80A87:

# less C0A80A87
menu hshift 15
menu width 49
prompt 1
display msgs/debian/f1.txt
timeout 100
f1 msgs/debian/f1.txt
f2 msgs/debian/f2.txt
menu title Installer boot menu
menu color title	* #FFFFFFFF *
...
menu tabmsgrow 18
menu tabmsg Press ENTER to boot or TAB to edit a menu entry

default install

label install
	menu label ^Debian Squeeze Texto amd64
	menu default
	kernel squeeze-x86_64/vmlinuz
	append video=vesa:ywrap,mtrr vga=788 initrd=squeeze-x86_64/initrd.gz auto=true priority=critical locale=es_ES console-keymaps-at/keymap=es --

En este caso, no hemos dicho dónde encontrar el fichero para debian-installer. Ya lo sabemos.

Y ya casi por último, veamos los ficheros kickstart y debian-installer, a los que vamos a acceder desde apache:

# ls -1  /var/www/instalaciones/
...
ks_rh6_134.ks
squeeze_preseed_135.cfg

Todos conocéis el formato de los .ks; son simples y fáciles de leer.

#version=RHEL6
install
url --url=http://192.168.10.40/instalaciones/centos/6.0-x86_64/
lang es_ES.UTF-8
keyboard es
network --device eth0 --onboot yes --bootproto static --ip 192.168.10.134 --netmask 255.255.255.0 --gateway 192.168.10.1 --nameserver 192.168.10.1 --hostname fn134.forondarena.net
rootpw  --iscrypted contraseña
firewall --service=ssh
authconfig --enableshadow --passalgo=sha512 --enablefingerprint
selinux --enforcing
timezone --utc Europe/Madrid
bootloader --location=mbr --driveorder=vda --append="crashkernel=auto rhgb quiet"
clearpart --all --drives=vda
part /boot --fstype=ext4 --size=200
part pv.gjsqnW-TYE3-SDwW-646R-0SSI-hhDa-ZF6bXm --grow --size=200
volgroup vg_primario --pesize=4096 pv.gjsqnW-TYE3-SDwW-646R-0SSI-hhDa-ZF6bXm
logvol / --fstype=ext4 --name=lv_root --vgname=vg_primario --size=7400
logvol swap --name=lv_swap --vgname=vg_primario --size=588
##repo --name="CentOS"  --baseurl=http://192.168.10.1/instalaciones/centos/6.0-x86_64/ --cost=100

halt

%packages
@core
@server-policy
@spanish-support
openssh*
%end

Es un ejemplo autogenerado para una máquina virtual tipo KVM. No le hagáis mucho caso al contenido en sí. Lo dicho, tenéis un kilo de documentación, e incluso aplicaciones para hacer estos ficheros.

Otra cosa muy diferente es debian-installer. La verdad, no conozco a mucha gente que no se haya atascado con esto en algún momento. El concepto es sencillo, con una opción para cada una de las opciones de cada uno de los menús que pueden aparecer durante el proceso de instalación. El problema es que, esto tan fácil de decir, es un listado …. largo; y con algunos algoritmos, como el del cálculo de espacio para particiones, nada claros. Además, como suele ser demasiado frecuente, el que decidió los nombres de las opciones…. bueno, que no me parece demasiado intuitivo, aunque si estás familiarizado con Debian te haces enseguida.

No voy a escribir un ejemplo completo. Os vais a tener que conformar con una pequeña muestra:

# cat /var/www/instalaciones/squeeze_preseed_135.cfg
...
# Keyboard selection.
d-i console-tools/archs select at
d-i console-keymaps-at/keymap select es
d-i keyboard-configuration/xkb-keymap select es
### Network configuration
d-i netcfg/choose_interface select eth0
d-i netcfg/disable_dhcp boolean true
# Static network configuration.
d-i netcfg/get_nameservers string 192.168.10.1
d-i netcfg/get_ipaddress string 192.168.10.135
d-i netcfg/get_netmask string 255.255.255.0
d-i netcfg/get_gateway string 192.168.10.1
d-i netcfg/confirm_static boolean true
...
# Root password, encrypted using an MD5 hash.
d-i passwd/root-password-crypted password contraseña
...
### Partitioning
d-i partman-auto/disk string /dev/sda
d-i partman-auto/method string lvm
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-lvm/device_remove_lvm_span boolean true
d-i partman-auto/purge_lvm_from_device boolean true
d-i partman-auto-lvm/new_vg_name string vg_forondarenanet
d-i partman-basicmethods/method_only boolean false
d-i partman-auto/expert_recipe string boot-root :: \
100 100 100 ext3 \
        $defaultignore{ } \
        $primary{ } \
        device{ /dev/sda } \
        $bootable{ } \
        method{ format } \
        format{ } \
        use_filesystem{ } \
        filesystem{ ext3 } \
        mountpoint{ /boot } \
        . \
200 1000 -1 ext4 \
        $defaultignore{ } \
        $primary{ } \
        device{ /dev/sda } \
        method{ lvm } \
        vg_name{ vg_forondarenanet } \
        . \
300 4000 -1 ext4 \
        $lvmok{ } \
        lv_name{ lvroot } \
        in_vg{ vg_forondarenanet } \
        method{ format } \
        format{ } \
        use_filesystem{ } \
        filesystem{ ext4 } \
        mountpoint{ / } \
        . \
512 512 512 linux-swap \
        $lvmok{ } \
        in_vg{ vg_forondarenanet } \
        lv_name{ lvswap } \
        method{ swap } \
        format{ } \
        .
d-i partman-auto/choose_recipe select boot-root

d-i partman/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman/confirm_nochanges boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-lvm/confirm_nochanges boolean true
...
# Individual additional packages to install
d-i pkgsel/include string openssh-server less
d-i pkgsel/upgrade select safe-upgrade
popularity-contest popularity-contest/participate boolean false
d-i finish-install/reboot_in_progress note
d-i debian-installer/exit/halt boolean true
# This will power off the machine instead of just halting it.
d-i debian-installer/exit/poweroff boolean true
...

Y así podría seguir…

En este caso es una máquina virtual para ESX. Me interesa que veáis la parte de particionado. Aunque no os lo creáis, he definido una partición primaria /boot de unos 100M, y el resto en LVM, con aproximadamente 512M de swap, y el resto de espacio disponible para raíz.

Y poco más hace falta. Ahora bien, en este post, las notas que suelo añadir al final son más importantes que nunca.

Notas

Nota 1: Los ficheros de instalación automática de los ejemplos hacen un halt una vez finalizada la instalación. ¿Por qué? Porque no tengo automatizado que la BIOS cambie por arte de magia de arranque por PXE a arranque desde disco.

Nota 2: Más que una nota, un consejo. No os volváis locos con la configuración de ficheros de sistema, usuarios y aplicaciones. Tenéis herramientas mucho mejores para la configuración “fina”, así que plantearos añadir puppet, cfengine, chef, … en los bloques “%packages” y “d-i pkgsel/include”, y dejarles que hagan su trabajo al arrancar la máquina. Para CentOS (desde puppetlabs):

%post
....
/sbin/chkconfig --level 345 puppet on
/bin/echo "$PUPPETIP puppet" >> /etc/hosts
/bin/echo "nameserver $NAMESERVERIP" >> /etc/resolv.conf
hostname $hostname
# Write out the hostname to a file for reboot.
/bin/echo -e "NETWORKING=yes\nHOSTNAME=$hostname" > /etc/sysconfig/network
/usr/sbin/puppetd -tv

y, como mini-ejemplo, un poco diferente, para Debian:

d-i preseed/late_command string in-target wget -P /tmp/ http://servidor_web/script.sh; 
  in-target chmod +x /tmp/script.sh; in-target /tmp/script.sh

dejando que ese “script.sh” haga cosas como “sed -i ‘s/START=no/START=yes/’”, siguiendo la estructura de Debian de ficheros en default y todas estas cosas.

Nota 3: Lo más importante. Este post no es suficiente para hacer que las instalaciones automáticas os funcionen. Necesita trabajo. Si os atascáis con algo, tweet o comentario, y lo intentaremos solucionar.

Oct 022011
 

Ahora que ando reestructurando mi laboratorio, voy a aprovechar para documentar un par de aplicaciones que estoy moviendo al nuevo hierro.

En realidad, esto se sale un poco del objetivo de este blog, sobre todo teniendo en cuenta que ya hay kilos de documentación sobre, en este caso, Dovecot; pero bueno, a mí me va a servir como referencia rápida, y quizá os sea de utilidad a alguno de los cuatro que pasáis por aquí.

Empezamos con los buzones compartidos en Dovecot. Como siempre, pretendo ser lo más práctico posible, así que lo mejor que se me ha ocurrido es describir exactamente la forma en la que yo mismo tengo montado “el invento”.

El problema

Antes de nada, hablemos sobre las aplicaciones que debemos tener funcionando antes de empezar:

Necesitamos un servidor de correo propio (probablemente Postfix) en el que recibir los mensajes, ya sea porque el MX apunta a él o porque usamos software tipo Fetchmail, por ejemplo. Además, tenemos varios usuarios en nuestro sistema que acceden a su correo a través de Dovecot.

Entre el correo que reciben, además del privado para cada uno, es muy probable que los usuarios estén suscritos a varias listas: de seguridad, de usuarios, anuncios de nuevas versiones de x aplicación, …. Además, también hay cuentas tipo helpdesk y listas para un departamento o grupo que deben estar accesibles para varios usuarios simultaneamente.

Algo habitual en estos casos es, por un lado, que cada usuario gestione sus propias subscripciones a listas de correo, y por otro, para el caso de las cuentas tipo helpdesk de las que hemos hablado, el mandar una copia a cada uno de los n usuarios implicados. Obviamente, esto genera un montón de copias y mensajes repetidos que, aunque no tenga que suponer un enorme problema, sí que es más propenso a fallos, además de no dejar de ser “poco elegante”.

La solución

Bien, aquí ya entramos en la forma en la que yo lo hago, que no tiene que ser ni mucho menos la mejor o la única.

Vamos a empezar por el caso más fácil, los buzones públicos:

Por usar un ejemplo concreto, al hablar de “buzón público” me voy a referir a listas de correo generales y accesibles para todo el mundo. El único control de acceso que se va a hacer sobre ellas es a nivel de sistema de ficheros, por lo que recomiendo que los usuarios del sistema pertenezcan a un grupo concreto (o varios) siempre que quieran acceder a una u otra lista.

Y digo usuarios del sistema porque cada lista va a ser un usuario de sistema (mapeada con una cuenta de correo), de tal manera que la suscripción será única. Si quisieramos hacer que todo el mundo tuviera acceso a la lista de usuarios de Postfix, crearíamos el usuario “postlista”, por ejemplo, y nos suscribiríamos a la lista de Postfix con postlista _A_T_ forondarena.net (no existe ni existirá nunca, que conste, pero ahora ya tengo un spamtrap más :D ). Como podéis suponer, el correo que llegue a esta dirección se guardará físicamente en (esta parte de la configuración de Postfix/Dovecot os la dejo a vosotros):

drwxrwx--- 5 postlista postlista 4096 oct  1 17:57   /home/postlista/Maildir

Volviendo a los permisos a nivel de sistema operativo, hay algo importante a tener en cuenta: Dovecot 2.x (y con ello su agente de entrega local que usamos en Postfix) crea los ficheros de correo con los mismos permisos que su directorio principal, y por lo tanto debemos hacer que Maildir tenga 770, por ejemplo (siempre que optemos por los permisos a nivel de sistema operativo para limitar el acceso).

Una vez hecho esto vamos a crear un directorio común que nos va a servir como referencia para que los distintos usuarios “sepan” donde están las listas. La estructura va a ser la siguiente:

ls -lha /home/listas/Maildir
lrwxrwxrwx 1 root  root    21 sep 22 22:03 .postlista -> /home/postlista/Maildir

En el directorio “/home/listas” (o cualquier otro, no es un usuario del sistema) vamos a crear un enlace simbólico para cada uno de los buzones públicos.

¿Pero cómo sabe un usuario que puede acceder a esa lista pública?

Para esto usaremos los namespaces de IMAP (El que quiera entrar en el detalle sobre lo que son, que busque el RFC).

Dovecot define los namespaces en un bloque similar al siguiente, a veces en un único dovecot.conf, a veces en otro fichero. Debian, por ejemplo, divide los diferentes apartados de configuración en “.conf” diferentes:

namespace {
  type = public
  separator = /
  prefix = listas-public/
  location = maildir:/home/listas/Maildir:INDEX=~/Maildir/listas-public
  subscriptions = no
}

Adaptad la configuración a vuestro gusto, pero lo importante es que cuando el usuario “pepe” se loguee vía IMAP, va a poder suscribirse a todo lo listado en el directorio “/home/listas/Maildir”, que además va a aparecer en su Thunderbird en el subdirectorio listas-public. Además, queremos que cada usuario tenga los ficheros propios de control que usa Dovecot en su maildir privado.

Esta es la forma fácil de crear buzones públicos.

Sin embargo, IMAP define una extensión para ACLs (los interesados también tienen RFCs al respecto), con las que podemos definir un control de acceso mucho más detallado, y que nos permitirán hilar mucho más fino en lo que puede hacer el usuario x en el buzón del usuario z. Para que os hagáis una idea, y cogido directamente del wiki de Dovecot, las ACLs permiten establecer los siguientes permisos:

  • lookup: Mailbox is visible in mailbox list. Mailbox can be subscribed to
  • read: Mailbox can be opened for reading
  • write: Message flags and keywords can be changed, except \Seen and \Deleted
  • write-seen: \Seen flag can be changed
  • write-deleted: \Deleted flag can be changed
  • insert: Messages can be written or copied to the mailbox
  • post: Messages can be posted to the mailbox by LDA, e.g. from Sieve scripts
  • expunge: Messages can be expunged
  • create: Mailboxes can be created (or renamed) directly under this mailbox (but not necessarily under its children, see ACL Inheritance section above) (renaming also requires delete rights)
  • delete: Mailbox can be deleted
  • admin: Administration rights to the mailbox (currently: ability to change ACLs for mailbox)

En el wiki de Dovecot podéis ver que letra corresponde a cada permiso. Por ahora lo dejamos y pasamos a la creación de un buzón compartido. Por cierto, no sé si está claro que con “buzón” me puedo estar refiriendo también a carpetas como “Trash”, “Sent”, o cualquier otra que creemos a mano.

Vamos a suponer que tenemos un usuario “helpdesk” en el que recibimos todo el correo destinado a “helpdesk _A_T_ forondarena.net” (otro spamtrap).

/home/helpdesk/Maildir

Helpdesk es otro usuario del sistema, y su Maildir tiene permisos 770, para limitar el acceso también a nivel de sistema operativo.

Ahora que ya tenemos el usuario del sistema y que estamos recibiendo correo, vamos a activar el soporte para ACLs en Dovecot. Es muy sencillo, sólo hay que añadir un par de plugins, ya sea en dovecot.conf, o en el fichero que defina vuestra distribución.

...
mail_plugins = acl
...
protocol imap {
  mail_plugins = $mail_plugins imap_acl
}

Y una vez tenemos los plugins, añadimos la configuración básica:

plugin {
   acl = vfile
}

Recordemos que queremos que el usuario “pepe” pueda acceder a todo el buzón “helpdesk”. Para conseguirlo, vamos a hacer login con el usuario helpdesk, y vamos a usar el propio protocolo IMAP para dar acceso a pepe:

# openssl s_client -connect 192.168.10.20:993 
CONNECTED(00000003)
..... (información SSL) .....
* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN] Dovecot ready.
. login helpdesk password
. OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS MULTIAPPEND UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS ACL RIGHTS=texk] Logged in
. setacl INBOX pepe lrwstipekxacd
. OK Setacl complete.

Dicho de otra forma, hemos permitido “lrwstipekxacd” (cada permiso de la tabla anterior es una de estas letras) en el INBOX de helpdesk al usuario pepe.

¿Pero cómo sabe pepe que puede acceder a helpdesk?

Pues con los namespaces, claro. Vamos a crear uno de tipo “shared”:

namespace {
  ...
  type = shared
  separator = /
  prefix = buzones-shared/%%u/
  location = maildir:%%h/Maildir:INDEX=~/Maildir/buzones-shared/%%u
  ...
}

Igual que con los buzones públicos, cuando pepe acceda a su cuenta va a ver un directorio buzones-shared en el que aparecerá helpdesk. ¿Verdad?

Pues no, porque no hay forma (con un rendimiento razonable) de hacer que Dovecot se recorra todos los buzones configurados (todos esos %%h y %%u), y sepa a cuáles tiene acceso pepe. A fin de cuentas, pepe y helpdesk son usuarios completamente diferentes, y no hay un enlace en un directorio claramente identificado, como “/home/listas” en el caso de los buzones públicos.

¿Entonces qué?

Es aquí donde entran en juego lo que en Dovecot se llaman “directorios”, y que vamos a usar para decir, en este caso a pepe, que puede acceder a helpdesk. La configuración es la siguiente:

plugin {
  acl_shared_dict = file:/etc/dovecot/acls/shared-mailboxes
}

Quien dice “file:” dice base de datos o fichero .db. En cualquier caso, para este ejemplo usaremos un fichero en texto plano, “/etc/dovecot/acls/shared-mailboxes”. Tened en cuenta, una vez más, que pepe tiene que poder leer el contenido de este fichero, y que además le interesa poder crear un lock en el directorio mientras está trabajando con él (vigilad los permisos unix, usad el directorio que queráis).

El contenido de shared-mailboxes es el siguiente:

shared/shared-boxes/user/pepe/helpdesk
1

Es una sintaxis que me parece particularmente “retorcida”, pero es lo que hay si no queréis usar base de datos.

Y con esto lo tenemos ya “casi todo” (ver notas al final del post). Cuando pepe abra su Thunderbird y liste las carpetas a las que puede suscribirse verá el inbox de helpdesk, en este caso a partir de la carpeta “buzones-shared”.

Notas

Primera nota:

En Dovecot, una vez que definimos un namespace, también tenemos que definir explicitamente el namespace privado. Por lo tanto, la configuración completa, en lo que a namespaces se refiere, tiene que parecerse a esto:

namespace {
  inbox = yes
  location = 
  prefix = 
  separator = /
  subscriptions = yes
  type = private
}
namespace {
  location = maildir:/home/listas/Maildir:INDEX=~/Maildir/listas-public
  prefix = listas-public/
  separator = /
  subscriptions = no
  type = public
}
namespace {
  location = maildir:%%h/Maildir:INDEX=~/Maildir/buzones-shared/%%u
  prefix = buzones-shared/%%u/
  separator = /
  subscriptions = no
  type = shared
}

Una vez más, adaptad la configuración a vuestras preferencias. Por ejeplo, quizá os venga bien añadir un “list = children” en vuestros namespaces.

Segunda nota:

¿Si habéis hecho pruebas antes de llegar al final, os habéis dado cuenta de que, una vez configurados los buzones shared, las listas públicas han dejado de verse?

¡Claro!, hemos empezado a usar ACLs, así que hay que dar acceso a pepe a las listas. Muy fácil, nos logueamos con el usuario de la lista que queramos abrir, y damos acceso a pepe:

...
* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=PLAIN] Dovecot ready.
. login postlista password
. OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS MULTIAPPEND UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS ACL RIGHTS=texk] Logged in
. setacl INBOX pepe lrwstipekxacd
. OK Setacl complete.
. logout

Tercera nota:

Los ficheros dovecot-acl tienen el siguiente contenido de ejemplo:

user=pepe akxeilprwts

Pero no los editéis directamente. Usad setacl, getacl, myrights, …, o mejor aún, alguna extensión de Thunderbird que lo haga por vosotros.

Y para terminar, tened en cuenta, una vez más, que este post no es una referencia para hacer copy/paste. Necesita trabajo (poco) si queréis hacerlo funcionar.

Aug 152011
 

Pasamos a la tercera parte de la serie, en la que ya vamos a ver un ejemplo concreto de lo que puede ser un sencillo interfaz para la gestión de logs usando Solandra. Como siempre, todo el código y los ficheros de configuración más importantes están en Github.

Recordemos los objetivos que nos hemos marcado:

  • Ser capaces de gestionar un volumen muy importante de logs, con la máxima escalabilidad y disponibilidad.
  • Poder añadir los logs en el sistema de una forma sencilla.
  • Tener un interfaz web desde el que poder visualizar los datos.

Ya hemos hablado sobre la escalabilidad de Cassandra en los posts anteriores, así que no vamos a volver a entrar en este punto. Veamos los otros dos:

Inserción de datos en el sistema

Solandra es básicamente una adaptación de Solr, por lo que en realidad vamos a tratar conceptos propios de esta aplicación en lo que queda de post. Cuando veamos algo específico de Solandra, lo señalaré.

Uno de los elementos más importantes de la configuración del sistema es el schema; que viene a ser el lugar en el que se definen los atributos que forman cada “documento” (correo en este caso) que se quiere indexar. Como veremos más adelante, una vez creada esta estructura de datos, usaremos el comando curl para insertarla en el cluster a través de una url determinada. Además, Solandra permite trabajar con varios schemas diferentes de manera simultanea.

Simplicando un poco, el schema está compuesto por dos grandes bloques: La definición de los tipos de datos y la lista de atributos que forman cada documento.

Solr ofrece muchos tipos de datos ya creados de antemano: numéricos, texto, fechas, …. Por si esto fuera poco, la aplicación permite definir nuevos tipos siempre que se considere necesario. Para la gestión de logs de correo, por ejemplo, nos podría venir bien un tipo específico para las direcciones email:

...
<fieldType name="email" class="solr.TextField" >
   <analyzer>
      <tokenizer class="solr.PatternTokenizerFactory" pattern="@" />
      <filter class="solr.LowerCaseFilterFactory" />
      <filter class="solr.TrimFilterFactory" />
   </analyzer>
</fieldType>
...

Este tipo se basa en el TextField clásico, pero forma los tokens alrededor de la “@”. De esta manera, facilitaremos las búsquedas tanto en base a la parte local de las direcciones como al dominio. Además, elimina los espacios y convierte las mayúsculas en minúsculas.

El siguiente paso es la definición de los atributos que componen cada documento, y que por supuesto van a ser de alguno de los tipos disponibles en el schema.

<fields>
    <field name="id" type="uuid" indexed="true" stored="true" required="true" />
    <field name="in_from" type="email" indexed="true" stored="true" />
    <field name="in_size" type="tint" indexed="false" stored="true" />
    <field name="in_fentrada" type="tdate" indexed="true" stored="true" />
    <field name="in_to" type="email" indexed="true" stored="true" />
    <field name="in_to_adicional" type="email" indexed="true" stored="true" multiValued="true" />
    <field name="in_fsalida" type="tdate" indexed="true" stored="true" multiValued="true" />
    <field name="in_estado" type="string" indexed="false" stored="true" multiValued="true" />

    <dynamicField name="*" type="ignored" multiValued="true" />
</fields>

Casi no hace falta explicar nada. En este sencillo ejemplo tenemos un identificador, un origen, un tamaño, una fecha de entrada y una fecha de entrega. Además, con cada mensaje vamos a guardar el estado de entrega para cada destino (pueden haberse realizado varios intentos, por ejemplo a causa del greylisting), y la lista de destinatarios adicionales para los que iba dirigido.

Obviamente, es una estructura limitada. En un entorno real se debería guardar mucha más información (antivirus, antispam, expansión de aliases, …).

Y con esto ya lo tenemos. Sólo queda volcar el schema en el cluster:

curl http://ip_cluster:8983/solandra/schema/correo --data-binary @/root/schema_correo.xml -H 'Content-type:text/xml; charset=utf-8'

Como hemos dicho, al igual que un “/schema/correo”, se podría definir un “/schema/web”, por ejemplo, y usarlos simultáneamente.

El volcado de datos

El volcado de datos se puede hacer de varias formas, pero vamos a limitarnos al uso de ficheros xml. Lo mejor es ver un ejemplo:

<add allowDups="false">
   <doc>
      <field name="in_from">origenspam@example.com</field>
      <field name="id">b309842a-1cf7-11e0-9759-f896edbeae14</field>
      <field name="in_fentrada">2011-04-30T00:17:24Z</field>
      <field name="in_size">941</field>
      <field name="in_to">destino1@target.example.net</field>
      <field name="in_fsalida">2011-04-30T00:17:25Z</field>
      <field name="in_estado">0 - 192.168.10.24_accepted_message./Remote_host_said:_250_2.0.0_Ok:_queued_as_DF2BD74CA71/</field>
      <field name="in_to_adicional">destino1@target.example.net</field>
      <field name="in_to_adicional">destino2@target.example.net</field>
      <field name="in_to_adicional">destino3@target.example.net</field>
      <field name="in_to_adicional">destino4@target.example.net</field>
   </doc>
   <doc>
      <field name="in_from">origenspam@example.com</field>
      <field name="id">b30991b7-1cf7-11e0-9759-e819bb7f7b58</field>
      <field name="in_fentrada">2011-04-30T00:17:24Z</field>
      <field name="in_size">941</field>
      <field name="in_to">destino2@target.example.net</field>
      <field name="in_fsalida">2011-04-30T00:17:24Z</field>
      <field name="in_estado">0 - 192.168.10.24_accepted_message./Remote_host_said:_250_2.0.0_Ok:_queued_as_A945A21CD6B/</field>
      <field name="in_to_adicional">destino1@target.example.net</field>
      <field name="in_to_adicional">destino2@target.example.net</field>
      <field name="in_to_adicional">destino3@target.example.net</field>
      <field name="in_to_adicional">destino4@target.example.net</field>
   </doc>
   <doc>
      <field name="in_from">cliente1@example.net</field>
      <field name="id">b43de232-1cf7-11e0-9759-d71ac36a357f</field>
      <field name="in_fentrada">2011-04-30T00:04:41Z</field>
      <field name="in_size">6077</field>
      <field name="in_to">greylister@example.com</field>
      <field name="in_fsalida">2011-04-30T00:07:27Z</field>
      <field name="in_estado">0 - 172.16.0.14_does_not_like_recipient./Remote_host_said:_450_4.7.1_<cliente1@example.net>:_Sender_address_rejected:_Message_delayed_now.Retry_later,_please./Giving_up_on_172.16.0.14./</field>
      <field name="in_fsalida">2011-04-30T00:12:38Z</field>
      <field name="in_estado">1 - 172.16.0.14_does_not_like_recipient./Remote_host_said:_450_4.7.1_<cliente1@example.net>:_Sender_address_rejected:_Message_delayed_now._Retry_later,_please./Giving_up_on_172.16.0.14./</field>
      <field name="in_fsalida">2011-04-30T00:31:53Z</field>
      <field name="in_estado">2 - 172.16.0.14_does_not_like_recipient./Remote_host_said:_451_4.3.0_<greylister@example.com>:_Temporary_lookup_failure/Giving_up_on_172.16.0.14./</field>
      <field name="in_fsalida">2011-04-30T01:04:43Z</field>
      <field name="in_estado">3 - 172.16.0.14_accepted_message./Remote_host_said:_250_2.0.0_Ok:_queued_as_CB6A3D4A217/</field>
      <field name="in_to_adicional">greylister@example.com</field>
   </doc>
</add>

El volcado, otra vez, es muy sencillo.

curl http://ip_cluster:8983/solandra/correo/update -F stream.file=/tmp/volcado.xml

La conversión de logs desde el más que probable modo texto de syslog a xml, y de ahí al cluster de Solandra, queda fuera de esta serie de posts. De hecho, un servidor piensa que esto es lo realmente importante y difícil para llevar este proyecto a la práctica de una manera “seria”.

El Interfaz

¿Qué mejor que un interfaz web para mostrar los datos que hemos almacenado en Solandra? ¡Sorpresa! la gente detŕas del proyecto ajax-solr ya ha hecho la mayoría del trabajo, así que sólo nos queda modificar un puñado de ficheros, algo de código JavaScript, y ya lo tendremos.

Un detalle más: No queremos permitir el acceso directo a Solandra desde la web, así que necesitamos un proxy que filtre las consultas y las redirija al puerto de Solandra (tcp/8983 por defecto), y que en mi laboratorio escucha en localhost. En este caso, como casi siempre que quiero programar algún tipo de servicio para Internet sin dedicarle mucho tiempo, he usado node.js. Para este ejemplo, y por jugar un poco con GeoIP, he escrito un sencillo proxy que permite conexiones sólo si tienen como referer forondarena.net y si vienen desde Europa o América del Norte. Como no podría ser de otra forma, el código de este proxy también está disponible en Github.


(Nota: Esta demo es un Solr normal, pero el funcionamiento es idéntico al de Solandra).

Conclusiones

El “mercado” está lleno de soluciones de todo tipo que nos pueden ayudar en la gestión de logs. Hay aplicaciones comerciales, como Splunk, sistemas basados en software libre, como Solr, tecnología que nos puede permitir crecer “ilimitadamente”, como Cassandra, Hadoop o Hbase, pero que requieren algo de trabajo; y también tenemos los mágnificos sistemas de bases de datos, como Mysql o Postgresql. ¿Cuál elegir?

En una primera fase, una buena base de datos con un sencillo interfaz web o un Solr estándar pueden servir perfectamente para gestionar todo tipo de logs. De hecho, tanto Mysql como Solr ofrecen alternativas para el particionado que pueden permitir este esquema en la segunda, tercera o cuarta fase.

Un buen consejo que escuche hace tiempo es el de “no arreglar lo que no está roto”. Sólo deberíamos plantearnos el uso de tecnología que probablemente no conozcamos tan bien como las anteriores cuando realmente sea necesario. Llegado ese momento, adelante. Como siempre, la comunidad detrás del software libre es activa y está dispuesta a ayudar. Por si esto fuera poco, cada vez son más comunes las empresas que ofrecen servicios alrededor de este tipo de soluciones, y que pueden asesorarnos llegado el caso.

Aug 052011
 

Seguimos con el segundo post de la serie, en el que pasamos a dar una descripción un poco más técnica de los componentes necesarios para poner en marcha todo lo descrito en el primero. No vamos a entrar en demasiado detalle. En todo caso, una vez conocidas las aplicaciones es más fácil buscar información en la red.

Aunque Solandra puede encargarse de la instalación de Cassandra, aquí vamos a usar los componentes por separado.

Cassandra
Cassandra es un tipo de base de datos creado siguiendo los principios propuestos por Dynamo (Amazon) y por BigTable (Google). El que quiera entrar en detalle tiene bibliografía y mucha documentación disponible.

Explicar el modelo de datos, la replicación o los niveles de consistencia va más allá del objetivo de este post. Lo más interesante en nuestro contexto es dejar claro que Cassandra es una base de datos completamente distribuida y descentralizada, en la que todas las máquinas del cluster cumplen el mismo y único rol, sin distinciones entre “maestros”, “esclavos”, “catálogos”, …. Esto significa que añadir capacidad a un cluster de Cassandra supone básicamente añadir más hierro. Nada más.

La instalación puede complicarse todo lo que queramos, pero lo básico es:

# Cuidado con la versión
cd /tmp/ && wget http://apache.rediris.es/cassandra/0.8.2/apache-cassandra-0.8.2-bin.tar.gz
cd /usr/local/ && tar xvzf /tmp/apache-cassandra-0.8.2-bin.tar.gz 

A partir de aquí se crean los directorios de datos y logs (por ejemplo /var/lib/cassandra y /var/log/cassandra), y se adaptan los ficheros de configuración (que están en /usr/local/apache-cassandra-0.8.2/conf). El que más nos interesa ahora es cassandra.yaml. En github hay un ejemplo de configuración de este fichero, aunque sea casi por defecto, de cada uno de los tres nodos que he usado en esta prueba.

Hay un par de opciones de configuración que pueden servir para comprender la estructura del sistema. Un cluster de Cassandra se entiende como un anillo en el que cada uno de los nodos gestiona un volumen determinado de datos (replicación aparte). Básicamente estamos hablando de una serie de claves (hashes) que se distribuyen de una forma más o menos equilibrada entre todas las máquinas. Aunque no sea estrictamente necesario, como para esta prueba he usado un número fijo de nodos (3), he asignado ya desde el comienzo un 33% de datos a cada uno. Por supuesto, en un entorno real en el que se añaden y quitan nodos dinámicamente la gestión es diferente. La configuración en mi laboratorio es la siguiente:

# Nodo 1
initial_token: 0
# Nodo 2
initial_token: 56713727820156410577229101238628035242
# Nodo 3
initial_token: 113427455640312821154458202477256070485

La otra opción que merece la pena comentar es “seed_provider”. Cassandra usa un protocolo tipo Gossip para distribuir la información entre los nodos. Esto significa que, cuando se añade un nuevo miembro al cluster, es suficiente con indicarle un servidor activo del mismo. El protocolo se encarga de propagar esta nueva información en todos los nodos. Por lo tanto, la configuración se limita a especificar una (o varias) IPs activas:

seed_provider:
    - class_name: org.apache.cassandra.locator.SimpleSeedProvider
      parameters:
          - seeds: 192.168.10.145

Hay mucha más opciones de configuración, por supuesto, pero lo dejamos aquí.

En este momento ya se podría ejecutar Cassandra, pero esperaremos a instalar Solandra.

Solandra
Hablemos antes de lo que es Solr, una vez más, muy muy por encima.

Solr es una plataforma de búsqueda construida sobre la librería Lucene. Simplificando mucho, y en el contexto de este post, ofrece un interfaz XML (es el que nos interesa aquí, pero no el único) para añadir documentos, y un API HTTP a través de cual recibir resultados en formato JSON (entre otros).

Todo se verá más claro cuando creemos el schema para la gestión de logs. Para el que quiera profundizar más en Solr, aquí tiene un libro.

Volvamos a Solandra. La instalación es sencilla. Una vez descargado el tar.gz desde github, y con ant y los binarios de java en el path, ejecutamos lo siguiente en cada uno de los nodos:

# El nombre del fichero cambia
cd /usr/local && tar xvzf /tmp/tjake-Solandra-4f3eda9.tar.gz 
cd tjake-Solandra-4f3eda9/
ant -Dcassandra=/usr/local/apache-cassandra-0.8.2 cassandra-dist

Si todo va bien, en unos minutos la salida estándar mostrará lo siguiente:

....
cassandra-dist:
     [copy] Copying 36 files to /usr/local/apache-cassandra/lib
     [copy] Copying 8 files to /usr/local/apache-cassandra/conf
     [copy] Copying 1 file to /usr/local/apache-cassandra/bin
     [echo] Libraries successfully copied into cassandra distribution
     [echo] Start the cassandra server with /usr/local/apache-cassandra/bin/solandra command

BUILD SUCCESSFUL
Total time: 2 minutes 43 seconds

Durante la instalación deberían haberse copiado las librerías y scripts en el arbol de Cassandra. Incluyendo algunos ficheros de configuración, que en este caso dejamos por defecto, aunque lo normal sería que los adaptásemos.

Sin más, vamos a arrancar Solandra (y con ello Cassandra), en cada nodo. Como no hemos hecho ningún cambio en el logueo, vamos a ver todos los mensajes por la salida estándar:

cd /usr/local/apache-cassandra-0.8.2/bin/
./solandra &

Y con esto ya debería estar todo listo. El siguiente paso es añadir el schema (parecido a como se haría en un Solr estándar), volcar los datos, y preparar el interfaz web. Pero esto es cosa de un tercer post. No pensaba escribirlo, pero bueno, este ya es demasiado largo.

Aug 022011
 

Allá por el 2008, Rackspace(Mailtrust) publicaba algunos datos sobre la forma en la que había ido evolucionado su sistema de gestión de logs de la infraestructura de correo electrónico, y que por aquel entonces ya superaba holgadamente los 100GB de crecimiento diario. Junto a este documento, en una de las referencias bibliográficas sobre Hadoop, esta misma empresa explicaba, con algo de detalle técnico, la forma en la que habían implementado su sistema, basado sobre todo en Hadoop y Lucene+Solr.

Aunque Hadoop siga siendo una solución magnífica para la gestión de logs en el contexto del Software Libre (siempre hablando de volúmenes de datos realmente muy grandes), en esta serie de posts vamos a ver cómo podemos llevar la idea de Rackspace a la práctica usando otro tipo de tecnología, y más concretamente, Cassandra.

En realidad, como mis tres lectores no quieren posts demasiado largos, en lugar de entrar en los detalles de lo que sería una implementación más o menos “casera”, vamos a usar una de las aplicaciones que Datastax (una empresa que da servicios comerciales para Cassandra) está potenciando como parte de su ecosistema alrededor de Cassandra, y que se llama Solandra.

El problema
Repasemos, simplificando un poco, la evolución de Rackspace:

  1. En una primera fase, los logs se almacenan en máquinas individuales. Cuando hay alguna incidencia, algún técnico tiene que entrar a hacer un grep. Si el negocio va bien, llegará un momento en el que el tiempo perdido haciendo estas búsquedas será, por lo menos, “crispante”.
  2. En la segunda fase, los logs pasan a gestionarse a través de un syslog centralizado. En realidad, esta no es más que una versión algo mejorada de la primera evolución, pero al menos facilita el trabajo. En cualquier caso, en el fondo se sigue perdiendo mucho tiempo en la búsqueda manual en logs.
  3. La solución más natural en este punto es volcar los datos a una base de datos, y con ello a algún tipo de interfaz web. Dejaremos a un lado el desarrollo del frontend y de los scripts que cargan los datos en la bbdd (que pueden no ser en absoluto triviales, en función de la complejidad de la plataforma).

Hasta aquí, vale, todo es razonablemente sencillo. Sin embargo, cuando se gestionan digamos que 25 millones de mensajes al día, y cuando se quieren mantener 2 años de información (es un decir), nos encontramos con un problema.

¿Cómo se soluciona?

Aquí ya cada uno toma decisiones en función de su capacidad, su presupuesto, los perfiles que tiene disponibles, …. En algunos casos, mantener 30 días en base de datos (lo que genera la mayoría de incidentes), puede ser suficiente. En otros casos, se trabaja con los mecanismos que ofrecen las bases de datos para escalar (el framework Gizzard de Twitter es un estupendo ejemplo, aunque no hablemos de logs). Y por último, algunos pasan a otras soluciones, ya sean de pago o libres. En el caso de Rackspace, por ejemplo, su opción fue Hadoop y Lucene+Solr.

Una vez más, cada una de estas opciones puede ser “la mejor” en función del entorno en el que se desarrolle. Pero claro, si quiero seguir con este post tengo que optar por la tercera alternativa, obviamente :) .

Vamos por partes:

  1. Queremos almacenar un volumen de datos muy significativo. Para unos pocos GB todo esto no tiene demasiado sentido.
  2. Queremos que lo único necesario para aumentar la capacidad sea añadir nuevo hardware. Nada más. Ni cambios en la programación, ni cambios en la arquitectura.
  3. Queremos poder hacer consultas complejas sobre estos logs, en base a origen, destino, rangos de fechas, …. Por ejemplo, sería estupendo poder consultar todo el spam enviado a cuentas tipo info@ en toda la plataforma en un periodo de tiempo concreto.
  4. Aunque el tiempo real no es un requerimiento, es preferible poder hacer estas consultas y recibir los resultados al momento, en una misma sesión de navegador.

Para conseguir los puntos 1 y 2 Hadoop es una solución estupenda. Para 3 y 4 es necesario más trabajo. El acceso “casi inmediato” a los datos se conseguiría con alternativas como HBase, tan de moda ahora que Facebook ha empezado a usarlo. Además, siempre disponemos de Pig y Hive para simplificar las consultas. Ahora bien, de una u otra manera, con Hadoop es bastante probable que tengamos que programar bastante código.

La otra alternativa viene de la mano de Cassandra. Una vez más, los puntos 1 y 2 son inmediatos con esta tecnología. Al igual que con Hadoop, 3 y 4 no lo son; pero gracias a la aplicación llamada Solandra, que no deja de ser un Solr que guarda sus índices en Cassandra, podemos conseguir la capacidad de búsqueda de Lucene, el interfaz tipo REST que ofrece Solr, y la escalabilidad de Cassandra. Todo en uno.

El post se ha alargado un poco. Dejamos la parte práctica para el segundo (y último) mensaje de esta serie.

Jul 182011
 

Update: La versión de github del script ya es capaz de monitorizar n pids en ejecución y n programas que todavía no lo estén.

Para retomar un poco el blog, y como hace mucho que no escribo nada sobre SystemTap, me he planteado otro sencillo ejercicio “de repaso”.

La verdad es que en la actualidad ya hay un buen montón de artículos, documentación y ejemplos sobre lo que se puede hacer con SystemTap, ya sea para la inspección del kernel como para la de aplicaciones tipo mysql, postgresql o python. El rpm de Scientific Linux 6, por ejemplo, incluye más de 80 ejemplos de todo tipo, desde el análisis de procesos hasta el del tráfico de red.

Aún así, como digo, aquí va un ejemplo más.

El problema
Vamos a suponer que tenemos un proceso ejecutando en una máquina, y que queremos vigilar los ficheros que va abriendo.

Para ir un poco más lejos, queremos que se muestren también los ficheros abierto por los hijos de ese proceso principal (sólo un nivel por ahora).

Y ya puestos, queremos poder limitar la monitorización a los ficheros de ciertas rutas. Por ejemplo, podemos querer mostrar sólo los ficheros que se abrán en /etc, o en /lib.

Vamos, que el ejercicio es prácticamente una extensión de uno de los scripts de un post anterior de este mismo blog. Pero que le voy a hacer, sólo se me ocurren ejercicios sobre ficheros.

Resumiendo, y poniendo un ejemplo, tenemos un servidor postfix, con sus habituales procesos hijo:

master─┬─pickup
       │─qmgr
       └─tlsmgr

Queremos saber que ficheros abren todos los procesos que cuelgan de master, ya sean los tres que se están ejecutando en este momento, ya sean los nuevos que se vayan creando. Además, queremos poder limitar el número de ficheros vigilados a una serie de rutas determinadas.

La solución
El núcleo de la solución está en dos “probes”:

  • syscall.open.return
  • syscall.close.return

O sea, las llamadas al sistema open y close. Más concretamente, al momento en el que terminan de ejecutarse (return).

Además, vamos a usar el probe “begin” para leer argumentos, preparar variables y demás parafernalia.

Comenzamos:

probe begin {
        procesoArgumento = $1
        printf ("El pid que vamos a tratar es %d\n", procesoArgumento);
        if (argc > 1) {
                for (i=2; i <= argc; i++) {
                        ficherosEnPath[argv[i]] = argv[i]
                        numeroPaths += 1
                }
        }
}

Poca explicación hace falta. En $1 tenemos el primer argumento, que va a ser el pid a vigilar. A partir del segundo argumento, y de manera opcional, se pueden incluir una serie de rutas a monitorizar. Todas estas son llamadas válidas:

stap -v monitorFicheros.stp $(pidof master)
stap -v monitorFicheros.stp $(pidof master) /etc
stap -v monitorFicheros.stp $(pidof master) /etc /usr/lib

Una vez definido este bloque básico, pasamos a la llamada al sistema "open":

probe syscall.open.return {
        proceso = pid()
        padre = ppid()
        hilo = tid()
        ejecutable = execname()
        insertarEnTabla = 0

        if ( (procesoArgumento == proceso) || (procesoArgumento == padre) || (procesoArgumento == hilo) ) {
                if ( (procesoArgumento == proceso) && (env_var("PWD") != "") ) {
                        pwd = env_var("PWD")
                }
                localpwd = (isinstr(env_var("PWD"), "/"))?env_var("PWD"):pwd;

                filename = user_string($filename)
                descriptor = $return

                filename = (substr(filename, 0, 1) == "/")?filename:localpwd . "/" . filename;

                if ([proceso,padre,hilo,descriptor] in tablaProcesos)  {
                        printf ("{codigo: \"error\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor)
                } else {
                        if (descriptor >= 0) {
                                if (numeroPaths > 0 ) {
                                        foreach (ruta in ficherosEnPath) {
                                                if (substr(filename, 0, strlen(ruta)) == ruta) {
                                                        insertarEnTabla = 1
                                                        break
                                                }
                                        }
                                }
                                if ( (insertarEnTabla == 1) || (numeroPaths == 0) ) {
                                        tablaProcesos[proceso,padre,hilo,descriptor] = gettimeofday_ms()
                                        tablaFicheros[proceso,padre,hilo,descriptor] = filename
                                        printf ("{codigo: \"open\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d, date: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor, tablaProcesos[proceso,padre,hilo,descriptor])
                                }
                        }
                }
        }
}

A través de las funciones pid(), ppid() y tid() vemos si el proceso que está ejecutando open en este momento es el proceso que nos interesa o un hijo suyo.

Si cumple este requerimiento, pasamos al bloque que revisa si el fichero que se está abriendo está en el path que nos interesa. En este caso, para hacer el ejercicio más completo, he optado por dar unas cuantas vueltas "de más", y he usado la función env_var("PWD") para acceder al entorno del proceso. En la práctica esta no es la mejor forma, y por eso hay más control en el probe, ya que no siempre existe la variable PWD en el entorno de los procesos que llegan a este open.

La llamada al sistema open carga las variables $return (el valor de retorno de la función es el id del descriptor del fichero) y $filename (el nombre del fichero). Ojo! Cada llamada al sistema tiene unos argumentos, y con ello "ofrece" unas variables para nuestros scripts. Por ejemplo, mientras que en open tenemos filename, flags y mode; en close tenemos fd, filp, files, fdt y retval. El propio SystemTap, Google y el código fuente del kernel son ... vuestros amigos.

En cualquier caso, lo importante es que, cuando se dan todas las condiciones, hacemos lo siguiente:

tablaProcesos[proceso,padre,hilo,descriptor] = gettimeofday_ms()
tablaFicheros[proceso,padre,hilo,descriptor] = filename
printf ("{codigo: \"open\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d, date: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor, tablaProcesos[proceso,padre,hilo,descriptor])

Con el printf escribimos la información del fichero abierto por salida estándar. Las dos tablas (tablaProcesos y tablaFicheros) sirven para guardar el momento en el que se ha abierto el fichero (con una precisión de milisegundos), y el nombre del fichero en sí mismo, con lo que lo tendremos más a mano cuando pasemos a la llamada al sistema close. Para identificar un fichero usamos el índice formado con [el proceso, el padre, el hilo y el descriptor] del fichero que se ha abierto.

Con toda la información en su sitio ya tenemos la primera parte del script. Seguimos:

probe syscall.close.return {
        proceso = pid()
        padre = ppid()
        hilo = tid()
        descriptor = $fd
        ejecutable = execname()

        if ( ( procesoArgumento == proceso ) || ( procesoArgumento == padre ) || (procesoArgumento == hilo ) ) {
                if ([proceso,padre,hilo,descriptor] in tablaProcesos) {
                        filename = tablaFicheros[proceso,padre,hilo,descriptor]
                        date = gettimeofday_ms() - tablaProcesos[proceso,padre,hilo,descriptor]
                        printf ("{codigo: \"close\", proceso: \"%s\", pid: %d, ppid: %d, tid: %d, fichero: \"%s\", descriptor: %d, date: %d}\n", ejecutable, proceso, padre, hilo, filename, descriptor, date)
                        delete tablaProcesos[proceso,padre,hilo,descriptor]
                        delete tablaFicheros[proceso,padre,hilo,descriptor]
                }
        }
}

Fácil. Si se está haciendo un close de uno de los ficheros monitorizados, se calcula el tiempo que ha estado abierto y se muestra por la salida estándar.

Un ejemplo de ejecución del monitor es el siguiente:

# stap -v monitorFicheros.stp $(pidof master)
Pass 1: parsed user script and 76 library script(s) using 96688virt/22448res/2744shr kb, in 120usr/10sys/133real ms.
Pass 2: analyzed script: 8 probe(s), 22 function(s), 7 embed(s), 8 global(s) using 251840virt/48200res/3952shr kb, in 420usr/120sys/551real ms.
Pass 3: using cached /root/.systemtap/cache/02/stap_02e370ac4942b188c248e7ec11ac8e2c_19586.c
Pass 4: using cached /root/.systemtap/cache/02/stap_02e370ac4942b188c248e7ec11ac8e2c_19586.ko
Pass 5: starting run.

El pid que vamos a tratar es 1082
{codigo: "open", proceso: "smtpd", pid: 5817, ppid: 1082, tid: 5817, fichero: "/etc/hosts", descriptor: 12, date: 1310920975000}
{codigo: "close", proceso: "smtpd", pid: 5817, ppid: 1082, tid: 5817, fichero: "/etc/hosts", descriptor: 12, date: 0}

Sorpresa! Los printf se formatéan como datos json, por si alguna vez quisiera hacer algo con node.js y aplicaciones como log.io.

En cuanto a los "date: 0", se debe a que la precisión de milisegundos es demasiado baja para estos ficheros y para esta prueba sin tráfico de correo de ningún tipo.

El código completo (en este post sólo falta la definición de variables globales) está accesible en github, como siempre.

Limitaciones y trabajo futuro
Como ya he comentado, este script está lejos de ser óptimo. Además de mi propia incapacidad, en algunos momentos he optado por dar más vueltas de las necesarias para usar así más funciones y variables.

Mi primer objetivo era permitir la monitorización de procesos que todavía no estuvieran en ejecución. En esta versión, sin embargo, el proceso principal sí tiene que estar ejecutando antes de lanzar el stap. Esto no es algo técnicamente complicado, pero no se hace solo, y no aporta demasiado desde el punto de vista conceptual al post, así que lo he dejado.

¿Y qué pasa si el pidof da más de un pid? Me habéis pillado. Por ahora sólo trato un pid.

Para el que esté interesado, SystemTap permite guardar la salida estándar en un fichero usando el parámetro -o. A partir de aquí es trivial que otro script perl, python, bash, node.js o lo que sea trabaje con él.