sábado, 8 de septiembre de 2012

SVN y Apache

Normalmente, cuando preparo el ambiente de desarrollo para un nuevo proyecto de PHP, me meto a mi servidor y creo un repositorio de SVN, al cual luego me conecto usando svn+ssh. Sin embargo, en este proyecto necesito trabajar con personas que usan Windows, y como era de esperarse, a TortoiseSVN no le gusta mucho svn+ssh. Tuve entonces que configurar el repositorio para poder acceder por http.

Estos son los pasos para habilitar SVN por http:

  1. sudo apt-get install libapache2-svn
  2. Usar a2enmod para habilitar los siguientes modulos de apache:
    • dav_module
    • fs_module
    • dav_svn_module
    • authz_svn_module
  3. Crear el repositorio
    svnadmin create /var/svn
  4. Agregar esto a la configuración de apache (Yo lo puse en /etc/apache2/sites-available/svn):
    <Location /svn>
    DAV svn
    SVNPath /var/svn
    AuthType Basic
    AuthName "SVN Repository"
    AuthUserFile security/svnpasswd
    Require valid-user
    </Location>
    

  5. Crear un directorio para guardar el archivo de las contraseñas
    sudo mkdir /etc/apache2/security 


  6. Agregar usuarios:
    sudo htpasswd svnpasswd usuario



Algunos puntos importantes a considerar:
  • NO poner el repositorio dentro del DocumentRoot de Apache. Esto crea conflictos.
  • Para crear el working copy dentro del servidor, usar file:// para el URL, no http://. De lo contrario, saldrán errores como este:

    svn: Commit failed (details follow):
    svn: Processing MERGE request response failed: Element type "http:" must be followed by either attribute specifications, ">" or "/>". (/svn/proyecto/trunk/) 
    svn: MERGE request failed on '/svn/proyecto/trunk'
    

sábado, 4 de agosto de 2012

Cómo configurar el fetchMode de Zend_Db

Por alguna razón, en ningún lugar esta documentado como establecer el "fetch mode" del adaptador de Zend_Db usando el archivo de configuración application.ini. En todos lados explican como configurarlo en el bootstrap, o incluso sugieren manualmente configurar el objeto cada vez que lo usas. Pues despues de andar analizando la clase Zend_Db_Adapter_Abstract, descubrí que esto es lo que hay que agregar al application.ini:

resources.db.params.options.fetchMode = obj

Con esto, toda la configuración se queda dentro del archivo, sin necesidad de agregar código extra en el bootstrap ni andar manipulando el adaptador cada vez que lo necesitas.

domingo, 15 de julio de 2012

Problemas con la codificación de caracteres en MySQL

Recientemente me topé con un problema en donde no lograba hacer que los caracteres especiales se guardaran y extrayeran correctamente de una base de datos en MySQL.

Después de investigar un rato, encontré que la configuración del servidor estaba un poco errática:

mysql> show variables LIKE '%character%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | latin1                     | 
| character_set_connection | latin1                     | 
| character_set_database   | latin1                     | 
| character_set_filesystem | binary                     | 
| character_set_results    | latin1                     | 
| character_set_server     | utf8                       | 
| character_set_system     | utf8                       | 
| character_sets_dir       | /usr/share/mysql/charsets/ | 
+--------------------------+----------------------------+
8 rows in set (0.00 sec)


Mi intención era guardar todos los datos usando UTF8, no latin1. En este caso, mi configuración estaba causando que las conexiones al servidor se crearan esperando un tipo de codificación, pero guardando los datos en otro. Podría pelearme con la configuración de MySQL, pero este servidor ya esta muy viejo y cada vez que cambio algo se rompen otras 10 cosas. Para solucionarlo rápidamente, mejor usamos PHP para cambiar la configuración de forma dinámica. Esto lo ponemos en el bootstrap de la aplicación:

$db_charset = $db->fetchRow("SHOW VARIABLES LIKE 'character_set_database'");
$db->query("SET NAMES '" . $db_charset->Value . "'"); 
$db->query("SET SESSION character_set_server = '$db_charset->Value'"); 

Sigue existiendo el problema con los datos que ya estaban guardados, pero de ahora en adelante los datos nuevos funcionarán sin problema.

martes, 5 de junio de 2012

Alineando las particiones de un volumen RAID 5

"DISK FAILURE IS IMMINENT!"

Este es el mensaje que me apareció recientemente mientras me encontraba trabajando tranquilamente en mi computadora. El mensaje apareció no una, sino en repetidas ocasiones en ventanas emergentes que poco ayudaban a controlar el pánico. Además de llenarnos de terror, este mensaje es un buen recordatorio de porqué debemos tener un buen sistema de respaldo. En mi caso, utilizo un conjunto de 4 discos de 1TB configurados en RAID 5. Esto me permite tener un nivel aceptable de redundancia, donde mis datos quedan intactos siempre y cuando solo falle un disco a la vez.

Después de cerrar todas las ventanas y desactivar futuras amenazas de muerte de datos, me puse a investigar el supuesto daño.

$ sudo smartctl --health /dev/sdb

smartctl 5.41 2011-06-09 r3365 [x86_64-linux-3.0.0-19-generic] (local build)
Copyright (C) 2002-11 by Bruce Allen, http://smartmontools.sourceforge.net

=== START OF READ SMART DATA SECTION ===
SMART overall-health self-assessment test result: FAILED!
Drive failure expected in less than 24 hours. SAVE ALL DATA.
Failed Attributes:
ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE      UPDATED  WHEN_FAILED RAW_VALUE
  5 Reallocated_Sector_Ct   0x0033   023   023   036    Pre-fail  Always   FAILING_NOW 3168


Basado en mi experiencia, puedo decir que en este punto al disco le queda un lapso de vida de un día hasta 3 meses. Si, así de poco confiable es SMART, y así de impredecibles son los discos duros. Podría jugármela y esperar a que ese "Pre-fail" se convierta en muerte total, pero decidí mejor comprar un disco nuevo y quitarme este pendiente de una vez.

La conveniencia de tener un sistema RAID 5 también trae consigo algunas complicaciones, y fue con este nuevo disco que encontré una de ellas. Poco sabía que dentro del mundo de los discos duros existe un nuevo concepto llamado Advanced Formatting. En realidad el concepto solo es nuevo para mi, ya existía desde hace muchos años, pero fue hasta el 2011 cuando los fabricantes comenzaron a mudar la mayoría de sus discos a este formato. Básicamente, se trata de que antes se usaban discos duros con sectores de 512 bytes, y ahora se usan de 4096 para mayor eficiencia. Si te interesa conocer a detalle como funciona y las ventajas que Advanced Formatting otorga, este pdf de Hitachi es ampliamente recomendable.

Como era de esperarse, los discos que ya tenía, aunque los compré en el 2011, son de sectores 512 bytes. Por lo tanto, cuando intenté agregar el disco a mi arreglo usando Ubuntu Disk Utility, me tocó lidiar con mensajes como este:

WARNING: The partition is misaligned by 1024 bytes. This may result in very poor performance. Repartitioning is suggested.

¿Mal desempeño? ¡A nadie le gusta eso! Comprobé que efectivamente mi disco era uno de los nuevos:

$ cat /sys/block/sdd/queue/physical_block_size
4096

$ cat /sys/block/sdd/queue/logical_block_size
512

Al hacer la misma prueba con mis otros discos, el tamaño fue 512 en los dos casos. Esto empezó a preocuparme. Una alternativa era devolver este disco a Amazon y buscar alguna reliquia que funcionara con el tamaño antiguo, pero la realidad es que iba a tener que actualizarme eventualmente. ¿Sería posible agregar un disco con sectores de diferente tamaño a un volumen RAID existente?

La respuesta es que si, es posible, y la verdad es que esto poco tiene que ver con el problema de la partición mal alineada. El asunto aquí es que el Disk Utility esta intentando crear una partición basándose en los discos existentes, que tienen sectores de diferente tamaño. La solución es simplemente crear la partición manualmente utilizando gparted. Dado que RAID utiliza bloques mucho mas grandes (en mi caso de 64KB), lo único que importa es que el tamaño del bloque sea un múltiplo del tamaño de los sectores.

$ sudo gparted

Esta aplicación nos permite crear una nueva partición en el disco, y además alinearla por megabyte, no por cilindro como lo esta haciendo Disk Utility. Simplemente le ponemos que inicie en 1MB y termine en el máximo espacio disponible. Después nos vamos a "Manage Flags" y habilitamos la bandera de RAID. Con esto queda preparada nuestra partición para agregarla al arreglo utilizando mdadm:

$ sudo mdadm --add /dev/md0 /dev/sdd
$ sudo mdadm --detail /dev/md0
$ cat /proc/mdstat
md0 : active raid5 sdd[3](S) sdc1[0] sdb1[2] sde1[1]
      1953519872 blocks level 5, 64k chunk, algorithm 2 [3/3] [UUU]

Como podemos ver, nuestro nuevo disco ahora esta marcado como disco de reserva ([S]pare), y estamos listos para desactivar el disco moribundo:

$ sudo mdadm --manage /dev/md0 --fail /dev/sdb1
$ cat /proc/mdstat
md0 : active raid5 sdd[3] sdc1[0] sdb1[4](F) sde1[1]
      1953519872 blocks level 5, 64k chunk, algorithm 2 [3/2] [UU_]
      [>....................]  recovery =  0.0% (155976/976759936) finish=208.6min speed=77988K/sec

Parece que todo va muy bien. El disco viejo ya esta marcado como [F]ailed y mdadm ya comenzó a usar el nuevo para regenerar el arreglo. Ahora si podemos quitar el disco viejo por completo:

$ sudo mdadm --manage /dev/md0 --remove /dev/sdb1

$ cat /proc/mdstat
md0 : active raid5 sdd[3] sdc1[0] sde1[1]
      1953519872 blocks level 5, 64k chunk, algorithm 2 [3/2] [UU_]
      [>....................]  recovery =  1.7% (16652688/976759936) finish=226.0min speed=70789K/sec


Le tomará un buen rato regenerar el arreglo, pero una vez mas, RAID salvó el día.

domingo, 3 de junio de 2012

Configuración de PowerDNS

Para que procrastinar hoy, si lo puedo hacer mañana...

Hace ya más de dos años, contraté un cloud server con Rackspace, cuya finalidad era eventualmente reemplazar un servidor moribundo que tengo en GoDaddy. Durante ese tiempo me ha servido para realizar todo tipo de pruebas, pero nunca me dediqué a configurarlo para lo que realmente era su objetivo.

Para no seguir pagando la renta de dos servidores, hoy pensé en seguir en donde me había quedado. Durante mis pruebas, la seguridad de mi servidor desafortunadamente pasó a segundo plano, y como era de esperarse, al revisar los logs encontré evidencia de intrusos. Como realmente no tenía información importante en ese servidor, decidí utilizar la excelente herramienta de Rackspace para borrar todo y volver a empezar de cero, ahora utilizando Ubuntu 12.04 LTS.

En este post seguiré paso a paso la configuración incial del servidor, llegando hasta el punto en que logremos tener una instalación funcional de PowerDNS.

Elegí PowerDNS porque necesito tener un backend basado en MySQL, y lograr esto con BIND se complica un poco. Además, de todos programas de DNS (excluyendo a BIND), este es el que me pareció mas popular, y esto normalmente se traduce a mejor documentación...aunque después de terminar la configuración he de decir que hay algunas cosas no están tan bien documentadas (como la integración con DNSSEC). Aún así, creo que fue una buena elección.

Una vez que la imagen haya terminado de cargarse en el servidor, lo primero que hay que hacer es conectarse por ssh y cambiar el password de root.

$ passwd 

Me siento sucio al estar haciendo todo como root, entonces mejor creamos un usuario normal, y lo agregamos a la lista de sudoers:

$ adduser kenneth
$ adduser kenneth sudo

Accedemos usando el nuevo usuario e instalamos los paquetes iniciales:

$ sudo apt-get update
$ sudo apt-get install vim subversion rar unrar apache2 php5 php5-mysql mysql-server php5-mcrypt ack-grep pdns-backend-mysql

Como podrás notar, en este punto ya estamos instalando PowerDNS. Apt se encargará de conseguir los demás paquetes requeridos. En este paso también quedarán instalados MySQL y Apache 2.

Lo primero que me gusta hacer con la instalación de Apache, es cambiar el DocumentRoot de lugar, para que quede en /www. En este directorio descargamos y descomprimimos los paquetes de PHPMyAdmin y PowerAdmin.

Después de configurar PHPMyAdmin, podemos fácilmente crear un usuario y una base de datos para PowerDNS.

El siguiente schema incluye los campos nuevos requeridos en la versión 3.0 de PDNS:

create table domains (
 id   INT auto_increment,
 name   VARCHAR(255) NOT NULL,
 master   VARCHAR(128) DEFAULT NULL,
 last_check  INT DEFAULT NULL,
 type   VARCHAR(6) NOT NULL,
 notified_serial INT DEFAULT NULL, 
 account         VARCHAR(40) DEFAULT NULL,
 primary key (id)
) Engine=InnoDB;

CREATE UNIQUE INDEX name_index ON domains(name);

CREATE TABLE records (
  id              INT auto_increment,
  domain_id       INT DEFAULT NULL,
  name            VARCHAR(255) DEFAULT NULL,
  type            VARCHAR(10) DEFAULT NULL,
  content         VARCHAR(64000) DEFAULT NULL,
  ttl             INT DEFAULT NULL,
  prio            INT DEFAULT NULL,
  change_date     INT DEFAULT NULL,
  primary key(id)
) Engine=InnoDB;

CREATE INDEX rec_name_index ON records(name);
CREATE INDEX nametype_index ON records(name,type);
CREATE INDEX domain_id ON records(domain_id);

create table supermasters (
  ip VARCHAR(25) NOT NULL, 
  nameserver VARCHAR(255) NOT NULL, 
  account VARCHAR(40) DEFAULT NULL
) Engine=InnoDB;

-- Cambios necesarios para la version 3.0

create table domainmetadata (
 id   INT auto_increment,
 domain_id       INT NOT NULL,
 kind   VARCHAR(16),
 content TEXT,
 primary key(id)
);

create index domainmetaidindex on domainmetadata(domain_id);               


create table cryptokeys (
 id  INT auto_increment,
 domain_id      INT NOT NULL,
 flags  INT NOT NULL,
 active  BOOL,
 content TEXT,
 primary key(id)
);   

create index domainidindex on cryptokeys(domain_id);           

alter table records add ordername      VARCHAR(255);
alter table records add auth bool;
create index orderindex on records(ordername);

create table tsigkeys (
 id  INT auto_increment,
 name  VARCHAR(255), 
 algorithm VARCHAR(50),
 secret  VARCHAR(255),
 primary key(id)
);

create unique index namealgoindex on tsigkeys(name, algorithm);
alter table records change column type type VARCHAR(10);

Ahora podemos empezar a configurar PowerDNS. Empezamos por deshabilitar las funciones de DNSSEC:

#/etc/powerdns/pdns.d/pdns.local.gmysql

gmysql-dnssec=no

Después probamos que PDNS funcione y que la conexión a MySQL sea exitosa:

$ /etc/init.d/pdns monitor
Jun 03 18:53:29 Creating backend connection for TCP
% Jun 03 18:53:29 gmysql Connection successful

Si todo parece estar bien, podemos empezar a configurar PowerAdmin. Esta es una interfaz web que facilita la interacción con la base de datos de PowerDNS.
Para configurarlo, primero debemos asegurarnos de que apache tenga el acceso correcto a los archivos:

$ sudo chown -R www-data /www/poweradmin

Navegamos a http://host/powerdns/install para entrar al script de configuración. Aquí se crearán unas tablas extra en la base de datos para guardar plantillas y usuarios.

Hablando de plantillas, ya que esté configurado PowerAdmin, podemos crear una plantilla como la siguiente:

Name Type Content TTL Priority
[ZONE] SOA localhost hostmaster@[ZONE] [SERIAL] 86400
[ZONE] NS ns1.knet.mx 86400
[ZONE] NS ns2.knet.mx 86400
*.[ZONE] CNAME [ZONE] 120
[ZONE] MX mail.[ZONE] 120 25
[ZONE] CNAME 173.203.206.44 86400 10

Después de agregar la zona "example.com", podemos probar la resolución usando dig y host:

$ dig example.com
$ host example.com knet.mx

Ese último comando básicamente quiere decir "Pregúntale a knet.mx cuáles son los registros DNS de example.com".


¡Y listo! Con esto queda el servidor preparado para atender los dominios que quieras, solo hay que agregarlos utilizando la plantilla.



martes, 28 de febrero de 2012

Como instalar Xdebug

Xdebug es un debugger para PHP extremadamente versátil. Además de las funciones esenciales de un debugger común, incluye un profiler y un tracer.

Xdebug se puede instalar directo de los repositorios de Ubuntu:
sudo apt-get install php5-xdebug

Una vez instalado, agregamos esto a la configuración:
#/etc/php5/apache2/conf.d/xdebug.ini

#verificar que sea la ruta correcta 
zend_extension=/usr/lib/php5/20090626/xdebug.so

xdebug.remote_enable=On
xdebug.remote_host="localhost"
xdebug.remote_port=9000

xdebug.profiler_enable=0
xdebug.profiler_enable_trigger=1

# Aquí se guardan los archivos de salida. Puede ser la ruta que 
# quieras pero el directorio ya debe existir y ademas debe 
# tener permisos de escritura para el usuario que ejecuta el 
# script
xdebug.profiler_output_dir="/tmp/xdebug"

xdebug.show_mem_delta=1
xdebug.trace_format=1
xdebug.trace_enable_trigger=1
xdebug.auto_trace=0

# Aquí aplican las mismas condiciones que para el profiler
xdebug.trace_output_dir="/tmp/xdebug"


Después de hacer las modificaciones, guardamos el archivo y reiniciamos apache para que los cambios hagan efecto.

Profiler


El profiler nos sirve para identificar las funciones dentro de nuestro script que son llamadas con mas frecuencia, y además nos permite saber cuales son las mas lentas.

Para iniciar el profiler desde el browser, mandar un parametro extra por GET:

http://example.com/pagina.php?XDEBUG_PROFILE=1

Para inciar el profiler desde la línea de comandos:

php -d xdebug.profiler_enable=1 pagina.php

En los dos casos, Xdebug generará un archivo de texto con una gran cantidad de información sobre la ejecución del script. La cantidad de información es tan grande que es practicamente imposible analizar el archivo a simple vista. Para esto usamos KCachegrind, una herramienta que sirve para interpretar los archivos que genera xdebug.

sudo apt-get install kcachegrind

Tracer


El tracer nos permite hacer un análisis del uso de memoria del script.

Para iniciar el profiler desde el browser:

http://example.com/pagina.php?XDEBUG_TRACE=1

Para inciar el tracer desde la línea de comandos:

php -d xdebug.auto_trace=1 pagina.php

Al igual que el profiler, el archivo generado por el tracer no es realmente útil por si solo, pero podemos utilizar este script para generar un listado mas práctico.

php tracefile-analyser.php trace.2043925204.xt memory-own 20