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.

 

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.

 

Bueno, en realidad el nombre del post no se corresponde con este pequeño artículo, en el que sólo voy a ejecutar un “dd” y un “hdparm” sobre un par de particiones, en discos diferentes, bajo un mismo grupo LVM.

Resumiendo:

Tengo dos discos SATA de 500G, baratos, de poco más de 50 euros cada uno. Cada disco tiene dos particiones de 100G independientes, y otras dos de 400G como particiones LVM, para dar un total de 800G disponibles para el gestor de volúmenes.

Una de las particiones de 100G la dejo vacía mientras no la necesite. La otra la uso para la raíz y /boot del sistema.

En un principio sólo pongo /home en LVM, y luego, según se valla llenando “/”, voy creando otros volúmenes, por ejemplo para /var. Todo un poco rebuscado, pero así mantengo fresco lo poco que sé sobre LVM.

La máquina es una i7 a 2.80GHz, con memoria de sobra. Además, está completamente idle. Como hago copias de seguridad a menudo no me preocupa demasiado que se fastidien los discos. Por lo tanto, me he ahorrado el fakeraid de la placa base y el raid por software de linux. Por cierto, esta prueba se ha hecho sobre una Debian Squeeze de 64 bits básica, y un kernel 2.6.32 compilado a mano.

La creación de los volúmenes no tiene misterio y usa los parámetros por defecto. En otros posts quizá publique pruebas diferentes:

pvcreate /dev/sda2
pvcreate /dev/sdb2

vgcreate vg_prueba /dev/sda2 /dev/sdb2

lvcreate -i2 -L200G -nlv_home vg_prueba

mkfs.ext3 /dev/mapper/vg_prueba-lv_home

mount /dev/mapper/vg_prueba-lv_home /home

Como se puede ver, he utilizado un stripe de 2 y he formateado la partición como ext3.
Ejecutamos ahora escrituras secuenciales con dd sobre ese punto de montaje:

# time dd if=/dev/zero of=/home/prueba/test_escritura bs=1024 count=1048576
1048576+0 records in
1048576+0 records out
1073741824 bytes (1,1 GB) copied, 2,94397 s, 365 MB/s

real	0m2.945s
user	0m0.098s
sys	0m2.700s

Y ahora hdparm:

# hdparm -tT /dev/mapper/vg_prueba-lv_home 

/dev/mapper/vg_prueba-lv_home:
 Timing cached reads:   18472 MB in  2.00 seconds = 9245.75 MB/sec
 Timing buffered disk reads:  716 MB in  3.01 seconds = 238.02 MB/sec

Lo dicho, más adelante quizá publique alguna prueba más con diferentes sistemas de ficheros o parámetros LVM.

Update 1: He hecho una instalación de 32 bits para ver los resultados. En este caso el dd no llegaba ni remotamente a 250 MB/s.
Update 2: He hecho otra prueba con otros módulos de memoria que me han dejado, y los resultados son muuuy diferentes. Siempre hablo del más sencillo “dd” que se puede hacer, claro:

time dd if=/dev/zero of=/home/foron/test_escritura2 bs=1024 count=1048576
1048576+0 records in
1048576+0 records out
1073741824 bytes (1,1 GB) copied, 1,97434 s, 544 MB/s

real	0m1.975s
user	0m0.097s
sys	0m1.863s
 

A veces (seguro que pocas), nos encontramos ante una máquina que por algún motivo ha dejado de trabajar como se esperaba.
Digamos que el rendimiento o el IO del equipo baja, sin motivo visible. Digamos que no hay logs que indiquen problemas, ni errores del sistema. Nada.
Este es el momento para el kung fu, el voodoo o todo tipo de técnicas adivinatorias que tanto usamos los informáticos. Muchas veces es suficiente, pero no siempre, y no hay nada peor que no ser capaz de dar una explicación a un problema.

systemtap

Supongamos que tenemos la arquitectura de la imagen, con un par de balanceadores de carga, unas cuantas máquinas iguales que pueden ser servidores web, ftp, pop, smtp o cualquier otra cosa, y un par de servidores de almacenamiento nfs.
Teniendo en cuenta que los balanceadores de carga suelen dar la posibilidad de asignar diferentes pesos a cada máquina balanceada, ¿Por qué no usar uno de estos servidores para la monitorización del sistema? Es perfectamente posible enviar un poco menos de tráfico a la máquina destinada a SystemTap, de tal manera que no afecte al rendimiento global, para que podamos darnos un mecanismo para tener nuestra infraestructura controlada.

En este post sólo voy a referenciar algunos scripts que están disponibles en la web y en el redpaper. Además, el propio software viene con muchos ejemplos. En CentOS se puede instalar el rpm “systemtap-testsuite” para probar varios scripts.

Vamos a empezar con un par de ejemplos para usar más en un entorno académico que en la práctica.

Digamos que queremos aprender “lo que pasa” cuando hacemos un “ls” en un directorio de una partición ext3. Con este sencillo script:

#! /usr/bin/env stap

probe module("ext3").function("*").call
{
   printf("%s -> %s\n", thread_indent(1), probefunc())
}
probe module("ext3").function("*").return
{
   printf("%s <- %s\n", thread_indent(-1), probefunc())
}

Vamos a hacer un printf cada vez que comience y termine la ejecución de cualquier función del módulo ext3. En el printf vamos a tabular el nombre de la función que se ha ejecutado. El resultado es algo así:

   ...
     2 ls(31789): <- ext3_permission
     0 ls(31789): -> ext3_dirty_inode
     8 ls(31789):  -> ext3_journal_start_sb
    21 ls(31789):  <- ext3_journal_start_sb
    26 ls(31789):  -> ext3_mark_inode_dirty
    31 ls(31789):   -> ext3_reserve_inode_write
    35 ls(31789):    -> ext3_get_inode_loc
    39 ls(31789):     -> __ext3_get_inode_loc
    52 ls(31789):     <- __ext3_get_inode_loc
    56 ls(31789):    <- ext3_get_inode_loc
    61 ls(31789):   <- ext3_reserve_inode_write
    67 ls(31789):   -> ext3_mark_iloc_dirty
    74 ls(31789):   <- ext3_mark_iloc_dirty
    77 ls(31789):  <- ext3_mark_inode_dirty
    83 ls(31789):  -> __ext3_journal_stop
    87 ls(31789):  <- __ext3_journal_stop
    90 ls(31789): <- ext3_dirty_inode
     0 ls(31789): -> ext3_permission
   ...

Como he dicho, no es algo demasiado práctico, pero puede servir a algún profesor para suspender a un buen porcentaje de pobres alumnos :-)
Sigamos con algún otro ejemplo. Digamos que queremos programar un "nettop" que nos diga qué conexiones se están abriendo en una máquina en cada momento. Con un script similar al siguiente lo tendremos en unos minutos:

#! /usr/bin/env stap

global ifxmit, ifrecv

probe netdev.transmit
{
  ifxmit[pid(), dev_name, execname(), uid()] <<< length
}

probe netdev.receive
{
  ifrecv[pid(), dev_name, execname(), uid()] <<< length
}

function print_activity()
{
  printf("%5s %5s %-7s %7s %7s %7s %7s %-15s\n",
         "PID", "UID", "DEV", "XMIT_PK", "RECV_PK",
         "XMIT_KB", "RECV_KB", "COMMAND")

  foreach ([pid, dev, exec, uid] in ifrecv-) {
    n_xmit = @count(ifxmit[pid, dev, exec, uid])
    n_recv = @count(ifrecv[pid, dev, exec, uid])
    printf("%5d %5d %-7s %7d %7d %7d %7d %-15s\n",
           pid, uid, dev, n_xmit, n_recv,
           n_xmit ? @sum(ifxmit[pid, dev, exec, uid])/1024 : 0,
           n_recv ? @sum(ifrecv[pid, dev, exec, uid])/1024 : 0,
           exec)
  }
  print("\n")
  delete ifxmit
  delete ifrecv
}

probe timer.ms(5000), end, error
{
  print_activity()
}

¿Qué es lo que hemos hecho? Hemos generado tres "probes". Por un lado, cuando se recibe o trasmite a través de la red añadimos a los arrays "ifxmit, ifrecv" información sobre qué proceso y en qué interfaz ha enviado o recibido. Por otro lado, cada 5000 ms o cuando haya un error o termine el script mostramos la información por pantalla. El resultado puede ser algo similar a lo siguiente:

  PID   UID DEV     XMIT_PK RECV_PK XMIT_KB RECV_KB COMMAND
31485   500 eth0          1       1       0       0 sshd           

y pasados unos miles de milisegundos ...

  PID   UID DEV     XMIT_PK RECV_PK XMIT_KB RECV_KB COMMAND
15337    48 eth0          4       5       5       0 httpd
31485   500 eth0          1       1       0       0 sshd

Como se ve, tengo una sesión ssh abierta permanentemente, y he consultado una página web en el servidor monitorizado.
Por supuesto, esto puede no ser muy práctico en servidores muy activos, pero puede dar alguna idea a algún administrador.
Igual que hemos hecho un "nettop", también podemos hacer un "disktop" que muestre un resultado como el siguiente:

Sat Apr 11 17:22:03 2009 , Average:   0Kb/sec, Read:       0Kb, Write:      0Kb
     UID      PID     PPID                       CMD   DEVICE    T        BYTES
      48    15342    15239                     httpd     dm-0    W          210
      48    15343    15239                     httpd     dm-0    W          210
      48    15337    15239                     httpd     dm-0    W          210
      48    15336    15239                     httpd     dm-0    W          210

Lo que hecho es hacer el mismo wget varias veces. Para el que tenga curiosidad, los procesos httpd (que por cierto usa el usuario id=48) ejecutando en el servidor son:

# pstree -p | grep http
        |-httpd(15239)-+-httpd(15336)
        |              |-httpd(15337)
        |              |-httpd(15338)
        |              |-httpd(15339)
        |              |-httpd(15340)
        |              |-httpd(15341)
        |              |-httpd(15342)
        |              `-httpd(15343)

El script disktop está disponible en "/usr/share/systemtap/testsuite/systemtap.examples/io" del rpm "systemtap-testsuite".

Volvamos al primer ejemplo del post. Teníamos un grupo de servidores que han dejado de funcionar como deben. Digamos que sospechamos de la velocidad con la que nuestros servidores nfs están sirviendo el contenido de los servidores web.
Podemos probar aquellos ficheros que necesiten más de 1 segundo para abrirse:

#!/usr/bin/stap

global open , names
probe begin {
        printf("%10s %12s %30s\n","Process" , "Open time(s)" , "File Name")
}
probe kernel.function("sys_open").return{
        open[execname(),task_tid(task_current()),$return] = gettimeofday_us()
        names[execname(),task_tid(task_current()),$return] = user_string($filename)
}
probe kernel.function("sys_close"){
        open_time_ms = (gettimeofday_us() - open[execname(),task_tid(task_current()), $fd])
        open_time_s = open_time_ms / 1000000
        if ((open_time_s >= 1) && (names[execname(),task_tid(task_current()), $fd] != "")) {
                printf("%10s %6d.%.5d %30s\n", execname(),
open_time_s,open_time_ms%1000000,names[execname(),task_tid(task_current()), $fd])
        }
}

Con este sencillo script estaría hecho:

   Process Open time(s)                      File Name
     httpd      8.471285 /var/www/html/lectura_lenta.html

En este caso al servidor web le ha costado 8.5 segundos servir el fichero lectura_lenta.html. Después será responsabilidad nuestra buscar los problemas.

En definitiva, systemtap es una herramienta muy completa, pero que como tal requiere algo de práctica para ser útil. No sé si alguna vez va a tener mucho éxito en entornos de producción, pero no está de más saber que existe.

 

Empiezo otra serie de dos posts. En este caso sobre algo que tenía “pendiente” desde hace ya tiempo. De hecho, normalmente escribiría mis propios ejemplos para publicarlos aquí, pero para ir más rápido me voy a limitar a referenciar los scripts que usaré para mostrar las funcionalidades de ….. SystemTap.
SystemTap es una herramienta que sirve para monitorizar en tiempo real lo que está pasando con un kernel linux. Quitando el detalle de que el kernel tiene que tener ciertas opciones compiladas (luego las comento), SystemTap tiene la gran ventaja de no requerir ningún tipo de reinicio para empezar a trabajar.

¿Qué podemos monitorizar con SystemTap?
Pues ….. prácticamente todo lo que queramos. La segunda parte de este post tratará algunos ejemplos, pero por dar alguna pista, con este software podemos desde vigilar los procesos que más I/O están generando a programarnos un “nettop” que diga los procesos que más están usando la red.

¿Cómo funciona SystemTap?
Con SystemTap se le dice al kernel que ejecute una rutina cuando ocurre un evento, que puede basarse, por citar dos ejemplos, en el tiempo (cada n segundos) o en una llamada del sistema (al ejecutarse un vfs_read).
Para esto se usa un lenguaje de programación propio con el que se definen los “probe points” y las diferentes funciones. A pesar de ofrecer muchas posibilidades, el propio lenguaje tiene un control especial sobre los bucles infinitos, el acceso a memoria o la recursividad, por poner tres ejemplos. ¿Por qué tanto control? Pues porque a partir de este código se genera un módulo de kernel que se carga en el sistema. Claro, no hace falta decir las consecuencias de un bucle “mal hecho” a tan bajo nivel.

¿Qué necesita el kernel de linux para poder usar SystemTap?
Para empezar, cómo no, necesitamos un núcleo capaz de cargar módulos. Después, deberemos activar el soporte para los distintos tipos de debug que tiene el kernel, como el del sistema de ficheros, el del propio kernel o los kprobes. Traducido a formato .config, necesitamos las opciones CONFIG_DEBUG_INFO, CONFIG_KPROBES, CONFIG_RELAY, CONFIG_DEBUG_FS, CONFIG_MODULES y CONFIG_MODULES_UNLOAD.
Algunas distribuciones incluyen kernels específicos con estas opciones ya activadas.

¿Por qué SystemTap y no strace o gdb?
Bueno, esta seguramente sea una buena pregunta, al menos hasta ver los ejemplos de la segunda parte de este post; pero resumiendo, con SystemTap podemos:

  • Ver de forma integrada y unificada lo que pasa en el kernel y en las aplicaciones que ejecuta.
  • Probar aplicaciones multihilo.
  • Monitorizar aplicaciones multiproceso, como las cliente-servidor, en las que ambos componentes son procesos independientes.
  • Monitorizar en tiempo real y a prácticamente la velocidad de ejecución original.
  • Escribir nuestros propios monitores que den detalles que aplicaciones “generalistas” no son capaces de dar.

¿Cuáles son los aspectos negativos del invento?
Evidentemente, no todos son ventajas con SystemTap. Podríamos hablar de los inconvenientes técnicos, porque las opciones de debug del kernel ralentizan un poco la velocidad del sistema, pero yo creo que los mayores problemas vienen por el aprendizaje necesario para usar el software. Para empezar, hay que tener un cierto conocimiento del kernel de linux; después, hay que saber las posibilidades del lenguaje de programación, y por último hay que ser capaz de interpretar los resultados. Todo esto desmoralizará a más de uno, seguro, que preferirá seguir con top, htop, vmstat y demás familia antes de meterse en este embolado.
Afortunadamente, ya hay multitud de scripts disponibles en Internet para todo tipo de situaciones.

Suelo terminar los posts con una referencia bibliográfica. En este caso no creo que haya ningún libro sobre SystemTap, pero sí que hay un redpaper de IBM (que debería pasar a ser un redBOOK pronto :-) ): SystemTap: Instrumenting the Linux Kernel for Analyzing Performance and Functional Problems

© 2012 forondarenanet Suffusion theme by Sayontan Sinha