diff --git a/asciidoc-to-tex.tex b/asciidoc-to-tex.tex deleted file mode 100644 index cdd0380..0000000 --- a/asciidoc-to-tex.tex +++ /dev/null @@ -1,1131 +0,0 @@ - - - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{(}\ExtensionTok{gwift{-}env}\KeywordTok{)} \ExtensionTok{fred@aerys}\NormalTok{:\textasciitilde{}/Sources/gwift$ tree .} -\ExtensionTok{.} -\NormalTok{├── }\ExtensionTok{gwift} -\NormalTok{│   ├── }\ExtensionTok{\_\_init\_\_.py} -\NormalTok{│   ├── }\ExtensionTok{asgi.py} -\NormalTok{│   ├── }\ExtensionTok{settings.py} -\NormalTok{│   ├── }\ExtensionTok{urls.py} -\NormalTok{│   ├── }\ExtensionTok{wish} -\NormalTok{│   │   ├── }\ExtensionTok{\_\_init\_\_.py} -\NormalTok{│   │   ├── }\ExtensionTok{admin.py} -\NormalTok{│   │   ├── }\ExtensionTok{apps.py} -\NormalTok{│   │   ├── }\ExtensionTok{migrations} -\NormalTok{│   │   │   └── }\ExtensionTok{\_\_init\_\_.py} -\NormalTok{│   │   ├── }\ExtensionTok{models.py} -\NormalTok{│   │   ├── }\ExtensionTok{tests.py} -\NormalTok{│   │   └── }\ExtensionTok{views.py} -\NormalTok{│   └── }\ExtensionTok{wsgi.py} -\NormalTok{├── }\ExtensionTok{Makefile} -\NormalTok{├── }\ExtensionTok{manage.py} -\NormalTok{├── }\ExtensionTok{README.md} -\NormalTok{├── }\ExtensionTok{requirements} -\NormalTok{│   ├── }\ExtensionTok{base.txt} -\NormalTok{│   ├── }\ExtensionTok{dev.txt} -\NormalTok{│   └── }\ExtensionTok{prod.txt} -\NormalTok{├── }\ExtensionTok{setup.cfg} -\NormalTok{└── }\ExtensionTok{tox.ini} - -\ExtensionTok{5}\NormalTok{ directories, 22 files} -\end{Highlighting} -\end{Shaded} - - - -\hypertarget{_structure_finale_de_notre_environnement}{% -\subsection{Structure finale de notre -environnement}\label{_structure_finale_de_notre_environnement}} - - - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{(}\ExtensionTok{gwift{-}env}\KeywordTok{)} \ExtensionTok{fred@aerys}\NormalTok{:\textasciitilde{}/Sources/gwift$ tree .} -\ExtensionTok{.} -\NormalTok{├── }\ExtensionTok{gwift} -\NormalTok{│   ├── }\ExtensionTok{\_\_init\_\_.py} -\NormalTok{│   ├── }\ExtensionTok{asgi.py} -\NormalTok{│   ├── }\ExtensionTok{settings.py} -\NormalTok{│   ├── }\ExtensionTok{urls.py} -\NormalTok{│   ├── }\ExtensionTok{wish} -\NormalTok{│   │   ├── }\ExtensionTok{\_\_init\_\_.py} -\NormalTok{│   │   ├── }\ExtensionTok{admin.py} -\NormalTok{│   │   ├── }\ExtensionTok{apps.py} -\NormalTok{│   │   ├── }\ExtensionTok{migrations} -\NormalTok{│   │   │   └── }\ExtensionTok{\_\_init\_\_.py} -\NormalTok{│   │   ├── }\ExtensionTok{models.py} -\NormalTok{│   │   ├── }\ExtensionTok{tests.py} -\NormalTok{│   │   └── }\ExtensionTok{views.py} -\NormalTok{│   └── }\ExtensionTok{wsgi.py} -\NormalTok{├── }\ExtensionTok{Makefile} -\NormalTok{├── }\ExtensionTok{manage.py} -\NormalTok{├── }\ExtensionTok{README.md} -\NormalTok{├── }\ExtensionTok{requirements} -\NormalTok{│   ├── }\ExtensionTok{base.txt} -\NormalTok{│   ├── }\ExtensionTok{dev.txt} -\NormalTok{│   └── }\ExtensionTok{prod.txt} -\NormalTok{├── }\ExtensionTok{setup.cfg} -\NormalTok{└── }\ExtensionTok{tox.ini} -\end{Highlighting} -\end{Shaded} - - - -\hypertarget{_authentification}{% -\section{Authentification}\label{_authentification}} - - -\begin{Shaded} -\begin{Highlighting}[] -\ImportTok{from}\NormalTok{ datetime }\ImportTok{import}\NormalTok{ datetime} - -\ImportTok{from}\NormalTok{ django.contrib.auth }\ImportTok{import}\NormalTok{ backends, get\_user\_model} -\ImportTok{from}\NormalTok{ django.db.models }\ImportTok{import}\NormalTok{ Q} - -\ImportTok{from}\NormalTok{ accounts.models }\ImportTok{import}\NormalTok{ Token } - - -\NormalTok{UserModel }\OperatorTok{=}\NormalTok{ get\_user\_model()} - - -\KeywordTok{class}\NormalTok{ TokenBackend(backends.ModelBackend):} - \KeywordTok{def}\NormalTok{ authenticate(}\VariableTok{self}\NormalTok{, request, username}\OperatorTok{=}\VariableTok{None}\NormalTok{, password}\OperatorTok{=}\VariableTok{None}\NormalTok{, }\OperatorTok{**}\NormalTok{kwargs):} - \CommentTok{"""Authentifie l\textquotesingle{}utilisateur sur base d\textquotesingle{}un jeton qu\textquotesingle{}il a reçu.} - -\CommentTok{ On regarde la date de validité de chaque jeton avant d\textquotesingle{}autoriser l\textquotesingle{}accès.} -\CommentTok{ """} -\NormalTok{ token }\OperatorTok{=}\NormalTok{ kwargs.get(}\StringTok{"token"}\NormalTok{, }\VariableTok{None}\NormalTok{)} - -\NormalTok{ current\_token }\OperatorTok{=}\NormalTok{ Token.objects.}\BuiltInTok{filter}\NormalTok{(token}\OperatorTok{=}\NormalTok{token, validity\_date\_\_gte}\OperatorTok{=}\NormalTok{datetime.now()).first()} - - \ControlFlowTok{if}\NormalTok{ current\_token:} -\NormalTok{ user }\OperatorTok{=}\NormalTok{ current\_token.user} - -\NormalTok{ current\_token.last\_used\_date }\OperatorTok{=}\NormalTok{ datetime.now()} -\NormalTok{ current\_token.save()} - - \ControlFlowTok{return}\NormalTok{ user} - - \ControlFlowTok{return} \VariableTok{None} -\end{Highlighting} -\end{Shaded} - -\begin{itemize} -\item - Sous-entend qu'on a bien une classe qui permet d'accéder à ces jetons - ;-) -\end{itemize} - -\begin{Shaded} -\begin{Highlighting}[] -\ImportTok{from}\NormalTok{ django.contrib.auth }\ImportTok{import}\NormalTok{ backends, get\_user\_model} - -\ImportTok{from}\NormalTok{ ldap3 }\ImportTok{import}\NormalTok{ Server, Connection, ALL} -\ImportTok{from}\NormalTok{ ldap3.core.exceptions }\ImportTok{import}\NormalTok{ LDAPPasswordIsMandatoryError} - -\ImportTok{from}\NormalTok{ config }\ImportTok{import}\NormalTok{ settings} - - -\NormalTok{UserModel }\OperatorTok{=}\NormalTok{ get\_user\_model()} - - -\KeywordTok{class}\NormalTok{ LdapBackend(backends.ModelBackend):} - \CommentTok{"""Implémentation du backend LDAP pour la connexion des utilisateurs à l\textquotesingle{}Active Directory.} -\CommentTok{ """} - \KeywordTok{def}\NormalTok{ authenticate(}\VariableTok{self}\NormalTok{, request, username}\OperatorTok{=}\VariableTok{None}\NormalTok{, password}\OperatorTok{=}\VariableTok{None}\NormalTok{, }\OperatorTok{**}\NormalTok{kwargs):} - \CommentTok{"""Authentifie l\textquotesingle{}utilisateur au travers du serveur LDAP.} -\CommentTok{ """} - -\NormalTok{ ldap\_server }\OperatorTok{=}\NormalTok{ Server(settings.LDAP\_SERVER, get\_info}\OperatorTok{=}\NormalTok{ALL)} -\NormalTok{ ldap\_connection }\OperatorTok{=}\NormalTok{ Connection(ldap\_server, user}\OperatorTok{=}\NormalTok{username, password}\OperatorTok{=}\NormalTok{password)} - - \ControlFlowTok{try}\NormalTok{:} - \ControlFlowTok{if} \KeywordTok{not}\NormalTok{ ldap\_connection.bind():} - \ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Login ou mot de passe incorrect"}\NormalTok{)} - \ControlFlowTok{except}\NormalTok{ (LDAPPasswordIsMandatoryError, }\PreprocessorTok{ValueError}\NormalTok{) }\ImportTok{as}\NormalTok{ ldap\_exception:} - \ControlFlowTok{raise}\NormalTok{ ldap\_exception} - -\NormalTok{ user, \_ }\OperatorTok{=}\NormalTok{ UserModel.objects.get\_or\_create(username}\OperatorTok{=}\NormalTok{username)} -\end{Highlighting} -\end{Shaded} - -On peut résumer le mécanisme d'authentification de la manière suivante: - -\begin{itemize} -\item - Si vous voulez modifier les informations liées à un utilisateur, - orientez-vous vers la modification du modèle. Comme nous le verrons - ci-dessous, il existe trois manières de prendre ces modifications en - compte. Voir également - \href{https://docs.djangoproject.com/en/stable/topics/auth/customizing/}{ici}. -\item - Si vous souhaitez modifier la manière dont l'utilisateur se connecte, - alors vous devrez modifier le \textbf{backend}. -\end{itemize} - -\hypertarget{_modification_du_moduxe8le}{% -\subsection{Modification du modèle}\label{_modification_du_moduxe8le}} - -Dans un premier temps, Django a besoin de manipuler -\href{https://docs.djangoproject.com/en/1.9/ref/contrib/auth/\#user-model}{des -instances de type \texttt{django.contrib.auth.User}}. Cette classe -implémente les champs suivants: - -\begin{itemize} -\item - \texttt{username} -\item - \texttt{first\_name} -\item - \texttt{last\_name} -\item - \texttt{email} -\item - \texttt{password} -\item - \texttt{date\_joined}. -\end{itemize} - -D'autres champs, comme les groupes auxquels l'utilisateur est associé, -ses permissions, savoir s'il est un super-utilisateur, -\ldots\hspace{0pt} sont moins pertinents pour le moment. Avec les -quelques champs déjà définis ci-dessus, nous avons de quoi identifier -correctement nos utilisateurs. Inutile d'implémenter nos propres -classes, puisqu'elles existent déjà :-) - -Si vous souhaitez ajouter un champ, il existe trois manières de faire. - -\hypertarget{_extension_du_moduxe8le_existant}{% -\subsection{Extension du modèle -existant}\label{_extension_du_moduxe8le_existant}} - -Le plus simple consiste à créer une nouvelle classe, et à faire un lien -de type \texttt{OneToOne} vers la classe -\texttt{django.contrib.auth.User}. De cette manière, on ne modifie rien -à la manière dont Django authentife ses utlisateurs: tout ce qu'on fait, -c'est un lien vers une table nouvellement créée, comme on l'a déjà vu au -point {[}\ldots\hspace{0pt}voir l'héritage de modèle{]}. L'avantage de -cette méthode, c'est qu'elle est extrêmement flexible, et qu'on garde -les mécanismes Django standard. Le désavantage, c'est que pour avoir -toutes les informations de notre utilisateur, on sera obligé d'effectuer -une jointure sur le base de données, ce qui pourrait avoir des -conséquences sur les performances. - -\hypertarget{_substitution}{% -\subsection{Substitution}\label{_substitution}} - -Avant de commencer, sachez que cette étape doit être effectuée -\textbf{avant la première migration}. Le plus simple sera de définir une -nouvelle classe héritant de \texttt{django.contrib.auth.User} et de -spécifier la classe à utiliser dans votre fichier de paramètres. Si ce -paramètre est modifié après que la première migration ait été effectuée, -il ne sera pas pris en compte. Tenez-en compte au moment de modéliser -votre application. - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{AUTH\_USER\_MODEL }\OperatorTok{=} \StringTok{\textquotesingle{}myapp.MyUser\textquotesingle{}} -\end{Highlighting} -\end{Shaded} - -Notez bien qu'il ne faut pas spécifier le package \texttt{.models} dans -cette injection de dépendances: le schéma à indiquer est bien -\texttt{\textless{}nom\ de\ l’application\textgreater{}.\textless{}nom\ de\ la\ classe\textgreater{}}. - -\hypertarget{_backend}{% -\subsubsection{Backend}\label{_backend}} - -\hypertarget{_templates}{% -\subsubsection{Templates}\label{_templates}} - -Ce qui n'existe pas par contre, ce sont les vues. Django propose donc -tout le mécanisme de gestion des utilisateurs, excepté le visuel (hors -administration). En premier lieu, ces paramètres sont fixés dans le -fichier `settings -\textless{}\url{https://docs.djangoproject.com/en/1.8/ref/settings/\#auth\%3E\%60_}. -On y trouve par exemple les paramètres suivants: - -\begin{itemize} -\item - \texttt{LOGIN\_REDIRECT\_URL}: si vous ne spécifiez pas le paramètre - \texttt{next}, l'utilisateur sera automatiquement redirigé vers cette - page. -\item - \texttt{LOGIN\_URL}: l'URL de connexion à utiliser. Par défaut, - l'utilisateur doit se rendre sur la page \texttt{/accounts/login}. -\end{itemize} - -\hypertarget{_social_authentification}{% -\subsubsection{Social-Authentification}\label{_social_authentification}} - -Voir ici : \href{https://github.com/omab/python-social-auth}{python -social auth} - -\hypertarget{_un_petit_mot_sur_oauth}{% -\subsubsection{Un petit mot sur OAuth}\label{_un_petit_mot_sur_oauth}} - -OAuth est un standard libre définissant un ensemble de méthodes à -implémenter pour l'accès (l'autorisation) à une API. Son fonctionnement -se base sur un système de jetons (Tokens), attribués par le possesseur -de la ressource à laquelle un utilisateur souhaite accéder. - -Le client initie la connexion en demandant un jeton au serveur. Ce jeton -est ensuite utilisée tout au long de la connexion, pour accéder aux -différentes ressources offertes par ce serveur. `wikipedia -\textless{}\url{http://en.wikipedia.org/wiki/OAuth\%3E\%60_}. - -Une introduction à OAuth est -\href{http://hueniverse.com/oauth/guide/intro/}{disponible ici}. Elle -introduit le protocole comme étant une \texttt{valet\ key}, une clé que -l'on donne à la personne qui va garer votre voiture pendant que vous -profitez des mondanités. Cette clé donne un accès à votre voiture, tout -en bloquant un ensemble de fonctionnalités. Le principe du protocole est -semblable en ce sens: vous vous réservez un accès total à une API, -tandis que le système de jetons permet d'identifier une personne, tout -en lui donnant un accès restreint à votre application. - -L'utilisation de jetons permet notamment de définir une durée -d'utilisation et une portée d'utilisation. L'utilisateur d'un service A -peut par exemple autoriser un service B à accéder à des ressources qu'il -possède, sans pour autant révéler son nom d'utilisateur ou son mot de -passe. - -L'exemple repris au niveau du -\href{http://hueniverse.com/oauth/guide/workflow/}{workflow} est le -suivant : un utilisateur(trice), Jane, a uploadé des photos sur le site -faji.com (A). Elle souhaite les imprimer au travers du site beppa.com -(B). Au moment de la commande, le site beppa.com envoie une demande au -site faji.com pour accéder aux ressources partagées par Jane. Pour cela, -une nouvelle page s'ouvre pour l'utilisateur, et lui demande -d'introduire sa "pièce d'identité". Le site A, ayant reçu une demande de -B, mais certifiée par l'utilisateur, ouvre alors les ressources et lui -permet d'y accéder. - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [} - \StringTok{"django.contrib..."} -\NormalTok{]} -\end{Highlighting} -\end{Shaded} - -peut être splitté en plusieurs parties: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [} - -\NormalTok{]} - -\NormalTok{THIRD\_PARTIES }\OperatorTok{=}\NormalTok{ [} - -\NormalTok{]} - -\NormalTok{MY\_APPS }\OperatorTok{=}\NormalTok{ [} - - - -\hypertarget{_logging_2}{% -\subsection{Logging}\label{_logging_2}} - -\hypertarget{_muxe9thode_de_duxe9ploiement}{% -\section{Méthode de déploiement}\label{_muxe9thode_de_duxe9ploiement}} - - -\hypertarget{_duxe9ploiement_sur_debian}{% -\section{Déploiement sur Debian}\label{_duxe9ploiement_sur_debian}} - - -\begin{Shaded} -\begin{Highlighting}[] -\ExtensionTok{apt}\NormalTok{ update} -\ExtensionTok{groupadd}\NormalTok{ {-}{-}system webapps } -\ExtensionTok{groupadd}\NormalTok{ {-}{-}system gunicorn\_sockets } -\ExtensionTok{useradd}\NormalTok{ {-}{-}system {-}{-}gid webapps {-}{-}shell /bin/bash {-}{-}home /home/gwift gwift } -\FunctionTok{mkdir}\NormalTok{ {-}p /home/gwift } -\FunctionTok{chown}\NormalTok{ gwift:webapps /home/gwift } -\end{Highlighting} -\end{Shaded} - -\begin{itemize} -\item - On ajoute un groupe intitulé \texttt{webapps} -\item - On crée un groupe pour les communications via sockets -\item - On crée notre utilisateur applicatif; ses applications seront placées - dans le répertoire \texttt{/home/gwift} -\item - On crée le répertoire home/gwift -\item - On donne les droits sur le répertoire /home/gwift -\end{itemize} - -\hypertarget{_installation_des_duxe9pendances_systuxe8mes}{% -\subsection{Installation des dépendances -systèmes}\label{_installation_des_duxe9pendances_systuxe8mes}} - -La version 3.6 de Python se trouve dans les dépôts officiels de CentOS. -Si vous souhaitez utiliser une version ultérieure, il suffit de -l'installer en parallèle de la version officiellement supportée par -votre distribution. - -Pour CentOS, vous avez donc deux possibilités : - -\begin{Shaded} -\begin{Highlighting}[] -\ExtensionTok{yum}\NormalTok{ install python36 {-}y} -\end{Highlighting} -\end{Shaded} - -Ou passer par une installation alternative: - -\begin{Shaded} -\begin{Highlighting}[] -\FunctionTok{sudo}\NormalTok{ yum {-}y groupinstall }\StringTok{"Development Tools"} -\FunctionTok{sudo}\NormalTok{ yum {-}y install openssl{-}devel bzip2{-}devel libffi{-}devel} - -\FunctionTok{wget}\NormalTok{ https://www.python.org/ftp/python/3.8.2/Python{-}3.8.2.tgz} -\BuiltInTok{cd}\NormalTok{ Python{-}3.8*/} -\ExtensionTok{./configure}\NormalTok{ {-}{-}enable{-}optimizations} -\FunctionTok{sudo}\NormalTok{ make altinstall } -\end{Highlighting} -\end{Shaded} - -\begin{itemize} -\item - \textbf{Attention !} Le paramètre \texttt{altinstall} est primordial. - Sans lui, vous écraserez l'interpréteur initialement supporté par la - distribution, et cela pourrait avoir des effets de bord non souhaités. -\end{itemize} - - - -\hypertarget{_configuration_de_lapplication}{% -\subsection{Configuration de -l'application}\label{_configuration_de_lapplication}} - -\begin{Shaded} -\begin{Highlighting}[] -\VariableTok{SECRET\_KEY=}\OperatorTok{\textless{}}\KeywordTok{set} \ExtensionTok{your}\NormalTok{ secret key here}\OperatorTok{\textgreater{}} -\VariableTok{ALLOWED\_HOSTS=}\ExtensionTok{*} -\VariableTok{STATIC\_ROOT=}\NormalTok{/var/www/gwift/static} -\VariableTok{DATABASE=} -\end{Highlighting} -\end{Shaded} - -\begin{itemize} -\item - La variable \texttt{SECRET\_KEY} est notamment utilisée pour le - chiffrement des sessions. -\item - On fait confiance à django\_environ pour traduire la chaîne de - connexion à la base de données. -\end{itemize} - -\hypertarget{_cruxe9ation_des_ruxe9pertoires_de_logs}{% -\subsection{Création des répertoires de -logs}\label{_cruxe9ation_des_ruxe9pertoires_de_logs}} - -\begin{verbatim} -mkdir -p /var/www/gwift/static -\end{verbatim} - -\hypertarget{_cruxe9ation_du_ruxe9pertoire_pour_le_socket}{% -\subsection{Création du répertoire pour le -socket}\label{_cruxe9ation_du_ruxe9pertoire_pour_le_socket}} - -Dans le fichier \texttt{/etc/tmpfiles.d/gwift.conf}: - -\begin{verbatim} -D /var/run/webapps 0775 gwift gunicorn_sockets - -\end{verbatim} - -Suivi de la création par systemd : - -\begin{verbatim} -systemd-tmpfiles --create -\end{verbatim} - -\hypertarget{_gunicorn}{% -\subsection{Gunicorn}\label{_gunicorn}} - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\#!/bin/bash} - -\CommentTok{\# defines settings for gunicorn} -\VariableTok{NAME=}\StringTok{"gwift"} -\VariableTok{DJANGODIR=}\NormalTok{/home/gwift/webapps/gwift} -\VariableTok{SOCKFILE=}\NormalTok{/var/run/webapps/gunicorn\_gwift.sock} -\VariableTok{USER=}\NormalTok{gwift} -\VariableTok{GROUP=}\NormalTok{gunicorn\_sockets} -\VariableTok{NUM\_WORKERS=}\NormalTok{5} -\VariableTok{DJANGO\_SETTINGS\_MODULE=}\NormalTok{config.settings\_production} -\VariableTok{DJANGO\_WSGI\_MODULE=}\NormalTok{config.wsgi} - -\BuiltInTok{echo} \StringTok{"Starting }\VariableTok{$NAME}\StringTok{ as }\KeywordTok{\textasciigrave{}}\FunctionTok{whoami}\KeywordTok{\textasciigrave{}}\StringTok{"} - -\BuiltInTok{source}\NormalTok{ /home/gwift/.venvs/gwift/bin/activate} -\BuiltInTok{cd} \VariableTok{$DJANGODIR} -\BuiltInTok{export} \VariableTok{DJANGO\_SETTINGS\_MODULE=$DJANGO\_SETTINGS\_MODULE} -\BuiltInTok{export} \VariableTok{PYTHONPATH=$DJANGODIR}\NormalTok{:}\VariableTok{$PYTHONPATH} - -\BuiltInTok{exec}\NormalTok{ gunicorn }\VariableTok{$\{DJANGO\_WSGI\_MODULE\}}\NormalTok{:application }\KeywordTok{\textbackslash{}} -\ExtensionTok{{-}{-}name} \VariableTok{$NAME} \KeywordTok{\textbackslash{}} -\ExtensionTok{{-}{-}workers} \VariableTok{$NUM\_WORKERS} \KeywordTok{\textbackslash{}} -\ExtensionTok{{-}{-}user} \VariableTok{$USER} \KeywordTok{\textbackslash{}} -\ExtensionTok{{-}{-}bind}\NormalTok{=unix:}\VariableTok{$SOCKFILE} \KeywordTok{\textbackslash{}} -\ExtensionTok{{-}{-}log{-}level}\NormalTok{=debug }\KeywordTok{\textbackslash{}} -\ExtensionTok{{-}{-}log{-}file}\NormalTok{={-}} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_supervision_keep_alive_et_autoreload}{% -\subsection{Supervision, keep-alive et -autoreload}\label{_supervision_keep_alive_et_autoreload}} - -Pour la supervision, on passe par Supervisor. Il existe d'autres -superviseurs, - -\begin{Shaded} -\begin{Highlighting}[] -\ExtensionTok{yum}\NormalTok{ install supervisor {-}y} -\end{Highlighting} -\end{Shaded} - -On crée ensuite le fichier \texttt{/etc/supervisord.d/gwift.ini}: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{[}\ExtensionTok{program}\NormalTok{:gwift]} -\VariableTok{command=}\NormalTok{/home/gwift/bin/start\_gunicorn.sh} -\VariableTok{user=}\NormalTok{gwift} -\VariableTok{stdout\_logfile=}\NormalTok{/var/log/gwift/gwift.log} -\VariableTok{autostart=}\NormalTok{true} -\VariableTok{autorestart=}\NormalTok{unexpected} -\VariableTok{redirect\_stdout=}\NormalTok{true} -\VariableTok{redirect\_stderr=}\NormalTok{true} -\end{Highlighting} -\end{Shaded} - -Et on crée les répertoires de logs, on démarre supervisord et on vérifie -qu'il tourne correctement: - -\begin{Shaded} -\begin{Highlighting}[] -\FunctionTok{mkdir}\NormalTok{ /var/log/gwift} -\FunctionTok{chown}\NormalTok{ gwift:nagios /var/log/gwift} - -\ExtensionTok{systemctl}\NormalTok{ enable supervisord} -\ExtensionTok{systemctl}\NormalTok{ start supervisord.service} -\ExtensionTok{systemctl}\NormalTok{ status supervisord.service} -\NormalTok{● }\ExtensionTok{supervisord.service}\NormalTok{ {-} Process Monitoring and Control Daemon} - \ExtensionTok{Loaded}\NormalTok{: loaded (/usr/lib/systemd/system/supervisord.service}\KeywordTok{;} \ExtensionTok{enabled}\KeywordTok{;} \ExtensionTok{vendor}\NormalTok{ preset: disabled)} - \ExtensionTok{Active}\NormalTok{: active (running) }\ExtensionTok{since}\NormalTok{ Tue 2019{-}12{-}24 10:08:09 CET}\KeywordTok{;} \ExtensionTok{10s}\NormalTok{ ago} - \ExtensionTok{Process}\NormalTok{: 2304 ExecStart=/usr/bin/supervisord {-}c /etc/supervisord.conf (code=exited, status=0/SUCCESS)} - \ExtensionTok{Main}\NormalTok{ PID: 2310 (supervisord)} - \ExtensionTok{CGroup}\NormalTok{: /system.slice/supervisord.service} -\NormalTok{ ├─}\ExtensionTok{2310}\NormalTok{ /usr/bin/python /usr/bin/supervisord {-}c /etc/supervisord.conf} -\NormalTok{ ├─}\ExtensionTok{2313}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} -\NormalTok{ ├─}\ExtensionTok{2317}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} -\NormalTok{ ├─}\ExtensionTok{2318}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} -\NormalTok{ ├─}\ExtensionTok{2321}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} -\NormalTok{ ├─}\ExtensionTok{2322}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} -\NormalTok{ └─}\ExtensionTok{2323}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} -\FunctionTok{ls}\NormalTok{ /var/run/webapps/} -\end{Highlighting} -\end{Shaded} - -On peut aussi vérifier que l'application est en train de tourner, à -l'aide de la commande \texttt{supervisorctl}: - -\begin{Shaded} -\begin{Highlighting}[] -\VariableTok{$$}\NormalTok{$ }\ExtensionTok{supervisorctl}\NormalTok{ status gwift} -\ExtensionTok{gwift}\NormalTok{ RUNNING pid 31983, uptime 0:01:00} -\end{Highlighting} -\end{Shaded} - -Et pour gérer le démarrage ou l'arrêt, on peut passer par les commandes -suivantes: - -\begin{Shaded} -\begin{Highlighting}[] -\VariableTok{$$}\NormalTok{$ }\ExtensionTok{supervisorctl}\NormalTok{ stop gwift} -\ExtensionTok{gwift}\NormalTok{: stopped} -\ExtensionTok{root@ks3353535}\NormalTok{:/etc/supervisor/conf.d\# supervisorctl start gwift} -\ExtensionTok{gwift}\NormalTok{: started} -\ExtensionTok{root@ks3353535}\NormalTok{:/etc/supervisor/conf.d\# supervisorctl restart gwift} -\ExtensionTok{gwift}\NormalTok{: stopped} -\ExtensionTok{gwift}\NormalTok{: started} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_configuration_du_firewall_et_ouverture_des_ports}{% -\subsection{Configuration du firewall et ouverture des -ports}\label{_configuration_du_firewall_et_ouverture_des_ports}} - -\begin{verbatim} -et 443 (HTTPS). -\end{verbatim} - -\begin{verbatim} -firewall-cmd --permanent --zone=public --add-service=http -firewall-cmd --permanent --zone=public --add-service=https -firewall-cmd --reload -\end{verbatim} - -\begin{itemize} -\item - On ouvre le port 80, uniquement pour autoriser une connexion HTTP, - mais qui sera immédiatement redirigée vers HTTPS -\item - Et le port 443 (forcément). -\end{itemize} - -\hypertarget{_installation_dnginx}{% -\subsection{Installation d'Nginx}\label{_installation_dnginx}} - -\begin{verbatim} -yum install nginx -y -usermod -a -G gunicorn_sockets nginx -\end{verbatim} - -On configure ensuite le fichier \texttt{/etc/nginx/conf.d/gwift.conf}: - -\begin{verbatim} -upstream gwift_app { - server unix:/var/run/webapps/gunicorn_gwift.sock fail_timeout=0; -} - -server { - listen 80; - server_name ; - root /var/www/gwift; - error_log /var/log/nginx/gwift_error.log; - access_log /var/log/nginx/gwift_access.log; - - client_max_body_size 4G; - keepalive_timeout 5; - - gzip on; - gzip_comp_level 7; - gzip_proxied any; - gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; - - - location /static/ { - access_log off; - expires 30d; - add_header Pragma public; - add_header Cache-Control "public"; - add_header Vary "Accept-Encoding"; - try_files $uri $uri/ =404; - } - - location / { - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_redirect off; - - proxy_pass http://gwift_app; - } -} -\end{verbatim} - -\begin{itemize} -\item - Ce répertoire sera complété par la commande \texttt{collectstatic} que - l'on verra plus tard. L'objectif est que les fichiers ne demandant - aucune intelligence soit directement servis par Nginx. Cela évite - d'avoir un processus Python (relativement lent) qui doive être - instancié pour servir un simple fichier statique. -\item - Afin d'éviter que Django ne reçoive uniquement des requêtes provenant - de 127.0.0.1 -\end{itemize} - -\hypertarget{_mise_uxe0_jour}{% -\subsection{Mise à jour}\label{_mise_uxe0_jour}} - -Script de mise à jour. - -\begin{Shaded} -\begin{Highlighting}[] -\FunctionTok{su}\NormalTok{ {-} }\OperatorTok{\textless{}}\NormalTok{user}\OperatorTok{\textgreater{}} -\BuiltInTok{source}\NormalTok{ \textasciitilde{}/.venvs/}\OperatorTok{\textless{}}\NormalTok{app}\OperatorTok{\textgreater{}}\NormalTok{/bin/activate} -\BuiltInTok{cd}\NormalTok{ \textasciitilde{}/webapps/}\OperatorTok{\textless{}}\NormalTok{app}\OperatorTok{\textgreater{}} -\FunctionTok{git}\NormalTok{ fetch} -\FunctionTok{git}\NormalTok{ checkout vX.Y.Z} -\ExtensionTok{pip}\NormalTok{ install {-}U requirements/prod.txt} -\ExtensionTok{python}\NormalTok{ manage.py migrate} -\ExtensionTok{python}\NormalTok{ manage.py collectstatic} -\BuiltInTok{kill}\NormalTok{ {-}HUP }\KeywordTok{\textasciigrave{}}\FunctionTok{ps}\NormalTok{ {-}C gunicorn fch {-}o pid }\KeywordTok{|} \FunctionTok{head}\NormalTok{ {-}n 1}\KeywordTok{\textasciigrave{}} -\end{Highlighting} -\end{Shaded} - -\begin{itemize} -\item - \url{https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file} -\end{itemize} - -\hypertarget{_configuration_des_sauvegardes}{% -\subsection{Configuration des -sauvegardes}\label{_configuration_des_sauvegardes}} - -Les sauvegardes ont été configurées avec borg: -\texttt{yum\ install\ borgbackup}. - -C'est l'utilisateur gwift qui s'en occupe. - -\begin{verbatim} -mkdir -p /home/gwift/borg-backups/ -cd /home/gwift/borg-backups/ -borg init gwift.borg -e=none -borg create gwift.borg::{now} ~/bin ~/webapps -\end{verbatim} - -Et dans le fichier crontab : - -\begin{verbatim} -0 23 * * * /home/gwift/bin/backup.sh -\end{verbatim} - -\hypertarget{_rotation_des_jounaux}{% -\subsection{Rotation des jounaux}\label{_rotation_des_jounaux}} - -\begin{Shaded} -\begin{Highlighting}[] -\ExtensionTok{/var/log/gwift/*}\NormalTok{ \{} - \ExtensionTok{weekly} - \ExtensionTok{rotate}\NormalTok{ 3} - \FunctionTok{size}\NormalTok{ 10M} - \ExtensionTok{compress} - \ExtensionTok{delaycompress} -\NormalTok{\}} -\end{Highlighting} -\end{Shaded} - -Puis on démarre logrotate avec \# logrotate -d /etc/logrotate.d/gwift -pour vérifier que cela fonctionne correctement. - -\hypertarget{_ansible}{% -\subsection{Ansible}\label{_ansible}} - -TODO - -\hypertarget{_duxe9ploiement_sur_heroku}{% -\section{Déploiement sur Heroku}\label{_duxe9ploiement_sur_heroku}} - -\href{https://www.heroku.com}{Heroku} est une \emph{Plateform As A -Service} paas, où vous choisissez le \emph{service} dont vous avez -besoin (une base de données, un service de cache, un service applicatif, -\ldots\hspace{0pt}), vous lui envoyer les paramètres nécessaires et le -tout démarre gentiment sans que vous ne deviez superviser l'hôte. Ce -mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà -parlé plus tôt - raison de plus pour que notre application soit -directement prête à y être déployée, d'autant plus qu'il ne sera pas -possible de modifier un fichier une fois qu'elle aura démarré: si vous -souhaitez modifier un paramètre, cela reviendra à couper l'actuelle et -envoyer de nouveaux paramètres et recommencer le déploiement depuis le -début. - -\begin{figure} -\centering -\includegraphics{images/deployment/heroku.png} -\caption{Invest in apps, not ops. Heroku handles the hard stuff --- -patching and upgrading, 24/7 ops and security, build systems, failovers, -and more --- so your developers can stay focused on building great -apps.} -\end{figure} - -Pour un projet de type "hobby" et pour l'exemple de déploiement -ci-dessous, il est tout à fait possible de s'en sortir sans dépenser un -kopek, afin de tester nos quelques idées ou mettre rapidement un -\emph{Most Valuable Product} en place. La seule contrainte consistera à -pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra -être fait en configurant un \emph{bucket compatible S3}, par exemple -chez Amazon, Scaleway ou OVH. - -Le fonctionnement est relativement simple: pour chaque application, -Heroku crée un dépôt Git qui lui est associé. Il suffit donc d'envoyer -les sources de votre application vers ce dépôt pour qu'Heroku les -interprête comme étant une nouvelle version, déploie les nouvelles -fonctionnalités - sous réserve que tous les tests passent correctement - -et les mettent à disposition. Dans un fonctionnement plutôt manuel, -chaque déploiement est initialisé par le développeur ou par un membre de -l'équipe. Dans une version plus automatisée, chacun de ces déploiements -peut être placé en fin de \emph{pipeline}, lorsque tous les tests -unitaires et d'intégration auront été réalisés. - -Au travers de la commande \texttt{heroku\ create}, vous associez donc -une nouvelle référence à votre code source, comme le montre le contenu -du fichier \texttt{.git/config} ci-dessous: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ create} -\ExtensionTok{Creating}\NormalTok{ app... done, ⬢ young{-}temple{-}86098} -\ExtensionTok{https}\NormalTok{://young{-}temple{-}86098.herokuapp.com/ }\KeywordTok{|} \ExtensionTok{https}\NormalTok{://git.heroku.com/young{-}temple{-}86098.git} - -\NormalTok{$ }\FunctionTok{cat}\NormalTok{ .git/config} -\NormalTok{[}\ExtensionTok{core}\NormalTok{]} - \ExtensionTok{repositoryformatversion}\NormalTok{ = 0} - \ExtensionTok{filemode}\NormalTok{ = false} - \ExtensionTok{bare}\NormalTok{ = false} - \ExtensionTok{logallrefupdates}\NormalTok{ = true} - \ExtensionTok{symlinks}\NormalTok{ = false} - \ExtensionTok{ignorecase}\NormalTok{ = true} -\NormalTok{[}\ExtensionTok{remote} \StringTok{"heroku"}\NormalTok{]} - \ExtensionTok{url}\NormalTok{ = https://git.heroku.com/still{-}thicket{-}66406.git} - \ExtensionTok{fetch}\NormalTok{ = +refs/heads/*:refs/remotes/heroku/*} -\end{Highlighting} -\end{Shaded} - -IMPORTANT: - -\begin{verbatim} -Pour définir de quel type d'application il s'agit, Heroku nécessite un minimum de configuration. -Celle-ci se limite aux deux fichiers suivants: - -* Déclarer un fichier `Procfile` qui va simplement décrire le fichier à passer au protocole WSGI -* Déclarer un fichier `requirements.txt` (qui va éventuellement chercher ses propres dépendances dans un sous-répertoire, avec l'option `-r`) -\end{verbatim} - -Après ce paramétrage, il suffit de pousser les changements vers ce -nouveau dépôt grâce à la commande \texttt{git\ push\ heroku\ master}. - -Heroku propose des espaces de déploiements, mais pas d'espace de -stockage. Il est possible d'y envoyer des fichiers utilisateurs -(typiquement, des media personnalisés), mais ceux-ci seront perdus lors -du redémarrage du container. Il est donc primordial de configurer -correctement l'hébergement des fichiers média, de préférences sur un -stockage compatible S3. s3 - -Prêt à vous lancer ? Commencez par créer un compte: -\url{https://signup.heroku.com/python}. - -\hypertarget{_configuration_du_compte_heroku}{% -\subsection{Configuration du compte -Heroku}\label{_configuration_du_compte_heroku}} - -+ Récupération des valeurs d'environnement pour les réutiliser -ci-dessous. - -Vous aurez peut-être besoin d'un coup de pouce pour démarrer votre -première application; heureusement, la documentation est super bien -faite: - -\begin{figure} -\centering -\includegraphics{images/deployment/heroku-new-app.png} -\caption{Heroku: Commencer à travailler avec un langage} -\end{figure} - -Installez ensuite la CLI (\emph{Command Line Interface}) en suivant -\href{https://devcenter.heroku.com/articles/heroku-cli}{la documentation -suivante}. - -Au besoin, cette CLI existe pour: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - macOS, \emph{via} `brew ` -\item - Windows, grâce à un - \href{https://cli-assets.heroku.com/heroku-x64.exe}{binaire x64} (la - version 32 bits existe aussi, mais il est peu probable que vous en - ayez besoin) -\item - GNU/Linux, via un script Shell - \texttt{curl\ https://cli-assets.heroku.com/install.sh\ \textbar{}\ sh} - ou sur \href{https://snapcraft.io/heroku}{SnapCraft}. -\end{enumerate} - -Une fois installée, connectez-vous: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ login} -\end{Highlighting} -\end{Shaded} - -Et créer votre application: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ create} -\ExtensionTok{Creating}\NormalTok{ app... done, ⬢ young{-}temple{-}86098} -\ExtensionTok{https}\NormalTok{://young{-}temple{-}86098.herokuapp.com/ }\KeywordTok{|} \ExtensionTok{https}\NormalTok{://git.heroku.com/young{-}temple{-}86098.git} -\end{Highlighting} -\end{Shaded} - -\begin{figure} -\centering -\includegraphics{images/deployment/heroku-app-created.png} -\caption{Notre application est à présent configurée!} -\end{figure} - -Ajoutons lui une base de données, que nous sauvegarderons à intervalle -régulier: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ addons:create heroku{-}postgresql:hobby{-}dev} -\ExtensionTok{Creating}\NormalTok{ heroku{-}postgresql:hobby{-}dev on ⬢ still{-}thicket{-}66406... free} -\ExtensionTok{Database}\NormalTok{ has been created and is available} -\NormalTok{ ! }\ExtensionTok{This}\NormalTok{ database is empty. If upgrading, you can transfer} -\NormalTok{ ! }\ExtensionTok{data}\NormalTok{ from another database with pg:copy} -\ExtensionTok{Created}\NormalTok{ postgresql{-}clear{-}39693 as DATABASE\_URL} -\ExtensionTok{Use}\NormalTok{ heroku addons:docs heroku{-}postgresql to view documentation} - -\NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ pg:backups schedule {-}{-}at }\StringTok{\textquotesingle{}14:00 Europe/Brussels\textquotesingle{}}\NormalTok{ DATABASE\_URL} -\ExtensionTok{Scheduling}\NormalTok{ automatic daily backups of postgresql{-}clear{-}39693 at 14:00 Europe/Brussels... done} -\end{Highlighting} -\end{Shaded} - -TODO: voir comment récupérer le backup de la db :-p - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# Copié/collé de https://cookiecutter{-}django.readthedocs.io/en/latest/deployment{-}on{-}heroku.html} -\ExtensionTok{heroku}\NormalTok{ create {-}{-}buildpack https://github.com/heroku/heroku{-}buildpack{-}python} - -\ExtensionTok{heroku}\NormalTok{ addons:create heroku{-}redis:hobby{-}dev} - -\ExtensionTok{heroku}\NormalTok{ addons:create mailgun:starter} - -\ExtensionTok{heroku}\NormalTok{ config:set PYTHONHASHSEED=random} - -\ExtensionTok{heroku}\NormalTok{ config:set WEB\_CONCURRENCY=4} - -\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_DEBUG=False} -\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_SETTINGS\_MODULE=config.settings.production} -\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_SECRET\_KEY=}\StringTok{"}\VariableTok{$(}\ExtensionTok{openssl}\NormalTok{ rand {-}base64 64}\VariableTok{)}\StringTok{"} - -\CommentTok{\# Generating a 32 character{-}long random string without any of the visually similar characters "IOl01":} -\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_ADMIN\_URL=}\StringTok{"}\VariableTok{$(}\ExtensionTok{openssl}\NormalTok{ rand {-}base64 4096 }\KeywordTok{|} \FunctionTok{tr}\NormalTok{ {-}dc }\StringTok{\textquotesingle{}A{-}HJ{-}NP{-}Za{-}km{-}z2{-}9\textquotesingle{}} \KeywordTok{|} \FunctionTok{head}\NormalTok{ {-}c 32}\VariableTok{)}\StringTok{/"} - -\CommentTok{\# Set this to your Heroku app url, e.g. \textquotesingle{}bionic{-}beaver{-}28392.herokuapp.com\textquotesingle{}} -\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_ALLOWED\_HOSTS=} - -\CommentTok{\# Assign with AWS\_ACCESS\_KEY\_ID} -\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_ACCESS\_KEY\_ID=} - -\CommentTok{\# Assign with AWS\_SECRET\_ACCESS\_KEY} -\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_SECRET\_ACCESS\_KEY=} - -\CommentTok{\# Assign with AWS\_STORAGE\_BUCKET\_NAME} -\ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_STORAGE\_BUCKET\_NAME=} - -\FunctionTok{git}\NormalTok{ push heroku master} - -\ExtensionTok{heroku}\NormalTok{ run python manage.py createsuperuser} - -\ExtensionTok{heroku}\NormalTok{ run python manage.py check {-}{-}deploy} - -\ExtensionTok{heroku}\NormalTok{ open} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_configuration}{% -\subsection{Configuration}\label{_configuration}} - -Pour qu'Heroku comprenne le type d'application à démarrer, ainsi que les -commandes à exécuter pour que tout fonctionne correctement. Pour un -projet Django, cela comprend, à placer à la racine de votre projet: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Un fichier \texttt{requirements.txt} (qui peut éventuellement faire - appel à un autre fichier, \textbf{via} l'argument \texttt{-r}) -\item - Un fichier \texttt{Procfile} ({[}sans - extension{]}(\url{https://devcenter.heroku.com/articles/procfile)}!), - qui expliquera la commande pour le protocole WSGI. -\end{enumerate} - -Dans notre exemple: - -\begin{verbatim} -# requirements.txt -django==3.2.8 -gunicorn -boto3 -django-storages -\end{verbatim} - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# Procfile} -\ExtensionTok{release}\NormalTok{: python3 manage.py migrate} -\ExtensionTok{web}\NormalTok{: gunicorn gwift.wsgi} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_huxe9bergement_s3}{% -\subsection{Hébergement S3}\label{_huxe9bergement_s3}} - -Pour cette partie, nous allons nous baser sur -l'\href{https://www.scaleway.com/en/object-storage/}{Object Storage de -Scaleway}. Ils offrent 75GB de stockage et de transfert par mois, ce qui -va nous laisser suffisament d'espace pour jouer un peu 😉. - -\includegraphics{images/deployment/scaleway-object-storage-bucket.png} - -L'idée est qu'au moment de la construction des fichiers statiques, -Django aille simplement les héberger sur un espace de stockage -compatible S3. La complexité va être de configurer correctement les -différents points de terminaison. Pour héberger nos fichiers sur notre -\textbf{bucket} S3, il va falloir suivre et appliquer quelques étapes -dans l'ordre: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y - en a - \textbf{littéralement} - des dizaines. -\item - Ajouter la librairie \texttt{boto3}, qui s'occupera de "parler" avec - ce type de protocole -\item - Ajouter la librairie \texttt{django-storage}, qui va elle s'occuper de - faire le câblage entre le fournisseur (\textbf{via} \texttt{boto3}) et - Django, qui s'attend à ce qu'on lui donne un moteur de gestion - \textbf{via} la clé - {[}\texttt{DJANGO\_STATICFILES\_STORAGE}{]}(\url{https://docs.djangoproject.com/en/3.2/ref/settings/\#std:setting-STATICFILES_STORAGE}). -\end{enumerate} - -La première étape consiste à se rendre dans {[}la console -Scaleway{]}(\url{https://console.scaleway.com/project/credentials}), -pour gérer ses identifiants et créer un jeton. - -\includegraphics{images/deployment/scaleway-api-key.png} - -Selon la documentation de -\href{https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html\#settings}{django-storages}, -de -\href{https://boto3.amazonaws.com/v1/documentation/api/latest/index.html}{boto3} -et de -\href{https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/}{Scaleway}, -vous aurez besoin des clés suivantes au niveau du fichier -\texttt{settings.py}: - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{AWS\_ACCESS\_KEY\_ID }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}ACCESS\_KEY\_ID\textquotesingle{}}\NormalTok{)} -\NormalTok{AWS\_SECRET\_ACCESS\_KEY }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}SECRET\_ACCESS\_KEY\textquotesingle{}}\NormalTok{)} -\NormalTok{AWS\_STORAGE\_BUCKET\_NAME }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}AWS\_STORAGE\_BUCKET\_NAME\textquotesingle{}}\NormalTok{)} -\NormalTok{AWS\_S3\_REGION\_NAME }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}AWS\_S3\_REGION\_NAME\textquotesingle{}}\NormalTok{)} - -\NormalTok{AWS\_DEFAULT\_ACL }\OperatorTok{=} \StringTok{\textquotesingle{}public{-}read\textquotesingle{}} -\NormalTok{AWS\_LOCATION }\OperatorTok{=} \StringTok{\textquotesingle{}static\textquotesingle{}} -\NormalTok{AWS\_S3\_SIGNATURE\_VERSION }\OperatorTok{=} \StringTok{\textquotesingle{}s3v4\textquotesingle{}} - -\NormalTok{AWS\_S3\_HOST }\OperatorTok{=} \StringTok{\textquotesingle{}s3.}\SpecialCharTok{\%s}\StringTok{.scw.cloud\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_REGION\_NAME,)} -\NormalTok{AWS\_S3\_ENDPOINT\_URL }\OperatorTok{=} \StringTok{\textquotesingle{}https://}\SpecialCharTok{\%s}\StringTok{\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_HOST, )} - -\NormalTok{DEFAULT\_FILE\_STORAGE }\OperatorTok{=} \StringTok{\textquotesingle{}storages.backends.s3boto3.S3Boto3Storage\textquotesingle{}} -\NormalTok{STATICFILES\_STORAGE }\OperatorTok{=} \StringTok{\textquotesingle{}storages.backends.s3boto3.S3ManifestStaticStorage\textquotesingle{}} - -\NormalTok{STATIC\_URL }\OperatorTok{=} \StringTok{\textquotesingle{}}\SpecialCharTok{\%s}\StringTok{/}\SpecialCharTok{\%s}\StringTok{/\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_ENDPOINT\_URL, AWS\_LOCATION)} - -\CommentTok{\# General optimization for faster delivery} -\NormalTok{AWS\_IS\_GZIPPED }\OperatorTok{=} \VariableTok{True} -\NormalTok{AWS\_S3\_OBJECT\_PARAMETERS }\OperatorTok{=}\NormalTok{ \{} - \StringTok{\textquotesingle{}CacheControl\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}max{-}age=86400\textquotesingle{}}\NormalTok{,} -\NormalTok{\}} -\end{Highlighting} -\end{Shaded} - -Configurez-les dans la console d'administration d'Heroku: - -\includegraphics{images/deployment/heroku-vars-reveal.png} - -Lors de la publication, vous devriez à présent avoir la sortie suivante, -qui sera confirmée par le \textbf{bucket}: - -\begin{Shaded} -\begin{Highlighting}[] -\ExtensionTok{remote}\NormalTok{: {-}{-}{-}{-}{-}}\OperatorTok{\textgreater{}}\NormalTok{ $ python manage.py collectstatic {-}{-}noinput} -\ExtensionTok{remote}\NormalTok{: 128 static files copied, 156 post{-}processed.} -\end{Highlighting} -\end{Shaded} - -\includegraphics{images/deployment/gwift-cloud-s3.png} - -Sources complémentaires: - -\begin{itemize} -\item - {[}How to store Django static and media files on S3 in - production{]}(\url{https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/}) -\item - {[}Using Django and - Boto3{]}(\url{https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/}) -\end{itemize} - -\hypertarget{_autres_outils}{% -\section{Autres outils}\label{_autres_outils}} - - - - -\hypertarget{_arborescences}{% -\subsection{Arborescences}\label{_arborescences}} - -\begin{Shaded} -\begin{Highlighting}[] - -\end{Highlighting} -\end{Shaded} - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# \textless{}app\textgreater{}/management/commands/rebuild.py} - -\CommentTok{"""This command manages Closure Tables implementation} - -\CommentTok{It adds new levels and cleans links between entities.} -\CommentTok{This way, it\textquotesingle{}s relatively easy to fetch an entire tree with just one tiny request.} - -\CommentTok{"""} - -\ImportTok{from}\NormalTok{ django.core.management.base }\ImportTok{import}\NormalTok{ BaseCommand} - -\ImportTok{from}\NormalTok{ rps.structure.models }\ImportTok{import}\NormalTok{ Entity, EntityTreePath} - - -\KeywordTok{class}\NormalTok{ Command(BaseCommand):} - \KeywordTok{def}\NormalTok{ handle(}\VariableTok{self}\NormalTok{, }\OperatorTok{*}\NormalTok{args, }\OperatorTok{**}\NormalTok{options):} -\NormalTok{ entities }\OperatorTok{=}\NormalTok{ Entity.objects.}\BuiltInTok{all}\NormalTok{()} - - \ControlFlowTok{for}\NormalTok{ entity }\KeywordTok{in}\NormalTok{ entities:} -\NormalTok{ breadcrumb }\OperatorTok{=}\NormalTok{ [node }\ControlFlowTok{for}\NormalTok{ node }\KeywordTok{in}\NormalTok{ entity.breadcrumb()]} - -\NormalTok{ tree }\OperatorTok{=} \BuiltInTok{set}\NormalTok{(EntityTreePath.objects.}\BuiltInTok{filter}\NormalTok{(descendant}\OperatorTok{=}\NormalTok{entity))} - - \ControlFlowTok{for}\NormalTok{ idx, node }\KeywordTok{in} \BuiltInTok{enumerate}\NormalTok{(breadcrumb):} -\NormalTok{ tree\_path, \_ }\OperatorTok{=}\NormalTok{ EntityTreePath.objects.get\_or\_create(} -\NormalTok{ ancestor}\OperatorTok{=}\NormalTok{node, descendant}\OperatorTok{=}\NormalTok{entity, weight}\OperatorTok{=}\NormalTok{idx }\OperatorTok{+} \DecValTok{1} -\NormalTok{ )} - - \ControlFlowTok{if}\NormalTok{ tree\_path }\KeywordTok{in}\NormalTok{ tree:} -\NormalTok{ tree.remove(tree\_path)} - - \ControlFlowTok{for}\NormalTok{ tree\_path }\KeywordTok{in}\NormalTok{ tree:} -\NormalTok{ tree\_path.delete()} -\end{Highlighting} -\end{Shaded} - - - -Don't make me think, or why I switched from JS SPAs to Ruby On Rails -\url{https://news.ycombinator.com/item?id=30206989\&utm_term=comment} diff --git a/chapters/authentication.tex b/chapters/authentication.tex index 32a63bf..0961b2c 100644 --- a/chapters/authentication.tex +++ b/chapters/authentication.tex @@ -86,4 +86,132 @@ class TokenBackend(backends.ModelBackend): return user return None -\end{minted} \ No newline at end of file +\end{minted} + +Ceci sous-entend qu'on a bien une classe qui permet d'accéder à ces jetons. + +\begin{minted}{python} +from django.contrib.auth import backends, get_user_model +from ldap3 import Server, Connection, ALL +from ldap3.core.exceptions import LDAPPasswordIsMandatoryError + +from config import settings + +UserModel = get_user_model() + + +class LdapBackend(backends.ModelBackend): + """Implémentation du backend LDAP pour la connexion des utilisateurs à + l'Active Directory. + """ + def authenticate(self, request, username=None, password=None, **kwargs): + """Authentifie l'utilisateur au travers du serveur LDAP. + """ + ldap_server = Server(settings.LDAP_SERVER, get_info=ALL) + ldap_connection = Connection( + ldap_server, + user=username, + password=password + ) + + try: + if not ldap_connection.bind(): + raise ValueError("Login ou mot de passe incorrect") + except (LDAPPasswordIsMandatoryError, ValueError) as ldap_exception: + raise ldap_exception + + user, _ = UserModel.objects.get_or_create(username=username) + + return user +\end{minted} + + +On peut résumer le mécanisme d'authentification de la manière suivante: + +\begin{itemize} +\item + Si vous voulez modifier les informations liées à un utilisateur, orientez-vous vers la modification du modèle. Comme nous le verrons ci-dessous, il existe trois manières de prendre ces modifications en + compte. Voir également \href{https://docs.djangoproject.com/en/stable/topics/auth/customizing/}{ici}. +\item + Si vous souhaitez modifier la manière dont l'utilisateur se connecte, alors vous devrez modifier le \textbf{backend}. +\end{itemize} + +\section{Modélisation} + +Dans un premier temps, Django a besoin de manipuler \href{https://docs.djangoproject.com/en/1.9/ref/contrib/auth/\#user-model}{des instances de type \texttt{django.contrib.auth.User}}. Cette classe implémente les champs suivants: + +\begin{itemize} +\item + \texttt{username} +\item + \texttt{first\_name} +\item + \texttt{last\_name} +\item + \texttt{email} +\item + \texttt{password} +\item + \texttt{date\_joined}. +\end{itemize} + +D'autres champs, comme les groupes auxquels l'utilisateur est associé, ses permissions, savoir s'il est un super-utilisateur, \ldots\hspace{0pt} sont moins pertinents pour le moment. Avec les quelques champs déjà définis ci-dessus, nous avons de quoi identifier correctement nos utilisateurs. Inutile d'implémenter nos propres classes, puisqu'elles existent déjà. + +Si vous souhaitez ajouter un champ, il existe trois manières de faire. + +\subsection{Extension du modèle existant} + +Le plus simple consiste à créer une nouvelle classe, et à faire un lien de type \texttt{OneToOne} vers la classe \texttt{django.contrib.auth.User}. De cette manière, on ne modifie rien à la manière dont Django authentife ses utlisateurs: tout ce qu'on fait, c'est un lien vers une table nouvellement créée, comme on l'a déjà vu au point {[}\ldots\hspace{0pt}voir l'héritage de modèle{]}. L'avantage de cette méthode, c'est qu'elle est extrêmement flexible, et qu'on garde les mécanismes Django standard. Le désavantage, c'est que pour avoir toutes les informations de notre utilisateur, on sera obligé d'effectuer une jointure sur le base de données, ce qui pourrait avoir des conséquences sur les performances. + +\subsection{Substitution du modèle} + +Avant de commencer, sachez que cette étape doit être effectuée \textbf{avant la première migration}. Le plus simple sera de définir une nouvelle classe héritant de \texttt{django.contrib.auth.User} et de spécifier la classe à utiliser dans votre fichier de paramètres. Si ce paramètre est modifié après que la première migration ait été effectuée, il ne sera pas pris en compte. Tenez-en compte au moment de modéliser votre application. + +\begin{minted}{python} + AUTH_USER_MODEL = 'myapp.MyUser' +\end{minted} + + +Notez bien qu'il ne faut pas spécifier le package \texttt{.models} dans cette injection de dépendances: le schéma à indiquer est bien \texttt{\textless{}nom\ de\ l’application\textgreater{}.\textless{}nom\ de\ la\ classe\textgreater{}}. + +\subsection{Social-auth} + +Voir ici : \href{https://github.com/omab/python-social-auth}{python +social auth} + +\subsection{OAuth} + +OAuth est un standard libre définissant un ensemble de méthodes à implémenter pour l'accès (l'autorisation) à une API. Son fonctionnement se base sur un système de jetons (Tokens), attribués par le possesseur de la ressource à laquelle un utilisateur souhaite accéder. + +Le client initie la connexion en demandant un jeton au serveur. Ce jeton est ensuite utilisée tout au long de la connexion, pour accéder aux différentes ressources offertes par ce serveur. `wikipedia \textless{}\url{http://en.wikipedia.org/wiki/OAuth\%3E\%60_}. + +Une introduction à OAuth est \href{http://hueniverse.com/oauth/guide/intro/}{disponible ici}. Elle +introduit le protocole comme étant une \texttt{valet\ key}, une clé que l'on donne à la personne qui va garer votre voiture pendant que vous profitez des mondanités. Cette clé donne un accès à votre voiture, tout +en bloquant un ensemble de fonctionnalités. Le principe du protocole est semblable en ce sens: vous vous réservez un accès total à une API, tandis que le système de jetons permet d'identifier une personne, tout +en lui donnant un accès restreint à votre application. + +L'utilisation de jetons permet notamment de définir une durée d'utilisation et une portée d'utilisation. L'utilisateur d'un service A peut par exemple autoriser un service B à accéder à des ressources qu'il +possède, sans pour autant révéler son nom d'utilisateur ou son mot de passe. + +L'exemple repris au niveau du \href{http://hueniverse.com/oauth/guide/workflow/}{workflow} est le suivant : un utilisateur(trice), Jane, a uploadé des photos sur le site faji.com (A). Elle souhaite les imprimer au travers du site beppa.com (B). Au moment de la commande, le site beppa.com envoie une demande au site faji.com pour accéder aux ressources partagées par Jane. Pour cela, une nouvelle page s'ouvre pour l'utilisateur, et lui demande d'introduire sa "pièce d'identité". Le site A, ayant reçu une demande de B, mais certifiée par l'utilisateur, ouvre alors les ressources et lui permet d'y accéder. + + + +\section{Templates} + +Ce qui n'existe pas par contre, ce sont les vues. Django propose donc +tout le mécanisme de gestion des utilisateurs, excepté le visuel (hors +administration). En premier lieu, ces paramètres sont fixés dans le +fichier `settings +\textless{}\url{https://docs.djangoproject.com/en/1.8/ref/settings/\#auth\%3E\%60_}. +On y trouve par exemple les paramètres suivants: + +\begin{itemize} +\item + \texttt{LOGIN\_REDIRECT\_URL}: si vous ne spécifiez pas le paramètre + \texttt{next}, l'utilisateur sera automatiquement redirigé vers cette + page. +\item + \texttt{LOGIN\_URL}: l'URL de connexion à utiliser. Par défaut, + l'utilisateur doit se rendre sur la page \texttt{/accounts/login}. +\end{itemize} \ No newline at end of file diff --git a/chapters/contexts-processors.tex b/chapters/contexts-processors.tex index 360c7f1..c86ed25 100644 --- a/chapters/contexts-processors.tex +++ b/chapters/contexts-processors.tex @@ -24,9 +24,7 @@ def git_describe(request) -> str: \end{minted} -Ceci aura pour effet d'ajouter les deux variables \texttt{git\_describe} -et \texttt{git\_date} dans tous les contextes de tous les templates de -l'application. +Ceci aura pour effet d'ajouter les deux variables \texttt{git\_describe} et \texttt{git\_date} dans tous les contextes de tous les templates de l'application. \begin{minted}{python} TEMPLATES = [ diff --git a/chapters/deployment-processes.tex b/chapters/deployment-processes.tex new file mode 100644 index 0000000..78ec93d --- /dev/null +++ b/chapters/deployment-processes.tex @@ -0,0 +1,5 @@ +\chapter{Processus de mises à dispositions} + +\section{Waffle} + +\section{...} diff --git a/chapters/deployments.tex b/chapters/deployments.tex index 6ba2b04..76eac9f 100644 --- a/chapters/deployments.tex +++ b/chapters/deployments.tex @@ -17,8 +17,62 @@ Une fois que ces utilisateurs seront configurés, nous pourrons passer à l'éta La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, ... Il existe des dizaines d'hébergements typés VPS (\textbf{Virtual Private Server}). A vous de choisir celui qui vous convient \footnote{Personnellement, j'ai un petit faible pour Hetzner Cloud}. +\begin{verbatim} + apt update + groupadd --system webapps + groupadd --system gunicorn_sockets + useradd --system --gid webapps --shell /bin/bash --home /home/gwift gwift + mkdir -p /home/gwift + chown gwift:webapps /home/gwift +\end{verbatim} + + +\begin{itemize} + \item + On ajoute un groupe intitulé \texttt{webapps} + \item + On crée un groupe pour les communications via sockets + \item + On crée notre utilisateur applicatif; ses applications seront placées + dans le répertoire \texttt{/home/gwift} + \item + On crée le répertoire home/gwift + \item + On donne les droits sur le répertoire /home/gwift +\end{itemize} + \section{Dépendances systèmes} + +La version 3.6 de Python se trouve dans les dépôts officiels de CentOS. +Si vous souhaitez utiliser une version ultérieure, il suffit de +l'installer en parallèle de la version officiellement supportée par +votre distribution. + +Pour CentOS, vous avez donc deux possibilités : + +\begin{verbatim} + yum install python36 -y +\end{verbatim} + +Ou passer par une installation alternative: + +\begin{verbatim} + sudo yum -y groupinstall "Development Tools" + sudo yum -y install openssl-devel bzip2-devel libffi-devel + wget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tgz + cd Python-3.8*/ + ./configure --enable-optimizations + sudo make altinstall +\end{verbatim} + +\begin{itemize} + \item + \textbf{Attention !} Le paramètre \texttt{altinstall} est primordial. + Sans lui, vous écraserez l'interpréteur initialement supporté par la + distribution, et cela pourrait avoir des effets de bord non souhaités. +\end{itemize} + \section{Base de données} @@ -87,7 +141,8 @@ Finalement, on peut créer la DB: Idem, installation, configuration, backup, tout ça. A copier de grimboite, je suis sûr d’avoir des notes là-dessus. -\section{Environment utilisateur} + +\section{Préparation de l'environment utilisateur} \begin{verbatim} su - gwift @@ -117,3 +172,281 @@ suivantes: gunicorn config.wsgi:application --bind localhost:3000 --settings =config.settings_production \end{verbatim} + +\section{Configuration de l'application} + +\begin{verbatim} + SECRET_KEY= + ALLOWED_HOSTS=* + STATIC_ROOT=/var/www/gwift/static + DATABASE= +\end{verbatim} + +\begin{itemize} + \item + La variable \texttt{SECRET\_KEY} est notamment utilisée pour le + chiffrement des sessions. + \item + On fait confiance à django\_environ pour traduire la chaîne de + connexion à la base de données. + \end{itemize} + +\section{Création des répertoires de logs} + + +\begin{verbatim} + mkdir -p /var/www/gwift/static +\end{verbatim} + +\section{Socket} + +Dans le fichier \texttt{/etc/tmpfiles.d/gwift.conf}: + +\begin{verbatim} + D /var/run/webapps 0775 gwift gunicorn_sockets - +\end{verbatim} + +Suivi de la création par systemd : + +\begin{verbatim} + systemd-tmpfiles --create +\end{verbatim} + +\section{Gunicorn} + +\begin{verbatim} + #!/bin/bash + # defines settings for gunicorn + NAME="gwift" + DJANGODIR=/home/gwift/webapps/gwift + SOCKFILE=/var/run/webapps/gunicorn_gwift.sock + USER=gwift + GROUP=gunicorn_sockets + NUM_WORKERS=5 + DJANGO_SETTINGS_MODULE=config.settings_production + DJANGO_WSGI_MODULE=config.wsgi + echo "Starting $NAME as `whoami`" + source /home/gwift/.venvs/gwift/bin/activate + cd $DJANGODIR + export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE + export PYTHONPATH=$DJANGODIR:$PYTHONPATH + exec gunicorn ${DJANGO_WSGI_MODULE}:application \ + --name $NAME \ + --workers $NUM_WORKERS \ + --user $USER \ + --bind=unix:$SOCKFILE \ + --log-level=debug \ + --log-file=- +\end{verbatim} + +\section{Supervsion, keepalive et autoreload} + +Pour la supervision, on passe par Supervisor. Il existe d'autres +superviseurs, + +\begin{verbatim} + yum install supervisor -y +\end{verbatim} + +On crée ensuite le fichier \texttt{/etc/supervisord.d/gwift.ini}: + +\begin{verbatim} + [program:gwift] + command=/home/gwift/bin/start_gunicorn.sh + user=gwift + stdout_logfile=/var/log/gwift/gwift.log + autostart=true + autorestart=unexpected + redirect_stdout=true + redirect_stderr=true +\end{verbatim} + +Et on crée les répertoires de logs, on démarre supervisord et on vérifie +qu'il tourne correctement: + +\begin{verbatim} + $ mkdir /var/log/gwift + $ chown gwift:nagios /var/log/gwift + $ systemctl enable supervisord + $ systemctl start supervisord.service + $ systemctl status supervisord.service + supervisord.service - Process Monitoring and Control Daemon + Loaded: loaded (/usr/lib/systemd/system/supervisord.service; enabled; + vendor preset: disabled) + Active: active (running) since Tue 2019-12-24 10:08:09 CET; 10s ago + Process: 2304 ExecStart=/usr/bin/supervisord -c /etc/supervisord.conf (code + =exited, status=0/SUCCESS) + Main PID: 2310 (supervisord) + CGroup: /system.slice/supervisord.service + - 2310 /usr/bin/python /usr/bin/supervisord -c + /etc/supervisord.conf + - 2313 /home/gwift/.venvs/gwift/bin/python3 + /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... + - 2317 /home/gwift/.venvs/gwift/bin/python3 + /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... + - 2318 /home/gwift/.venvs/gwift/bin/python3 + /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... + - 2321 /home/gwift/.venvs/gwift/bin/python3 + /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... + - 2322 /home/gwift/.venvs/gwift/bin/python3 + /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... + - 2323 /home/gwift/.venvs/gwift/bin/python3 + /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... + ls /var/run/webapps +\end{verbatim} + + +On peut aussi vérifier que l'application est en train de tourner, à +l'aide de la commande \texttt{supervisorctl}: + +\begin{verbatim} + supervisorctl status gwift + gwift RUNNING pid 31983, uptime 0:01:00 + supervisorctl stop gwift + gwift: stopped + root@ks3353535:/etc/supervisor/conf.d# supervisorctl start gwift + gwift: started + root@ks3353535:/etc/supervisor/conf.d# supervisorctl restart gwift + gwift: stopped + gwift: started +\end{verbatim} + +\section{Firewall} + +\begin{verbatim} + et 443 (HTTPS). +\end{verbatim} + +\begin{verbatim} + firewall-cmd --permanent --zone=public --add-service=http + firewall-cmd --permanent --zone=public --add-service=https + firewall-cmd --reload +\end{verbatim} + +\begin{itemize} + \item + On ouvre le port 80, uniquement pour autoriser une connexion HTTP, + mais qui sera immédiatement redirigée vers HTTPS + \item + Et le port 443 (forcément). +\end{itemize} + +\section{Reverse proxy} + + +\begin{verbatim} + yum install nginx -y + usermod -a -G gunicorn_sockets nginx + \end{verbatim} + + On configure ensuite le fichier \texttt{/etc/nginx/conf.d/gwift.conf}: + + \begin{verbatim} + upstream gwift_app { + server unix:/var/run/webapps/gunicorn_gwift.sock fail_timeout=0; + } + + server { + listen 80; + server_name ; + root /var/www/gwift; + error_log /var/log/nginx/gwift_error.log; + access_log /var/log/nginx/gwift_access.log; + + client_max_body_size 4G; + keepalive_timeout 5; + + gzip on; + gzip_comp_level 7; + gzip_proxied any; + gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; + + + location /static/ { + access_log off; + expires 30d; + add_header Pragma public; + add_header Cache-Control "public"; + add_header Vary "Accept-Encoding"; + try_files $uri $uri/ =404; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + + proxy_pass http://gwift_app; + } + } + \end{verbatim} + + \begin{itemize} + \item + Ce répertoire sera complété par la commande \texttt{collectstatic} que + l'on verra plus tard. L'objectif est que les fichiers ne demandant + aucune intelligence soit directement servis par Nginx. Cela évite + d'avoir un processus Python (relativement lent) qui doive être + instancié pour servir un simple fichier statique. + \item + Afin d'éviter que Django ne reçoive uniquement des requêtes provenant + de 127.0.0.1 + \end{itemize} + +\section{Mise à jour} + +\begin{verbatim} + u - + source ~/.venvs//bin/activate + cd ~/webapps/ + git fetch + git checkout vX.Y.Z + pip install -U requirements/prod.txt + python manage.py migrate + python manage.py collectstatic + kill -HUP `ps -C gunicorn fch -o pid | head -n 1` +\end{verbatim} + + +\begin{itemize} + \item + \url{https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file} +\end{itemize} + +\section{Logrotate} + +\begin{verbatim} + /var/log/gwift/* { + weekly + rotate 3 + size 10M + compress + delaycompress + } +\end{verbatim} + +Puis on démarre logrotate avec \# logrotate -d /etc/logrotate.d/gwift +pour vérifier que cela fonctionne correctement. + +\section{Sauvegardes} + +Les sauvegardes ont été configurées avec borg: +\texttt{yum\ install\ borgbackup}. + +C'est l'utilisateur gwift qui s'en occupe. + +\begin{verbatim} +mkdir -p /home/gwift/borg-backups/ +cd /home/gwift/borg-backups/ +borg init gwift.borg -e=none +borg create gwift.borg::{now} ~/bin ~/webapps +\end{verbatim} + +Et dans le fichier crontab : + +\begin{verbatim} +0 23 * * * /home/gwift/bin/backup.sh +\end{verbatim} + +\section{Ansible} + diff --git a/chapters/graphs.tex b/chapters/graphs.tex new file mode 100644 index 0000000..4827e10 --- /dev/null +++ b/chapters/graphs.tex @@ -0,0 +1,2 @@ +\chapter{Graphs} + diff --git a/chapters/heroku.tex b/chapters/heroku.tex index 329dcc6..8cefc62 100644 --- a/chapters/heroku.tex +++ b/chapters/heroku.tex @@ -1,2 +1,292 @@ \chapter{PaaS - Heroku} + +\href{https://www.heroku.com}{Heroku} est une \emph{Plateform As A Service} paas, où vous choisissez le \emph{service} dont vous avez besoin (une base de données, un service de cache, un service applicatif, ..., vous lui envoyer les paramètres nécessaires et le tout démarre gentiment sans que vous ne deviez superviser l'hôte. Ce mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà parlé plus tôt - raison de plus pour que notre application soit directement prête à y être déployée, d'autant plus qu'il ne sera pas +possible de modifier un fichier une fois qu'elle aura démarré: si vous souhaitez modifier un paramètre, cela reviendra à couper l'actuelle et envoyer de nouveaux paramètres et recommencer le déploiement depuis le +début. + +\begin{figure} +\centering +\includegraphics{images/deployment/heroku.png} +\caption{Invest in apps, not ops. Heroku handles the hard stuff --- +patching and upgrading, 24/7 ops and security, build systems, failovers, +and more --- so your developers can stay focused on building great +apps.} +\end{figure} + +Pour un projet de type "hobby" et pour l'exemple de déploiement ci-dessous, il est tout à fait possible de s'en sortir sans dépenser un kopek, afin de tester nos quelques idées ou mettre rapidement un \emph{Most Valuable Product} en place. La seule contrainte consistera à pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra être fait en configurant un \emph{bucket compatible S3}, par exemple chez Amazon, Scaleway ou OVH. + +Le fonctionnement est relativement simple: pour chaque application, Heroku crée un dépôt Git qui lui est associé. Il suffit donc d'envoyer les sources de votre application vers ce dépôt pour qu'Heroku les interprête comme étant une nouvelle version, déploie les nouvelles fonctionnalités - sous réserve que tous les tests passent correctement - et les mettent à disposition. Dans un fonctionnement plutôt manuel, chaque déploiement est initialisé par le développeur ou par un membre de l'équipe. Dans une version plus automatisée, chacun de ces déploiements peut être placé en fin de \emph{pipeline}, lorsque tous les tests +unitaires et d'intégration auront été réalisés. + +Au travers de la commande \texttt{heroku\ create}, vous associez donc une nouvelle référence à votre code source, comme le montre le contenu du fichier \texttt{.git/config} ci-dessous: + +\begin{verbatim} + $ heroku create + Creating app... done, -> young-temple-86098 + https://young-temple-86098.herokuapp.com/ | https://git.heroku.com/young- + temple-86098.git + + $ cat .git/config + [core] + repositoryformatversion = 0 + filemode = false + bare = false + logallrefupdates = true + symlinks = false + ignorecase = true + [remote "heroku"] + url = https://git.heroku.com/still-thicket-66406.git + fetch = +refs/heads/*:refs/remotes/heroku/* +\end{verbatim} + + +\begin{verbatim} + Pour définir de quel type d'application il s'agit, Heroku nécessite un minimum de configuration. + Celle-ci se limite aux deux fichiers suivants: + + * Déclarer un fichier `Procfile` qui va simplement décrire le fichier à passer au protocole WSGI + * Déclarer un fichier `requirements.txt` (qui va éventuellement chercher ses propres dépendances dans un sous-répertoire, avec l'option `-r`) +\end{verbatim} + +Après ce paramétrage, il suffit de pousser les changements vers ce +nouveau dépôt grâce à la commande \texttt{git\ push\ heroku\ master}. + +Heroku propose des espaces de déploiements, mais pas d'espace de +stockage. Il est possible d'y envoyer des fichiers utilisateurs +(typiquement, des media personnalisés), mais ceux-ci seront perdus lors +du redémarrage du container. Il est donc primordial de configurer +correctement l'hébergement des fichiers média, de préférences sur un +stockage compatible S3. s3 + +Prêt à vous lancer ? Commencez par créer un compte: +\url{https://signup.heroku.com/python}. + +\section{Configuration du compte} + ++ Récupération des valeurs d'environnement pour les réutiliser ci-dessous. + +Vous aurez peut-être besoin d'un coup de pouce pour démarrer votre première application; heureusement, la documentation est super bien faite: + +\begin{figure} +\centering +\includegraphics{images/deployment/heroku-new-app.png} +\caption{Heroku: Commencer à travailler avec un langage} +\end{figure} + +Installez ensuite la CLI (\emph{Command Line Interface}) en suivant \href{https://devcenter.heroku.com/articles/heroku-cli}{la documentation suivante}. + +Au besoin, cette CLI existe pour: + +\begin{enumerate} +\item + macOS, \emph{via} `brew ` +\item + Windows, grâce à un + \href{https://cli-assets.heroku.com/heroku-x64.exe}{binaire x64} (la + version 32 bits existe aussi, mais il est peu probable que vous en + ayez besoin) +\item + GNU/Linux, via un script Shell + \texttt{curl\ https://cli-assets.heroku.com/install.sh\ \textbar{}\ sh} + ou sur \href{https://snapcraft.io/heroku}{SnapCraft}. +\end{enumerate} + +Une fois installée, connectez-vous: + +\begin{verbatim} + $ heroku login +\end{verbatim} + +Et créer votre application: + +\begin{verbatim} + $ heroku create + Creating app... done, -> young-temple-86098 + https://young-temple-86098.herokuapp.com/ | https://git.heroku.com/young- + temple-86098.git +\end{verbatim} + +\begin{figure} + \centering + \includegraphics{images/deployment/heroku-app-created.png} + \caption{Notre application est à présent configurée!} +\end{figure} + + + +Ajoutons lui une base de données, que nous sauvegarderons à intervalle +régulier: + + +\begin{verbatim} + $ heroku addons:create heroku-postgresql:hobby-dev + Creating heroku-postgresql:hobby-dev on -> still-thicket-66406... free + Database has been created and is available + ! This database is empty. If upgrading, you can transfer + ! data from another database with pg:copy + Created postgresql-clear-39693 as DATABASE_URL + Use heroku addons:docs heroku-postgresql to view documentation + + $ heroku pg:backups schedule --at '14:00 Europe/Brussels' DATABASE_URL + Scheduling automatic daily backups of postgresql-clear-39693 at 14:00 + Europe/Brussels... done +\end{verbatim} + +(+ voir comment récupérer les backups) + +\begin{verbatim} + # Copié/collé de https://cookiecutter- django.readthedocs.io/en/latest/deployment-on-heroku.html + heroku create --buildpack https://github.com/heroku/heroku-buildpack-python + heroku addons:create heroku-redis:hobby-dev + heroku addons:create mailgun:starter + heroku config:set PYTHONHASHSEED=random + heroku config:set WEB_CONCURRENCY=4 + heroku config:set DJANGO_DEBUG=False + heroku config:set DJANGO_SETTINGS_MODULE=config.settings.production + heroku config:set DJANGO_SECRET_KEY="$(openssl rand -base64 64)" + + # Generating a 32 character-long random string without any of the visually + similar characters "IOl01": + heroku config:set DJANGO_ADMIN_URL="$(openssl rand -base64 4096 | tr -dc 'A- + HJ-NP-Za-km-z2-9' | head -c 32)/" + + # Set this to your Heroku app url, e.g. 'bionic-beaver-28392.herokuapp.com' + heroku config:set DJANGO_ALLOWED_HOSTS= + + # Assign with AWS_ACCESS_KEY_ID + heroku config:set DJANGO_AWS_ACCESS_KEY_ID= + + # Assign with AWS_SECRET_ACCESS_KEY + heroku config:set DJANGO_AWS_SECRET_ACCESS_KEY= + + # Assign with AWS_STORAGE_BUCKET_NAME + heroku config:set DJANGO_AWS_STORAGE_BUCKET_NAME= + git push heroku master + heroku run python manage.py createsuperuser + heroku run python manage.py check --deploy + heroku open +\end{verbatim} + +\section{Type d'application} + +Pour qu'Heroku comprenne le type d'application à démarrer, ainsi que les commandes à exécuter pour que tout fonctionne correctement. Pour un projet Django, cela comprend, à placer à la racine de votre projet: + +\begin{enumerate} +\item + Un fichier \texttt{requirements.txt} (qui peut éventuellement faire appel à un autre fichier, \textbf{via} l'argument \texttt{-r}) +\item + Un fichier \texttt{Procfile} ({[}sans extension{]}(\url{https://devcenter.heroku.com/articles/procfile)}!), + qui expliquera la commande pour le protocole WSGI. +\end{enumerate} + +Dans notre exemple: + +\begin{verbatim} +# requirements.txt +django==3.2.8 +gunicorn +boto3 +django-storages +\end{verbatim} + +\begin{verbatim} + # Procfile + release: python3 manage.py migrate + web: gunicorn gwift.wsg +\end{verbatim} + +\section{Hébergement S3} + +Pour cette partie, nous allons nous baser sur +l'\href{https://www.scaleway.com/en/object-storage/}{Object Storage de +Scaleway}. Ils offrent 75GB de stockage et de transfert par mois, ce qui +va nous laisser suffisament d'espace pour jouer un peu. + +\includegraphics{images/deployment/scaleway-object-storage-bucket.png} + +L'idée est qu'au moment de la construction des fichiers statiques, +Django aille simplement les héberger sur un espace de stockage +compatible S3. La complexité va être de configurer correctement les +différents points de terminaison. Pour héberger nos fichiers sur notre +\textbf{bucket} S3, il va falloir suivre et appliquer quelques étapes +dans l'ordre: + +\begin{enumerate} +\def\labelenumi{\arabic{enumi}.} +\item + Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y + en a - \textbf{littéralement} - des dizaines. +\item + Ajouter la librairie \texttt{boto3}, qui s'occupera de "parler" avec + ce type de protocole +\item + Ajouter la librairie \texttt{django-storage}, qui va elle s'occuper de + faire le câblage entre le fournisseur (\textbf{via} \texttt{boto3}) et + Django, qui s'attend à ce qu'on lui donne un moteur de gestion + \textbf{via} la clé + {[}\texttt{DJANGO\_STATICFILES\_STORAGE}{]}(\url{https://docs.djangoproject.com/en/3.2/ref/settings/\#std:setting-STATICFILES_STORAGE}). +\end{enumerate} + +La première étape consiste à se rendre dans {[}la console +Scaleway{]}(\url{https://console.scaleway.com/project/credentials}), +pour gérer ses identifiants et créer un jeton. + +\includegraphics{images/deployment/scaleway-api-key.png} + +Selon la documentation de +\href{https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html\#settings}{django-storages}, +de +\href{https://boto3.amazonaws.com/v1/documentation/api/latest/index.html}{boto3} +et de +\href{https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/}{Scaleway}, +vous aurez besoin des clés suivantes au niveau du fichier +\texttt{settings.py}: + +\begin{minted}{python} + AWS_ACCESS_KEY_ID = os.getenv('ACCESS_KEY_ID') + AWS_SECRET_ACCESS_KEY = os.getenv('SECRET_ACCESS_KEY') + AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME') + AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME') + AWS_DEFAULT_ACL = 'public-read' + AWS_LOCATION = 'static' + AWS_S3_SIGNATURE_VERSION = 's3v4' + AWS_S3_HOST = 's3.%s.scw.cloud' % (AWS_S3_REGION_NAME,) + AWS_S3_ENDPOINT_URL = 'https://%s' % (AWS_S3_HOST, ) + + DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' + STATICFILES_STORAGE = 'storages.backends.s3boto3.S3ManifestStaticStorage' + STATIC_URL = '%s/%s/' % (AWS_S3_ENDPOINT_URL, AWS_LOCATION) + + # General optimization for faster delivery + AWS_IS_GZIPPED = True + AWS_S3_OBJECT_PARAMETERS = { + 'CacheControl': 'max-age=86400', + } +\end{minted} + + +Configurez-les dans la console d'administration d'Heroku: + +\includegraphics{images/deployment/heroku-vars-reveal.png} + +Lors de la publication, vous devriez à présent avoir la sortie suivante, +qui sera confirmée par le \textbf{bucket}: + +\begin{verbatim} + remote: -----> $ python manage.py collectstatic --noinput + remote: 128 static files copied, 156 post-processed +\end{verbatim} + +\includegraphics{images/deployment/gwift-cloud-s3.png} + +Sources complémentaires: + +\begin{itemize} +\item + {[}How to store Django static and media files on S3 in + production{]}(\url{https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/}) +\item + {[}Using Django and + Boto3{]}(\url{https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/}) +\end{itemize} \ No newline at end of file diff --git a/chapters/trees.tex b/chapters/trees.tex index c5467f5..2a69a5e 100644 --- a/chapters/trees.tex +++ b/chapters/trees.tex @@ -1,2 +1,36 @@ \chapter{Arborescences} +On a un exemple de remplissage/vidage d'une closure table, mais il faudrait en fait présenter les listes adjacentes et les autres structures de données. +Comme ça on pourra introduire les graphs juste après. + +\begin{minted}{python} +# /management/commands/rebuild.py +"""This command manages Closure Tables implementation +It adds new levels and cleans links between entities. +This way, it's relatively easy to fetch an entire tree with just one tiny +request. +""" + +from django.core.management.base import BaseCommand + +from structure.models import Entity, EntityTreePath + + +class Command(BaseCommand): + def handle(self, *args, **options): + entities = Entity.objects.all() + + for entity in entities: + breadcrumb = [node for node in entity.breadcrumb()] + tree = set(EntityTreePath.objects.filter(descendant=entity)) + + for idx, node in enumerate(breadcrumb): + tree_path, _ = EntityTreePath.objects.get_or_create( + ancestor=node, descendant=entity, weight=idx + 1 + ) + + if tree_path in tree: + tree.remove(tree_path) + for tree_path in tree: + tree_path.delete() +\end{minted} \ No newline at end of file diff --git a/main.tex b/main.tex index 5662f5e..7967314 100644 --- a/main.tex +++ b/main.tex @@ -59,10 +59,10 @@ \include{chapters/models.tex} \include{chapters/migrations.tex} \include{chapters/administration.tex} +\include{chapters/urls.tex} \include{chapters/forms.tex} \include{chapters/authentication.tex} - -\chapter{Context Processors} +\include{chapters/context-processors.tex} \chapter{Conclusions} @@ -72,23 +72,15 @@ \include{chapters/deployments.tex} \include{chapters/heroku.tex} \include{chapters/deployment-tools.tex} - -\chapter{Ressources} - +\include{chapters/deployment-processes.tex} \chapter{Conclusions} \include{parts/soa.tex} \include{chapters/api.tex} -\include{chapters/trees.tex} - -\chapter{A/B Testing} - -\chapter{Modèles et relations} - \include{chapters/filters.tex} -\include{chapters/urls.tex} - +\include{chapters/trees.tex} +\include{chapters/graphs.tex} \include{chapters/i18n.tex} \chapter{Conclusions} diff --git a/parts/soa.tex b/parts/soa.tex index da3adc1..834cf70 100644 --- a/parts/soa.tex +++ b/parts/soa.tex @@ -1,10 +1,21 @@ \part{Services Oriented Applications} Nous avons fait exprès de reprendre l'acronyme d'une \emph{Services Oriented Architecture} pour cette partie. L'objectif est de vous mettre -la puce à l'oreille quant à la finalité du développement: que -l'utilisateur soit humain, bot automatique ou client Web, l'objectif est -de fournir des applications résilientes, disponibles et accessibles. +la puce à l'oreille quant à la finalité du développement: que l'utilisateur soit humain, bot automatique ou client Web, l'objectif est de fournir des applications résilientes, disponibles et accessibles. -Dans cette partie, nous aborderons les vues, la mise en forme, la mise -en page, la définition d'une interface REST, la définition d'une -interface GraphQL et le routage d'URLs. +Dans cette partie, nous aborderons les vues, la mise en forme, la mise en page, la définition d'une interface REST, la définition d'une interface GraphQL et le routage d'URLs. + +\begin{quote} + + Don't make me think, or why I switched from JS SPAs to Ruby On Rails + \url{https://news.ycombinator.com/item?id=30206989\&utm_term=comment} + +\end{quote} + +On a parcouru les templates et le mode "monolithique de DJango". +Maintenant, on va aborder différentes options: + +\begin{enumerate} + \item Le mode "intermédiaire", qui consiste à garder tous les mécanismes internes à Django, mais à ajouter une couche de dynamisme au travers d'une (légère) couche de JavaScript ou via HTMX. + \item Le mode "warrior", qui consiste lui à ajouter une API, d'abord pour vos clients et utilisateurs, mais aussi pour votre propre consommation \footnote{Aussi intitulé "Eat your own dog's food"} +\end{enumerate}