lindev.fr

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

27 août 2014

Django déploiement avec Nginx Gunicorn et Supervisord

Je vois souvent passer des questions sur le forums ou autres fils de discussion concernant la mise en production d'un projet Django.
Ici je vais présenter une façon de faire, utilisant Nginx Gunicorn et Supervisord .

Prérequis

Sur une Débian toute fraiche,

Nginx

sudo apt-get install nginx

Virtualenv

Car nous travaillons toujours avec un environnement virtuel, dans lequel nous allons y installer les dépendances de notre projet.
Ce qui permet de ne pas avoir de conflit de version avec d'autres éventuels projets déjà en prod.

pip install virtualenv virtualenvwrapper

Note : Si vous n'avez pas l'outil pip de disponible, installez-le comme ceci

sudo apt-get install python-setuptools
sudo easy_install pip

Puis dans votre fichier ~/.bashrc , ajoutez les lignes suivantes, pour l'autocompletion

export VIRTUALENVWRAPPER_VIRTUALENV_ARGS='--no-site-package'
export WORKON_HOME=$HOME/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh

Gunicorn

Pourquoi pas utiliser uWSGI ? humm ... car je préfère Gunicorn qui me donne toute satisfaction sur mes projets en prod. donc ... pourquoi pas !

sudo apt-get install libevent-dev
pip install gunicorn

Supervisor

Installation simple :

sudo pip install supervisor

Récupération du fichier de conf par défaut

echo_supervisord_conf > /etc/supervisord.conf

Dans le fichier de conf, afin de pouvoir manager votre projet via l'interface web, il faut dé-commenter et paramétrer les lignes suivantes

[inet_http_server]         ; inet (TCP) server disabled by default
port=*:9001        ; (ip_address:port specifier, *:port for all iface)
username=user              ; (default is no username (open server))
password=123               ; (default is no password (open server))

Note: Changer le username et password évidemment

L'interface web sera disponible à l'adresse : http://ipduserveur:9001

Pour le lancement automatique au démarrage du système, vous pouvez utiliser ces scripts d'init:

  1. debian init supervisor
  2. debian init supervisor

Installer votre projet

Notre environnement est prêt, nous allons commencer par installer le projet Django dans l’environnement virtuel.

Création du virtualenv

mkvirtualenv monprojet

Si vous utilisez git, cloner votre projet, sinon, copier le à l'endroit que vous souhaitez, ( pour l'exemple ce sera /var/www/monprojet )

Installation des dépendances du projet

si vous utilisez également les environnements virtuels pour développer ( ce que je conseille ) vous pouvez alors enregistrer la liste des libs python installées dans cette environnement, afin de pouvoir également les installer ( avec la même version ) sur un autre serveur.

Pour avoir la liste des libs :

pip freeze > requirements.txt

puis sur votre serveur, pour installer les libs depuis un export freeze :

pip install -r requirements.txt

Configuration de Django

Il vous faudra peut-être toucher un peu à votre fichier settings.py pour le passer du mode debug au mode production.
Personnellement j'utilise une autre subtilité qui me permet de ne pas avoir à toucher au fichier settings.py ( j'expliquerai celà dans un autre billet ) .

N'oubliez pas de vérifier le paramètre STATIC_ROOT

STATIC_ROOT = '/var/www/static_monprojet/'

C'est le répertoire ou seront copiés les fichiers statiques du projet, pour ensuite être servis par Nginx

Ce répertoire DOIT EXISTER

Une fois le répertoire créé, nous allons y "placer/lier" les fichiers statiques du projet

python manage.py collectstatic --link

Perso je préfère y mettre des liens symboliques .. ( les gouts et les couleurs ... )

Liaison du projet avec Gunicorn

Nous allons créer un fichier dans notre projet qui sera utilisé pour le lancement du/des process Gunicorn ( vous devrez adapter les valeurs dans ce script )

vim /var/www/monprojet/monprojet/gunicorn.sh

Et voici le contenu

#!/bin/bash
  set -e
  NUM_WORKERS=2
  USER=www-data
  GROUP=www-data
  ADDRESS=127.0.0.1:5002
  cd /var/www/monprojet
  source /home/monuser/.virtualenvs/monprojet/bin/activate
  exec gunicorn monprojet.wsgi:application -w $NUM_WORKERS --bind=$ADDRESS \
    --user=$USER --group=$GROUP --log-level=debug

Puis on le rend exécutable

chmod 777 /var/www/monprojet/monprojet/gunicorn.sh

Configuration Supervisord

Notre projet est prêt, afin de lancer Gunicorn automatiquement, nous allons utiliser supervisord, qui en plus de s'occuper de démarrer le projet automatiquement au démarrage du système, va aussi le relancer en cas de crash, gérer les logs et vous donner la main pour arrêter ré-demarrer les process, via l'interface web ou en mode console.

supervisord.png

Ajoutez à la fin du fichier de configuration /etc/supervisord.conf les lignes suivantes ( paramètres à adapter selon votre cas )

[program:guni_monprojet]
directory=/var/www/monprojet/monprojet
user = www-data
autostart=true
autorestart=true
stdout_logfile=/var/log/monprojet.log
redirect_stderr=true
stopsignal=QUIT
command = /var/www/monprojet/monprojet/monprojet.sh

Ne reste plus qu'à redémarrer supervisor pour prendre en compte la nouvelle config.

Pour le lancer manuellement

sudo supervisord -c /etc/supervisord.conf

Vhost Nginx

Dernier point, la création du vhost de Nginx, pour diriger les requêtes vers Gunicorn qui écoute sur le port 5002 ( il est aussi possible d'utiliser un fichier socket à la place )

vim /etc/nginx/sites-enabled/monprojet

Et voilà le contenu ( fonctionnel, mais vous pouvez l'adapter )

upstream us_monprojet {
        server 127.0.0.1:5002;
}


server {
        listen 80;

        root /var/www/monprojet;

        gzip             on;
        gzip_min_length  1000;
        gzip_proxied     expired no-cache no-store private auth;
        gzip_types       text/plain application/xml text/css text/javascript application/x-javascript application/x-shockwave-flash video/x-flv;
        gzip_disable     "MSIE [1-6]\.";


        server_name monprojet.com;
        charset utf-8;

        client_max_body_size 75M;


        location ~ /\.ht {
            deny  all;
        }

        location /favicon.ico {
                alias /var/www/monprojet/monprojet/static/favicon.ico;

                if (-f $request_filename) {
                        access_log off;
                        expires max;
                }

        }


        location /media {
                alias /var/www/monprojet/monprojet/media;

                if (-f $request_filename) {
                        access_log off;
                        expires max;
                }

        }

        location /static {
                alias /var/www/static_monprojet/;

                if (-f $request_filename) {
                        access_log off;
                        expires max;
                }

        }
location / {

                if (-f $request_filename) {
                        access_log off;
                        expires max;
                }       

                #gunicornParams
                if (!-f $request_filename) {
                        proxy_pass         http://us_monprojet;
                        break;
                }       
                proxy_redirect     off;
                proxy_set_header   Host             $host;
                proxy_set_header   X-Real-IP        $remote_addr;
                proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

        }       
}       

Ne reste plus qu'à activer le vhost, et relancer nginx

ln -s /etc/nginx/sites-available/monprojet /etc/nginx/sites-enabled/
service nginx reload

16 juil. 2014

Django, générer du xlsx

Les exports ... on est tous amené dans un projet à devoir se farcir des exports de données pour de l'analyse comptable, statistiques ou de l'analyse de production.

Bien souvent je ne cherchai pas trop loin pour sortir les données ( il faut dire que ce n'est pas ce qu'il y a de plus sexy comme travail ), j'optais pour un vulgaire fichier CSV!
Bien pratique, et rapide à sortir mais il faut avouer que ce n'est pas ce qu'il y a de plus présentable aux clients finaux, et il faut vendre le fait que la personne qui va exploiter ce genre d'export, va devoir commencer par structurer le fichier csv pour le rendre exploitable . ( et je ne parle même pas des encodages ! )

Bref ... ça fait porc !

xlsxwriter

Je me suis donc tourné vers une lib très bien documentée et qui fonctionne à merveille . xlsxwriter
Qui plus est très rapide à mettre en place et à prendre en main .

voyez vous même avec l'exemple N°1 de la documentation

import xlsxwriter

# Create a workbook and add a worksheet.
workbook = xlsxwriter.Workbook('Expenses01.xlsx')
worksheet = workbook.add_worksheet()

# Some data we want to write to the worksheet.
expenses = (
    ['Rent', 1000],
    ['Gas',   100],
    ['Food',  300],
    ['Gym',    50],
)

# Start from the first cell. Rows and columns are zero indexed.
row = 0
col = 0

# Iterate over the data and write it out row by row.
for item, cost in (expenses):
    worksheet.write(row, col,     item)
    worksheet.write(row, col + 1, cost)
    row += 1

# Write a total using a formula.
worksheet.write(row, 0, 'Total')
worksheet.write(row, 1, '=SUM(B1:B4)')

workbook.close()

Voilà pour la lib xlsxwriter, je ne vais pas vous faire un tutoriel de la doc qui est très bien faite .
Par contre, regardons comment l'intégrer à une vue Django .

Django

L'objectif, est de générer le fichier xlsx ci-dessus, depuis une vue Django, mais sans enregistrer le fichier sur le disque du serveur pour ensuite le servir, non, nous allons le générer en mémoire et l'envoyer avec les entêtes qui vont bien pour avoir une belle boite de dialogue comme ceci

xlsx.png

StringIO et cStringIO

Afin d'éviter de générer un fichier sur le disque à chaque exportation, nous allons travailler en mémoire exclusivement.
Pour y parvenir, nous allons utiliser la lib StringIO ou cStringIO si elle est dispo.
StringIO permet de travailler sur un object qui réagit comme un fichier text ( read, write, ... ) sauf qu'on est en mémoire et non sur le disque .

Voilà à quoi va ressembler la vue Django :

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO
buffer=StringIO()

    
import xlsxwriter

def export(self):
    """ Export Démo  """

    # Create a workbook and add a worksheet.
    workbook = xlsxwriter.Workbook(buffer, {'constant_memory': True})
    worksheet = workbook.add_worksheet()

    # Some data we want to write to the worksheet.
    expenses = (
        ['Rent', 1000],
        ['Gas',   100],
        ['Food',  300],
        ['Gym',    50],
    )

    # Start from the first cell. Rows and columns are zero indexed.
    row = 0
    col = 0

    # Iterate over the data and write it out row by row.
    for item, cost in (expenses):
        worksheet.write(row, col,     item)
        worksheet.write(row, col + 1, cost)
        row += 1

    # Write a total using a formula.
    worksheet.write(row, 0, 'Total')
    worksheet.write(row, 1, '=SUM(B1:B4)')

    workbook.close()


    #Reponse envoyée au navigateur
    response = HttpResponse(buffer.getvalue(), mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
    response['Content-Disposition'] = "attachment; filename=test_export.xlsx"

    return response

Petite liste des types mime .. toujours pratique .

Et voilà le travail .. simple et efficace .
Maintenant ne vous reste plus qu'à mettre en forme vos exports afin qu'ils soient immédiatement exploitables par les principaux intéressés.

C'est pas grand chose, mais ça fait du bien de partager ;)
Ch.

17 janv. 2014

Django, gérer les FormSets dynamiquement

Les formsets sous Django sont très pratiques, notamment pour gérer les formulaires à "champs multiples".

qu'est-ce que j'entends par "champs multiples" ... ceci :

champs_multiples.png

Dans ce formulaire, j'ai donc deux champs uniques

  1. Largeur
  2. Hauteur

Puis vient enfin mon formset qui est composé de trois champs

  1. Désignation
  2. Quantité
  3. Liste : R | RV

J'ai donc au final un formulaire dont je ne connais pas à l'avance la quantité de données à traiter puisque le formset permet d'ajouter dynamiquement ses champs. Et c'est précisément ce que nous allons voir dans ce billet .

  • Comment créer un formset
  • Comment le rendre dynamique pour l'utilisateur ( ajout / suppression )
  • Comment valider les données en une seule ligne ( Merci Django :) )

Créer un Formset

Nous allons reproduire le formulaire présenté ci-dessus , nous aurons donc un formulaire classique , puis un formset de trois champs .

Formulaire classique

class ConfForm(forms.Form):
        """
        Formulaire de configuration du module
        """
        format_x = forms.IntegerField( label="Largeur", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'mm','class':'form-control input-sm'}) )
        format_y = forms.IntegerField( label="Hauteur", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'mm','class':'form-control input-sm'}) )

On ne peut pas plus .. classique pour les détails je vous laisse voir la doc de Django

Le formset

class SorteForm(forms.Form):

    #Champs multiples
    designation = forms.CharField( label="Désignation", required=True, widget=forms.TextInput(attrs={'placeholder':'','class':'form-control input-sm'}) )
    quantite = forms.IntegerField( label="Quantité", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'','class':'form-control input-sm'}) )
    rectoverso = forms.ChoiceField(label='',choices = [(False,'Recto Seul'),(True,'Recto Verso')],required=True,widget=forms.Select( attrs={'class':'form-control input-sm'}))

Vous allez me dire ... "c'est une déclaration de formulaire classique" .. et bien oui rien ne différencie au niveau déclaration un formulaire qui sera utilisé de façon classique, d'un formulaire qui sera utilisé au sein d'un formset .

Tout ceci pour en venir au fait qu'un formset, n'est ni plus, ni moins qu'un mécanisme proposé par Django pour gérer l'affichage, la validation des données et quelques autres petites choses comme les limitations min/max en un minimum d'efforts pour le développeur ...

Ça tombe bien je suis fainéant pour ce qui est de la gestion de formulaires ( pas vous ? ).

Maintenant que nous avons nos deux déclarations de formulaires, voyons comment les implémenter dans une vue.

Dans la vue

Note : Je ne vais pas expliquer ici la mise en forme CSS du formulaire, ce n'est pas l'objectif du billet, sachez juste que j'utilise BootstrapV3 dans la présentation du début.

Commençons avec une vue standard ... vide

def show_form(self, request):
    """ Affichage du formulaire + formset """

    # Formulaire standard à deux champs
    form = ConfForm(auto_id=True)

    #Création du formset avec une seule itération : extra=1
    sorteForm = formset_factory(SorteForm,extra=1)
        
    # Récupération du formulaire géré par le mécanisme formset
    formset = sorteForm()

    # Affichage du template 
    return render_to_response('my_form.html', {'form':form, 'sorteForm':formset}, context_instance=RequestContext(request) )

Le template

D'habitude, pour afficher un formulaire sans mise en forme particulière , la balise form se suffit à elle même.
Pour le formset , c'est la même chose, si ce n'est que nous allons devoir mettre cette balise dans une itération, car rappelez-vous, un formset peut contenir plusieurs formulaires.

Ce qui va donc nous donner : ( sans mise en forme )

<!-- formulaire standard -->
{{ form }}

<!-- Champs utilisés par le mécanisme formset de Django -->
{{ sorteForm.management_form }}

<!-- itération des formulaires du formset -->
{% for form in sorteForm %}
    
    {{ form }}

{% endfor %}

La balise management_form est utilisée par le mécanisme formset pour gérer les éventuels ajouts dynamiques, modifications, etc ... bref .. fonctionner correctement .
Voici son contenu

<!-- nombre de formulaire (s) -->
<input id="id_form-TOTAL_FORMS" type="hidden" value="1" name="form-TOTAL_FORMS">
<!-- nombre de formulaires à l'origine --> 
<input id="id_form-INITIAL_FORMS" type="hidden" value="1" name="form-INITIAL_FORMS">
<!-- nombre max de formulaires gérés si ajout dynamique -->
<input id="id_form-MAX_NUM_FORMS" type="hidden" value="1000" name="form-MAX_NUM_FORMS">

La validation et l'enregistrement

Oui oui .. je sais .. pour le moment il n'y a rien de dynamique, pas d'ajout / suppression ... patience, commençons par boucler la boucle avec la gestion des données POST, nous verrons ensuite pour dynamiser le formulaire .

En effet, l'étape de validation et traitement, ne varie pas selon le nombre de formulaire(s) dans le formset, c'est là toute la force des formset avec Django .

Voici la vue de validation , qui reçoit les données POST

    def save_form(self):
        """  Check & Enregistre les données   """

        # Check si la méthode est bien en POST
        if request.method == 'POST':

            # Instancie et "bound" le formulaire
            form = ConfForm( request.POST, auto_id=True )

            # Création du formset
            sorteForm = formset_factory( SorteForm )
            # "Bound" le formset
            formset = sorteForm( request.POST )

            #Verifie si les données du formulaire simple sont valides
            if form.is_valid() :
                # Vérifie le(s) formulaire(s) du formset 
                if formset.is_valid():

                    #Affiche les données du formulaire simple
                    print form.cleaned_data

                    # Itération du formset pour afficher les données 
                    for fs in formset:
                        print fs.cleaned_data
                        
                else:
                    print = str( formset.errors )
            else:
                print = str( form.errors )

Forcément ici le code ne sert à rien, si ce n'est à expliquer comment valider formulaire + formset pour un traitement des données .. ( enregistrement en base, actions, mails etc ... ou comme ici un simple affichage ;) )

Dynamisme

Nous voilà avec un super formulaire + formset, qui s'affichent correctement, avec les données validées ( peu importe le nombre de formulaire(s) dans le formset ), mais maintenant nous allons voir comment ajouter / supprimer dynamiquement un formulaire dans le formset .

A partir de maintenant tout se passe dans le template , le principe est simple, nous ajoutons un boutton "+" pour ajouter un formulaire au formset et, pour chaque formulaire du formset un boutton "-" qui permettra de retirer le retirer .

Pour ajouter un formulaire au formset, nous allons faire appel à du javascript, qui va cloner un élément du DOM, ré-indexer les formulaires ( du formset ) puis mettre à jour la valeur du champ de management form-TOTAL_FORMS.

Pourquoi ré-indexer

Si l'on regarde le code généré par le formset, les champs sont indexés pour ne pas mélanger les données entres formulaires, vérifier la cohérence des données etc ..

<!-- Form 1 -->
<input id="id_form-0-designation" class="form-control input-sm" type="text" value="V2" placeholder="" name="form-0-designation">
<input id="id_form-0-quantite" class="form-control input-sm" type="text" value="1000" placeholder="" name="form-0-quantite">
....
<!-- Form n -->
<input id="id_form-n-designation" class="form-control input-sm" type="text" value="V2" placeholder="" name="form-n-designation">
<input id="id_form-n-quantite" class="form-control input-sm" type="text" value="1000" placeholder="" name="form-n-quantite">
...

Formulaire vide

Quel formulaire copier dans le DOM .. et puis .. je ne souhaite pas copier le contenu des champs ...ça tombe bien , encore une fois Django nous livre tout sur un plateau d'argent, grâce à l'attribut empty_form.

Nous allons donc dans un div hors de la balise form inclure le code suivant {{ sorteForm.empty_form }} ce qui va donc nous donner

<div id="my_form">
    <form action="#" name="form_test">
    <!-- Le formulaire standard -->
    {{ form }}

    <button class="btn btn-success btn-sm" id="bt_add_sorte" type="button">+</button>

    {{ sorteForm.management_form }}
        <!-- Les formulaires du formset -->
        <div id="formsetZone">
            {% for form in sorteForm %}
                <div class="nsorte">
                {{ form }}
                </div>
            {% endfor %}
        </div>
    </form>
</div>

<!-- Element à copier pour un ajout au formset -->
<div style="display:none;">
    <div id="eform" class="nsorte" >
        {{ sorteForm.empty_form }}
        <div class='btn btn-warning btn-sm bt_rm_sorte'>
                <i class='glyphicon glyphicon-trash'></i>
        </div>
    </div>
</div>

Voilà à quoi ressemble ce formulaire vide

<label for="id_form-__prefix__-designation">Désignation&nbsp;:</label>
<input class="form-control input-sm" id="id_form-__prefix__-designation" name="form-__prefix__-designation" placeholder="" type="text">
<label for="id_form-__prefix__-quantite">Quantité&nbsp;:</label>
<input class="form-control input-sm" id="id_form-__prefix__-quantite" name="form-__prefix__-quantite" placeholder="" type="text">
<select class="form-control input-sm" id="id_form-__prefix__-rectoverso" name="form-__prefix__-rectoverso">
<option value="False">Recto Seul</option>
<option value="True">Recto Verso</option>
</select>

L'objectif pour l'ajout est donc :

  1. Binder l’événement onclick du bouton avec l'id bt_add_sorte
  2. Cloner le contenu du div avec l'id eform
  3. Ajouter le données clonées à la fin du div id formsetZone
  4. Ré-indexer les formulaires du formset pour remplacer prefix par le bon numéro de formulaire
  5. Incrémenter la valeur du champ de management avec l'id id_form-TOTAL_FORMS

Pour la suppression :

  1. Binder les boutons dont la classe est bt_rm_sorte
  2. Enlever du DOM le formulaire à retirer ( conteneur parent avec la classe nsorte )
  3. Re-indexer les formulaires du Formset
  4. Décrémenter a valeur du champ de management avec l'id id_form-TOTAL_FORMS

vive le javascript

Tout ça en javascript, grâce à JQuery

$(document).ready(function(){

    /**************************************************************************
    *
    *                                      Gesion ADD REMOVE Formset 
    *
    ***************************************************************************/

    index_form = function( fset, index ){

        $(fset).find(':input').each(function() {
            var name = $(this).attr('name').replace( new RegExp('(\_\_prefix\_\_|\\d)') , index );
            var id = 'id_' + name;
            $(this).attr({'name': name, 'id': id});
        });

        $(fset).find('label').each(function() {
            var newFor = $(this).attr('for').replace( new RegExp('(\_\_prefix\_\_|\\d)') , index );
            var id = 'label_' + newFor;
            $(this).attr({'id':id, 'for':newFor});
        });

    }

    reindex_formset = function( formset_zone ){

        var formset = $(formset_zone).find( '.nsorte' );
        for( var cpt=0;cpt<formset.length;cpt++ ){
            index_form( formset[cpt], cpt );
        };

        $("#id_form-TOTAL_FORMS").val( parseInt( cpt ) );

    };



    /**************************************************************************
    *
    *                               Gesion Des evenements formulaire
    *
    ***************************************************************************/


    set_event = function(){
            //Bind le(s) bt delete sorte
            $(".bt_rm_sorte").on('click',function(){
                $(this).parents(".nsorte").remove();
                reindex_formset( "#formsetZone" );
            });
    };

    $("#bt_add_sorte").on('click',function(){

        //Copy eform
        $( "#eform" ).clone(true).appendTo( $("#formsetZone") );

        reindex_formset( "#formsetZone" );

    });

    set_event();


});

Conclusion

Nous avons fait le tour pour gérer les formset dynamiquement, au diable les galères pour gérer ce genre de cas , merci Django .

Ch.

31 oct. 2013

Déployer votre app Python automatiquement avec fabric .

Petite mise en place

Lorsque vous développez en Python, contrairement à PHP, ce dernier compile le code à la première "interprétation", puis génère des fichiers "bytecode", avec l’extension .pyc.
Une fois cette génération faite, le fichier source .py du même nom sera ignoré, et ce jusqu'au redémarrage du process.
Le redémarrage du process, vérifiera les modifications apportées aux sources ( .py ) afin de les recompiler et exécuter le code mis à jour.

Tout ça pour en venir au fait, que lorsque vous développez en python ( par exemple une app. web avec Django ), lorsque vous envoyez vos maj sur le serveur de production, vos modifications n'auront aucun effet tant que vous n'avez pas redémarré les process ( uwsgi, ou autre .. ) .

Ce qui peut vous surprendre au début, si vous venez du monde PHP ou les modifications sont prises en compte instantanément.
Bien que vous pouvez avoir quelque chose de similaire si vous utilisez PHP avec le cache APC ... mais ce n'est pas le sujet ici.

Étapes manuelles

Donc si je prends mon projet Django, voici les étapes manuelles à appliquer :

Sur mon poste de développement :

git add xx ; s'il y a des fichiers sources à ajouter au dépôt
git commit -a -m "Commentaire de mon commit"
git push

Sur mon serveur de prod :

cd /mon/depot
git pull 
# vim settings.py pour des update de version de cache etc .. 
workon monenvironnementvirtuel
python manage.py collectstatic
supervisorctl MonApp reload

Étapes avec fabric

Sur mon poste de développement :
fab backup
Sur le serveur
fab deploy

Bon ... voyons maintenant comment mettre en place fabric .

Installation et fabfile.py

Commençons par installer la librairie ( sur le serveur et le poste de dev. )

workon monenvironnement
pip install fabric

Puis nous allons créer notre fichier qui va se charger d'automatiser les différentes étapes manuelles: fabfile.py

#! -*- coding:utf-8 -*-
from fabric.api import local,prefix
from fabric.colors import green

import re

PROJECT_NAME = "MonApp" # Nom de l'app
SUPERVISOR_APP_NAME = "MonApp" # Nom de l'app supervisor à redémarrer
VIRTUALENV_NAME = "MonApp"  #Nom de l'environnement virtuel à utiliser

def backup():
    #Sauvegarde les librairies utilisées par le projet ( pour les installer si besoin sur le serveur .. sans réfléchir )
    local('pip freeze > requirements.txt')

    #Ajoute les éventuels fichiers non "trackés" 
    local('git add .')

    git_status = local( 'git status', capture=True )
    print git_status

    regx = re.compile(r"(nothing to commit)")
    if not regx.search( git_status ):

        print(green("Il y a des choses à commiter !"))

        print("Entrez votre commentaire de commit : ")
        comment = raw_input()

        #Renseignement du commentaire du commit
        local('git commit -m "%s"' % comment)
        local('git push')
    else:
        print(green("Rien à commiter !"))


def deploy():
    """
    Déploie l'app sur le "serveur" courant 
    """

    #Récupère la dernière version du dépôt
    local('git pull')

    #Permet de changer la version des fichiers statiques à servir 
    print(green("Version du cache à appliquer []:"))
    vcache = raw_input()
    xs = vcache.strip()
    if xs[0:1] in '+-': xs = xs[1:]
    if xs.isdigit():
        init_cache(int(xs))


    # Active ou désactive le mode DEBUG
    print(green("Désactiver le mode DEBUG ( y/n ) ? [y]"))
    disable_debug = raw_input()
    print disable_debug
    if disable_debug == 'y' or disable_debug=='':
        print(green("Passage du mode DEBUG à FALSE"))
        switch_debug(False)
    else:
        print(green("Passage du mode DEBUG à TRUE"))
        switch_debug(True)

    #Récupère les éventuels nouveaux fichiers statiques 
    local(django_manage( 'collectstatic --link', VIRTUALENV_NAME ),capture=False,shell='/bin/bash')

    #Redémarre le processus supervisord
    local('supervisorctl restart '+SUPERVISOR_APP_NAME)



def init_cache(change_to):

    local( 'cp '+ PROJECT_NAME +'/settings.py '+ PROJECT_NAME +'/settings.bak' )
    sed = "sed 's/^VERSION_JS_CSS = [0-9]*$/VERSION_JS_CSS = %s/' "+ PROJECT_NAME+"/settings.bak > "+PROJECT_NAME+"/settings.py"
    local(sed % (change_to))

    local('rm '+PROJECT_NAME+'/settings.bak')

def switch_debug(change_to):

    local( 'cp '+ PROJECT_NAME +'/settings.py '+ PROJECT_NAME +'/settings.bak' )
    sed = "sed 's/^DEBUG = [a-zA-Z]*$/DEBUG = %s/' "+ PROJECT_NAME+"/settings.bak > "+PROJECT_NAME+"/settings.py"
    local(sed % (change_to))

    local('rm '+PROJECT_NAME+'/settings.bak')

Et voilà en deux commandes, vous avez mis à jour votre application sur le serveur de production, chargé les éventuels fichier statiques dans le répertoire prévu à cet effet, modifié le fichier settings.py pour y indiquer s'il faut invalider les fichiers statiques ( css, js .. ) afin de forcer les navigateurs à charger les nouvelles versions, activé ou non le mode DEBUG de Django, et pour finir relancé les process uwsgi grâce à supervisor ( commande supervisorctl ).

Il est possible de tout faire depuis son poste de développement, Fabric peu se connecter sur les serveurs distants pour déployer l'application, ce qui peu s'avérer très utile lorsque vous avez plusieurs frontaux à mettre à jour .

Backup

Fabric_backup.png

Deploy

Fabric_deploy.png

Source:

Fabric : http://docs.fabfile.org/en/1.8/

15 sept. 2013

Mezzanine CMS

mezzanine.png je vais présenter dans ce post, le CMS Mezzanine, qui se veut être une alternative à Wordpress.
Mezzanine est un projet bâti sur l'excellent framework Django, aujoud'hui avec plus de 250k téléchargements, il est devenu une référence dans les solutions CMS basées sur python.

Présentation

Mezzanine possède des aspects intéressants se rapprochant de Wordpress, une interface intuitive permettant de gérer des pages statiques avec une édition simplifiée et rapide grâce au WYSIWYG, directement depuis les pages en ligne.
Une programmation de mise en ligne / hors ligne, une gestion multi-langues, la personnalisation des thèmes avec une granularité élevée ( un thème par page si vous le souhaitez ). Sans oublier la compatibilité multi-sites, vous permettant de gérer plusieurs sites avec une seule instance de Mezzanine.

Et encore bien d'autres aspects intéressants, comme:

  • Une bonne documentation
  • Un moteur de recherche intégré et directement fonctionnel
  • Le coté modulaire de Django ( modules mis à disposition par la communauté )
  • Une gestion de galeries Photos à 100% intégré
  • Détection des types de clients ( smartphones, tablettes ... )
  • Une gestion de migration depuis d'autres Blogs
  • L'intégration de Disqus
  • Intégration de Gravatar
  • Google analitycs
  • Fils de discutions Twitter
  • Intégration de bit.ly
  • Intégration d'Akismet ( Filtre anti-spam des commentaires )

...

Liste de sites utilisant Mezzanine

Installation

Super Mezzanine non ?
On va le tester.
Ce qui est bien avec les projets basés sous Django, c'est qu'il n'y a pas besoin de passer par un "setup" sur un serveur web, vhost BDD etc ... non pas besoin !
Il faut juste avoir python d'installé sur le poste .

VirtualEnv

Commençons par créer un environnement virtuel ( un minimum quand même ).

mkvirtualenv mezzanine

Puis installons mezzanine

pip install mezzanine

Nous voilà avec les librairies mezzanine de disponibles, nous pouvons donc créer notre site, appelé "monsite" pour l'occasion ( innovant comme nom .. non ? )

mezzanine-project monsite
cd monsite

Nous allons maintenant initialiser la base de données, ( sqlitle par défaut .. )

python manage.py createdb --noinput

Fin du setup de l'environnement de développement.

Premiers Tests

Bien notre site est prêt, nos allons pouvoir le tester et se familiariser avec la partie admin et l'édition en ligne via le WYSIWYG

python manage.py runserver

Et voilà ne vous reste plus qu'à vous rendre à l'adresse suivante : http://127.0.0.1:8000
Et pour la partie Admin : http://127.0.0.1:8000/admin

Le login et mot de passe par défaut pour la partie Admin sont :

  • Login : admin
  • Mot de passe: default

Conclusion

Voilà pour la première présentation de mezzanine, je n'ai pas encore décidé du sujet du prochain article ... "personnaliser le thème de Mezzanine", "installer des modules tiers" ou encore "mise en production d'un projet Mezzanine" ou ... vous avez envie de quoi ? ( laissez un commentaire si vous avez une envie particulière. ).

Bonne découverte,

Ch.

Référence : Mezzanine

- page 1 de 2