SecNot

jun 22, 2015

Inicializar la base de datos en Django

Algunas veces es necesario inicializar la base de datos antes de poder usar una aplicación, si esta aplicación no va a reutilizarse no es ningún problema, en cambio si es algo que planeas usar en varios proyectos, es mucho mas práctico añadir los datos de inicialización en la misma aplicación y cargarlos usando loaddata. Imaginemos una aplicación para la gestión de los productos de una tienda, en la que tenemos los siguientes modelos:

#tienda/models.py
from django.db import models

class Categoria(models.Model):

    nombre = models.CharField(max_length=80)
    slug = models.SlugField(unique=True, db_index=True)
    descripcion = models.TextField(max_length=2000)


class Producto(models.Model):

    nombre = models.Charfield(max_length=200)
    descripcion models.TextField(max_length=2000)
    precio = models.DecimalField()
    categoria = models.ForeignKey(Categoria)

Django permite crear un directorio llamado fixtures dentro de la aplicación donde almacenar archivos de datos que luego pueden ser volcados a la base de datos para inicializarla.

Estos archivos pueden crearse usado tres formatos JSON, YAML, XML, yo creo que los dos primeros son la mejor opción, más fáciles de leer y modificar manualmente. Veamos un ejemplo para JSON:

[
{
    "model": "tienda.categoria",
    "fields": {
        "nombre": "Ordenadores portatiles",
        "slug": "portatiles",
        "descripcion": "Ordenadores portatiles de 13 a 17 pulgadas",
    },
    "pk": 100000
},
{
    "model": "tienda.categoria",
    "fields": {
        "nombre": "Tablets",
        "slug": "tablets",
        "descripcion": "Tablets android",
    },
    "pk": 100001
},
{
    "model": "tienda.producto",
    "fields": {
        "nombre": "ASUS MeMO Pad 10 ME103K 16GB",
        "precio": "120.50",
        "descripcion": "La ASUS MeMO Pad 10 se creó pensando en...",
        "categoria": 100001,
    }
}
]

y los mismos datos en YAML:

- model: tienda.categoria
pk: 100000
fields:
    nombre: "Ordenadores portatiles"
    slug: "portatiles"
    descripcion: "Ordenadores portatiles de 13 a 17 pulgadas"

-model: tienda.categoria
pk: 100001
fields:
    nombre: "Tablets"
    slug: "tablets"
    descripcion: "Tablets Android"

- model: tienda.producto
pk: 100000
fields:
    nombre: "ASUS MeMO Pad 10 ME103K 16GB"
    precio: "120.50"
    descripcion: "La ASUS MeMO Pad 10 se creó pensando en..."
    categoria: 100001

Si tienes el archivo de inicialización en la ruta tienda/fixtures/categorias.json, se puede cargar los datos con:

$ python manage.py loaddata categorias

Por último aunque es posible crear y editar manualmente los archivos, Django proporciona una herramienta para volcar la base de datos a un archivo usando cualquiera de los formatos soportado, con lo que se puede usar el interfaz Admin para crear los datos y luego volcarlos con:

$ python manage.py dumpdata tienda --indent 4 --format json --output tienda/fixtures/categorias.json

Y eso es todo por hoy.

Click to read and post comments

may 24, 2015

Crear servidores KVM en Ubuntu 14.04

Algunas veces necesito una maquina virtual para probar aplicaciones antes de desplegarlas, o para trastear con alguna configuración, este tutorial describe el método para crearlas más rápido y sencillo que he encontrado.

Instalación

En el huésped en el que se van a alojar las maquinas, son necesarios los siguientes paquetes:

$ sudo apt-get install ubuntu-virt-server qemu-kvm libvirt-bin ubuntu-vm-builder bridge-utils

desde donde vayas gestionar las maquinas, es recomendable instalar:

$ sudo apt-get install virt-manager

Configuracion de red

El siguiente paso es modificar la configuración de red del anfitrión, yo prefiero usar bridging de manera que las maquinas virtuales tengan asignadas direcciones de red validas. El funcionamiento es ingenioso, se crea un switch virtual usando la tarjeta de red física, al que los huéspedes se conectan como si se tratara de una conexión real.

Solo es necesario modificar en archivo /etc/network/interfaces, por ejemplo si este es el archivo original:

#/etc/network/interfaces

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
    address 192.168.0.42
    network 192.168.0.0
    netmask 255.255.255.0
    broadcast 192.168.0.255
    gateway 192.168.0.1
    dns-nameserver 80.53.60.25

Debemos convertir el interfaz eth0 en br0 y añadir las opciones:

#/etc/network/interfaces

auto lo
iface lo inet loopback

auto br0
iface br0 inet static
    address 192.168.0.42
    network 192.168.0.0
    netmask 255.255.255.0
    broadcast 192.168.0.255
    gateway 192.168.0.1
    dns-nameserver 80.53.60.25
    bridge_ports eth0
    bridge_stp off
    bridge_fd 0
    bridge_maxwait 4

Tras esto reinicia el sistema o el servicio de red.

$ /etc/init.d/networking restart
$ reboot

Configuración del sistema

Añade un usuario al grupo de libvirtd y kvm para que pueda manipulas las maquinas, sin necesidad de ser root:

$ sudo adduser `id -un` libvirtd
$ sudo adduser `id -un` kvm

Crea un directorio donde almacenar las imágenes KVM, por ejemplo:

$ mkdir /home/kvm-images
$ chown libvirt-qemu:libvirtd /home/kvn-images

Adicionalmente puedes crea un par de claves ssh para el usuario, de forma que no tengas que introducir la clave cada vez que administras remotamente la máquina. En este post explico como se hace.

Crear la máquina virtual

Con todo configurado ya estamos listos para crear la máquina con el comando ubuntu-vm-builder:

$ sudo ubuntu-vm-builder kvm trusty \
    --domain servidor \
    --hostname kvm1 \
    --arch amd64 \
    --mem 2048 \
    --cpus 1 \
    --rootsize 20000 \
    --swapsize 2048 \
    --destdir /home/kvm-images/kvm2 \
    --user secnot \
    --pass secreto \
    --bridge br0 \
    --ip 192.168.0.201 \
    --mask 255.255.255.0 \
    --net 192.168.0.0 \
    --bcast 192.168.0.255 \
    --gw 192.168.0.1 \
    --dns 80.53.60.25 \
    --components main,universe \
    --addpkg acpid \
    --addpkg openssh-server \
    --addpkg linux-image-generic \
    --addpkg unattended-upgrades \
    --libvirt qemu:///system ;

Aunque todos esos parámetros parecen muy complicados, sólo la primera linea es necesaria, las demás permiten especificar la configuración para el huésped:

  • destdir - Directorio en el que quieres que se cree la imagen.
  • domain, hostname - Dominio y nombre de host del huésped.
  • arch - Arquitectura de la maquina a instalar.
  • mem, cpus - Cuanta memoria y CPUs son asignadas al huésped.
  • rootsize, swapsize - Tamaño del disco raíz en MB (por defecto 4000MB), y de la partición de intercambio.
  • user, pass - Nombre de usuario y clave para la primera cuenta de usuario en el huésped.
  • bridge - Indica a que bridge conectar el interfaz de red del huésped.
  • ip, mask, bcast, gw, dns - Configuración de red para el huésped.
  • mirror, components - Indica al huésped de que repositorio descargar los paquetes, útil para acelerar la instalacion si tienes copias locales.
  • addpkg - Paquetes a instalar en el huesped durante su creación. Algunos paquetes son imprescindibles, acpid para poder apagar la maquina, openssh-server para poder conectarse remotamente, y linux-image-generic para evitar el error "This kernel does not support a non-PAE CPU"
  • libvirt - Indica al instalador en que host debe instalar la maquina.
  • ssh-key - Añade la clave pública suministrada (ruta absoluta) a la lista de claves autorizadas del root.

Podemos comprobar que está funcionando correctamente con virsh:

$ virsh list --all
Id    Nombre                         Estado
----------------------------------------------------
1     kvm1                           ejecutando

Si no fue arrancado automáticamente, puedes hacerlo a mano con:

$ virsh start kvm1
Se ha iniciado el dominio kvm1

y detenerlo con:

$ virsh shutdown kvm1
El dominio kvm1 está siendo apagado

Gestión remota

Si además quieres gestionar de forma remota todos los huéspedes KVM, instala virt-manager, y añade la dirección ip del anfitrión con Archivo>Añadir conexión... usando como nombre de usuario la cuenta que añadiste a los grupos de kvm. El resultado debería ser similar a esto:

Ventana principal de virt-manager

Si haces doble click en cualquiera de las máquinas, puedes abrir una consola, o la página de detalles de configuración seleccionando vista en la barra de tareas:

Ventana detalles virt-manager
Click to read and post comments

abr 24, 2015

Scripts que se ejecutan sólo una vez

A veces es necesario un script shell que se ejecute una sola vez, por ejemplo cuando estas trabajando en una imagen de una maquina virtual o una distribución, y necesitas que se ejecute un script la primera vez que arranca el sistema. Estos scripts son faciles de escribir pero hay que tener cuidado con ciertos detalles que pueden dar problemas.

La primera aproximación es eliminar el script tras se ejecutado.

#!/bin/bash

# Tus comandos aqui

# Elimina el script antes de salir
rm $0

El problema de éste sistema es que $0 no devuelve la ruta del script sino que devuelve el nombre usado para su invocación, si se usó ruta absoluta no hay problema, pero si se usó relativa puede darse el caso que si se ha cambiado el directorio de trabajo durante su ejecución, no borrara el script o incluso puede borrar un archivo con el mismo nombre en otro directorio.

La solución es obtener la ruta absoluta del script antes al inicio, y usarla para borrar el archivo al finalizar.

#!/bin/bash
# Obtener ruta absoluta
RUTA_ABSOLUTA=$(cd `dirname "${BASH_SOURCE[0]}"` && pwd)/`basename "${BASH_SOURCE[0]}"`

# Tus comandos aqui

# Elimina el script
rm $RUTA_ABSOLUTA

Esto es suficiente en la mayoría de los casos, pero en algunas situaciones no es posible eliminar el script porque puede ser necesario posteriormente. En estos casos se usa un flag para determinar si el script ya se ha ejecutado.

#!/bin/bash

FLAG="/var/log/archivo-flag.log"
if [ ! -f $FLAG ]; then
    echo "Primera ejecucion del script"
    # Crear flag para recordar que se ejecuto
    touch $FLAG
else
    echo "El script solo se puede ejecutar una vez"
    exit 0
fi

# Tus comandos aqui

Aquí se ha usado un archivo como flag que es creado en la primera ejecución, cuando se intenta ejecutar el script posteriormente se detecta la existencia del archivo y no se continua con la ejecución. Si quisieras ejecutarlo una vez mas, no hay mas que eliminar el archivo flag.

Este sistema tampoco es perfecto, si por algún error se borra el archivo flag el script se ejecutaría de nuevo, por lo que hay que tener cuidado con donde y con que permisos se crea.

Y eso es todo, elige el método que mas te guste.

Click to read and post comments

abr 17, 2015

Crea tu tienda online con Django y Paypal REST I

He leído infinidad de tutoriales sobre PayPal REST API, desde los más técnicos a los más prácticos, y en todos ellos lo que he echado de menos es que profundizaran un poco más en como integrar paypal en una aplicación real. Este tutorial es la primera parte de una serie, que pretende llenar ese hueco, y esta pensado como un complemento a esos otros tutoriales, mostrando distintos ejemplos de uso de PayPal.

Antes de empezar recomiendo que te familiarizes con la biblioteca Paypal Python SDK, porque el propósito de éste tutorial no es explicar su funcionamiento.

La tienda

Para éste primer tutorial la aplicación que voy a usar como ejemplo es una tienda de libros, en concreto una tienda de ebooks. Su funcionamiento es muy sencillo, cada libro tiene un botón de compra que al ser pulsado redirige al cliente a la página de pago de paypal, cuando la transacción se finaliza, el libro es enviado a su dirección de correo.

El proyecto esta compuesto por dos "apps" principales, libros y pagos. La primera gestiona el contenido de la tienda, con cuatro modelos Autor, Genero, Editorial, y Libro. Siendo este último el modelo más importante.

# libros.models.py

class Libro(PublicadoMixin, ImagenMixin):
    """Novels are never displayed, and are used to link together
    all the editions, with the common information among them
    authors, genre..."""

    # Informacion basica del libro
    titulo = models.CharField(max_length=200, db_index=True)
    titulo_completo = models.CharField(max_length=300, blank=True)
    numero_paginas = models.PositiveIntegerField(default=0)
    resumen = models.TextField(max_length=2000, blank=True)
    fecha_publicacion = models.DateField(blank=True, null=True)
    idioma = LanguageField(default='es')

    # Informacion sobre edicion y editorial
    numero_edicion = models.PositiveSmallIntegerField(default=1)
    editorial = models.ForeignKey(Editorial, blank=True, null=True)

    # Algunos libros pueden tener multiples autores
    autores = models.ManyToManyField('Autor')

    # Generos del libro
    generos = models.ManyToManyField('Genero')

    # Ruta del ebook en los distintos formatos
    epub = models.FileField(upload_to='ebooks/epub/', blank=True)
    mobi = models.FileField(upload_to='ebooks/mobi/', blank=True)

    # ISBN del libro.
    isbn = ISBNField(blank=True)

    # Precio del libro
    precio = models.DecimalField(max_digits=6, decimal_places=2)

    class META:
        ordering = ['-fecha_publicacion']

    def get_absolute_url(self):
        return reverse('libro-detail', args=[str(self.id),])

    def __str__(self):
        return self.titulo

La segunda "app" pagos se encarga de generar y aceptar los pagos por PayPal, y tiene un único modelo que se usa para registrar la información de la transacción, en concepto de que libro se realizaron, y la información del cliente proporcionada por PayPal.

# pagos.models.py

from django.db import models
from decimal import Decimal
from libros.models import Libro

class PagoPaypalManager(models.Manager):
    def crear_pago(self, payment_id, libro):
        pago=self.create(libro=libro,
            payment_id=payment_id,
            precio=libro.precio)
        return pago

class PagoPaypal(models.Model):
    # Foreign Key hacia el libro de este pago
    libro = models.ForeignKey(Libro)

    # Identificador de paypal para este pago
    payment_id = models.CharField(max_length=64, db_index=True)

    # Id unico asignado por paypal a cada usuario no cambia aunque
    # la dirección de email lo haga.
    payer_id = models.CharField(max_length=128, blank=True, db_index=True)

    # Dirección de email del cliente proporcionada por paypal.
    payer_email = models.EmailField(blank=True)

    # Guardamos una copia del precio de libro, porque puede variar en el tiempo
    precio = models.DecimalField(max_digits=8, decimal_places=2,
                default = Decimal('0.00'))

    pagado = models.BooleanField(default=False)

    objects = PagoPaypalManager()

Por último para introducir los datos de libros, editoriales, y demás información se usa el interfaz de administración de Django.

Credenciales de Paypal

El primer paso para desarrollar una aplicación con PayPal es crear una cuenta, habilitar REST Api en Paypal Developer, y obtener las credenciales y cuentas de desarrollo. Después solo hay que añadirlas en el archivo settings.py

PAYPAL_MODE= "sandbox"
PAYPAL_CLIENT_ID  = "EBWKjlELKMYqRNQ6sYvFo64FtaRLRR5BdHEESmha49TM",
PAYPAL_CLIENT_SECRET = "EO422dn3gQLgDbuwqTjzrFgFtaRLRR5BdHEESmha49TM"

Paypal

En las páginas con la información de cada uno de los libros, se encuentran los respectivos botones de compra, con los cuales un cliente puede iniciar el proceso de pago a traves de PayPal.

Imagén de la pagina de compra de un libro

Veamos código del botón de compra en el template:

<a href="{% url 'pago-paypal' libro.pk %}" class="btn btn-info btn-lg pull-right">
    Comprar {{libro.precio}}€
</a>

El botón redirige al usuario a 'pago-paypal' con el identificador del libro como parámetro, ese alias corresponde con la vista pagos.views.PaypalView, donde se encuentra la lógica para iniciar el proceso de pago del libro.

# pagos.views.py

class PaypalView(RedirectView):

    permanent = False

    def _generar_lista_items(self, libro):
        """ """
        items = []
        items.append({
            "name":     str(libro),
            "sku":      str(libro.id),
            "price":    ('%.2f' % libro.precio),
            "currency": "EUR",
            "quantity": 1,
        })
        return items

    def _generar_peticion_pago_paypal(self, libro):
        """Crea el diccionario para genrar el pago paypal de libro"""
        peticion_pago = {
            "intent": "sale",
            "payer": {"payment_method": "paypal"},
            "redirect_urls": {
                "return_url": settings.SITE_URL+reverse('aceptar-pago-paypal'),
                "cancel_url": settings.SITE_URL},

            # Transaction -
            "transactions": [ {
                # ItemList
                "item_list":{
                    "items": self._generar_lista_items(libro)},

                # Amount
                "amount": {
                    "total": ('%.2f' % libro.precio),
                    "currency": 'EUR'},

                #Description
                "description": str(libro),}
            ]}

        return peticion_pago

    def _generar_pago_paypal(self, libro):
        """Genera un pago de paypal para libro"""
        paypalrestsdk.configure({
            "mode":         settings.PAYPAL_MODE,
            "client_id":    settings.PAYPAL_CLIENT_ID,
            "client_secret":settings.PAYPAL_CLIENT_SECRET,})

        pago_paypal = paypalrestsdk.Payment(self._generar_peticion_pago_paypal(libro))

        if pago_paypal.create():
            for link in pago_paypal.links:
                if link.method == "REDIRECT":
                    url_pago = link.href
        else:
            raise Exception(pago.error)

        return url_pago, pago_paypal

    def get_redirect_url(self, *args, **kwargs):
        """Extrae el libro que el usuario quiere comprar, genera un pago de
        paypal por el precio del libro, y devuelve la direccion de pago que
        paypal generó"""
        libro = get_object_or_404(Libro, pk=int(kwargs['libro_pk']))
        url_pago, pago_paypal = self._generar_pago_paypal(libro)

        # Se añade el identificador del pago a la sesion para que PaypalExecuteView
        # pueda identificar al ususuario posteriorment
        self.request.session['payment_id'] = pago_paypal.id

        # Por ultimo salvar la informacion del pago para poder determinar que
        # libro le corresponde, al terminar la transaccion.
        PagoPaypal.objects.crear_pago(pago_paypal.id, libro)

        return url_pago

Esta vista se encarga de crear un pago para el libro con el identificador proporcionado, y redirigir al usuario a la dirección proporcionada por PayPal para completar la transacción.

Para implementar esta vista he usado una Class Based View que hereda de RedirectView, y he redefinido el método get_redirect_url, para que obtenga el modelo del libro seleccionado, genere un pago de Paypal con los datos de ese libro, y por último devuelva la dirección a la que el usuario debe ser redirigido para finalizar el pago.

En la página de PayPal aparecerá el título del libro, su número de artículo, y el precio.

Imagen de la pagina de paypal para el pago de un libro

Una vez el usuario termina el pago es redirigido a la url de retorno, en la que se debe "ejecutar" el pago para finalizar la transacción, esta dirección de retorno se especificó en el método _generar_peticion_pago_paypal(), de la clase PaypalView.

"return_url": settings.SITE_URL+reverse('aceptar-pago-paypal'),

En esta url se encuentra la vista PaypalExecuteView que acepta el pago, guarda la información del cliente, marca el registro como pagado, y envía el email que contiene el libro a la dirección de email del usuario proporcionada por PayPal.

# pagos.views.py

class PaypalExecuteView(TemplateView):

    template_name = 'pagos/paypal_exito.html'

    def _enviar_ebook_email(self, registro_pago):
        """Enviar Email con el libro al cliente """
        #TODO: adjuntar los archivos del modelo libro
        libro = registro_pago.libro
        mensaje = "Gracias por su compra de %s" % libro.titulo
        send_mail('TiendaEbook', mensaje,
            from_email = settings.DEFAULT_FROM_EMAIL,
            recipient_list=[registro_pago.payer_email,])

    def _aceptar_pago_paypal(self, payment_id, payer_id):
        """Aceptar el pago del cliente, actualiza el registro con los datos
        del cliente proporcionados por paypal"""
        registro_pago = get_object_or_404(PagoPaypal, payment_id=payment_id)
        pago_paypal = paypalrestsdk.Payment.find(payment_id)
        if pago_paypal.execute({'payer_id': payer_id}):
            registro_pago.pagado = True
            registro_pago.payer_id = payer_id
            registro_pago.payer_email = pago_paypal.payer['payer_info']['email']
            registro_pago.save()else:
        else:
            raise HttpResponseBadRequest

        return registro_pago

    def get(self, request, *args, **kwargs):
        """Extraer identificacion de paypal del cliente, la id del pago,
        aceptar el pago, y enviar el email."""
        context = self.get_context_data(**kwargs)
        try:
            payer_id = request.GET['PayerID']
            payment_id = request.session['payment_id']
        except Exception:
            raise HttpResponseBadRequest

        registro_pago = self._aceptar_pago_paypal(payment_id, payer_id)
        self._enviar_ebook_email(registro_pago)

        return self.render_to_response(context)

Y eso es todo, el código está disponible en Github.

En la próxima entrega de este tutorial mejoraré algunas de las deficiencias de la aplicación, añadiendo un carro de la compra para simplificar la compra de varios libros, y un sistema de descarga para no tener que enviar el libro por correo.

Click to read and post comments

abr 13, 2015

Como comprimir directorios en Linux

Comprimir directorios en Linux es imprescindible para muchas tareas de backup y administración, en éste artículo se muestran dos métodos usando los formatos tar.gz y zip.

Tar Gzip

El formato mas usado en sistemas Unix/Linux es tar.gz que es un proceso de dos pasos primero se usa el programa tar para unir todos los archivos a comprimir en uno solo, sobre el que luego se usa el compresor gzip. Esta secuencia es tan común que el comando tar incluye una opción para comprimir directamente el archivo al finalizar.

Puede comprimir un directorio usando:

$ tar -czvf nombre-directorio.tar.gz nombre-directorio

Donde

  • -z Comprime el archivo usando gzip
  • -c Crea un archivo
  • -v Verbose, escribe en pantalla información sobre el proceso de compresión
  • -f Nombre del archivo

Imagina que quieres comprimir tu directorio home

$ tar -czvf backup-directorio-usuario.tar.gz /home/usuario

También puedes comprimir todos los archivos dentro de directorio actual incluidos subdirectorios usando:

$ tar -czvf nombre-backup.tar.gz *

Para restaurar un archivo comprimido:

$ tar -xzvf backup-directorio-usuario.tar.gz

Donde

  • -x Indica que debe extraer los archivos

Zip

Algunas veces es necesario que el archivo sea descomprimido en otros sistemas operativos, en ese caso es útil usar el formato zip que tiene mayor compatibilidad.

Comprimir varios archivos en un solo zip:

$ zip archivos-comprimidos.zip archivo1 archivo2 archivo3

Comprimir todos los archivos del directorio sin incluir subdirectorios:

$ zip archivos.zip *

Comprimir un directorio completo incluyendo subdirectorios:

$ zip -r directorio-comprimido.zip /home/usuario

Puedes descomprimir archivos zip usando:

$ unzip archivo.zip
Click to read and post comments

abr 05, 2015

Eliminar el subrayado de los enlaces en html

Un problema que me encuentro una vez tras otra cuando estoy editando CSS, es la necesidad de eliminar el subrayado por defecto de los enlaces HTML.

La solución es muy sencilla pero siempre se me olvida y tengo que buscar las propiedades CSS. Esta vez va a ser la última, aqui dejo el código para futuras consultas:

a {
  border-bottom: none !important;
  text-decoration: none;
}

a:hover {
  border-bottom: none !important;
  text-decoration: none,
}
Click to read and post comments