Installer la Note Kfet en production
====================================

Cette page détaille comment installer la Note Kfet sur un serveur de production,
dédié uniquement à l'utilisation de la note. On supposera que le serveur tourne
avec un Debian Buster à jour.


Ajout des dépôts buster-backports
---------------------------------

Debian c'est bien, c'est stable, mais les paquets sont vite obsolètes.
En particulier, la version stable de Django dans la version stable de Debian est la
version 1.11, qui n'est plus maintenue par Django depuis déjà quelques mois.
Les versions stables de Django sont les versions 2.2 et 3.2, Debian Bullseye
utilisant la version 2.2.

Afin de permettre à ses utilisateur⋅rices d'utiliser les dernières fonctionnalités de
certains paquets, Debian a créé une distribution particulière, appelée
``buster-backports``. Cette distribution contient des paquets de la distribution
à venir « ``testing`` » (``bullseye`` à l'heure où cette documentation est écrite)
recompilés et réadapter pour fonctionner avec les paquets de la distribution stable
(``buster``). Ce qui nous intéresse est de pouvoir récupérer Django 2.2 depuis
cette distribution, et avoir donc une version maintenue de Django.

Plus de détails sur le wiki de Debian : `<https://wiki.debian.org/fr/Backports>`_.

Pour activer les backports, il suffit d'ajouter dans le fichier ``/etc/apt/sources.list`` :

.. code::

   deb     $MIRROR/debian buster-backports main contrib

où ``$MIRROR`` est votre miroir Debian favori, comme ``http://ftp.debian.org`` ou
``http://mirror.crans.org`` pour ce qui est de la version utilisée en production au BDE
sur les serveurs du Crans. Il suffit ensuite de faire un ``sudo apt update``.
Vérifiez que les paquets sont bien récupérés, en cherchant cette ligne :

.. code::

   Get:4 http://mirror.crans.org/debian buster-backports InRelease [46.7 kB]

.. warning::

   Avis aux futurs respos info : pensez à bien actualiser cette documentation lorsque
   Debian Bullseye sera sorti. En particulier, il ne sera pas déconnant de continuer
   à utiliser non pas buster-backports mais bullseye-backports pour installer la
   note avec Django 3.2 et non Django 2.2.

   Bien sûr, vous testerez sur un serveur qui n'est pas celui utilisé avant :)


Installation des dépendances nécessaires
----------------------------------------

On s'efforce pour récupérer le plus possible de dépendances via les paquets Debian
plutôt que via ``pip`` afin de faciliter les mises à jour et avoir une installation
plus propre. On peut donc installer tout ce dont on a besoin, depuis buster-backports :

.. code:: bash

   $ sudo apt update
   $ sudo apt install -t buster-backports --no-install-recommends \
       gettext git ipython3 \  # Dépendances basiques
       fonts-font-awesome libjs-bootstrap4 \  # Pour l'affichage web
       python3-bs4 python3-django python3-django-crispy-forms python3-django-extensions \
       python3-django-filters python3-django-oauth-toolkit python3-django-polymorphic \
       python3-djangorestframework python3-memcache python3-phonenumbers \
       python3-pil python3-pip python3-psycopg2 python3-setuptools python3-venv \
       texlive-xetex memcached

Ces paquets fournissent une bonne base sur laquelle travailler.

Pour les mettre à jour, il suffit de faire ``sudo apt update`` puis ``sudo apt upgrade``.


Téléchargement de la note
-------------------------

Tout comme en développement, on utilise directement le Gitlab du Crans pour récupérer
les sources.

On suppose que l'on veut cloner le projet dans le dossier ``/var/www/note_kfet``.

On clone donc le dépôt en tant que ``www-data`` :

.. code:: bash

   $ sudo -u www-data git clone https://gitlab.crans.org/bde/nk20.git /var/www/note_kfet

Par défaut, le dépôt est configuré pour suivre la branche ``main``, qui est la branche
stable, notamment installée sur `<https://note.crans.org/>`_. Pour changer de branche,
notamment passer sur la branche ``beta`` sur un serveur de pré-production (un peu comme
`<https://note-dev.crans.org/>`_), on peut faire :

.. code:: bash

   $ sudo -u www-data git checkout beta

.. warning::
   Avis aux successeurs : notamment pour le serveur de production derrière
   `<https://note.crans.org>`_, il peut être intéressant de créer un paquet Python
   de sorte à pouvoir installer la note en faisant directement
   ``pip install git+https://gitlab.crans.org/bde/nk20.git``, et avoir ainsi une
   installation plus propre en décourageant le développement en production.

   Voir par exemple comment le futur site d'inscription du Crans, Constellation,
   gère cela : `<https://gitlab.crans.org/nounous/constellation>`_.


Installation des dépendances Python non présentes dans les dépôts APT
---------------------------------------------------------------------

Même s'il est préférable d'installer les dépendances Python via les paquets APT,
tous ne sont malheureusement pas disponibles.

On doit donc récupérer les dépendances manquantes via pip.

Tout comme en développement, on préfère avoir un environnement virtuel dédié,
les ``sudo pip`` étant rarement compatibles avec les dépendances APT.

On construit donc un environnement virtuel et on installe les dépendances manquantes
dans cet environnement :

.. code:: bash

   $ cd /var/www/note_kfet
   $ python3 -m venv env
   $ . env/bin/activate
   (env) $ pip install -r requirements.txt

Normalement, seules les dépendances manquantes sont installées, les autres sont trouvées
globalement.

Plus d'informations sur les environnements virtuels dans la documentation officielle
de Python : `<https://docs.python.org/fr/3/tutorial/venv.html>`_.


Configuration de la note
------------------------

La configuration de la note se gère essentiellement via des paramètres d'environnement.
Ceux-ci sont lus via le fichier ``.env`` s'il existe, qui doit être placé à la racine
du projet cloné (donc dans ``/var/www/note_kfet/.env``). Un fichier d'exemple est situé
dans le fichier ``.env_example``, on peut donc faire un ``sudo cp .env_example .env``.

Attention aux permissions : le fichier doit être lu par ``www-data`` et écrit (rien
n'empêche de l'écrire en tant que root).

Le contenu de ce fichier :

.. code:: env

   DJANGO_APP_STAGE=prod
   DJANGO_DEV_STORE_METHOD=sqlite
   DJANGO_DB_HOST=localhost
   DJANGO_DB_NAME=note_db
   DJANGO_DB_USER=note
   DJANGO_DB_PASSWORD=CHANGE_ME
   DJANGO_DB_PORT=
   DJANGO_SECRET_KEY=CHANGE_ME
   DJANGO_SETTINGS_MODULE=note_kfet.settings
   CONTACT_EMAIL=tresorerie.bde@localhost
   NOTE_URL=localhost
   NOTE_MAIL=notekfet@localhost
   EMAIL_HOST=smtp.localhost
   EMAIL_PORT=25
   EMAIL_USER=notekfet@localhost
   EMAIL_PASSWORD=CHANGE_ME
   WIKI_USER=NoteKfet2020
   WIKI_PASSWORD=

Le paramètre ``DJANGO_APP_STAGE`` accepte comme valeur ``dev`` ou ``prod``.
En développement, les mails ne sont pas envoyés mais affichés dans les logs du
serveur. Les messages d'erreur sont directement affichés au lieu d'être envoyés
par mail. Les paramètres d'envoi de mail n'ont donc aucun effet. En développement,
il est également possible de choisir si l'on souhaite une base de données sqlite
(par défaut) ou si on veut se connecter à une base de données PostgreSQL (rentrer
``postgres`` dans ``DJANGO_DEV_STORE_METHOD``), auquel cas les paramètres de
base de données seront interprétés.

Les champs ``DJANGO_DB_`` sont relatifs à la connexion à la base de données PostgreSQL.

Le champ ``DJANGO_SECRET_KEY`` est utilisé pour la protection CSRF (voir la documentation
`<https://docs.djangoproject.com/fr/3.2/ref/csrf/>`_ pour plus de détails). Il s'agit d'une
clé sous forme de chaîne de caractère suffisamment longue (64 caractères paraît bien)
qui n'est pas à transmettre et qui évite d'autres sites malveillants de faire des requêtes
directement sur la note.

Le champ ``CONTACT_EMAIL`` correspond l'adresse mail que les adhérent⋅es peuvent contacter
en cas de problème. C'est là où le champ ``Nous contacter`` redirigera.

Le champ ``NOTE_URL`` correspond au nom de domaine autorisé à accéder au site. C'est également
le nom de domaine qui sera utilisé dans l'envoi de mails pour générer des liens. En
production, cela vaut ``note.crans.org``.

Le champ ``NOTE_MAIL`` correspond au champ expéditeur des mails envoyés, que ce soit
pour les rapports quotidiens / hebdomadaires / mensuels ou les mails d'erreur.
En production, ce champ vaut ``notekfet2020@crans.org``.

Les champs ``EMAIL_`` sont relatifs à la connexion au serveur SMTP pour l'envoi de mails.
En production, ``EMAIL_HOST`` vaut ``smtp.crans.org``, ``EMAIL_PORT`` vaut 25 (on reste sur
le réseau interne du Crans) et ``EMAIL_USER`` et ``EMAIL_PASSWORD`` sont vides (ce qui est
valide car la note est sur le réseau du Crans, qui est déjà pré-autorisé à envoyer des mails).

Les champs ``WIKI_USER`` et ``WIKI_PASSWORD`` servent à s'authentifier sur le compte Wiki
Crans ``NoteKfet2020``, pour notamment exporter automatiquement la liste des activités sur
le wiki.

Pour configurer la note, il est également possible de créer un fichier
``note_kfet/settings/secrets.py`` qui redéfinit certains paramètres, notamment la
liste des administrateurs ou certaines applications optionnelles, ou encore certains
éventuels mots de passe.

En production, ce fichier contient :

.. code:: python

   OPTIONAL_APPS = [
      'cas_server',
   #    'debug_toolbar'
   ]

   # When a server error occured, send an email to these addresses
   ADMINS = (
       ('Note Kfet', 'notekfet2020@lists.crans.org'),
   )


Génération d'une clé privé pour OIDC
------------------------------------

Pour pouvoir proposer le service de connexion Openid Connect (OIDC) par OAuth2, il y a
besoin d'une clé privé. Par défaut, elle est cherché dans le fichier `/var/secrets/oidc.key`
(sinon, il faut modifier l'emplacement dans les fichiers de configurations).

Pour générer la clé, il faut aller dans le dossier `/var/secrets` (à créer, si nécessaire) puis
utiliser la commande de génération :

.. code:: bash

   cd /var/secrets
   openssl genrsa -out oidc.key 4096


Configuration des tâches récurrentes
------------------------------------

Certaines opérations se font périodiquement, comme le rappel hebdomadaire pour les
personnes en négatif. On utilise pour cela un cron. Il suffit pour cela de copier
le fichier ``note.cron`` vers ``/etc/cron.d/note``, en veillant à ce qu'il appartienne
bien à ``root``.

Ce fichier contient l'ensemble des tâches récurrentes associées à la note.
Une page de documentation dédiée fera bientôt son apparition.

Sur un serveur de pré-production, on peut ne pas souhaiter activer ces tâches récurrentes.


Installation de la base de données PostgreSQL
---------------------------------------------

En production, on utilise une vraie base de données PostgreSQL et non un fichier
sqlite. Beaucoup plus facile pour faire éventuellement des requêtes (bien que pas
adapté pour Django) mais surtout bien mieux optimisé pour un serveur de production.

Pour installer la base de données, on commence par installer PostgreSQL :

.. code:: bash

   $ sudo apt install --no-install-recommends postgresql postgresql-contrib

PostgreSQL est désormais installé et lancé. On crée un compte ``note``, avec un
bon mot de passe (le même que donné à Django) :

.. code:: bash

   $ sudo -u postgres createuser -P note

Et on crée enfin une base de données nommée ``note_db`` appartenant à ``note``.

.. code:: bash

   $ sudo -u postgres createdb note_db -O note

La base de données est désormais prête à être utilisée.


Finir l'installation de Django
------------------------------

On commence par construire la base de données à partir des migrations enregistrées :

.. code:: bash

   $ ./manage.py migrate

On doit compiler les traductions (pour pouvoir les lire plus vite par la suite) :

.. code:: bash

   $ ./manage.py compilemessages

Les fichiers statiques (fiches de style, fichiers Javascript, images, ...) doivent
être exportées dans le dossier ``static`` :

.. code:: bash

   $ ./manage.py collectstatic

Et on peut enfin importer certaines données de base :

.. code:: bash

   $ ./manage.py loaddata initial

La note est désormais prête à être utilisée. Ne reste qu'à configurer un serveur Web.


Configuration de UWSGI
----------------------

On dispose d'une instance de la note fonctionnelle et bien configurée. Cependant, nous
n'avons pas encore de socket permettant d'intéragir avec le serveur. C'est le travail
de UWSGI.

On rappelle que la commande ``./manage.py runserver`` n'est pas conçue pour des serveurs
de production, contrairement à UWSGI.

On commence par installer UWSGI :

.. code:: bash

   $ sudo apt install --no-install-recommends uwsgi uwsgi-plugin-python3

On place ensuite le fichier de configuration UWSGI dans les applications installées.
Un fichier de configuration est présent à la racine du projet, contenant :

.. code:: ini

   [uwsgi]
   uid             = www-data
   gid             = www-data
   # Django-related settings
   # the base directory (full path)
   chdir           = /var/www/note_kfet
   # the virtualenv (full path)
   home            = /var/www/note_kfet/env
   wsgi-file       = /var/www/note_kfet/note_kfet/wsgi.py
   plugin          = python3
   # process-related settings
   # master
   master          = true
   # maximum number of worker processes
   processes       = 10
   # the socket (use the full path to be safe
   socket          = /var/www/note_kfet/note_kfet.sock
   # ... with appropriate permissions - may be needed
   chmod-socket    = 664
   # clear environment on exit
   vacuum          = true
   # Touch reload
   touch-reload    = /var/www/note_kfet/note_kfet/settings/__init__.py
   # Enable threads
   enable-threads  = true

Il suffit donc de créer le lien symbolique :

.. code:: bash

   $ sudo ln -s /var/www/note_kfet/uwsgi_note.ini /etc/uwsgi/apps-enabled/uwsgi_note.ini

On peut désormais relancer UWSGI :

.. code:: bash

   $ sudo systemctl restart uwsgi


Configuration de NGINX
----------------------

Nous avons désormais un socket qui nous permet de faire des connexions au serveur web,
placé dans ``/var/www/note_kfet/note_kfet.sock``. Cependant, ce socket n'est pas accessible
au reste du monde, et ne doit pas l'être : on veut un serveur Web Nginx qui s'occupe des
connexions entrantes et qui peut servir de reverse-proxy, notamment utile pour desservir
les fichiers statiques ou d'autres sites sur le même serveur.

On commence donc par installer Nginx :

.. code:: bash

   $ sudo apt install nginx

On place ensuite dans ``/etc/nginx/sites-available/nginx_note.conf`` le fichier de
configuration Nginx qui va bien, en remplaçant ``note.crans.org`` par ce qu'il faut :

.. code::

    # the upstream component nginx needs to connect to
    upstream note {
        server unix:///var/www/note_kfet/note_kfet.sock; # file socket
    }

    # Redirect HTTP to nk20 HTTPS
    server {
        listen 80 default_server;
        listen [::]:80 default_server;

        location / {
            return 301 https://note.crans.org$request_uri;
        }
    }

    # Redirect all HTTPS to nk20 HTTPS
    server {
        listen 443 ssl default_server;
        listen [::]:443 ssl default_server;

        location / {
            return 301 https://note.crans.org$request_uri;
        }

        ssl_certificate /etc/letsencrypt/live/note.crans.org/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/note.crans.org/privkey.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    }

    # configuration of the server
    server {
        listen 443 ssl;
        listen [::]:443 ssl;

        # the port your site will be served on
        # the domain name it will serve for
        server_name note.crans.org;
        charset     utf-8;

        # max upload size
        client_max_body_size 75M;

        # Django media
        location /media  {
            alias /var/www/note_kfet/media;
        }

        location /static {
            alias /var/www/note_kfet/static;
        }

        location /doc {
            alias /var/www/documentation;
        }

        # Finally, send all non-media requests to the Django server.
        location / {
            uwsgi_pass note;
            include /etc/nginx/uwsgi_params;
        }

        ssl_certificate /etc/letsencrypt/live/note.crans.org/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/note.crans.org/privkey.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    }

On peut enfin activer le site :

.. code::

   $ sudo ln -s /etc/nginx/sites-available/nginx_note.conf /etc/nginx/sites-enabled/nginx_note.conf

Si on peut se dire que recharger Nginx suffira, il n'en est rien : voir paragraphe suivant.


Génération d'un certificat SSL
------------------------------

Nginx va essayer de lire les certificats présents dans
``/etc/letsencrypt/live/note.crans.org/``, mais ce dossier n'existe pas encore.

On doit donc générer un certificat pour permettre les connexions HTTPS. Cela est permis
grâce à ``certbot``, qu'on s'empresse d'installer :

.. code:: bash

   $ sudo apt install certbot python3-certbot-nginx

Le plugin pour nginx permet de certifier que le serveur a bien les droits pour
``note.crans.org`` grâce à Nginx, le BDE n'ayant a priori aucune raison de pouvoir
gérer le nom de domaine ``crans.org``.

On place dans le dossier ``/etc/letsencrypt/conf.d`` (qu'on crée au besoin) un fichier
nommé ``nk20.ini`` :

.. code:: ini

    # To generate the certificate, please use the following command
    # certbot --config /etc/letsencrypt/conf.d/nk20.ini certonly

    # Use a 4096 bit RSA key instead of 2048
    rsa-key-size = 4096

    # Always use the staging/testing server
    # server = https://acme-staging.api.letsencrypt.org/directory

    # Uncomment and update to register with the specified e-mail address
    email = notekfet2020@lists.crans.org

    # Uncomment to use a text interface instead of ncurses
    text = True

    # Use Nginx challenge
    authenticator = nginx

En exécutant ``certbot``, il va lire les fichiers de configuration Nginx et générer les
certificats qu'il faut en créant un point d'entrée pour le serveur.

Il faut néanmoins que la configuration soit valide. Les certificats n'existant pas encore,
la configuration nginx est donc pour l'instant invalide. Il faut alors temporairement
commenter les parties de la configuration qui traitent des certificats et relancer ``nginx``
(``sudo systemctl reload nginx``).

On peut ensuite exécuter ``certbot`` :

.. code:: bash

   $ certbot --config /etc/letsencrypt/conf.d/nk20.ini certonly

L'instruction ``certonly`` indique à ``certbot`` qu'il se contente de générer le certificat,
sans chercher à l'installer. Si tout s'est bien passé, l'installation se fait simplement
en décommentant les lignes préalablement commentées.

Un certificat généré de la sorte expire au bout de 3 mois. Néanmoins, certbot tourne
régulièrement pour renouveler les certificats actifs automatiquement. Il n'y a donc
plus rien à faire.

Après avoir rechargé la configuration de ``Nginx``, rendez-vous sur
`<https://note.crans.org>`_ (ou votre site) pour vérifier que tout fonctionne correctement :)


Mettre à jour la note
---------------------

Pour mettre à jour la note, il suffit a priori, après avoir mis à jour les paquets APT,
de faire un ``git pull`` dans le dossier ``/var/www/note_kfet``.

Les éventuelles nouvelles migrations de la base de données doivent être appliquées :

.. code:: bash

   $ ./manage.py migrate

Les nouvelles traductions compilées :

.. code:: bash

   $ ./manage.py compilemessages

Les nouveaux fichiers statiques collectés :

.. code:: bash

   $ ./manage.py collectstatic

Et enfin les nouvelles fixtures installées :

.. code:: bash

   $ ./manage.py loaddata initial

Une fois tout cela fait, il suffit de relancer le serveur UWSGI :

.. code:: bash

   $ sudo systemctl restart uwsgi


Avec Ansible
------------

Tout ce travail peut sembler très laborieux et peut mériter d'être automatisé.
Toutefois, il est essentiel de bien comprendre comment chaque étape de l'installation
fonctionne.

Un playbook Ansible a été écrit permettant de réaliser toutes les tâches décrites ci-dessus.
Il se trouve dans le dossier ``ansible``.

Ansible s'installe sur votre propre machine (et non sur le serveur) en installant simplement
le paquet ``ansible``.

Pour déployer la note sur un serveur vierge, commencez par copier le fichier ``hosts_example``
en le nommant ``hosts``. Ajoutez votre propre serveur, dans la section correspondante.

Dans le dossier ``host_vars``, créez un fichier dont le nom est l'adresse du serveur, avec
l'extension ``.yml``.

Dans ce fichier, remplissez :

.. code:: yaml

   ---
   note:
     server_name: note.crans.org
     git_branch: main
     cron_enabled: true
     email: notekfet2020@lists.crans.org


en adaptant à votre configuration.

Il suffit ensuite de lancer ``./base.yml -l urldevotreserveur``.

Pour une première installation, vous devrez renseigner le mot de passe de la base de données
pour créer le compte ``note``. Vous devrez ensuite également refaire quelques ajustements
pour générer le certificat, voir la partie ``certbot``. La configuration du fichier
``.env`` sera également à faire à la main.

Cependant, pour mettre à jour, lancer cette commande suffit.


Copier une base de données
--------------------------

On peut vouloir périodiquement copier la base de données de production vers le serveur
de développement, afin de travailler avec des données à jour.

On aura besoin de pouvoir accéder aux deux bases de données. On commence donc si ce n'est
pas déjà fait par créer un utilisateur sur les deux serveurs :

.. code:: bash

   ynerant@bde-note:~$ sudo -u postgres createuser -s ynerant

On réinitialise **sur le serveur de développement** la base de données présente, en
éteignant tout d'abord le serveur Web :

.. code:: bash

   ynerant@bde-note-dev:~$ sudo systemctl stop uwsgi
   ynerant@bde-note-dev:~$ sudo -u postgres dropdb note_db
   ynerant@bde-note-dev:~$ sudo -u postgres createdb -O note note_db

Et on copie enfin la base de données, en une seule ligne via SSH :

.. code:: bash

   ynerant@bde-note:~$ pg_dump note_db | ssh note-dev.crans.org "psql note_db -f -"

On peut enfin redémarrer le serveur Web. Les données ont bien été copiées.

.. caution::

   On ne copiera **jamais** des données d'adhérent⋅es sur une machine personnelle.
   Ce type d'opération doit s'effectuer impérativement entre des serveurs du BDE.
