Hace unos meses publiqué un pequeño tutorial sobre el despliegue de aplicaciones Django, en el que mencioné Docker, pero no expliqué ni su funcionamiento ni sus ventajas, principalmente porque pensaba escribir un tutorial que por falta de tiempo se quedó en el tintero. Desde entonces han aparecido algunos buenos manuales, así que en su lugar he escrito este tutorial sobre como desplegar django usando Docker.

El tutorial asume que el lector posee conocimientos sobre la configuración de nginx y gunicorn, así como del funcionamiento de docker. Si no es así recomiendo leer mi tutorial despliegue de aplicaciones Django, y algún tutorial de docker, antes de continuar.

Introducción

En esencia el proceso de crear una imagen Docker es el mismo que hay que seguir para configurar cualquier servidor manualmente, cada uno de los pasos en el proceso se plasma en un archivo Dockerfile que es usado por Docker para construir la imagen. Por ejemplo para un contenedor de una aplicación django, será necesario:

  • Copiar la aplicación django al contenedor, e instalar sus dependencias.
  • Instalar y configurar gunicorn.
  • Instalar y configurar nginx.
  • Instalar supervisor y configurarlo para que arranque nginx y gunicorn.

Para conseguir todo esto, además del archivo Dockerfile, necesitamos los archivos de configuración de nginx, gunicorn, supervisor, y la aplicación django que se copiarán a la imagen. Todos estos archivos del proyecto deben estar en el mismo directorio que Dockerfile, por ejemplo la estructura usada en este tutorial es:

Docker/
|--Dockerfile
|--gunicorn-config.py
|--nginx-default
|--supervisor.conf
|--django_app/
|  |--manage.py
|  |--requirements.txt
|  |--static/
|  |--app1/
|  |  |--models.py
|  |  |--....
|  |--app2/
|  |  |--....

Descargar

El Dockerfile en este tutorial asume que la aplicación django esta en el subdirectorio django_app, que a su vez contiene el archivo requirements.txt con las dependencias del mismo, y el directorio static con todos los archivos estáticos.

Dockerfile

Toda la magia en la creación de una imagen está en el archivo Dockerfile, su sintaxis es muy clara:

FROM ubuntu:14.04
MAINTAINER secnot <secnot@secnot.com>

# Actualizacion de los 'sources' a la ultima version
RUN apt-get update

# Instalar los paquetes del sistema necesarios para python
RUN apt-get install -qy python \
                        python-dev \
                        python-pip \
                        python-setuptools \
                        build-essential

# Instalar algunas utilidades extras (opcional)
RUN apt-get install -qy vim \
                        wget \
                        net-tools \
                        git

# Instalamos resto aplicaciones
RUN apt-get install -qy nginx \
                        supervisor

###############################
#
#        Nginx
#
###############################

# Copiar la configuracion de nginx de la aplicion
ADD nginx-default /etc/nginx/sites-available/default

# Desactiva el modo demonio para arrancar el proceso con supervisor
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# Cambiar los permisos de nginx para poder ejecutar nginx como www-data
RUN chown -R www-data:www-data /var/lib/nginx

# Permitir el acceso al puerto 80 del contenedor
EXPOSE 80


##################################
#
#        Gunicorn y Django
#
################################

# Copiar aplicacion del subdirectorio django_app/ al directorio
# /django_app en el contenedor
ADD django_app /django_app
RUN chown -R www-data:www-data /django_app

# Si la aplicacion tiene dependencias de paquetes del del sistema
# este es un buen sitio para instalarlas, por ejemplo para PIL:
# RUN apt-get install -qy python-dev libjpeg-dev zlib1g-dev

# Usamos requirements.txt para instalar las dependencias de la
# aplicacion.
RUN pip install -r /django_app/requirements.txt

# Tambien se pueden instalar individualmente, por ejemplo:
# RUN pip install Django
# RUN pip install bleach
# ...

# Una buena medida de seguridad es alamacenar claves usuarios y
# otras credenciales de seguridad en variables de entorno.
# Se importan desde settings.py con:
#       PAYPAL_CLIENT_ID     = os.environ['PAYPAL_CLIENT_ID']
#       PAYPAL_CLIENT_SECRET = os.environ['PAYPAL_CLIENT_SECRET']
ENV PAYPAL_CLIENT_ID sdfasFASDRwefasFqasdfAsdfAsdFAsdfsDFaSDfWERtSDFg
ENV PAYPAL_CLIENT_SECRET ASAsdfarasDFaRasdFaSsdfghJdfGHDGsdTRSDfGErtAFSD

# Como precaucion se instala gunicorn, aunque deberia estar en
# requirements.txt
RUN pip install gunicorn

# Por ultimo se copia la configuracion de gunicorn.
ADD gunicorn-config.py /etc/gunicorn/config.py


#############################
#
#        Supervisor
#
############################

# Copiar la configuracion
ADD supervisor.conf /etc/supervisor/conf.d/django_app.conf

# Instalamos supervisor-stdout que permite que los logs, sean impresos
# en stdout. (ver supervisor.conf)
RUN pip install supervisor-stdout

# Establecer el directorio de trabajo
WORKDIR /django_app

# Comando por defecto que se ejecutara al arranque del contenedor,
# supervisor se encarga de gestionar nginx y gunicorn.
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]

Supervisor y Logging

El archivo supervisor.conf, contiene la configuración necesaria para que supervisord arranque nginx y gunicorn, y los reinicie en caso de que mueran:

[program:gunicorn]
command = /usr/local/bin/gunicorn -c /etc/gunicorn/config.py yourapp.wsgi:application
directory = /django_app
user = www-data
autostart = True
autorestart = True

[program:nginx]
command = /usr/sbin/nginx
autostart = True
autorestart = True

Pero esa no es la única función de supervisor, sino que también se encarga de guardar logs de las salidas stdout y stderr de cada programa que gestiona. Con django se puede aprovechar esta característica para que supervisor se encargue de gestionar los logs, solo es necesario imprimirlos en stdout en lugar de guardarlos en un archivo, editando settings.py de tu aplicación:

# settings.py
# .....
import sys

LOGGING = {
    'version': 1,
    'dissable_existing_loggers': False,

    'formatters': {
        'verbose': {
            'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
            'datefmt': "%Y-%m-%d %H:%M:%S",
        },
        'simple': {
            'format': "%(levelname)s %(message)s",
        },
    },

    'handlers': {
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
            'formatter': 'simple',
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
            'include_html': True,
        },
    },

    'loggers': {
        '': {
            'handlers': ['mail_admins', 'console'],
            'level': 'DEBUG',
        }
    },
}

El manejador StreamHandler redirige todos los logs de nivel INFO, o superior a la salida estándar, cuando supervisor recibe estos logs tiene dos opciones:

  • Almacenar los logs en un archivo, que a su vez puede o no estar montado mediante un volumen desde el host, para que los datos sean persistente:

    [program:gunicorn]
    command = /usr/local/bin/gunicorn -c /etc/gunicorn/config.py yourapp.wsgi:application
    directory = /django_app
    user = www-data
    autostart = True
    autorestart = True
    
    redirect_stderr=true
    stdout_logfile=/var/log/django.log
    

    Para usar un volumen, se debe iniciar el contenedor con la opción -v:

    $ docker run -v /var/log/django.log:/var/log/django.log -p 80:80 yourapp:nginx
    
  • Redirigir los logs a stdout para que sean gestionados por el host que inicio el contenedor. Para esto es necesario usar el manejador de eventos supervisor-stdout. Y la configuración de supervisor seria:

    [supervisord]
    nodaemon = true
    
    [program:gunicorn]
    command = /usr/local/bin/gunicorn -c /etc/gunicorn/config.py testsite.wsgi:application
    directory = /django_app
    user = www-data
    autostart = True
    autorestart = True
    stdout_events_enabled = true
    stderr_events_enabled = true
    
    [program:nginx]
    command = /usr/sbin/nginx
    autostart = True
    autorestart = True
    stdout_events_enabled = true
    stderr_events_enabled = true
    
    
    [eventlistener:stdout]
    command = supervisor_stdout
    buffer_size = 100
    events = PROCESS_LOG
    result_handler = supervisor_stdout:event_handler
    

    Para arrancar el contenedor, hay que añadir la opción -a stdout para que imprima la salida en stdout:

    $ sudo docker run --rm -a stdout -p 80:80 yourapp:nginx
    

    En el host se puede configurar supervisor para que arranque el contenedor, y al tiempo guardar los logs

Seguridad

Los contenedores docker NO son seguros, a pesar de lo que hayas leído es posible salir de un contenedor. Por diseño los contenedores comparten el mismo kernel que el host, y pueden hacer llamadas de sistema al mismo, de manera que cualquier vulnerabilidad del interfaz del kernel, es explotable desde un contenedor. La mejor política es tratar docker como una herramienta para desplegar aplicaciones de forma sencilla, que además proporciona una capa extra de seguridad. Dicho esto, es posible mejorar la seguridad con medidas sencillas:

  • NO ejecutes como root los procesos dentro del contenedor, usa un usuario sin privilegios:

    $ docker run -u=www-data yourapp:nginx
    

    con -u hay que indicar un usuario o uid existente en el contenedor.

  • Limita la memoria disponible para los procesos del contenedor con la opción -m, por ejemplo 200MB:

    $ docker run -m=200m yourapp:nginx
    
  • Limita el uso de cpu, permitiendo la ejecución únicamente en los nucleos/cpus especificados con --cpuset:

    $ docker run --cpuset=0,1 yourapp:nginx
    
  • NO uses volúmenes para montar el sistema de archivos del host en el contenedor, es cómodo pero es difícil de configurar correctamente para que sea seguro. Si no tiene mas remedio que usarlos, móntalos en modo lectura:

    $ docker run -v /host/static:/container/static:ro
    
  • Utiliza versiones reciente del kernel, como he comentado anteriormente todas las vulnerabilidades del kernel son explotables desde el contenedor, instala versiones recientes y esta atento a la aparición de nuevos exploits.

  • Usa una máquina virtual en la que ejecutar docker, de esta manera tienes lo mejor de los dos mundos, la seguridad de una VM, con la facilidad para desplegar aplicaciones de docker.

Comandos útiles

Una vez la aplicación esté correctamente configurada, puedes construir el contenedor ejecutando el siguiente comando desde el directorio Docker:

$ sudo docker build --rm:True -t yourapp:nginx .

y deberia aparecer al listar las imágenes disponibles:

$ sudo docker.io images

REPOSITORY     TAG     IMAGE ID        CREATED             VIRTUAL SIZE
yourapp        nginx   fb3367f96602    4 minutes ago       544.4 MB
ubuntu         14.04   826633116fdc    5 days ago          194.2 MB

y por ultimo se ejecuta el contenedor con:

$ sudo docker run --rm -a stdout -p 80:80 yourapp:nginx

El parámetro -p 80:80 le indica a docker que publique el puerto 80 del contenedor en el puerto 80 local, de forma que el contenedor sea accesible desde el exterior. -rm indica que el contenedor debe ser eliminado una vez finalice.

Por defecto para crear y ejecutar contenedores es necesario, tener privilegios de root, esto es debido a que el demonio docker se ejecuta como root, y solo es posible comunicarse con el a través de de un socket unix, propiedad del usuario root y el grupo docker.

Para que un usuario sea capaz de manejar contenedores, solo es necesario añadirlo al grupo docker, y reiniciar el demonio:

$ sudo usermod -a -G docker gowen
$ sudo service docker restart

Ver comentarios


Un problema muy común en cualquier aplicación django es la necesidad de almacenar algún dato confidencial para su funcionamiento, por ejemplo una clave, nombre de usuario, o identificador de un API, esto se suele hacer en el archivo de configuración settings.py, pero es una mala practica de seguridad.

La solución es definir variables de entorno en el shell, e importarlas desde settings.py, de manera que sea mas difícil que un fallo exponga la información.

#!/bin/bash
#.bashrc
export EMAIL_HOST_USER="tuemail@gmail.com"
export EMAIL_HOST_PASSWORD="tuclave"
#settings.py
import os

EMAIL_HOST_USER=os.environ['EMAIL_HOST_USER']
EMAIL_HOST_PASSWORD=os.environ['EMAIL_HOST_PASSWORD']

La limitación de este sistema cuando se está usando virtualenv, es que no permite tener distintos valores de una variable para cada entorno. Esto se puede solucionar usando los hooks .virtualenvs/app_env/bin/postactivate para establecer las variables al entrar en el entorno:

#!/bin/bash
# This hook is run after this virtualenv is activated.

export EMAIL_HOST_USER="tuemail@gmail.com"
export EMAIL_HOST_PASSWORD="tuclave"

y .virtualenvs/app_env/bin/predeactivate para limpiarlas al salir:

#!/bin/bash
# This hook is run before this virtualenv is deactivated.

unset EMAIL_HOST_USER
unset EMAIL_HOST_PASSWORD

Ver comentarios


Amazon S3 es un gran servicio para alojar los archivos de una página web, ofrece alta disponibilidad, fiabilidad, y seguridad (configurado correctamente). Pero en páginas con demasiado tráfico, con una gran cantidad de peticiones, y/o ancho de banda, como por ejemplo páginas que alojen gran cantidad de imágenes, puede hacer el coste de S3 prohibitivo.

En estos casos la alternativa es usar un CDN, o gestionar tu propio sistema de distribución. Un CDN puede resultar algo más barato dependiendo del tráfico, pero no significativamente. Gestionar tu propio servidor aumenta la complejidad de la aplicación, los posibles puntos de fallo, es menos escalable, y más difícil de administrar.

Existe una solución intermedia, que consiste en seguir almacenando los archivos en S3, y usar Varnish como proxy cache. De esta manera cuando una petición de un archivo llega, Varnish lo obtiene de S3, se lo envía al cliente, y guarda una copia en caché. Sucesivas peticiones de ese archivo se sirven directamente desde cache.

Con esta configuración no es necesario modificar el backend de almacenamiento ya que se sigue usando S3, es altamente escalable, facil de administrar, y permite seguir usando S3 como fallback.

Configuración de Varnish

Este artículo no es un tutorial de Varnish, si eso es lo que estas buscando, hay muchos disponibles en la red, pero te recomiendo que empieces por los enlaces al final del articulo.

Archivo de configuración para Varnish con S3 default.vcl:

/* Amazon S3 Backend */
backend s3 {
    .host = "nombrebucket.s3.amazonaws.com";
    .port = "80";
}


/* vcl_recv es llamado cada vez que una petición es recibida */
sub vcl_recv {

    /* Solo servimos peticiones GET y HEAD */
    if (req.request != "GET" && req.request != "HEAD") {
        return (pass);
    }

    /* Solo servimos archivos, asi que ni Cookies ni Autorizaciones */
    if (req.http.Authorization || req.http.Cookie) {
        return (pass);
    }

    set req.grace = 120s;
    set req.http.X-Forwarded-For = client.ip;

    /* Establecer backend para la peticion */
    set req.backend = s3;
    set req.http.host = "nombrebucket.s3.amazonaws.com";

    /* Eliminar de la cabecera todos los campos que pueden afectar la cache */
    unset req.http.Cache-Control;
    unset req.http.Pragma;
    unset req.http.Expires;

    /* Si el archivo es una imagen desactivamos compresion */
    if (req.url ~ "\.(jpeg|jpg|png|ico|svg|gif)$") {
        unset req.http.Accept-Encoding;
        return (lookup);
    }
    else if (req.url ~ "\.(css|js|txt)$") {
        return (lookup);
    }

    /* Demas tipos no soportados */
    return (pass);
}


/* Llamado cuando hay un hit en cache */
sub vcl_hit {
    return (deliver);
}


/* Llamado cuando hay un miss en cache */
sub vcl_miss {
    return (fetch);
}


/* Llamado cuando un documento se ha descargado exitosamente del backend */
sub vcl_fetch {
    /* Se eliminan cookies antes de que el objeto sea introducido en cache */
    unset beresp.http.Set-Cookie;

    /* Si es una imagen se desactiva compresion (Solo precaucion) */
    if (req.url ~ "\.(jpg|jpeg|png|ico|svg|gif)$") {
        set beresp.do_gzip = false;
    }

    /* Establecer fecha de caducidad para los datos en cache */
    set beresp.ttl = 1w; /* Una semana */
    set beresp.grace = 120s;

    /* Forzar cacheado por el cliente */
    set beresp.http.Expires = beresp.ttl;
    set beresp.http.Cache-Control = "max-age=604800"; /* 7 dias */

    /* Ocultar información añadida por amazon antes de almacenar */
    unset beresp.http.Server;
    unset beresp.http.x-amz-id-2;
    unset beresp.http.x-amz-request-id;
    unset beresp.http.x-amz-version-id;
    unset beresp.http.ETag;

    return (deliver);
}


sub vcl_deliver {

    /* Eliminar informacion superflua antes de enviar respuesta */
    unset resp.http.Via;
    unset resp.http.X-Varnish;

    /* DEBUG: Util para comprobar el correcto funcionamiento de la cache */
    /* unset resp.http.Age; */

    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
    } else {
        set resp.http.X-Cache = "MISS";
    }

    return (deliver);
}

Descargar

El archivo de configuración del demonio es /etc/default/varnish, y en el hay que modificar el puerto de escucha y especificar como queremos que Varnish almacene la cache, esto dependerá de la cantidad de memoria disponible, el tamaño del bucket, y la latencia del disco duro. En mi caso el servidor es un VPS con 512MB de RAM y una SSD de 20GB alojado en Digital Ocean, y como no tiene suficiente RAM, uso un archivo para la cache:

DAEMON_OPTS="-a :80 \
    -T localhost:6082 \
    -f /etc/varnish/default.vcl \
    -S /etc/varnish/secret \
    -s file,/var/lib/varnish/$INSTANCE/varnish_storage.bin,4G"

Si tienes disponible suficiente memoria para almacenar la cache en RAM, podrías usar la configuración:

DAEMON_OPTS="-a :80 \
    -T localhost:6082 \
    -f /etc/varnish/default.vcl \
    -S /etc/varnish/secret \
    -s malloc,4G"

Una vez configurado y reiniciado, puedes comprobar que esta funcionando correctamente con curl. Al descargar cualquier documento por primera vez debería contener X-Cache: MISS, sucesivas peticiones del mismo documento contendrán X-Cache: HIT si la cache esta funcionando.

$ curl -I static.tudominio.com/imagen.jpg
Last-Modified: Mon, 17 Mar 2014 17:51:08 GMT
Content-Type: image/jpeg
Expires: 604800.000
Cache-Control: max-age=604800
Content-Length: 9920
Accept-Ranges: bytes
Date: Sat, 22 Mar 2014 02:55:40 GMT
Age: 0
Connection: keep-alive
X-Cache: MISS

$ curl -I static.tudominio.com/imagen.jpg
Last-Modified: Mon, 17 Mar 2014 17:51:08 GMT
Content-Type: image/jpeg
Expires: 604800.000
Cache-Control: max-age=604800
Content-Length: 9920
Accept-Ranges: bytes
Date: Sat, 22 Mar 2014 02:55:40 GMT
Age: 8
Connection: keep-alive
X-Cache: HIT

Cuando compruebes que funciona ya puedes comentar la sección que añade el campo X-Cache.

Configurar Django

Si usas Django, una vez Varnish este funcionando hay que indicar a Storage, como generar la dirección para los archivos que antes se accedían a traves de s3.amazoaws.com. El parámetro AWS_S3_CUSTOM_DOMAIN permite apuntar al dominio que estés usando para tu servidor Varnish.

AWS_S3_CUSTOM_DOMAIN = 'servidorvarnish.tudominio.com'

Si no estás usando el puerto 80 puedes espeficicarlo con:

AWS_S3_CUSTOM_DOMAIN = 'www.tudominio.com:8080'

Ver comentarios


Los programas que recopilan información de una página web en busca de información se llaman crawler, spider, o arañas en castellano. Es una tarea tan común, que existen multitud de bibliotecas y frameworks con este propósito, en algunos casos simplificando la programación hasta el punto en el que se puede programar un crawler sencillo con menos de 100 lineas de código. Y eso es justamente lo que voy a hacer en este post.

Imaginemos que quiero crear una pequeña base de datos para organizar que libros he leído, y cuales me faltan por leer, pero no quiero tener que introducir manualmente la información de cada libro, así que voy a obtener toda la informacón de una página web, por ejemplo epublibre.

Para programar la araña me he decantado por Python, y un framework llamado Scrapy del que he leido mucho últimamente.

Instalación

La instalación de un paquete en python suele ser muy sencilla, pero en este caso he tenido algún problema con las dependencias, y he necesitado instalar algunos paquetes en el sistema:

$ apt-get install libxml2-dev libxslt-dev libffi-dev

Tras esto se instala scrapy con pip:

$ pip install scrapy

Uso

El primer paso es iniciar un nuevo proyecto:

$ scrapy startproject epublibre_crawler

Esto crea un directorio de proyecto con la siguiente estructura:

epublibre_crawler/
├── epublibre_crawler
│   ├── __init__.py
│   ├── items.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       └── __init__.py
└── scrapy.cfg

Una vez creado el proyecto, tenemos que definir los items que queremos extraer, o mejor dicho la clase donde se almacenaran los datos extraídos por scrapy. En mi caso el titulo del libro, y su autor es suficiente, así que añadimos al archivo items.py la subclase de Item:

from scrapy.item import Item, Field

class BookItem(Item):
    title = Field()
    author  = Field()

El siguiente paso es describir usando expresiones XPath, como se puede extraer la información del titulo y autor, de manera que Scrapy pueda diferenciarla del resto de código html de la página de cada libro.

Si miramos el código html de la página de un libro, el título está contenido en un div de la forma:

<div class="det_titulo" id="titulo_libro" style="display:inline-block;">
        Omega
</div>

Dado que el titulo tiene un identificador único "titulo_libro", es fácil de extraer:

xpath("//div[@id='titulo_libro']/text()[normalize-space()]")

para el nombre del autor, tenemos:

<div class="negrita aut_sec" style="display:inline-block;">
    <a href="http://www.epublibre.org/autor/index/425">Jack McDevitt</a>
</div>

este es el único div de la página que usa la clase aut_sec, así que podemos extraer el nombre con:

xpath("//div[@class='negrita aut_sec']/a/text()")

Una vez tenemos las reglas de extracción, hay que describir como son las direcciones de las páginas de libro a las que queremos aplicarlas. El formato es http://www.epublibre.org/libro/detalle/6467, con el numero final variando para cada libro:

'libro/detalle/\d+'

Por último hay que crear la araña que usara scrapy en el directorio spiders:

#epublibre_spider.py
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
from scrapy.selector import Selector

# Importar la clase donde almacenar los resultados
from epublibre_crawler.items import BookItem

class BookSpider(CrawlSpider):

    # Nombre de la araña.
    name = 'epublibre'

    # Dominios en los que el crawler tiene permiso a acceder
    allowed_domains = ['epublibre.org']

    # La direccion de inicio para el crawler
    start_urls = ['http://www.epublibre.org']

    # Regla para diferenciar los enlaces de libros y función que se les aplica
    rules = [Rule(SgmlLinkExtractor(allow=['/libro/detalle/\d+']), 'parse_book')]

    def parse_book(self, response):
        """ Parser para las pagina de detalle de los libros"""
        sel = Selector(response)

        # Creamos un nuevo libro y asignamos los valores extraidos a
        # los campos correspondientes.
        book = BookItem()
        author = sel.xpath("//div[@class='negrita aut_sec']/a/text()").extract()
        title = sel.xpath("//div[@id='titulo_libro']/text()[normalize-space()]").extract()

        # Con Strip eliminamos tabulaciones y linea nueva.
        book['title']  = title[0].strip("\t\n\r")
        book['author'] = author[0].strip("\t\n\r")

        return book

Antes de iniciar scrapy hay que modificar las configuración y limitar la velocidad a la que los datos son accedidos, para no crear un ataque DOS:

# Scrapy settings for epublibre_scrapy project
#
# For simplicity, this file contains only the most important settings by
# default. All the other settings are documented here:
#
#     http://doc.scrapy.org/en/latest/topics/settings.html
#

BOT_NAME = 'epublibre_crawler'

SPIDER_MODULES = ['epublibre_scrapy.spiders']
NEWSPIDER_MODULE = 'epublibre_scrapy.spiders'

# Maximo una pagina cada minuto
DOWNLOAD_DELAY = 60

# Identificate
# USER_AGENT = 'epublibre_crawler (+http://www.tudominio.com)'

Finalmente solo queda ejecutar scrapy para que recolecte la información y la guarde en un archivo en alguno de los formatos soportados (XML, JSON, CSV), o hasta directamente en una base de datos usando una pipeline. En mi caso JSON es el formato elegido:

$ scrapy crawl epublibre -o libros.json -t json

el nombre epublibre es el que asigné a la araña BookSpider en la variable name.

El formato del archivo de salida es el siguiente:

[{"author": ["Ray Bradbury"], "title": ["Fahrenheit 451"]},
 {"author": ["John Scalzi"], "title": ["Redshirts"]}
]

Nota: Este programa es sólo un ejemplo para explicar el funcionamiento de scrapy, si realmente deseas extraer la información de epublibre.org, la propia página distribuye un archivo CSV con todos los datos de los libros disponibles.

Ver comentarios


Una situación muy común en cualquier aplicación web es la necesidad de mostrar miniaturas de las imágenes subidas por los usuarios. Esto se puede conseguir usando CSS para escalar la imagen, cortarla, y hasta convertirla a blanco y negro, pero es un desperdicio de ancho de banda.

En Django existen varias apps para simplificar la tarea, pero mi preferida por su simplicidad y facilidad de integración es easy-thumbnails

Instalación

Usa pip para instalar en paquete:

$ pip install easy_thumbnails

Añade easy_thumbnails a la lista de aplicaciones instaladas en el archivo settings.py:

INSTALLED_APPS = (
    ...
    'easy_thumbnails',
)

Uso

Easy-thumbnails funciona generando dinámicamente miniaturas desde las imágenes originales. Cuando llega una petición y la miniatura no existe, o ha sido modificada, una nueva miniatura es generada y salvada.

Para mostrar las miniaturas en los templates, es necesario usar los tags proporcionados por easy_thumbnails, thumbnail y thumbnail-url:

{# Cargamos los tags de easy_thumbnails #}
{% load thumbnail %}
....
<div class="producto">
    <img src="{% thumbnail producto.foto 90x90 crop %}"/>
    <h3>{{ producto.nombre }}</h3>
</div>

En el tag indicamos la imagen fuente, el tamaño al que deseamos convertir la imagen, seguido de cualquier opción adicional, en este caso indicamos que corte la imagen si es necesario para llegar al tamaño seleccionado.

También es posible indicar solo una de las dimensiones de la imagen, de manera que la otra se escale para mantener las proporciones, sin deformar la imagen ni cortar los bordes.

{% load thumbnail %}
....
<div class="producto">
    <img src="{% thumbnail producto.foto 90x0 %}"/>
    <h3>{{ producto.nombre }}</h3>
</div>

Para simplificar la tarea y evitar errores, podemos crear un alias para las distintas configuraciones de las miniaturas, para ello hay que crear un diccionario llamado THUMBNAIL_ALIASES en settings.py, que contiene un las opciones de cada alias:

THUMBNAIL_ALIASES = {
    '': {
        'producto': {'size': (90, 90), 'crop': True},
        'cartel': {'size': (90, 0),},
    },
}

Luego podemos usar estos alias con:

{# Cargamos los tags de easy_thumbnails #}
{% load thumbnail %}
....
<div>
    <img src="{% producto.foto|thumbnail_url:'cartel' %}"/>
    <h3>{{ producto.nombre }}</h3>
</div>

ThumbnailerImageField

Este campo nos hace aún más fácil manejar las direcciones de las miniaturas en en template, y permite gestionar cuando son generadas. Para usarlo solo es necesario substituir en los modelos ImageField por ThumbnailerImageField:

from django.db import models
from easy_thumbnails.fields import ThumbnailerImageField

class Producto(models.Model):
    """Mi modelo de un producto"""
    foto = ThumbnailerImageField()
    text = models.TextField(text)

En los templates puedes obtener la dirección de la miniatura fácilmente usado su alias:

{% load thumbnail %}
....
<div>
    <img src="{{ producto.foto.cartel.url }}"/>
    <h3>{{ producto.nombre }}</h3>
</div>

Si la aplicación requiere que las miniaturas sean generadas en el momento que las imágenes son subidas, se pueden usar los manejadores de señales en models.py:

from easy_thumbnails.signals import saved_file
from easy_thumbnails.signal_handlers import generate_aliases_global

saved_file.connect(generate_aliases_global)

Integración con Amazon S3

En caso de que necesites usar Amazon S3 para almacenar imágenes, ya sea para servirlas directamente, o como backup, configurar easy_thumbnails es muy sencillo.

Primero necesitamos instalar django-storage y la biblioteca boto de python para S3.

$ pip install django-storage
$ pip install boto

Una vez instalados configuramos easy_thumbnails para indicarle que tiene que usar S3 como almacenamiento, en settings.py hay que añadir:

THUMBNAIL_DEFAULT_STORAGE ='storages.backends.s3boto.S3BotoStorage'
THUMBNAIL_BASEDIR = '_miniaturas/'

Ademas de indicar que use S3, lo he configurado para que guarde las miniaturas en un directorio aparte llamado '_miniaturas/', esto hace que sean más sencillas de gestionar.

Tras esto configuramos Django para que también guarde la imagen original en S3, añadimos las credenciales AWS, y el nombre del bucket a usar:

DEFAULT_FILE_STORAGE    = 'storages.backends.s3boto.S3BotoStorage'
AWS_S3_SECURE_URLS      = False     # Usar http en lugar de https
AWS_QUERYSTRING_AUTH    = False     # authentication sencilla
AWS_ACCESS_KEY_ID       = 'Tu_ACCESS_KEY_ID'
AWS_SECRET_ACCESS_KEY   = 'Tu_SECRET_ACCESS_KEY'
AWS_STORAGE_BUCKET_NAME = 'nombre_bucket'

Si usas S3 es recomendable usar señales para que easy_thumbnails calcule las miniaturas en el momento de creación de las imágenes.

Por último sincronizamos la base de datos

$ python manage.py syncdb

Ver comentarios


Esta semana he estado desarrollando una webapp django en un ordenador sin SSD, y el tiempo de ejecución era mucho mayor de lo normal, ya no recordaba lo lentos que son los discos duros especialmente su acceso aleatorio. Por supuesto me he puesto a investigar formas de acelerar el proceso.

Cuando estás desarrollando una aplicación django tienes tres opciones como base de datos MySQL, PostgreSQL, o SQLite. A la hora de hacer tests SQLite es indiscutiblemente la más rápida, ya que crea la base de datos en ram, pero no tiene soporte completo de ALTER TABLE, por lo que no es posible hacer migraciones. MySQL y PostgreSQL en cambio son lentas en la creación de bases de datos, pero soportan migraciones.

La solución perfecta es usar MySQL o PostgreSQL para el desarrollo, y usar SQLite en los tests. Esto lo podemos conseguir modificando el archivo de configuración cuando se van a ejecutar tests, para cambiar la base de datos a sqlite3.

Primero creamos un archivo de configuración alternativo test_settings.py, en el añadimos la configuración de la base de datos a usar en los tests:

# test_settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3', #
        'NAME': 'test_sqlitedb', # Ruta al archivo de la base de datos
    }
}

Tras esto solo queda modificar settings.py para que cargue el archivo de configuración cuando esté ejecutando tests, esto lo conseguimos comprobando si los argumentos de la linea de comandos contiene la palabra "test". Al final del archivo settings.py hay que añadir:

# ^^^^^^^^ Resto de configuracion settings.py ^^^^^^
# Cargar configuracion test_settings.py si se ejecuta manage.py test
import sys
if 'test' in sys.argv:
    try:
        from test_settings import *
    except ImportError:
        print "No se pudo encontrar el archivo test_settings.py"

El tiempo de ejecución de la aplicación usando PostgreSQL:

$ time python manage.py test nombre_app
Creating test database for alias 'default'...
...............
----------------------------------------------------------------------
Ran 15 tests in 1.047s

OK
Destroying test database for alias 'default'...

real    0m14.169s
user    0m1.204s
sys     0m0.136s

Y usando SQLite:

$ time python manage.py test nombre_app
Creating test database for alias 'default'...
...............
----------------------------------------------------------------------
Ran 15 tests in 0.299s

OK
Destroying test database for alias 'default'...

real    0m1.031s
user    0m0.872s
sys     0m0.100s

Una reducción del 90% (13 segundos), nada mal para una modificación tan sencilla.

Ver comentarios


ImageField proporciona una buena abstracción para la gestión imágenes, hace fácil la subida de los archivos a la pagina, y proporciona tags para mostrarlas en los template. Pero ciertos comportamientos por defecto del campo, hacen imposible usarlo sin modificaciones en prácticamente cualquier proyecto django.

Estas son algunas de las modificaciones que suelo hacer en casi cualquier modelo con un campo ImageField.

Cambiar el nombre del archivo antes de guardarlo

Cuando ImageField salva la imagen, lo hace en un archivo con el mismo nombre que el archivo original, esto es un problema puesto que si se salvan dos imágenes con el mismo nombre una sobreescribe a la otra.

Este comportamiento es admisible un blog, donde el autor puede controlar el nombre de cada imagen, en cambio en una página donde los usuarios puedan subir imágenes, no tardaría en ser un problema grave.

La solución es usar el parámetro de ImageField upload_to, para proporcionar un nombre de archivo aleatorio con el que salvar la imagen.

from django.db import models
from uuid import uuid4
from datetime import date
import os

class Post(models.Model):

    def _generar_ruta_imagen(instance, filename):
        # El primer paso es extraer la extension de la imagen del
        # archivo original
        extension = os.path.splitext(filename)[1][1:]

        # Generamos la ruta relativa a MEDIA_ROOT donde almacenar
        # el archivo, usando la fecha actual (año/mes)
        ruta = os.path.join('Imagenes', date.today().strftime("%Y/%m"))

        # Generamos el nombre del archivo con un identificador
        # aleatorio, y la extension del archivo original.
        nombre_archivo = '{}.{}'.format(uuid4().hex, extension)

        # Devolvermos la ruta completa
        return os.path.join(ruta, nombre_archivo)

    imagen = models.ImageField(upload_to=_generar_ruta_imagen)

    text = models.TextField(max_lenght=1000)

La función _generar_ruta_imagen es llamada la primera vez que se salva la imagen para generar la ruta. Si estudias su código, verás que ademas de generar el nombre del archivo, generamos la ruta donde es almacenado, esta ruta se genera con la fecha en el momento de salvado, de manera que cambia con el paso del tiempo. Con esto se evita que miles de imágenes se guarden en un solo directorio, haciéndolo difícil de listar y manipular.

Usar un Mixin para añadir imágenes a varios modelos

En el caso de que tengamos varios modelos que necesiten una imagen, podemos crear una clase abstracta que proporciona un campo imagen, que genere de forma correcta las rutas y nombres. Luego solo tenemos que heredar de ella en cada modelo que necesite esa funcionalidad.

from django.db import models
from uuid import uuid4
from datetime import date
import os

class ImagenMixin(models.Model):

    def _generar_ruta_imagen(instance, filename):
        # El primer paso es extraer la extension de la imagen del
        # archivo original
        extension = os.path.splitext(filename)[1][1:]

        # Generamos la ruta relativa a MEDIA_ROOT donde almacenar
        # el archivo, se usa el nombre de la clase y la fecha actual.
        directorio_clase = instance.__class__.__name__
        ruta = os.path.join('imagenes', directorio_clase,
            date.today().strftime("%Y/%m"))

        # Generamos el nombre del archivo con un identificador
        # aleatorio, y la extension del archivo original.
        nombre_archivo = '{}.{}'.format(uuid4().hex, extension)

        # Devolvermos la ruta completa
        return os.path.join(ruta, nombre_archivo)

    imagen = models.ImageField(upload_to=_generar_ruta_imagen)

    class Meta:
        abstract = True

class Post(ImagenMixin):
    text = models.TextField(max_lenght=1000)

class Libro(ImagenMixin):
    titulo = models.CharField(max_length=100)

El único cambio está en _generar_ruta_imagen de la clase abstracta, donde se añade a la ruta de salvado el nombre de la clase. De esta manera se separan las imágenes salvadas por cada clase en un directorio distinto. Por ejemplo los modelos Post y Libro guardarían sus imágenes en:

  • /MEDIA_ROOT/imagenes/Post/año/mes/
  • /MEDIA_ROOT/imagenes/Libro/año/mes/

Mostrar imágenes en el interfaz Admin

Si usas admin para añadir contenido a tu página web, es realmente incómodo no poder ver las imágenes directamente en su interfaz. Para solucionarlo creamos un método en el modelo, que devuelve un tag <img> apuntando a la dirección de la imagen:

from django.db import models
from django.utils.safestring import mark_safe


class Post(models.Model):

    imagen = models.ImageField()
    text = models.TextField(max_length=1000)

    def imagen_admin(self):
        if self.image:
            # Marcamos imagen como safe para evitar escape automatico
            return mark_safe(u'<img src="%s" />' % self.imagen.url))
        else:
            return '(Sin imagen)'

    # Para cambiar el nombre del campo en pantalla
    imagen_admin.short_description = 'Imagen'

    # En lugar de mark_safe podemos añadir:
    imagen_admin.allow_tags = True

Y modificamos el ModelAdmin del modelo para que muestre la imagen. Hay que asegurarse de añadir el nuevo método a readonly_fields:

from django.contrib import admin
from blog.models import Post

class PostAdmin(admin.ModelAdmin):
    # imagen_admin tiene que ser siempre readonly, si queremos modificar
    # la imagen hay que hacerlo a traves del campo imagen.
    readonly_fields = ('imagen_admin',)
    fields = ('imagen_admin', 'imagen', 'text',)

admin.site.register(Post, PostAdmin)

Si no quieres modificar tu modelo, puedes crear un método similar directamente en ModelAdmin:

from django.contrib import admin
from blog.models import Post

class PostAdmin(admin.ModelAdmin):

    def imagen_admin(self, obj):
        return u'<img src="%s" />' % obj.image.url

    imagen_admin.allow_tags = True

    readonly_fields = ('imagen_admin',)
    fields = ('imagen_admin', 'text')

admin.site.register(Post, PostAdmin)

Por último si estás usando el mixin de la sección anterior, ese es el mejor lugar donde añadir el método.

Eliminar el archivo de la imagen al borrar el modelo

El comportamiento por defecto de ImageField, es no borrar el archivo cuando el modelo es eliminado, para borrarlo es necesario conectarse a la señal pre_delete que se envía antes de la eliminación de cualquier modelo, y hacerlo manualmente.

from django.db import models
from django.db.models.signals import pre_delete, pre_save
from django.dispatch import receiver


class Post(models.Model):
    imagen = models.ImageField()
    text = models.TextField(max_length=1000)

# Usamos el Decorador receiver para ejecutar nuestra función
# cuando el Post el borrado.
@receiver(pre_delete, sender=Post)
def post_pre_delete_handler(sender, instance, **kwargs):
    instance.imagen.delete(False)

Ten en cuenta que si borras el archivo con el modelo, puedes perder datos si hay algún accidente, si el numero de imágenes almacenado no es grande es mejor no borrarlas. Si el número es grande, puedes usar una tarea asíncrona que borre los archivos huérfanos tras borrar los modelos, esto lo puedes conseguir con celery, cron, o similar.

Ver comentarios


Si estás desarrollando una aplicación web, algún día tendrás que dar el paso y hacerla pública. Inicialmente puedes usar un servicio como Heroku pero si tienes éxito, no te quedará mas remedio que gestionar tus propios servidores o VPS.

Por fortuna desplegar una aplicación django usando nginx y gunicorn, es más sencillo de lo que podría parecer. En esta pequeña guia trato de describir el proceso paso a paso:

1 - Crear un usuario para la aplicación django

El primer paso es crear un usuario con el que ejecutar la aplicación Django, esto nos permite organizar con facilidad un servidor donde estemos ejecutando varias aplicaciones, proporciona separación de privilegios, y limita el posible daño que pueda hacerse al sistema si la aplicación es comprometida.

Creamos un grupo al que pertenecerán todos los usuarios de las aplicaciones django, y un usuario al que le asignamos el nombre de la aplicación.

$ sudo addgroup --system webapps
$ sudo adduser  --system --ingroup webapps --home /webapps/appname appname

Al usar en modificador --system, al usuario se le asigna /bin/false como shell, y no tiene clave, por lo que no puede hacer login. Aunque esto es una buena medida de seguridad, es muy incomodo mientras se está configurando el sistema, así que asignamos un shell temporalmente:

$ sudo chsh -s /bin/bash appname

Esto nos permite usar sudo su appname para seguir con la instalación como el nuevo usuario.

2 - Instalar y configurar virtualenv

Virtualenv es la herramienta que nos permite aislar los paquetes requeridos por las aplicaciones, de manera que si dos aplicaciones necesitan paquetes que están en conflicto, no interfieran la una con la otra como ocurriría si instalásemos todos los paquetes directamente en el sistema. Instalamos virtualenv con:

$ sudo apt-get install python-virtualenv python-pip

Una vez instalado, hacemos login con el usuario que hemos creado, y dentro de su directorio usamos virtualenv para generar un nuevo entorno virtual:

$ sudo su appname
$ cd
$ mkdir virtualenvs
$ virtualenv --no-site-packages virtualenvs/app_env
$ source virtualenvs/app_env/bin/activate

Dentro del entorno, hay que instalar todos los paquetes que sean necesarios para la aplicación, puedes hacerlo uno a uno, o en bloque si tienes el archivo requirements.txt:

(app_env)$ pip install django
(app_env)$ pip install django-countries
(app_env)$ pip install django-mptt
(app_env)$ ....
(app_env)$ pip install -r requirements.txt

Si quieres profundizar en el funcionamiento de virtualenv sigue este tutorial

3 - Gunicorn + Supervisor

Gunicorn es el servidor WSGI que se encarga de servir la aplicación, pero necesita un programa que lo inicie al arranque, y lo monitorice para reiniciarlo si hay algún problema. La mejor solución es usar el gestor de procesos como supervisor. La instalación es sencilla:

$ sudo apt-get install supervisor

Para instalar gunicorn el método más sencillo es hacerlo dentro del entorno virtual de tu aplicación django.

$ source virtualenvs/app_env/bin/activate
(app_env)$ pip install gunicorn

Una vez todo está instalado, creamos un archivo de configuración para gunicorn gunicorn_conf.py en el directorio HOME del usuario, en el que indicamos la dirección y puerto en los que estará escuchando gunicorn:

# gunicorn_conf.py
workers = 3
bind = '127.0.0.1:9000'

A partir de aquí ya podemos salir del entorno virtual de la aplicación:

(app_env)$ deactivate

El siguiente paso es crear el archivo de configuración de supervisor en /etc/supervisor/conf.d/appname.conf:

[program:appname]
command=/webapps/appname/django_app/run_gunicorn.sh
directory = /webapps/appname/django_app/
user=appname
autostart=true
autorestart=true
priority=991
stopsignal=KILL

Tras esto creamos en el directorio de la aplicación el script run_gunicorn.sh que etablece el entorno virtualenv para la aplicación y despuer ejecuta gunicorn:

#!/bin/bash
source /webapp/appname/virtualenvs/app_env/bin/activate
exec /webapp/appname/virtualenvs/app_env/bin/gunicorn -c /webapps/appname/gunicorn_conf.py django_app.wsgi:application

Una vez configurado indicamos a supervisor que debe iniciar el nuevo servicio:

$ sudo supervisorctl start reread
appname: available
$ sudo supervisorctl update
appname: added process group
$ sudo supervisorctl status appname
appname                          RUNNING    pid 21710, uptime 0:00:07

Para detener e iniciar la aplicación podemos usar:

$ sudo supervisorctl stop appname
appname: stopped
$ sudo supervisorctl start appname
appname: started

Llegado a este punto si todo funciona correctamente, ya podemos desactivar el shell del usuario creado para la aplicación.

$ sudo -s /bin/false appname

Si prefieres que sea posible hacer login con el usuario, es recomendable que le asignes una clave.

4 - Nginx

Usamos Nginx para hacer de pasarela entre los clientes y gunicorn, y para servir los archivos estáticos de la aplicación. Para instalarlo:

$ sudo apt-get install nginx

Para configurar nginx, hay que crear un archivo /etc/ngix/sites-available/, en el especificaremos donde se debe conectar a gunicorn en la sección upstream, y la ruta a los archivos estáticos de tu aplicación en la sección server:

upstream django_app_server {
    # Dirección en la que está escuchando gunicorn
    server 127.0.0.1:9000 fail_timeout=0;
}

server {
    # listen 80 default deferred; # for Linux
    # listen 80 default accept_filter=httpready; # for FreeBSD
    listen 80 default;

    client_max_body_size 4G;
    server_name www.dominio.com;

    # ~2 seconds is often enough for most folks to parse HTML/CSS and
    # retrieve needed images/icons/frames, connections are cheap in
    # nginx so increasing this is generally safe...
    keepalive_timeout 5;

    # Ruta a tus archivos estaticos.
    location /static/ {
      alias /webapps/appname/django_app/static/;
      autoindex on;
    }


    location / {
      # an HTTP header important enough to have its own Wikipedia entry:
      #   http://en.wikipedia.org/wiki/X-Forwarded-For
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

      # enable this if and only if you use HTTPS, this helps Rack
      # set the proper protocol for doing redirects:
      # proxy_set_header X-Forwarded-Proto https;

      # pass the Host: header from the client right along so redirects
      # can be set properly within the Rack application
      proxy_set_header Host $http_host;

      # we don't want nginx trying to do something clever with
      # redirects, we set the Host: header above already.
      proxy_redirect off;

      # set "proxy_buffering off" *only* for Rainbows! when doing
      # Comet/long-poll stuff.  It's also safe to set if you're
      # using only serving fast clients with Unicorn + nginx.
      # Otherwise you _want_ nginx to buffer responses to slow
      # clients, really.
      # proxy_buffering off;

      # Try to serve static files from nginx, no point in making an
      # *application* server like Unicorn/Rainbows! serve static files.
      proxy_pass http://django_app_server;

    }
}

Descargar

Una vez configurado, hay que enlazar el archivo que hemos creado en /etc/nginx/sites-available/ a /etc/nginx/sites-enabled/, de esta manera nginx empezará a usar la configuración:

$ sudo ln -s /etc/nginx/sites-available/midominio /etc/nginx/sites-enabled/midominio

Por último reiniciamos nginx:

$ sudo service nginx restart

Nota: Los autores de gunicorn tiene un ejemplo más completo de un archivo de configuración nginx.conf

Alternativas

Por supuesto esta no es la única opción para desplegar django, ni es la mejor para todas las situaciones. Si estas administrando múltiples aplicaciones, y/o múltiples servidores quizás Docker o Dokku se ajuste mejor a tus necesidades. Y como comenté al inicio Heroku es una buena alternativa para páginas con poco tráfico.

Ver comentarios


Virtualenv es una herramienta para crear entornos virtuales para python.

Cuando mas de una aplicación python esta instalada en un mismo sistema, se puede dar el caso de que requieran versiones incompatibles de una biblioteca, que no sea posible instalarla en el directorio site-packages, o que no quieras actualizar las bibliotecas de la aplicación cuando se actualizen las del sistema.

Virtualenv soluciona todos estos problemas, permitiendo crear entornos virtuales en los que se pueden instalar bibliotecas y programas, de forma independiente del sistema, y otros entornos. Esto es especialmente útil durante el desarrollo y despliegue de aplicaciones.

Virtualenv

Instalación

Para instalar virtualenv, lo mejor es usar el gestor de paquetes de tu distribución, para hacer la herramienta disponible a todos los usuarios:

secnot@secnot:~$ sudo apt-get install python-virtualenv

Crear un entorno

Una vez instalado, vamos a crear un directorio donde almacenar todos nuetros entornos, por ejemplo:

secnot@secnot:~$ mkdir virtualenvs

Ahora creamos un nuevo entorno dentro del directorio, como no queremos que use ninguna libreria del sistema añadimos el parametro --no-site-packages

secnot@secnot:~$ virtualenv --no-site-packages virtualenvs/proyecto1_env

Instalar django en el entorno

Antes de instalar un nuevo paquete, tenemos que activar el entorno, para que cambie los directorios de busquesda a los del entorno:

secnot@secnot:~$ source virtualenvs/proyecto1_env/bin/activate

Ahora ya podemos instalar django y los paquetes que necesitemos para nuestra aplicación:

(proyecto1_env)secnot@secnot:~$ pip install django
(proyecto1_env)secnot@secnot:~$ pip install django-countries
(proyecto1_env)secnot@secnot:~$ django-admin.py startproject proyecto1

Para instalar un paquete no es necesario ser root, todos los paquetes se almacenan en los directorios creados dentro de nuestro directorio. Si te fijas verás que el prompt ha cambiado, indicando el entorno que está activo.

Una vez no necesitemos el entorno podemos salir ejecutando:

(proyecto1_env)secnot@secnot:~$ deactivate

Requirements

Una funcionalidad muy útil es la generación de una lista con todos los paquetes instalados en un entorno, esta normalmente se almacenan en un archivo llamado requirements.txt. Este archivo puede usarse después para duplicar los paquetes instalados al crear un nuevo entorno. Para generarla:

secnot@secnot:~$  source environments/env1/bin/activate
(env1)secnot@secnot:~$ pip freeze
Django==1.6
gunicorn==18.0
wsgiref==0.1.2
(env1)secnot@secnot:~$ pip freeze > requirements.txt

Si queremos instalar todos los paquetes de la lista en un entorno:

(env3)secnot@secnot:~$ pip install -r requirements.txt

Virtualenvwrapper

Virtualenvwrapper es un conjunto de scripts que automatizan la creación, borrado, y gestión de entornos. Se puede trabajar directamente con virtualenv sin ningún problema, pero virtualenvwrapper hace que todo el proceso sea un poco más sencillo y cómodo.

Instalación y configuración

Instalamos virtualenvwrapper con el gestor de paquetes:

secnot@secnot:~$ sudo apt-get install virtualenvwrapper

Por defecto los entornos se almacenan en el directorio .virtualenvs y no necesita mas configuración simplemente sal y vuelve ha hacer login en la cuenta para que se configuren las variables de entorno.

Si por qualquier razón en tu sistema no ha funcionado, puedes configurarlo manualmente y añadirlo en el script de inicio .bashrc:

secnot@secnot:~$ mkdir .virtualenvs
# /home/secnot/.bashrc
# Configuración de VIRTUALENVWRAPPER
export WORKON_HOME=$HOME/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh
# Ubuntu 14.04: source /etc/bash_completion.d/virtualenvwrapper

Uso

Estos son los comandos de virtualenvwrapper más usados, para mas detalles es recomendable leer la documentación:

  • mkvirtualenv: Crea un nuevo entorno, con la particularidad de que se convierte en el entorno activo.
  • cpvirtualenv: Duplica un entorno.
  • rmvirtualenv: Elimina un entorno, el entorno a elminar debes estar inactivo antes de eliminarlo.
  • allvirtualenv: Ejecuta un comando en todos los entornos.
  • workon: Selecciona el entorno activo, si no hay argumentos lista los entornos disponibles.
  • deactivate: Desactiva el entorno indicado.

El mejor método para entender como funciona es un ejemplo, así el vamos a crear dos entornos, despues instalar django en el primero de ellos, y por último eliminar el segundo entorno:

secnot@secnot:~$ mkvirtualenv --no-site-packages env1
New python executable in env1/bin/python
Installing setuotools, pip...done.
(env1)secnot@secnot:~$ mkvirtualenv --no-site-packages env2
New python executable in env2/bin/python
Installing setuotools, pip...done.
(env2)secnot@secnot:~$ workon env1
(env1)secnot@secnot:~$ pip install django
(env1)secnot@secnot:~$ workon
env1
env2
(env1)secnot@secnot:~$ deactivate
secnot@secnot:~$ rmvirtualenv env2
Removing env2...
secnot@secnot:~$ ...

Si quieres usar una versión de Python distinta a la por defecto del sistema, puedes especificarla en el momento de creación del entorno.

secnot@secnot:~$ mkvirtualenv -p /usr/bin/python3 --no-site-packages env1

Ver comentarios


En el artículo Ataquest TCP Comunes expliqué algunos de los ataques más comunes, en este post explico como configurar linux para protegerse de esos ataques, y alguno más.

Ataque SYN Flood

SYN Cookies

Linux implementa un metodo llamado SYN Cookies para la protección contra este ataque, la técnica se consiste en elegir el numero de secuencia del paquete SYN+ACK de manera que el servidor no necesite guardar el estado de la conexión, y pueda usas el numero de secuencia de la respuesta ACK del cliente, para reconstruir la entrada en la tabla de conexiones.

Desde la versión del kernel 2.6.33, donde el sistema se modificó para soportar window scaling, esta opción está activada por defecto. Puedes comprobarlo usando:

cat /proc/sys/net/ipv4/tcp_syncookies
sysctl -n net.ipv4.tcp_syncookies

Si no está activado, puedes editar el archivo de configuración /etc/sysctl.conf y añadir:

net.ipv4.tcp_syncookies = 1

Despues de salvar el archivo, puedes activarlo usando:

sudo sysctl -p

Un servidor con tcp_syncookies activado se comporta de forma normal, hasta que la cola de conexiones pendientes está llena, a partir de ese momento comienza a enviar SYN cookies hasta que la congestión se alivie.

Si el servidor esta soportando mucha carga, puede ocurrir que tcp_syncookies se active y cierre conexiones legítimas. En esos casos es importante tener en cuenta que aunque puede ser un alivio temporal, SYN Cookies está diseñado para proteger el sistema de ataques SYN Flood, y que usarlo para solucionar problemas de congestión reiterados es contraproducente.

Iptables

La segunda opción es usar iptables para limitar el número de paquetes SYN, que son aceptados para cada dirección IP en un intervalo de tiempo. Para ello usamos el patch recent de iptables.

iptables -A INPUT -p tcp -m state --state NEW -m recent --set --name sattack
iptables -A INPUT -p tcp -m state --state NEW -m recent --rcheck --name sattack --seconds 60 --hitcount 20 -j DROP

La primera regla añade la dirección de origen de los paquetes SYN a una tabla llamada sattack, la segunda regla comprueba si en los últimos 60 segundos ha habido más de 20 paquetes SYN, desde la dirección de origen del paquete, de ser así el paquete es descartado.

En caso de que se esté usando IP Spoofing junto a SYN Flood, la única opción restante es intenta detectar alguna particularidad de la cabecera, que permita diferenciar los paquetes del atacante del resto, por ejemplo si MSS no tiene un valor correcto:

iptables -t mangle -I PREROUTING -p tcp -m tcp --dport 80 -m state --state NEW -m tcpmss ! --mss 536:65535 -j DROP

Estas reglas pueden interferir con proxies, y con clientes detrás de un NAT, así que no es recomendable usarlas en un sistema que no esté bajo ataque.

TCP Scans

El sistema más sencillo y general para detectar/detener un scan, es usar puertos cerrados como centinelas, de manera que si detectamos un paquete que llega a alguno de esos puertos, podamos asumir que el paquete es parte de un scan, y bloquear el acceso de la ip de origen temporalmente.

Es probable que el scan se centre únicamente en los puertos de servicios más comunes, por lo que es recomendable usar como centinelas puertos reservados por algún servicio, pero que no estén abiertos en ese servidor, por ejemplo SMTP(25), DNS(53), NETBIOS(139). Ademas de esos puertos deberemos añadir cuantos sean necesarios, para intentar detectar un scan, antes de que el atacante consiga escanear un puerto abierto.

Por ejemplo si se ha cambiado el puerto por defecto de ssh es muy recomendable añadir el puerto 22. Ten cuidado de NO usar como centinela un puerto abierto.

# Añadimos los puertos ftp, telnet, smtp, netbios-ssn
iptables -A INPUT -p tcp --dport 21   -m recent --name portscan --set
iptables -A INPUT -p tcp --dport 23   -m recent --name portscan --set
iptables -A INPUT -p tcp --dport 25   -m recent --name portscan --set
iptables -A INPUT -p tcp --dport 139  -m recent --name portscan --set

# Usar como centinelas el puerto ssh y su substituto más común
iptables -A INPUT -p tcp --dport 22   -m recent --name portscan --set
iptables -A INPUT -p tcp --dport 2222 -m recent --name portscan --set

# Bloquear IP por 12 horas si el paquete esta en la tabla portscan
iptables -A INPUT -m recent --name portscan --rchek --seconds 43200 -j DROP

Algunos tipos de scan, usan paquetes construidos de manera que son fáciles de diferenciar del tráfico legítimo, en ese caso es posible descartarlos directamente:

# XMAS Scan
iptables -A INPUT -p tcp --tcp-flags ALL FIN,URG,PSH -j DROP

# NULL Scan
iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP

# SYN/RST Scan
iptables -A INPUT -p tcp --tcp-flags SYN,RST SYN,RST -j DROP

# SYN/FIN Scan
iptables -A INPUT -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP

IP Spoofing

Ip spoofing consiste en falsificar la dirección de origen de los paquetes, en conjunción con algún ataque (pej SYN Flood), hace más dificil detectar su origen, complica el filtrado de los paquetes, y según la configuración pueden permitir saltarse algún firewall.

La única posibilidad es eliminar los paquetes que lleguen a un interface de red, con una dirección de origen que no esten en el rango de direcciones validas para ese interface:

iptables -A INPUT -i eth0 -s 0.0.0.0/8 -j DROP
iptables -A INPUT -i eth0 -s 127.0.0.0/8 -j DROP
iptables -A INPUT -i eth0 -s 10.0.0.0/8 -j DROP
iptables -A INPUT -i eth0 -s 172.16.0.0/12 -j DROP
iptables -A INPUT -i eth0 -s 192.168.0.0/16 -j DROP
iptables -A INPUT -i eth0 -s 224.0.0.0/3 -j DROP

Tambien se puede activar la verificación de direcciones de origen del kernel, que proporciona protección anti spoofing. Para comprobar si está activo:

sysctl -n net.ipv4.conf.all.rp_filter

Si no lo esta edita /etc/sysctl.conf y añade:

# Proteccion contra IP Spoofing
net.ipv4.conf.all.rp_filter = 1

Ataque Smurf

Consiste en enviar paquetes de broadcast ICMP con la dirección de origen falseada, de manera que la respuesta al paquete por parte de todos los equipos en la red, genere una denegación de servicio en el equipo cuya direccion fue suplantada en los paquetes.

La solución es no responde a los paquetes ICMP de broadcast. El kernel se puede configurar para que ignore estos paquetes, así que iptables no son necesarias. Si por defecto no esta activado, añade a /etc/sysctl.cof:

# Ignorar peticiones echo broadcast para evitar ataque smurf
net.ipv4.icmp_echo_ignore_broadcasts = 1

Hoy en día es un ataque poco usado.

Ver comentarios