Déployer Mercurial (hg) « derrière » un serveur Web (Nginx) sous openSUSE

Logos

Dans un précédent billet, j'avais montré comment l'on pouvait exécuter une application Web écrite dans le langage Python sans faire intervenir de serveurs Web.

Aujourd'hui, nous allons voir le cas, où un serveur (en l'occurence Nginx) est déjà en place.

En fait, le but inavoué de cet article est de comprendre le système d'init, systemd utilisé par openSUSE en autre.

Dans les près-requis, on peut citer :

  • Savoir utiliser mercurial
  • Être root sur la machine

Dans un premier temps, nous allons voir la configuration du serveur, Nginx

Configuration du serveur

Rien de bien compliqué, la documentation est très complète.

Voici ma configuration du fichier nginx.conf (situé dans /etc/nginx).

user  nginx nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log;
#error_log  /var/log/nginx/error.log  notice;
#error_log  /var/log/nginx/error.log  info;

pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
    use epoll;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  /var/log/nginx/access.log  main;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;
    server_tokens     off;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  errements.net;

        charset utf-8;
        autoindex  on;
	index	index.html;

	# favicon.ico
	location = /favicon.ico {
		log_not_found	off;
		access_log	off;
	}

        location / {
            root   /srv/www/htdocs/bornem/;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /srv/www/htdocs/;
        }

	# Users directory support
	location ~ ^/~(.+?)(/.*)?$ {
		alias /home/$1/public_html$2;
	}
    }
}

Quelques explications concernant ce fichier.

  • Les journaux (d'erreurs et d'accès) seront stockés dans le dossier /var/log/nginx.
  • Le numéro du processus sera stocké dans le fichier /var/run/nginx.pid.
  • L'affichage du numéro de version du serveur est désactivé dans les en-têtes HTTP, la directive server_tokens est sur off.

Remarque : On peut analyser ces en-têtes avec le module Requests. (Il faut faire tourner le serveur, et lancer l'interpréteur Python) :

Voici le cas où la directive server_tokens est sur on (valeur par défaut).

olivier@bornem:~ $ python
Python 2.7.2 (default, Aug 19 2011, 20:41:43) [GCC] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
>>> r = requests.get("http://errements.net/")
>>> r.headers
{'date': 'Sat, 21 Jan 2012 09:41:08 GMT', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'content-type': 'text/html; charset=utf-8', 'server': 'nginx/1.0.10'}

La valeur server vaut nginx/1.0.10 (où 1.0.10 représente le numéro de version).

Si l'on désactive cette directive (server_tokens off;), on obtient ceci :

>>> r = requests.get("http://errements.net/")
>>> r.headers
{'date': 'Sat, 21 Jan 2012 09:54:32 GMT', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'content-type': 'text/html; charset=utf-8', 'server': 'nginx'}

Cette fois-ci, la valeur de serveur vaut nginx.

  • L'absence de favicon [1], à la racine du site, n'est pas « stockée » dans les logs.
[...]
	# favicon.ico
	location = /favicon.ico {
		log_not_found	off;
		access_log	off;
	}
[...]
  • La racine du serveur est située dans /srv/www/htdocs/bornem/. Pour les utilsateurs, elle est située dans $HOME/public_html (cette fonctionnalité est connue sous l'appellation UserDir).
[...]
	# Users directory support
	location ~ ^/~(.+?)(/.*)?$ {
		alias /home/$1/public_html$2;
	}
[...]

Nous pouvons maintenant lancer le serveur.

Démarrer Nginx

Sous openSUSE, les programmes pour lancer les daemons au démarrage sont situés dans /etc/init.d/.

root@bornem:~# ls /etc/init.d/ | grep nginx
nginx
root@bornem:~# sh /etc/init.d/nginx start
redirecting to systemctl
Failed to issue method call: Unit name .d/nginx.service is not valid.
root@bornem:~# 

On obtient une erreur, car il manque un fichier nginx.service.

Depuis la version 12.1, openSUSE intègre désormais systemd, comme service d'init.

Nous allons rechercher ce fichier.

root@bornem:~# find /etc/systemd/ -type f -name 'nginx.service' -print
root@bornem:~# 
root@bornem:~# find /lib/systemd/ -type f -name 'nginx.service' -print
root@bornem:~# 

Parmis les dossiers susceptibles de posséder un tel fichier, la recherche n'abouti pas, il faut donc le créer [2].

Dans les pièces jointes vous trouverez le fichier nginx.service.

Vous constaterez que j'ai commenté la ligne #WantedBy=multi-user.target, car j'ai fait les tests avec une interface graphique, d'où la cible graphical.target.

Nous allons placé ce fichier, dans le dossier /etc/systemd/system/default.target.wants (si il n'existe pas il faut le créer).

Désormais, nous pouvons lancer le serveur de cette façon :

root@bornem:~# systemctl start nginx.service

Mercurial

Dans cette partie nous allons nous intéressé à Mercurial.

L'affichage du « dépôt » se fera par l'intermédiaire du module hgweb. Pour cela j'ai écris un script Python (Cf. hgweb.wsgi, en pièces jointes).

Ce script est très simple, si aucun fichier de configuration lui est donné, il va rechercher tous les utilisateurs présents sur la machine (/etc/passwd), puis il va examiner le $HOME de chaque utilisateur à la recherche de fichiers .cfg contenant le mot clé [paths] [3] (pour l'instant, le script fonctionne uniquement pour un seul utilisateur). Le tout sera renvoyé à un serveur WSGI. Par défaut j'utilise waitress [4], mais si il est absent, le script utilisera le module wsgiref (en standard dans Python).

Le serveur WSGI passe par un socket réseau TCP/IP (network socket), c'est à dire qu'il transmet les données à une adresse IP locale (l'adresse de bouclage localhost).

Il nous faut maintenant trouver un moyen pour rediriger l'adresse IP locale (inaccessible depuis « l'extérieur ») vers le serveur (accessible grâce à une adresse IP connue). On parle alors de proxy.

Nginx possède une particularité intéressante, il est capable d'être utilisé comme reverse proxy, c'est à dire on passe par lui pour accéder aux ressources internes.

Configurer Nginx pour prendre en compte Mercurial

Nous allons donc utiliser le module proxy, de plus l'accès au dépôt se fera par l'intermédiaire d'un sous-domaine (vhost).

Vous devez avoir au préalable correctement configuré votre serveur DNS. Pour des tests en local, on peut passer par le fichier /etc/hosts.

On rajoute ces lignes :

[...]

http {
    }

    [...]

    # Subdomain settings
    #
    # Mercurial
    #
    server {
	listen 80;
	server_name hg.errements.net;

	access_log /var/log/nginx/access-hg.log;

	location / {
		root /srv/www/htdocs/vhosts/hg;
		autoindex	on;

	}
    }

[...]
}

On peut (re)lancer le serveur pour voir que tout fonctionne :

root@bornem:~# systemctl start nginx.service

Maintenant, on place notre script hgweb.wsgi dans le répertoire /srv/www/htdocs/vhosts/hg.

On exécute le script et on stoppe le serveur.

root@bornem:~# systemctl stop nginx.service
root@bornem:~# python /srv/www/htdocs/vhosts/hg/hgweb.wsgi &
serving on http://127.0.0.1:8500

ll s'agit de l'adresse IP mentionnée (vous pouvez changer le port) dans le script pour qu'il s'exécute. Il faut donc rajouter cette information au fichier de configuration de Nginx.

[...]

    # Subdomain settings
    #
    # Mercurial
    #
    server {
	listen 80;
	server_name hg.errements.net;

	access_log /var/log/nginx/access-hg.log;

	location / {
		root /srv/www/htdocs/vhosts/hg;
		autoindex	off;

		proxy_path	http://127.0.0.1:8500;
		proxy_set_header	Host	$host;
	}
    }

[...]

On relance le serveur, et on ouvre notre navigateur favori à l'adresse indiquée par server_name.

Automatiser le lancement du programme

Il va falloir créer un fichier .service, je me suis inspiré de ce billet, pour écrire le mien.

Dans le fichier /etc/init.d/after-local j'ai rajouté les mêmes lignes que celles tapées en console :

PYTHON_BIN=/usr/bin/python

NGINX_DIR="/srv/www/htdocs"
NGINX_VHOSTS="$NGINX_DIR/vhosts"

## Mercurial settings (subdomain, hg.)
if [ -e "$NGINX_VHOSTS/hg/hgweb.wsgi" ]; then 
	$PYTHON_BIN $NGINX_VHOSTS/hg/hgweb.wsgi
fi

Si l'on veut lancer plusieurs scripts, il faut placer les commandes en « arrière-plan » (on rajoute le caractère & à la fin de la ligne).

Le fichier .service qui va exécuter ce programme d'init s'appelle after-local.service (Cf. les pièces jointes).

root@bornem:~# systemctl start after-local.service

On peut rajouter ce fichier au répertoire /etc/systemd/system/default.target.wants pour qu'il soit lancé automatiquement au démarrage.

Cas pratique avec Gunicorn

Logo du serveur WSGI, Gunicorn

Dans cette dernière partie, nous allons nous intéressé à un serveur WSGI particulier, Gunicorn.

Nous allons supprimer notre premier script (hgweb.wsgi), pour le remplacer par web.py (Cf. pièces jointes). Ce serveur à un mode de fonctionnement particulier.

Ce programme, nous allons l'utilisé comme un « simple module Python » (je ne suis pas sûr à 100%, qu'il soit en mémoire dans sys.path), de ce fait, nous allons créer en plus un fichier __init__.py vide.

root@bornem:~# touch /srv/www/htdocs/vhosts/hg/__init__.py
root@bornem:~# chown -R nginx:nginx /srv/www/htdocs/

On peut lancer le programme et relancer le serveur :

root@bornem:~# cd /srv/www/htdocs/vhosts ; /usr/bin/gunicorn -p /var/run/gunicorn.pid -g nginx -u nginx -b 127.0.0.1:8500 hg.web:app &
root@bornem:~# systemctl start nginx.service

Nous sommes obligé de « descendre » dans le répertoire vhosts/ car les autres dossiers ne sont pas considérés comme des modules par Python.

On peut passer d'autres paramètres, notamment le nombre de workers (nombre de cœurs de la machine), ou passer par un fichier de configuration.

Gunicorn et systemd ?

La difficulté ici, c'est de pouvoir se déplacer dans le dossier souhaité. La lecture de la documentation, systemd.exec nous apprend, qu'il existe une directive WorkingDirectory, elle ne peut fonctionner que si le type est simple, alors que l'on pourrait s'attendre à mettre forking.

WorkingDirectory=

Takes an absolute directory path. Sets the working directory for executed processes.

En annexe vous trouverez le fichier gunicorn-hg.service, qui reprend la même commande lancée dans une console avec les spécificités de systemd.

Avec Gunicorn, au lieu de passé par un socket TCP/IP, on peut utiliser un socket unix (ou Berkeley socket), le fichier gunicorn-hg.service devient alors :

[Unit]
Description=Gunicorn for mercurial
Requires=local-fs.target syslog.target
After=syslog.target local-fs.target
Before=nginx.service

[Service]
Type=simple
PIDFile=/var/run/gunicorn.pid
WorkingDirectory=/srv/www/htdocs/vhosts
ExecStartPre=/bin/echo 'Starting gunicorn (mercurial)'
ExecStart=/usr/bin/gunicorn -p /var/run/gunicorn.pid -g nginx -u nginx -b unix:/tmp/gunicorn.socket hg.web:app
StandardOutput=syslog
StandardError=syslog
KillMode=process

[Install]
#WantedBy=multi-user.target
WantedBy=graphical.target

Et la partie du serveur :

[...]

    # Subdomain settings
    #
    # Mercurial
    #
    server {
	listen 80;
	server_name hg.errements.net;

	access_log /var/log/nginx/access-hg.log;

	location / {
		root /srv/www/htdocs/vhosts/hg;
		autoindex	off;

		proxy_path	http://unix:/tmp/gunicorn.socket;
		proxy_set_header	Host	$host;
	}
    }

[...]

J'avoue que dans ce cas, je préfère de loin cette configuration, à celle de passer par le script /etc/init.d/after-local. Je trouve que c'est plus « propre », et on exploite davantage systemd.

Notes

[1] Il s'agit d'une « petite image », qui s'affiche dans la barre du navigateur, lorsque l'on consulte une page HTML.

[2] Je vous conseille de lire la documentation concernant les fichiers .service.

[3] Cf. la documentation

[4] Je mets à disposition le fichier python-waitress.spec permettant de construire un RPM (testé uniquement sous openSUSE).