ka.da

Aller au contenu | Aller au menu | Aller à la recherche

Mot-clé - hébergement

Fil des billets - Fil des commentaires

mardi, avril 6 2010

Héberger des projets Django avec Nginx et Gunicorn

Mise à jour : j'ai finalement décidé d'utiliser supervisor pour gérer/monitorer ce genre de services,voyez ce billet.

Je continue l'optimisation de mon serveur web (cf le premier épisode) avec cette fois-ci mes projets Django que j'ai migré vers Nginx et Gunicorn. (veuillez noter que Gunicorn permet également de faire tourner d'autres types d'applications Python que Django)

Virtualenv

Pour gérer mes projets Django, j'utilise virtualenv et toute cette doc part donc du principe que vous l'utilisez également. Si ce n'est pas le cas 1. je vous encourage à le faire ;) 2. vous devrez adapter cette doc en conséquence.

Gunicorn

On commence par installer Gunicorn dans chacun des environnements virtuels hébergeant vos projets Django (cf plus bas pour les détails)

$ pip install gunicorn

ou

$ easy_install gunicorn

Ensuite pour chaque projet Django qui sera propulsé par Gunicorn, le plus simple est de créer un fichier de configuration gunicorn.conf.py pour indiquer les options du serveur à son lancement. Voici un exemple de fichier :

backlog = 2048
bind = "unix:/var/run/gunicorn/monprojet.sock"
pidfile = "/var/run/gunicorn/monprojet.pid"
daemon = True
debug = False
workers = 2
logfile = "/opt/django/www_domain_tld/log/gunicorn.log"
loglevel = "info"

Pour bind vous pouvez soit spécifier un socket Unix soit une adresse et un port TCP/IP. Veuillez consulter la doc configuration pour les détails.

Vous pouvez alors lancer gunicorn avec la commande suivante :

$ gunicorn_django -c gunicorn.conf.py

Bien sûr, comme pour l'article précédent que se passe-t-il en cas de redémarrage du serveur ? Il faut relancer les processus gunicorn un par un. Pour éviter ce genre de désagréments j'ai donc créé un script destiné au système de démarrage sysvinit de Debian.

Ce script a comme prérequis :

  • d'avoir pour chaque projet Django
    • un virtualenv dédié dans un répertoire www_domaine_tld
    • dans ce virtualenv un répertoire nommé monprojet avec les sources de celui-ci et notamment les fichiers importants pour Django (settings.py, urls.py...)
  • d'avoir un répertoire /etc/gunicorn/sites
  • dans ce répertoire vous faites un lien vers le virtualenv de chaque projet en lui donnant le nom du répertoire du projet, exemple :
# cd /etc/gunicorn/sites
# ln -s /opt/django/www_domaine_tld monprojet
  • le fichier gunicorn.conf.py est situé dans le virtualenv (donc dans le répertoire parent de celui des sources du projet)
  • il faut que le pid spécifié dans ce fichier soit de la forme monprojet.pid puisque son nom est ainsi extrapolable par rapport au nom du lien dans /etc/gunicorn/sites

Ce qui donne (pour éclaircir tout ça) une arborescence comme suit :

/etc/gunicorn/sites/
                    monprojet1 -> /opt/django/www_domaine1_tld      # lien vers /opt/django/www_domaine1_tld
                    monprojet2 -> /opt/django/www_domaine2_tld      # lien vers /opt/django/www_domaine2_tld

/opt/django/
            www_domaine1_tld/
                             bin/                         #
                             include/                     # répertoires créés par virtualenv
                             lib/                         # 
                             monprojet1/                  # répertoire contenant les sources du projet
                             gunicorn.conf.py             # fichier de configuration de gunicorn
            www_domaine2_tld/
                             bin/                         #
                             include/                     # répertoires créés par virtualenv
                             lib/                         # 
                             monprojet2/                  # répertoire contenant les sources du projet
                             gunicorn.conf.py             # fichier de configuration de gunicorn

Et voici le script /etc/init.d/gunicorn

#!/bin/sh 

### BEGIN INIT INFO
# Provides:       gunicorn
# Required-Start: $local_fs $syslog
# Required-Stop:  $local_fs $syslog
# Default-Start:  2 3 4 5
# Default-Stop:   0 1 6
# Short-Description: Gunicorn processes
### END INIT INFO

USER=www-data
NAME="gunicorn"
DAEMON="gunicorn_django"
CONFDIR=/etc/gunicorn/sites
PIDDIR=/var/run/gunicorn
VENV_ACTIVATION="source ../bin/activate"
CONFFILE="../gunicorn.conf.py"
OPTIONS="-c $CONFFILE"
RETVAL=0

# source function library
. /lib/lsb/init-functions

# pull in default settings
[ -f /etc/default/gunicorn ] && . /etc/default/gunicorn

start()
{
    echo $"Starting $NAME."
    cd $CONFDIR;
    for d in *; do
        echo -n $d;
        PIDFILE=$PIDDIR/$d.pid
        if [ -f $PIDFILE ]; then
            echo ": already started!"
        else
            cd $d;
            cd $d;
            su -c "$VENV_ACTIVATION; $DAEMON $OPTIONS" $USER && echo ": OK";
        fi
    done
    echo "done"
}

stop()
{
    echo $"Stopping $NAME:"
    cd $CONFDIR
    for d in *; do
        echo -n $d;
        if [ -f $PIDDIR/$d.pid ]
        then kill -QUIT `cat $PIDDIR/$d.pid` && echo ": OK" || echo ": failed";
        fi
    done
    echo "done"
}

reload()
{
    echo $"Reloading $NAME:"
    cd $CONFDIR
    for d in *; do
        echo -n $d;
        if [ -f $PIDDIR/$d.pid ]
        then kill -HUP `cat $PIDDIR/$d.pid` && echo ": OK" || echo ": failed";
        fi
    done
    echo "done"
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        reload
        ;;
    reload)
        reload
        ;;
    force-reload)
        stop && start
        ;;
    *)
        echo $"Usage: $0 {start|stop|restart}"
        RETVAL=1
esac
exit $RETVAL

Nginx

Pour Nginx, on retrouve un fichier de configuration semblable à celui-ci :

upstream www_domaine_tld {
    server      unix:/var/run/gunicorn/monprojet.sock fail_timeout=0;
}

server {
    listen      80;
    server_name www.domaine.tld;

    access_log  /var/log/nginx/www.domaine.tld.access.log;
    error_log   /var/log/nginx/www.domaine.tld.error.log;

    location /media {
        root    /opt/django/www.domaine.tld/lib/python2.5/site-packages/django/contrib/admin/;
      expires       30d;
    }

    root            /opt/django/www.domaine.tld/monprojet/media;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;       

        if (!-f $request_filename) {
            proxy_pass http://www_domaine_tld;
            break;
        }
    }
}

Et voili.

samedi, avril 3 2010

Faire tourner des applications Ruby on Rails avec Nginx et Unicorn

Mise à jour : j'ai finalement décidé d'utiliser supervisor pour gérer/monitorer ce genre de services, voyez ce billet.

Afin d'optimiser mon petit serveur virtuel chez Gandi qui, à force de lui ajouter des fonctions, des sites web, des utilisateurs avait de petits problèmes de charge au point de temps en temps de ne plus répondre, je migre du serveur web Apache vers Nginx. Étant donné que je fais tourner de tout et de rien sur Apache : du php, des cgi, du Ruby on Rails et du Django, l'opération s'avère un poil complexe, et je vais essayer de présenter petit à petit les solutions retenues pour servir chacune de ces technologies avec Nginx, en commençant par les applications Ruby on Rails.

Unicorn

Avec Apache, pour faire tourner mon application Ruby on Rails (Redmine) j'utilisais Passenger/mod_rails, parce qu'après moults recherches c'est la solution que j'avais trouvé. Le passage à Nginx m'a permis de me pencher à nouveau sur ce problème et j'ai décidé de me tourner vers Unicorn[1].

Pour installer unicorn le plus simple est de passer par l'outil de gestionnaire de packages Ruby gem :

# gem install unicorn

Afin de servir vos projets Rails, il faut lancer un processus Unicorn par projet, en spécifiant les options nécessaires. Le plus simple pour spécifier ces options est de créer un fichier de configuration pour Unicorn directement dans chaque projet Rails. Ce fichier doit être nommé unicorn.rb et situé dans le répertoire config du RAILS_ROOT c'est-à-dire du répertoire racine de votre projet.

Voici un exemple de fichier sur lequel vous pouvez vous baser pour créer votre propre configuration.

# configuration file for Unicorn (not Rack)
#
# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete
# documentation.

# Use at least one worker per core if you're on a dedicated server,
# more will usually help for _short_ waits on databases/caches.
worker_processes 2

# Help ensure your application will always spawn in the symlinked
# "current" directory that Capistrano sets up.
working_directory "/opt/redmine" # available in 0.94.0+

# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
listen "/var/run/unicorn/redmine.sock", :backlog => 64
#listen 8080, :tcp_nopush => true

# nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 30

# feel free to point this anywhere accessible on the filesystem
pid "/var/run/unicorn/redmine.pid"

# some applications/frameworks log to stderr or stdout, so prevent
# them from going to /dev/null when daemonized here:
stderr_path "/opt/redmine/log/unicorn.stderr.log"
stdout_path "/opt/redmine/log/unicorn.stdout.log"

# combine REE with "preload_app true" for memory savings
# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
  GC.copy_on_write_friendly = true

before_fork do |server, worker|
  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.connection.disconnect!

  # The following is only recommended for memory/DB-constrained
  # installations.  It is not needed if your system can house
  # twice as many worker_processes as you have configured.
  #
  # # This allows a new master process to incrementally
  # # phase out the old master process with SIGTTOU to avoid a
  # # thundering herd (especially in the "preload_app false" case)
  # # when doing a transparent upgrade.  The last worker spawned
  # # will then kill off the old master process with a SIGQUIT.
  # old_pid = "#{server.config[:pid]}.oldbin"
  # if old_pid != server.pid
  #   begin
  #     sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
  #     Process.kill(sig, File.read(old_pid).to_i)
  #   rescue Errno::ENOENT, Errno::ESRCH
  #   end
  # end
  #
  # # *optionally* throttle the master from forking too quickly by sleeping
  # sleep 1
end

after_fork do |server, worker|
  # per-process listener ports for debugging/admin/migrations
  # addr = "127.0.0.1:#{9293 + worker.nr}"
  # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)

  # the following is *required* for Rails + "preload_app true",
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.establish_connection

  # if preload_app is true, then you may also want to check and
  # restart any other shared sockets/descriptors such as Memcached,
  # and Redis.  TokyoCabinet file handles are safe to reuse
  # between any number of forked children (assuming your kernel
  # correctly implements pread()/pwrite() system calls)
end

Éditez les premières variables pour correspondre à votre configuration : nombre de processus au démarrage, répertoire de travail, écoute sur un socket ou un port TCP/IP, emplacement du fichier pid et des logs de capture de stdout et stderr.

Vous pouvez alors vous mettre dans le répertoire en question et lancer Unicorn comme suit :

/opt/redmine$ unicorn_rails  -c config/unicorn.rb -E production -D

Je n'ai malheureusement pas trouvé comment spécifier l'environnement à utiliser (le RAILS_ENV, ici production) autrement que dans la ligne de commande : l'indiquer dans le fichier de configuration ne marche pas... mais le premier qui trouve/sais merci de laisser un commentaire !

Tout ça c'est bien sympa mais ce n'est pas pratique de devoir lancer à la main chacun des processus unicorn nécessaire ni de devoir les relancer à la main si le serveur redémarre.

Pour pallier à ce problème, il faut créer un script de démarrage de Unicorn. Vu que ma Debian utilise un init à la Système V j'ai préféré créer un script pour ce système plutôt qu'utiliser un autre outil pour cela. Celui que j'utilise est adapté de ce site. Il suffit de vérifier que la partie des variables (notamment le RAILS_ENV, USER, DAEMON) correspond à votre configuration.

#!/bin/sh 

### BEGIN INIT INFO
# Provides:       unicorn
# Required-Start: $local_fs $syslog
# Required-Stop:  $local_fs $syslog
# Default-Start:  2 3 4 5
# Default-Stop:   0 1 6
# Short-Description: Unicorn processes
### END INIT INFO

RAILS_ENV="production"
USER=www-data
DAEMON="/var/lib/gems/1.8/bin/unicorn_rails"

NAME="unicorn"
CONFDIR=/etc/unicorn/sites
PIDDIR=/var/run/unicorn
CONFFILE=config/unicorn.rb
RETVAL=0

OPTIONS="-c $CONFFILE -D -E $RAILS_ENV"
# source function library
. /lib/lsb/init-functions

# pull in default settings
[ -f /etc/default/unicorn ] && . /etc/default/unicorn

test -x $DAEMON || exit 0

start()
{
    echo $"Starting $NAME."
    cd $CONFDIR;
    for d in *; do
        echo -n $d;
        cd $d;
        PIDFILE=$PIDDIR/$d.pid
        [ -f $PIDFILE ] && echo ": already started!"
        [ ! -f $PIDFILE ] && su -c "$DAEMON $OPTIONS" $USER && echo ": OK";
    done
    echo "done"
}

stop()
{
    echo $"Stopping $NAME:"
    cd $CONFDIR
    for d in *; do
        echo -n $d;
        if [ -f $PIDDIR/$d.pid ]
        then kill -QUIT `cat $PIDDIR/$d.pid` && echo ": OK" || echo ": failed";
        fi
    done
    echo "done"
}

reload()
{
    echo $"Reloading $NAME:"
    cd $CONFDIR
    for d in *; do
        echo -n $d;
        if [ -f $PIDDIR/$d.pid ]
        then kill -USR2 `cat $PIDDIR/$d.pid` && echo ": OK" || echo ": failed";
        fi
    done
    echo "done"
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        reload
        ;;
    reload)
        reload
        ;;
    force-reload)
        stop && start
        ;;
    *)
        echo $"Usage: $0 {start|stop|restart}"
        RETVAL=1Unicorn se lance en utilisant des sockets mais peut également utiliser TCP/IP.
esac
exit $RETVAL

Ce script est orienté Debian, et devra probablement être adapté pour d'autres distributions.

Pour le faire tourner il vous faut :

  • un répertoire /etc/unicorn/sites dans lequel vous mettez des liens symboliques vers vos projets Rails (le RAILS_ROOT plus exactement de chacun)
  • dans chacun des RAILS_ROOT de vos projets un fichier config/unicorn.rb comme indiqué au-dessus

Nginx

Ensuite pour utiliser Unicorn depuis Nginx, il vous suffit d'avoir une déclaration de virtualhost du type :


upstream redmine_domain_tld {
    server      unix:/var/run/unicorn/redmine.sock fail_timeout=0;
}

server {
    listen      80;
    server_name redmine.domain.tld;

    access_log  /var/log/nginx/redmine.domain.tld.access.log;
    error_log   /var/log/nginx/redmine.domain.tld.error.log;

    location /images {
        root    /opt/redmine/public/images;
    }

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;       

        if (!-f $request_filename) {
            proxy_pass http://redmine_domain_tld;
            break;
        }
    }
}

Et voilà. La suite de ma migration de Apache vers Nginx dans un prochain épisode.

Notes

[1] merci à Olivier Meunier de m'avoir rappeler l'existence de ce soft... auquel j'aurais dû penser tout de suite en connaissant gunicorn

dimanche, février 14 2010

Django : gérer plusieurs domaines avec un seul projet et les mêmes données

Problématique

Toujours en plein dans Django, mon projet actuel est de refaire tous mes sites web hébergés sur divers domaines en Django. Jusque là tout va bien je dirais. Ça se complique un peu quand l'idée est d'utiliser le même projet pour gérer tous ces sites web.

Pourquoi utiliser le même projet ? Plusieurs (bonnes) raisons à cela :

  • simplifier le développement, la maintenance et le déploiement : un projet à gérer c'est mieux que plusieurs
  • partager les mêmes données : c'est à mon avis le point essentiel qui m'intéresse : pouvoir créer du contenu (billet de blog, photo,...) et pouvoir facilement l'affecter à tel ou tel site ou à plusieurs à la fois (cela dépend également des applications, cf plus bas)
  • tout administrer de façon centralisée : c'est la suite du point précédent : pouvoir, dans une interface d'administration unique, créer du contenu et l'affecter à tel ou tel site web sans devoir changer d'interface d'admin

Je vais donc vous proposer ici la solution que j'ai retenu pour servir 2 domaines différents avec Django. Ceux-ci seront pour l'exemple www.domaine1.tld et www.domaine2.tld (ouaouh ! que c'est original ! - ça marche bien évidemment pour un sous-domaine...).

L'historique de cette configuration est expliqué en bas de ce billet pour ceux que ça intéresse.

note importante : puisqu'on m'a signalé que la solution présentée ici n'est pas forcément "propre" au sens Django puisqu'elle implique de changer le nom de divers fichiers de base d'un projet Django, et puisqu'après réflexion je suis d'accord avec cette remarque... voici quelques éclaircissements :

Si vous faites les choses le plus possible dans le style Django, en pensant application réutilisable il est en effet préférable de penser chaque domaine comme un projet séparé et donc d'isoler les fichiers plutôt de cette façon

www_domaine1_tld/
                 __init__.py
                 manage.py
                 settings.py
                 urls.py

www_domaine2_tld/
                 __init__.py
                 manage.py
                 settings.py
                 urls.py

ce qui permet de ne pas modifier les fichiers manage, settings et urls. Cela est encore facilité si votre projet est composé d'applications réutilisables, installables à la manière de modules Python (ce qui doit d'ailleurs être au maximum le cas).

La documentation ci-dessous est donc toujours valable mais il est probablement préférable d'utiliser cette manière de faire... à vous de voir maintenant...

Arborescence

Le premier point à voir est l'organisation des fichiers dans le répertoire du project. Afin de pas trop encombrer celui-ci, les fichiers ayant la même fonction (les fichiers settings.py, urls.py, les templates) sont déplacés dans des sous-répertoires, les fichiers manage.py restant eux à la racine, comme suit :

monprojet/
          www_domaine1_tld_manage.py
          www_domaine2_tld_manage.py
          settings/
                   __init__.py
                   www_domaine1_tld.py
                   www_domaine2_tld.py
                   global_settings.py
          urls/
               __init__.py
               www_domaine1_tld.py
               www_domaine2_tld.py
          templates/
                    www_domaine1_tld
                    www_domaine2_tld

il ne faut pas oublier les fichiers __init__.py aux endroits indiqués.

Fichiers manage.py

Pour exécuter des commandes Django, on peut utiliser $ django-admin.py <commande> <options> avec certaines contraintes (spécifier le fichier settings...) ou le "raccourci" $ ./manage.py <commande> <options>.

Dans le cas où l'on veut gérer plusieurs domaines, et où le fichier "settings" ne s'appelle pas settings.py, il faut créer un fichier manage.py différent par domaine.

Exemple avec www_domaine1_tld_manage.py.

#!/usr/bin/env python
from django.core.management import execute_manager
try:
    import settings.www_domaine1_tld
except ImportError:
    import sys
    sys.stderr.write("Error: Can't find the file 'settings.www_domaine1_tld.py' in the directory containing %r...." % __file__)
    sys.exit(1)

if __name__ == "__main__":
    execute_manager(settings.www_domaine1_tld)

Il suffira alors de lancer une commande comme la suivante pour exécuter des commandes Django sur un certain domaine

$ ./www_domaine1_tld_manage.py <commande> <options>

mise à jour : vu qu'on me l'a signalé, cette manipulation concernant les fichiers manage.py n'est pas nécessaire puisqu'il suffit d'ajouter l'option --settings=settings.www_domaine1_tld pour pouvoir utiliser ./manage.py de manière classique

Fichiers urls.py

Pour les fichiers urls, il suffit de créer les fichiers suivant l'arborescence indiquée au-dessus en utilisant des fichiers urls.py "classiques", avec la différence que si vous devez importer les "settings" du domaine il faut indiquer le module sous la forme settings.www_domaine1_tld

Framework "sites" de Django

Si vous voulez utiliser les mêmes applications sur plusieurs domaines sans pour autant devoir publier le même contenu partout, il faut alors utiliser des applications qui utilisent le framework "sites".

Vous devez dans ce cas :

  • créer des sites dans la base de données
  • et spécifier le SITE_ID qui va bien dans le fichier "settings" (voir plus bas)

Fichiers settings.py

Pour gérer les paramètres de chaque domaine/site on aura un fichier pour chacun ainsi qu'un fichier pour les paramètres globaux (pour les bases de données, les applications installées...).

Voici les paramètres spécifiques à chaque domaine, avec des commentaires.

# paramètres Django pour le site www.domaine1.tld

import os

# on charge ici les paramètres spécifiés dans global_settings.py
from global_settings import *

# on initialise BASE_DIR avec le répertoire de base du projet,
#  pour l'utiliser pour d'autres paramètres
BASE_DIR = os.path.join(os.path.dirname(__file__), '..')

# il faut indiquer ici l'identifiant du site (au sens Django), ansi que dans la base de données
# cf http://docs.djangoproject.com/en/dev/ref/contrib/sites/
SITE_ID = 1

# chemin absolu vers le répertoire des médias du domaine
MEDIA_ROOT = os.path.join(BASE_DIR, 'static', 'www_domaine1_tld')

# URL pour gérer les médias venant de MEDIA_ROOT
MEDIA_URL = 'http://www.domaine1.tld/static/'

# il faut ici initialiser une clé secrète distincte pour chaque domaine
SECRET_KEY = 'xxx'

# on spécifie ici le fichier urls.py
ROOT_URLCONF = 'monprojet.urls.www_domaine1_tld'

# on spécifie ici le répertoires des templates
TEMPLATE_DIRS = (
    os.path.join(BASE_DIR, 'templates', 'www_domaine1_tld')
)

# ce qui suit n'est pas obligatoire mais permet d'avoir un fichier spécifique
# en local afin par exemple de surcharger quelques paramètres pour le dev
try:
     from local_settings import *
except ImportError:
     pass

Et voici un exemple de fichier global_settings.py avec les paramètres qui peuvent/doivent être partagés.

DATABASE_ENGINE = ''
DATABASE_NAME = ''
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''

TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.load_template_source',
    'django.template.loaders.app_directories.load_template_source',
)

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.admin',
)

Serveur web/Python/WSGI

Pour cette partie, j'utilise pour ma part (pour l'instant) Apache+mod_wsgi mais je ne vais pas rentrer dans les détails puisque ça n'est pas spécifique au cas particulier qui nous intéresse ici.

J'ai un fichier pour Apache qui indique un VirtualHost pour chaque domaine et pointe vers un fichier WSGI pour chacun également.

Dans le fichier WSGI il faut faire attention à étendre le PYTHONPATH si nécessaire et indiquer le fichier settings correctement, un peu comme ce qui suit :

import sys

sys.path.append('/opt/django/monprojet/')
os.environ['DJANGO_SETTINGS_MODULE'] = 'monprojet.settings.www_domaine1_tld'

Historique

Au début de ma recherche sur cette problématique je me suis tout de suite dit que c'était le boulot de Django de faire ça, et quand j'ai compris que ce n'était pas le cas naturellement, je me suis orienté vers des applications qui, étendant Django, lui permettait de gérer cela. J'ai trouvé deux applications dont le but semble de combler ce manque :

Je ne me souviens plus pourquoi j'ai mis de côté django-multisite mais mes tests ont porté sur django-threaded-multihost. L'application faisait ce pourquoi elle était prévu mais avait un problème majeur : il n'y avait qu'un settings.py, un urls.py... pour tous les domaines servis et là je me suis rendu compte que ce n'était pas vraiment ce dont j'avais besoin puisque le but était de pouvoir servir le même contenu mais bien évidemment de façon différente... Retour à la case départ !

Heureusement je suis tombé sur une discussion sur IRC #django-fr entre david`bgk et cyberdelia concernant ce thème et cyberdelia m'a alors expliqué comment ses divers domaines (pour http://www.croisedanslemetro.com il me semble (j'aime le concept de ce site :) soit dit en passant) pointant vers un même projet étaient gérés... c'était ce qu'il me fallait ! Le temps que ça mûrisse, celui de m'y replonger, d'implémenter la solution et de la tester, je me suis dit qu'il serait intéressant de poster cette solution et me voilà donc... (en ajoutant le temps de rédaction de ce billet ;-)... j'espère que ça pourra être utile à d'autres.

mercredi, janvier 30 2008

Mercurial : partage de dépôts différents en http et https + push par https

J’ai commencé à regarder du côté des SCM[1] (marre de la gestion CPOLD ;-) il y a quelque temps déjà, et m’était tout d’abord arrêté sur Subversion [2] étant novice en la matière. Il s’est vite révélé peu adapté à ma pratique : “développement” sur plusieurs machines pas toujours online, et pas de serveur dédié toujours en ligne pour stocker mes dépôts. Depuis peu, ce dernier problème s’est réglé (merci Gandi Hébergement[3]) mais pour autant, j’ai abandonné Subversion pour Mercurial, un gestionnaire de code source distribué.

Je ne vais pas entrer ici dans les détails sur les caractéristiques d’un gestionnaire de code source distribué, ses avantages, ou même sur une présentation détaillée sur Mercurial et son mode de fonctionnement (je vous laisse visiter le site officiel pour cela). Je vais uniquement présenter des détails sur la configuration que j’ai mise en place afin de parvenir à partager aisément des dépôts différents via http et https, ainsi que comment faire pour autoriser la mise à jour de ceux-ci via https.


présentation des dépôts Mercurial sur le web


Imaginons que vos dépôts soient stockés dans /mercurial et que vous vouliez pouvoir donner un accès en lecture sur le web, à l’adresse http://hg.domain.tld. Nous allons configurer Apache et Mercurial pour ce faire.

Tout d’abord, nous allons créer un emplacement pour les fichiers web et le script CGI en Python pour Mercurial. Prenons /var/www/domain.tld/hg. Créons à cet endroit un répertoire hgweb dans lequel nous allons placer le script hgwebdir.cgi que vous pouvez trouver sur votre système (sous Debian /usr/share/doc/mercurial/examples/hgwebdir.cgi) ou sur le site de Mercurial. Éditez ce fichier pour avoir à la fin :

def make_web_app():
    return hgwebdir("/etc/mercurial/hgweb.config")

Nous allons ensuite créer le fichier /etc/mercurial/hgweb.config avec un contenu comme suit :

[paths]
depot1  =  /mercurial/depot1
depot2  =  /mercurial/depot2

On peut également utiliser la directive [collections] pour partager une hiérarchie de dépôts mais nous partons du principe que l’on veut contrôler précisément ce que l’on partage.

À partir de là, il nous reste à configurer Apache. Créez un fichier /etc/apache2/sites-available/hg.domain.tld afin de déclarez votre VirtualHost. Voici en exemple mon fichier

NameVirtualHost *:80

<VirtualHost *:80>
    ServerAdmin webmaster@hg.domain.tld

    ServerName hg.domain.tld  

    DocumentRoot /var/www/domain.tld/hg

    <Directory /var/www/domain.tld/hg/>
        Options FollowSymLinks +ExecCGI
        AddHandler cgi-script .cgi
        DirectoryIndex hgweb/hgwebdir.cgi
        AllowOverride None

        RewriteEngine on
        RewriteBase /hgweb
        RewriteRule ^$ hgwebdir.cgi  [L]
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteRule (.*) hgwebdir.cgi/$1  [QSA,L]
    </Directory>

    LogLevel warn
    ErrorLog /var/log/apache2/hg.domain.tld-error.log
.   CustomLog /var/log/apache2/hg.domain.tld-access.log combined
    
    ServerSignature Off
</VirtualHost>

Notez ici les règles de réécriture qui permettent de faire disparaître hgwebdir.cgi de vos URL. C’est ce qui m’a pris le plus de temps à trouver avant de tomber sur cette doc.

Activez votre nouveau “site” ainsi que les modules Apache dont vous aurez besoin

# a2ensite hg.domain.tld
# a2enmod mod_python
# a2enmod rewrite
# /etc/init.d/apache2 restart

Vous pouvez maintenant aller voir http://hg.domain.tld et parcourir vos dépôts Mercurial depot1 et depot2


partager des dépôts différents par https


Tout ça, c’est très bien, mais on aimerait également pouvoir avoir accès à certains dépôts que l’on ne veut pas donner en lecture à tout internet : des dépôts privés en somme. Nous allons donc partager ceux-ci sur un site sécurisé avec authentification.

Nous allons donc retourner dans notre répertoire /var/www/domain.tld/hg/hgweb, copier hgwebdir.cgi en hgwebdirssl.cgi et éditez ce fichier pour avoir :

def make_web_app():
    return hgwebdir("/etc/mercurial/hgwebssl.config")

Créons le fichier /etc/mercurial/hgwebssl.config avec :

[paths]
depot1  =  /mercurial/depot1
depot2  =  /mercurial/depot2
depot_prive3 = /mercurial/depot3
depot_prive4 = /mercurial/depot4

Et retournons éditer /etc/apache2/sites-available/hg.domain.tld pour y ajouter :

# en haut du fichier
NameVirtualHost *:443

# à la fin du fichier
<VirtualHost *:443>
    ServerAdmin webmaster@hg.domain.tld

    ServerName hg.domain.tld

    DocumentRoot /var/www/domain.tld/hg

    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/hg.domain.tld.pem
    SSLCertificateKeyFile /etc/apache2/ssl/hg.domain.tld.key

    <Directory /var/www/domain.tld/hg/>
        Options FollowSymLinks +ExecCGI
        AddHandler cgi-script .cgi
        DirectoryIndex hgweb/hgwebdirssl.cgi
        AllowOverride None

        RewriteEngine on
        RewriteBase /hgweb
        RewriteRule ^$ hgwebdirssl.cgi  [L]
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteRule (.*) hgwebdirssl.cgi/$1  [QSA,L]

        AuthUserFile /etc/mercurial/hgweb.htpasswd
        AuthGroupFile /dev/null
        AuthName "hg.domain.tld Mercurial repository"
        AuthType Basic
#       <LimitExcept GET OPTIONS>
                Require valid-user
#       </LimitExcept>
    </Directory>

    ErrorLog /var/log/apache2/hg.domain.tld-error.log

    LogLevel warn

    CustomLog /var/log/apache2/hg.domain.tld-access.log combined
    ServerSignature Off

</VirtualHost>

On retrouve ici les directives rewrite que nous avons utilisé dans la première partie, à la différence qu’elles pointent vers le CGI spécifique à notre configuration SSL, et quelques directives spécifiques justement à https/SSL. Bien évidemment, il vous faudra créer un fichier de mots de passe (nommé ici hgweb.htpasswd) avec :

# htpasswd -c /etc/mercurial/hgweb.htpasswd hguser

pour autoriser l’utilisateur hguser.

Notez les directives <LimitExcept GET OPTIONS> qui, si elles sont décommentées, permettent à tout le monde de visualiser les dépôts tout en demandant une authentification pour interagir via les commandes de Mercurial (clone, pull, push…).

Rechargez la configuration d’Apache et regardez la différence entre http://hg.domain.tld et https://hg.domain.tld


autoriser la mise à jour des dépôts via https


Pour autoriser le push via https, une fois le travail ci-dessus effectué, il suffit d’éditer la configuration des dépôts en modifiant le fichier .hg/hgrc de chaque projet. Éditez-le ou créez-le et insérez les lignes suivantes :

[web]
allow_push = hguser


utilisation


Voilà c’est fait, vous pouvez maintenant interagir très simplement avec vos dépôts Mercurial. Pour cloner un dépôt public

$ hg clone http://hg.domain.tld/depot1

Pour cloner un dépôt privé

$ hg clone https://hguser@hg.domain.tld/depot_prive3

Pour mettre à jour (push) ce même dépôt

$ hg push https://hguser@hg.domain.tld/depot_prive3

J’ajouterais que le plus dur est de gérer finement les droits d’accès aux divers fichiers nécessaires au bon fonctionnement de cette configuration. Pour faire simple, comme d’habitude, laissez le minimum de droits sur les fichiers de configuration (appartenance à l’utilisateur www-data et lecture pour lui), et pour les dépôts j’ai choisi la solution un groupe devel pour mon utilisateur principal ainsi que www-data et les droits pour ce groupe.

mise à jour : il semble nécessaire (depuis les dernières mises à jour de Mercurial ?) d’ajouter une ligne baseurl = dans la section [web] des fichiers .hg/hgrc afin de cacher les hgweb(dir).cgi dans les url.

Notes

[1] définition en français et en anglais

[2] cf cet article

[3] je sais que je n’ai pas parlé de cette nouvelle offre très intéressante de Gandi le registrar bien connu (pour sa qualité de service, son support et son engagement) alors allez voir le bar de gandi

vendredi, mars 3 2006

migration

Un rapide message pour vous expliquer le silence (qui devient pesant) sur ce blog.

J'ai d'abord, après quelques jours de vacances, migré ce site chez un autre hébergeur. En effet, l'hébergeur commercial chez qui j'étais ne me satisfaisait pas (panel d'administration mal conçu, aide insuffisante, et support par mail pas convaincant), et me suis donc intéressé aux hébergements associatifs et/ou non commerciaux. L'APINC a rapidement retenu mon attention, pour être l'hébergeur de plusieurs sites que je visite régulièrement et être mon serveur Jabber. Les principes de l'APINC (non-commercial, autogéré, administré par des bénévoles et destiné à des projets non marchands) correspondent parfaitement à ce que je veux, et la lecture de la charte m'a convaincu. J'ai donc rédigé un petit laïus pour expliquer pourquoi je désirais adhérer à cette association, ce que je voulais y héberger et en quoi je pouvais être utile, et, après presque une semaine d'attente[1] j'ai été admis.
Depuis je ne regrette pas mon choix, puisque les aspects techniques (l'extrême souplesse de GHS l'outil d'administration utilisé, la possibilité d'héberger plusieurs domaines, la rapidité de connexion FTP, etc, etc, etc.) m'ont confortés dans celui-ci. Alors :

merci à l'APINC.

apinc

Je suis également en train de 'redesigner' complètement le site, et faire des modifications profondes sur l'organisation des billets, donc ne vous étonnez des trucs bizarres qui peuvent parfois apparaître. Dans tous les cas, j'essaierais de vous tenir au courant.

À bientôt.

Notes

[1] à surveiller la page correspondant à mon "ticket" et lire les commentaires laissés par les adhérents (dont certains lecteurs de ce blog... que je salue ! n'hésitez pas à vous manifester, ça fait toujours plaisir)

dimanche, novembre 27 2005

achetez votre .info

Si vous avez envie d'avoir votre propre nom de domaine (que ce soit pour héberger votre site web, avoir des adresses e-mails sans dépendre de votre fournisseur ou d'un webmail comme yahoo, gmail...), c'est le moment ! En effet :

  • gandi propose en ce moment le '.info' à 1€ [cf le bar de Gandi] ;
  • 1&1 proposent eux gratuitement un pack comprenant un nom de domaine .info, et un hébergement de site web pendant 3 ans... [via monde de nerds]

update : vous pouvez également obtenir un .be sur namebay ou eurodns gratuitement... mais est-ce vraiment une bonne affaire ?