Déployer Mercurial (hg) « derrière » un serveur Web (Nginx) sous openSUSE
Par olivier le lundi 23 janvier 2012, 19:41 - Documentation - #
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.
[...] # 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
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).