PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Introduction à Ansible: l’outil du sysadmin paresseux mais pragmatique

vendredi 6 décembre 2013 à 11:43

Ceci est un post invité de VonTenia posté sous licence creative common 3.0 unported.

Je profite du fait que Sam & Max me donnent la parole pour vous parler d’Ansible, un programme très puissant et relativement simple dont je me sers depuis récemment (beaucoup trop tardivement à mon goût), mais qui a radicalement changé ma façon de gérer mes déploiements d’appli sur serveur.

Avant-propos : Ce guide s’adresse avant tout à ceux et celles ayant le minimum d’aisance avec les systèmes linux. Je pense qu’il est nécessaire de savoir marcher avant d’apprendre à courir, l’automatisation de configuration est une bonne chose (vous allez voir que vous ne pourrez plus vous en passer), mais si vous n’avez aucune idée de comment éditer un fichier de configuration, ou comment redémarrer un service, vous risqueriez bien d’être pris au dépourvu… Mieux vaut alors pour vous commencer par apprendre les bases de l’administration système puis revenir une fois à l’aise avec le concept.

Pourquoi utiliser un “Configuration Management Tool”

Vous vous dites : mon boulot c’est de coder, l’administration système c’est sympa 5 minutes mais ça me gonfle… Et pourtant, au final votre application sera accédée via vos serveurs, et selon leur fragilité, la satisfaction de vos clients pourrait en pâtir (ce malgré votre excellent code parfaitement testé).

En tant que dev, il serait risible pour vous de ne pas versionner votre code ou ne pas le tester. Pourtant c’est ce que vous faites avec vos systèmes en n’utilisant pas de CfM. Et personne n’est à l’abri des aléas de la vie, par exemple:

Bref, le genre de risque qu’on apprend à identifier quand on passe sa certif ITIL…

Les alternatives aux CfM

Je vous vois venir, me disant que vous ne m’avez pas attendu pour envisager les situations précédentes. Vous avez déjà un plan de secours, à savoir :

Ansible à votre secours

Ansible est un outil open-source de gestion de configuration écrit en python (aussi dispo en version commerciale avec une interface graphique et un service de déploiement). La configuration se fait via des fichiers appelés “Playbooks”. Citons parmi les avantages :

Ansible ne sert pas qu’à déployer votre infrastructure, il peut aussi servir à tester et s’assurer que tous les services qui sont censés fonctionner soient bien tous actifs, et que tous les fichiers de configurations sont bien à jour. Autant vous dire que plus vous avez de machines, plus ça devient intéressant.

Je sens que j’écris beaucoup et que j’ai déjà perdu la moitié des lecteurs. Aussi je vous invite à suivre ce petit tutoriel que j’ai préparé rien que pour vous parce que vous êtes quand même sympa.

Tutoriel : Déployer une app django

Nous allons essayer de déployer l’application Django–an-app-at-a-time sur un système Debian wheezy en utilisant Ansible.

1. Préparer la machine cible

Pour les besoins du test, créer un serveur tout neuf sous Debian Wheezy :

2. Installer Ansible

Sur votre machine hôte (que j’assume être sous Ubuntu pour simplifier):

Installez Ansible, via pip (de façon globale sans passer par virtualenv) :
$ sudo pip install ansible

Générez clefs privée/publique si vous n’en avez pas déjà :
$ ssh-keygen

Copiez la clef publique sur le serveur cible (qui sera désigné par 192.168.1.1 dans ce tutoriel, mais bien entendu remplacez par l’adresse de votre serveur cible).
$ ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.1.1

Créez le fichier /etc/ansible/hosts qui contiendra la liste des serveurs à gérer, et placez-y l’adresse de votre serveur:
$ sudo vim /etc/ansible/hosts
192.168.1.1

Testez que le serveur soit bien accessible:
$ ansible all -m ping -u root
devrait retourner:
192.168.1.1 | success >> {
"changed": false,
"ping": "pong"
}

Bravo, Ansible est installé et peut communiquer avec votre serveur cible. En avant pour la magie !


3. Récupérer le Playbook de démo et l’exécuter

Clonez mon repo github concocté avec amour et exécutez le playbook:

$ git clone https://github.com/Remiz/playbook-demo.git
$ cd playbook-demo/
$ ansible-playbook site.yml

Maintenant, plus qu’à attendre…

3. Admirer le résultat

Visitez le site hébergé à l’adresse de votre serveur (dans mon exemple http://192.168.1.1)

Votre réaction la plus normale devrait être la suivante :

Je vous invite maintenant à ouvrir le playbook site.yml et essayer de comprendre. Durant ce court laps de temps, nous avons:

Pas mal en 5 minutes, non ? Maintenant si vous ne me croyez pas, je vous invite à vous connecter sur votre serveur

$ ssh myproject@192.168.1.1

et tester les commandes suivantes :

$ date
$ sudo iptables -L
$ ps -ef | grep fail2ban
$ ps -ef | grep gunicorn

Notez que je n’ai pas utilisé runserver de Django, tout est proprement déployé sur une stack gunicorn/supervisor/virtualenv, bref je me suis pas foutu de votre gueule. Le Playbook est à vous, c’est cadeau. J’espère qu’il vous servira comme base pour vos futurs déploiements, et si jamais vous vous rendez compte que vous gagnez un temps fou à l’utiliser, n’hésitez pas à me payer une pinte si vous êtes de passage au Canada.

Une autre expérience intéressante consiste à relancer l’exécution du playbook :

$ ansible-playbook site.yml

Tout devrait aller beaucoup plus vite, et à la place de “changed” après chaque instruction, vous devriez lire “ok”. Ce qui veut dire qu’un playbook est plus intelligent qu’un bête script, et ne se contente pas d’exécuter des instructions, Ansible va garantir quel tel service soit bien actif et qu’il utilise bien le dernier fichier de conf. Ce qui en fait l’outil parfait pour tester vos systèmes automatiquement.

La syntaxe Playbook

Le but de ce tutoriel n’est que de vous présenter Ansible, aussi je ne rentrerai pas trop dans les détails et je vous inviterai à vous rendre sur le site officiel pour une documentation plus complète.

Un playbook est avant tout composé de tâches :

- name: Texte qui décris votre tâche
module: option=value

Une tâche va donc appeler un module Ansible, dont la fonction peut être de copier un fichier, démarrer un service, clôner un repository… Il y en a vraiment beaucoup. Chaque module reçoit des paramètres tels que : un fichier de configuration source (sur votre machine hôte), un path de destination, un package apt à installer… Référez vous à la doc pour savoir quels paramètres sont acceptés.

Exemples :

- name: Démarrer fail2ban
service: name=fail2ban state=started enabled=true

va s’assurer que le service appelé fail2ban soit bien démarré (le démarrer si ce n’est pas le cas), mais aussi s’assurer qu’il soit bien présent au démarrage du système. Quand je vous disait que la syntaxe est très simple (même plus simple qu’avec des scripts shell).

Autre exemple:

- name: Configurer Nginx
template: src=templates/nginx.conf.j2
dest=/etc/nginx/sites-enabled/{{ user }}.conf
notify: restart nginx

se traduit par : récupérer le template de conf dans le répertoire local templates/ (le parser avec les bonnes valeurs), et placer le résultat dans le répertoire de conf de Nginx (en utilisant le nom d’utilisateur comme nom du fichier). Enfin redémarrer nginx via un handler (uniquement si le contenu du fichier de conf a changé).

Conclusion (et aller plus loin)

Je vous conseille de lire la documentation officielle, elle est plutôt bien faite, dites-vous que je ne connaissais pas du tout cet outil il y a deux semaines et je m’en sert désormais régulièrement (et je suis du genre slow-learner). Renseignez vous particulièrement sur les rôles que vous pouvez donner à vos serveurs, ce qui vous permet de diviser vos playbooks (frontend, cluster DB, worker celery…), et ce qui encourage aussi la réutilisation (par exemple j’ai toujours un rôle “common” qui inclus tout ce qui est nécessaire à l’ensemble de mes serveurs : utilisateur admin, sécurité, timezone…). Comme on n’apprend jamais aussi bien que par l’exemple, n’hésitez pas à vous inspirer des exemples issus de la doc (l’outil évolue vite et certains ne sont plus entièrement valides, mais c’est toujours bon à prendre).

Si vous voulez pousser l’automatisation jusqu’à l’extrême, il est aussi possible de configurer Ansible sur vos serveurs pour se connecter à un repo git, récupérer les playbooks et fichiers de conf appropriés et s’auto-configurer…

Voila, mon rôle s’arrête ici et libre à vous d’en apprendre plus. Au final j’espère avoir tenu ma promesse d’éclairer vos esprit sur les miracles de l’automatisation.

flattr this!

En Python 3, le type bytes est un array d’entiers

jeudi 5 décembre 2013 à 17:00

Le plus gros changement quand on passe de Python 2 à Python 3, c’est la gestion des chaînes de caractères.

Pour rappel :

Si vous avez besoin d’une mise à jour sur l’encoding en Python, on a un article pour ça.

Comme toute entrée ou sortie est forcément un flux d’octets, mais pas forcément dans le même encodage, Python 2.7 pouvait poser problème pour le débutant qui essayait de comprendre pourquoi son programme plantait, bordel de merde.

La version 3 prend plusieurs mesures pour éviter les bugs vicieux liés à l’encodage de caractères:

La plupart du temps, quand on va manipuler du texte, on va donc toujours manipuler de l’unicode, en Python 3. Ce dernier va nous forcer à faire le décodage / encodage au bon moment.

Mais il restera quelques fois le besoin de manipuler du bytes, et ce type a subi un lifting…

La base

Créer un array d’octets (le type bytes‘, en Python 3) demande de préfixer une chaîne avec ‘b’ :

>>> s = b'I am evil, stop laughing!'
>>> type(s)
<class 'bytes'>
>>> print(s)
b'I am evil, stop laughing!'

Première remarque, on ne peut plus utiliser ce type pour afficher quoi que ce soit, puisque l’affichage est une représentation du type (appel à __repr__), et pas du texte mis en forme. Déjà Python vous indique la couleur : si vous voulez manipulez du texte, n’utilisez pas ce type.

Comparez avec le type unicode :

>>> u = s.decode('utf8')
>>> type(u)
<class 'str'>
>>> print(u)
I am evil, stop laughing!

L’affichage marche comme on s’y attend. Bref, vous êtes forcé de toujours rester sur de l’unicode (le type str en Python 3, ce qui porte à confusion) si vous manipulez du texte. Heureusement, c’est quasiment toujours ce que vous aurez.

Par exemple, si vous ouvrez un fichier en Python 3 :

>>> content = open('/etc/fstab').read()
>>> type(content)
<class 'str'>

C’est du texte. A moins de demander qu’il soit ouvert en mode binaire :

>>>> content = open('/etc/fstab', 'rb').read()
>>> type(content)
<class 'bytes'>

Une autre différence MAJEURE, c’est que, si dans Python 2.7, les arrays d’octets pouvaient être manipulés comme un array de lettres :

>>> s = 'I put the goal in golem...' 
>>> s[0] # en Python 2.7
>>> 'I'

En Python 3, les array d’octets sont au mieux manipulables comme un array d’entiers :

>>> s = b'I put the goal in golem...'
>>> s[0] # en Python 3
73

La représentation sous forme de lettre est gardée pour l’initialisation pour des raisons pratiques, mais sous le capot, il se passe ça:

>>> bytes([73, 32, 112, 117, 116, 32, 116, 104, 101, 32, 103, 111, 97, 108, 32, 105, 110, 32, 103, 111, 108, 101, 109, 46, 46, 46])
b'I put the goal in golem...'

D’ailleurs, on ne peut même plus faire d’opérations de formatage avec des octets comme en Python 2.7 :

>>> b"Welcome to the league of %s" % input('')
Draven
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for %: 'bytes' and 'str'

format() ne marche pas non plus. On est assez proche du tableau d’octets en C, sauf qu’en plus, on ne peut pas le modifier :

>>> s = b"My right arm is a lot stronger than my left arm."
>>> s[0] = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

Les arrays d’octets sont donc maintenant essentiellement des outils de communication avec le monde extérieur.

Bytearray

Il existe encore des raisons de manipuler des arrays d’octets : les applications scientifiques. Typiquement, les algos de crypto opérent sur des arrays d’octets.

Pour cette raison, Python 3 vient également avec un nouveau type de base : bytearray, un array d’octets modifiable.

>>> s = bytearray(b"this tasted purple !")
>>> s[2:4] = b'at'
>>> print(s)
bytearray(b'that tasted purple !')

Et on a toutes les opérations de liste dessus, comme append, pop(), etc :

>>> for x in b' ,puY':
...     s.insert(0, x)
... 
>>> print(s)
bytearray(b'Yup, that tasted purple !')

Attention par contre, ces opérations attendent un entier en paramètres et NON un array d’octets.

Et un dernier détail :

>>> isinstance(bytes, bytearray)
False
>>> isinstance(bytearray, bytes)
False

Différence entre string et array d’octets

Il est facile de confondre tout ce merdier.

En Python 2.7, le type str était un array d’octets, et on le manipulait comme une chaîne, d’où la difficulté de transition.

En Python 3, bien qu’on puisse créer un array d’octets avec une syntaxe utilisant des lettres, ils ne sont plus du tout utilisés pour la manipulation de texte. Si vous voulez manipuler du texte qui vient de l’extérieur de votre programme, il faudra toujours le décoder pour obtenir un type str (qui est l’ancien type unicode de Python 2.7).

Le décodage sera fait automatiquement dans la plupart des cas, et plantera si on tombe sur un cas où vous devez le faire à la main et que vous ne le faites pas. Du coup, plus de difficulté à trouver d’où vient ce bug d’encoding, car on a toujours l’erreur à la source.

En ce sens, Python 3 est beaucoup plus clair : les octets d’un côté, le texte de l’autre. Bon, tout ça c’est de la surcouche, au final, tout est octet. Mais on a rarement envie de manipuler un octet directement, sinon on coderait encore en assembleur.

Avec ce système, Python 3 est le langage le plus sain que j’ai pu rencontrer dans sa gestion de l’encodage : il ne cache rien, oblige l’utilisateur à coder avec de bonnes habitudes, facilite le débugage et met sur le devant de la scène la problématique de l’encoding, qui est le plus souvent cachée vite fait sous le tapis.

L’alternative intelligente la plus proche étant celle de node.js, qui interdit tout simplement la plupart des encodings dans son API.

La bonne nouvelle ? 99% du temps, vous n’aurez même pas à vous en soucier, car ASCII est inclus dans UTF8, et ce sont les encodings les plus utilisés. Avec Python 3 forçant UTF8 par défaut partout et des chaînes en unicode dès le départ, il n’y a presque rien à faire. Je doute que la plupart des gens aient même à manipuler le type bytes.

flattr this!

Ecrire une commande Django

mercredi 4 décembre 2013 à 08:01

Quand vous avez à faire un script pour un projet Django, il est pratique de l’avoir sous forme de sous-commande de manage.py : c’est portable d’un projet à l’autre et ça permet d’utiliser l’ORM et les templates sans réglage puisqu’on a accès à tous les settings, automatiquement.

Malheureusement le wrapper de Django destiné à cela date un peu, il est plutôt lourd, pas très souple et utilise des pratiques qui ne sont plus au goût du jour depuis quelques années. Rien de rédhibitoire, mais tout de même.

D’abord, il faut placer sa commande dans un fichier portant le nom de la commande, et dans un package management.commands d’une de vos apps.

Par exemple, si vous voulez faire une commande “nettoyer_godmichets” dans l’application “sex_shop”, il faudra la mettre bien profondément dans le tréfonds de votre projet :

racine
│   ├── sex_shop
│   │   ├── management
│   │   │   ├── commands
│   │   │   │   ├── nettoyer_godmichets.py
│   │   │   │   ├── __init__.py
│   │   │   │   └── __init__.pyc
│   │   │   ├── __init__.py

Notez les fichiers __init__.py, indispendables sinon votre commande ne sera pas trouvée. Oh, et ‘sex_shop’ doit être dans INSTALLED_APPS.

Ensuite, votre commande doit être une classe héritant de BaseCommand. Sa méthode handle() sera appelée automatiquement au lancement de la commande.

from django.core.management.base import BaseCommand
from sex_shop.models import Godmichet
 
class Command(BaseCommand):
 
    def handle(self, *args, **options):
        for god in Godmichet.objects.all():
            god.clean()

Et vous pouvez lancer la commande :

./manage.py nettoyer_godmichets

Néanmoins généralement on voudra avoir un peu de décorum.

 
from optparse import make_option
 
from django.core.management.base import BaseCommand
from sex_shop.models import Godmichet, Marque
 
 
class Command(BaseCommand):
 
    # ici on peut mettre un message d'aide
    help = 'Fait briller dard dard les dards'
 
    # optionellement une aide pour les arguments
    args = 'marque1, [marque2], marque3'
 
    # On peut ajouter des options passables à la commande
    option_list = BaseCommand.option_list + (
        make_option('--dry-run',
            action='store_true',
            dest='dry_run',
            default=False,
            help='Affichage uniquement, aucune action réelle'),
        )
 
    def handle(self, *args, **options):
 
        # on retrouve dans args les arguments positionnels de la commande
 
        if not args:
            for god in Godmichet.objects.all():
                # Plutôt que print(), on peut utiliser ce wrapper
                # pour ecrire sur le terminal. Cela permet de bénéficier
                # automatiquement de l'option --verbosity
                self.stdout.write('Processing %s' % god.name)
 
                # La valeur des options est passée via kwargs.
                if not options['dry_run']:
                    god.clean()
        else:
            # l'utilisateur a passé des marques ? On nettoie que les gods
            # de ces marques...
            for marque in args:
                try:
                    gods = Marque.objects.get(name=marque).gods.all()
                    for god in gods:
                        self.stdout.write('Processing %s' % god.name)
 
                        if not options['dry_run']:
                            god.clean()
 
                except Marque.DoesNotExist:
                    # si la marque n'existe pas, on fait une erreur
                    # ceci arrête le script, retourne un code d'erreur 1
                    # et met le texte en rouge
                    raise CommandError('La marque %s n'existe pas' %s marque)

Et voilà, votre commande accepte maintenant optionnellement qu’on lui passe une liste de marques et/ou une option --dry-run:

./manage.py nettoyer_godmichet devilmaycry choucroutebestfriend --dry-run

En plus de ça, la commande accepte automatiquement :

Qui ont le même effet que sur toutes les commandes django officielles.

flattr this!

Conséquences de print devenant une fonction en Python 3

mardi 3 décembre 2013 à 09:31

Devoir utiliser print() au lieu de print m’arrache la gueule, je dois l’avouer. J’ai l’impression que ces deux parenthèses ma prennent 5 heures à taper, là où avant mon petit espace était à portée de pouce.

Mais évidement, la décision de faire de print une fonction est parfaitement rationnelle, et en voici tous les avantages…

print n’est plus une déclaration, mais une expression

Il y a deux types d’instructions en Python : les déclarations (‘statement’ dans la langue de Chuck Norris) et les expressions.

Les déclarations sont des instructions indépendantes : while, var = 'valeur', try, def, etc. On ne peut pas les mettre dans une expression, seulement les imbriquer dans une autre déclaration.

Les expressions, elles, sont imbricables dans n’importe quoi, et elles retournent toujours quelques chose quelque chose, fusse None. Parmis les expressions on retrouve : les lambdas, les listes en intentions, les appels de fonctions, etc.

Avant, print était une déclaration, ce qui était très peu souple. Maintenant, c’est un appel de fonction, et donc une expression.

On peut donc utiliser print dans une autre expression et ainsi :

Dans une lambda :

crier = lambda x, *a, **k: print(x.upper(), *a, **k)

Dans une liste en intention :

[print(x) for x in range(10)]

Plus de syntaxe bizarre

Comment supprimer le saut de ligne quand on print avec l’ancienne syntaxe ?

print "Hello", # <- notez la virgule
print "You"
Hello You

Avec la nouvelle syntaxe :

print("Hello ", end='')
print('You')

Comment rediriger le print vers stderr avec l’ancienne syntaxe ?

>>> print >> sys.stderr, 'arg !'

Avec la nouvelle ?

>>> print('arg !', file=sys.stderr)

Comment faire une liste séparée par des virgules avec l’ancienne syntaxe ?

>>> l = ['Des petits trous', 'des petits trous', 'toujours des petits trous']
>>> print ', '.join(l)
Des petits trous, des petits trous, toujours des petits trous

Avec la nouvelle ?

>>> print(*l, sep=', ')

En gros, la syntaxe est unifiée, plus besoin de penser à tous ces cas particuliers. Et en plus on peut demander de l’aide avec help(print) alors qu’avant ça faisait une syntaxe error.

On peut récupérer une référence sur
print

Et donc la passer en paramètre pour faire de l’injection de dépendance :

def truc(a, print=print):
    # faire un truc avec a
    print(a); # on peut utiliser print normalement
 
import logging
log = loging.getLogger()
truc(machin, print=log) # on print pas, on log !
truc(autre_machin, print=lambda *a, **k: None) # ignore les prints !

Et également appliquer tous les outils de programmation fonctionnelle de Python :

import sys
from functools import partial
error = partial(print, file=sys.stderr)
error('Wrong !') # va directement sur stderr

Activer print() en Python 2.7

Si vous voulez prendre toute de suite de bonnes habitudes, vous pouvez faire, en Python 2.7 :

from __future__ import print_function

flattr this!

Le point sur le blog (et une mise à jour)

lundi 2 décembre 2013 à 18:43

On a fait pas mal de hors sujets ces derniers temps.

Les articles sur le voyage ont encore du sens sur le blog, puisque Max et moi voyageons beaucoup. Et puis le dernier était très bon.

Mais on commence à être loin de Python et du cul. J’ai rien contre, il faut juste savoir doser.

Du coup il est possible qu’on refuse (ou publie un peu plus tard) certaines contributions.

Comme d’habitude, on répond toujours à tous les mails. Parfois avec des mois de retard. Oui, des mois. Mais on répond (ou alors vous êtes dans nos spams ^^).

Je profite de cet article qui est un grand fourre tout pour remercier ÉNORMÉMENT tous les contributeurs qui ont pondu des articles invités et ont corrigé nos fautes :

Mais aussi tous ceux qui prennent le temps d’écrire des commentaires pertinents. Franchement, la qualité des commentaires sur ce blog est juste hallucinante.

J’espère que j’en oublie pas, sinon vous avez le droit de me fouetter :-)

Histoire de terminer sur une note de joie, j’ai mis à jour le dump du blog, que vous pouvez télécharger ici sous forme de zip.

Grâce à des gentils gens sur Twitter, le dump contient maintenant aussi un backup de tous nos tweets.

flattr this!

Error happened! 0 - count(): Argument #1 ($value) must be of type Countable|array, null given In: /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php:428 http://ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/?123 #0 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(999): VroumVroum_Blog->update() #1 /var/www/ecirtam.net/autoblogs/autoblogs/sametmaxcom_a844ada43a979e3b1395ab9acb6afafb84340999/index.php(1): require_once('...') #2 {main}