PROJET AUTOBLOG


Sam et Max

source: Sam et Max

⇐ retour index

Debugger Python à distance avec rpdb 7

vendredi 9 janvier 2015 à 09:33

Vous aimez pdb parce que c’est cool. Et vous adorez pdbpp parce que c’est trop cool.

Mais parfois vous n’avez pas accès à une console sur votre process : il est derrière un nginx, ou même sur une machine distante.

rpdb vient résoudre ce problème en lançant un serveur telnet qui donne accès à votre debugger.

pip install rpdb

Puis :

import rpdb; rpdb.set_trace()

Et après vous prenez votre client telnet favoris, et vous accédez à votre débugger :

telnet 127.0.0.1 4444

Bien entendu, si vous êtes à distance, remplacez 127.0.0.1 par l’ip de la machine. Le port est configurable également :

import rpdb
debugger = rpdb.Rpdb(port=12345)
debugger.set_trace()

Et derrière, ça lance pdb, donc pdbpp est lancé automatiquement si il est installé. Joie.

Conclusions de la migration 10

jeudi 8 janvier 2015 à 10:15

La migration de serveur est terminée. Le blog, le multiboards, IndexError et 0bin on été rétablis. On en a profité pour remettre sur pied AllThatCounts qui avait été délaissé durant le dernier crash.

On quitte donc LeaseWeb, qui nous a forcé à migré 3 fois avec ses machines qui ont planté. En plus deux fois la partition /tmp était corrompue, ce qui rend le backup particulièrement compliqué. On notera que leur SAV nous posait des questions du genre “si vous installez ça, et lancez cette commande, ça donne quoi ?”, alors qu’on leur a bien notifié qu’on avait un disque monté en lecture seul du fait du FS en vrac…

On est passé chez Cinfu, car on peut les payer en Bitcoin. Livraison du serveur rapide, installation sans histoire, et finalement une migration beaucoup moins chiante que la dernière fois car on a fait les gros bourrins : rsync + mysqldump bien large. Ce qui a pris le plus de temps c’était de résoudre les centaines de problèmes de permissions que ça a créé, les trucs que ça aurait pas du écraser, etc.

On avait + de 100000 spams dans la poubelle des commentaires, qui prenaient 300 Mo des 400 mo de la taille totale de la BDD du blog. Un petit :

DELETE FROM wp_comments WHERE comment_approved = 'trash';

A accéléré la migration vu qu’on a du transférer la base 3 fois et la réimporter autant à cause d’erreurs diverses. Note à nous-même : arrêter de mettre des .bak dans /tmp parce que c’est “juste pour 5 minutes”. Après on se fait avoir comme des débutants au reboot.

C’est là qu’on voit qu’on est dev, et pas admin.

Rajouter dans wp-config.php :

define('EMPTY_TRASH_DAYS', 30);

Nous évitera d’avoir à repenser à tout ça. C’est con mais faut le savoir.

J’en profite pour témoigner mon amour immodéré pour mosh. Parce que faire tout ça sur une connexion thai avec 300ms de ping minimum et une coupure toutes les 10 minutes, avec SSH, c’est juste un enfer.

Sous Centos, faut installer les repos EPEL et yum install mosh derrière.

Sous Ubuntu, fait installer le ppa ppa:keithw/mosh et apt-get install mosh derrière.

Certains serveurs ont un pare feu tatillon, et il faut rajouter dans la section :RH-Firewall-1-INPUT - [0:0] de iptable :

-A RH-Firewall-1-INPUT -p udp --dport 60000:61000 -j ACCEPT

D’autres ont des problèmes de locales:

apt-get install --reinstall language-pack-fr
dpkg-reconfigure locales

Parfois y a aucun problème. C’est juste qu’on a pas un parc homogène, avec des bécanes vieilles de 1000 ans, alors forcément…

Mais Max a fait des devis, et si on passe au cloud avec les 2G/s de BP et les To de disque dur qu’on consomme, on multiplie les prix par 10 d’hébergement. Faire les trucs à la main, c’est chiant, mais c’est économe.

Pour mosh, pas de serveur à lancer, juste remplacer ssh par mosh dans la commande quand on se connecte. Des fois je lance avec --predict=experimental car je suis impatient et le retour de frappe est plus rapide, mais le cursor fait des mouvements bizarres, faut s’habituer.

Bon, on retourne faire des trucs plus productifs.

Black awk down 8

mardi 6 janvier 2015 à 11:26

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

Introduction (sans douleur)

Qui n’a jamais rêvé d’avoir un shell Unix un peu plus pythonic ? Les oneliners en sed et Awk bien chiadés, c’est l’apanage des grands barbus en sandales et ça déchire, mais ça reste cryptique et la manipulation de liste et de chaînes de caractères est tout de même limitée. On peut dégainer perl en one line, comme ça c’est encore plus puissant mais moins lisible…

Et python dans tout ça ? En oneliner, ça craint, car le principe d’indentation ne facilite pas les choses et on finit avec une imbrication de bazar de parenthèses illisible.

La solution ? Pour se faire plaisir : une bonne pyp. Le bidule s’installe avec… pip (c’est une méta pipe).

pip install pyp

pyp (Python Power at the Prompt) va enchanter votre shell unix et vous envoyer au 7ème ciel. Petit tour d’horizon de la merveille.

Pyp et les strings

C’est la base quand on bricole des oneliners en shell, tripoter des strings.

ls *JPG | pyp "'mv', p, p.replace('JPG', 'jpg')"

Revoyons l’action au ralenti :

On voit ici que l’on utilise la méthode replace de la classe str python. On peut utiliser n’importe quelle méthode de str : lower, upper, title, strip(tease), count etc. Avec des petits bonus comme refindall(re) :-)

Ça donne quoi ma brave dame ? Et bien si on avait des fichiers appelés porn1.JPG et ass.JPG notre jolie commande renverrait :

mv porn1.JPG porn1.jpg
mv ass.JPG ass.jpg

Ok, bien gentil me direz-vous, mais qu’en fait-on ? Et bien ou l’on pipe ça dans le shell ( | sh) ou alors on passe l’argument -x (ou –execute) à pyp ou directement exécuter le résultat.

ls *JPG | pyp "'mv', p, p.replace('JPG', 'jpg')" | sh
# ou bien
ls *JPG | pyp -x "'mv', p, p.replace('JPG', 'jpg')"

Pourquoi on aime bien python ? Car il slice le bread comme un chef. Pyp aussi, oeuf corse, quelques exemples :

echo "sam-et-max" | pyp "p.split('-')[2]"
# max
 
# La même chose en plus court. Le m, comme minus, sous-entend qu'on split sur "-"
echo "sam-et-max" | pyp "m[2]"
# max
 
echo "sam-et-max" | pyp "m[0], m[2]"
# sam max

Comme vous avez vu, il y a un petit raccourci sympa avec m (comme minus) au lieu de p. C’est pas fini, vous en avez d’autres : d (dot) pour un point, w (whitespace) pour une espace, u pour underscore, s pour slash… Ils ont pensé à tout.

Des pyp à la chaîne

On tout de suite envie de remettre le couvert avec une autre pyp. C’est possible et même encouragé. Le symbole utilisé est le pipe unix | dans une commande pyp. Ce n’est donc pas un pipe du shell mais un pipe interne de pyp. Vous suivez ? Un petit exemple :

echo "sam et max" | pyp "w[0] | 'avant : ', o, 'après :', p"
# avant :  sam et max après : sam

Le p représente toujours le paramètre courant. Donc dans la seconde partie du pipe, c’est le premier élément de la liste issu du split de la chaîne “sam et max” sur l’espace (w comme whitespace). Vous suivez toujours ? Oui, bon. Et donc le o ? Et bien c’est le paramètre d’origine non modifié. Et il y a h (comme history ou comme hâlibi !) pour avoir le paramètre p à toutes les étapes de la chaîne de pipe… Beaucoup de puissance on vous dit !

On peut aussi rouler^w faire des join facilement avec les mêmes raccourcis. Par exemple on split sur les espaces puis join sur slash :

echo "sam et max aiment le lourd en fin de soirée" | pyp "w[:6]|s"
# sam/et/max/aiment/le/lourd

Une liste de pyp avec une belle pp

S’il y a un bien un truc qui envoie du bois avec python, c’est la gestion des listes. Pyp n’est pas en reste. C’est pp qui contient les listes. En avant :

ls | pyp "pp[3]"
# Affiche le troisième fichier

On peut utiliser toutes les méthodes standards de la classe list (sort, count etc.) plus quelques bonus :

# Équivalent de | sort | uniq

ls | pyp "pp.uniq()"
 
# Additionne tous les PID des process de machine.
# Ça ne sert à rien, mais c'est un exemple hein ?
ps -ef | pyp "w[1] | int(p) | sum(pp)"
 
# Voilà qui est plus utile. 
# Au passage, on note que la ligne d'en-tête est ignoré et ne fait pas planter pyp.
# Malin le lapin.
df -m | pyp "w[1] | int(p) | sum(pp)"

pyp au naturel

Pour compléter les fonctions python, pyp propose quelques fonctions maisons tout à fait sympathiques. Florilège :

echo "Sam Bill Max" | pyp "p.kill('Bill')"  # quand c'est pas bill, c'est kenny...
# Sam  Max
 
echo 124 | pyp "(int(p)/2+30)"
# 91
 
ls  | pyp "n, p"
# Liste des fichiers avec un numéro croissant devant
 
echo /home/fox/video/gangbang.avi  | pyp "'Dir='+p.dir, '\nFile='+p.file, '\nExt='+p.ext" # basename, dirname et ses amis
# Dir=/home/fox/video 
# File=gangbang.avi 
# Ext=avi
 
# On définit le séparateur de liste comme l'espace, 
# puis on regroupe la liste par deux élément
# et enfin on fait un join sur le caractère tiret (minus)
echo "1 2 3 4 5 6" | pyp "pp.delimit(' ') | pp.divide(2) | m"
# 1-2
# 3-4
# 5-6
 
ps -ef | pyp "w[1] | int(p) | max(pp)" 
# (le PID le plus grand)

pyp en intention ou intention de pyp ?

Il ne faut pas se gêner, on peut utiliser les excellentes listes en intention de Python. Voici tous les PID impaires :

ps -ef | pyp "w[1] | int(p) | [i for i in pp if i%2] | p "

L’industrie de la pyp c’est un truc de macro

Les bonnes choses, ça se garde, alors voilà :

ps -ef | pyp "w[1] | int(p) | [i for i in pp if i%2] | p " -s odd  # on enregistre la macro 'odd'
 
ps ef | pyp odd # c'est aussi simple que cela ;-)

pyp sur grand écran

Cette petite pépite est développée par Sony Pictures Imageworks pour ses besoins internes. Le développement n’est pas très collaboratif (un commit par release…) mais reste ouvert aux contributions externes. C’est sous une licence de type BSD.
Les loulous ont fait une jolie vidéo de tutorial très bling bling. C’est l’avantage de bosser dans le milieu :

Et bien plus encore…

Cet outil merveilleux possède encore des fonctionnalités qui vont vous le faire adorer comme son mode interactif avec une coloration syntaxique sympathique, la gestion de l’historique dans le pipe et beaucoup d’autres. Adieu sed, awk, uniq, sort et con sort, vous étiez mes premiers amours, mais pyp vous chasse dehors.

Ce n’est pas la seule ni la première tentative de faire la peau d’awk avec python. Sam avait déjà écrit un article sur une autre solution aussi appelée pyped.

C’est donc parfait ?

Oui, bien entendu. Aucun défaut, aucun bug. Ok… voici les limites :

Amusez vous bien, celui qui fait la plus jolie commande pyp dans les commentaires aura le droit à un bisous.

Les managers le détestent : faites tourner WAMP dans Django avec cette astuce insolite 9

dimanche 4 janvier 2015 à 20:45

Il existe une lib appelée crochet qui permet de faire marcher des API de twisted entre deux bouts de code bloquants. Certes, ça ne marche qu’en 2.7 et c’est pas hyper performant, mais on peut faire des trucs mignons du genre cette démo qui mélange flask et WAMP.

C’est du pur Python, pas de process externe à gérer, c’est presque simple.

Bref, si on veut utiliser WAMP avec une app synchrone comme flask, c’est un bon moyen de s’y mettre. On aura jamais des perfs fantastiques, mais on peut pusher vers le browser.

Du coup je me suis demandé si on pouvait faire ça avec Django.

Évidement, ça a été un peu plus compliqué car par défaut runserver lance plusieurs workers et fait un peu de magie avec les threads. Mais après un peu de bidouillage, ça marche !

On peut utiliser WAMP, directement dans Django.

Suivez le guide

D’abord, on installe tout le bouzin (python 2.7, souvenez-vous) :

pip install crossbar crochet django

Il vous faudra un Django 1.7, le tout dernier, car il possède une fonctionnalité qui nous permet de lancer du code quand tout le framework est chargé.

Vous vous faites votre projet comme d’hab, et vous ouvrez le fichier de settings et au lieu de mettre votre app dans INSTALLED_APPS, vous rajoutez ça :

INSTALLED_APPS = (
    '...',
    'votreapp.app.VotreAppConfig'
)

Puis dans le module de votre app, vous créez un fichier app.py, qui va contenir ça:

# -*- coding: utf-8 -*-
 
import crochet
 
from django.apps import AppConfig
 
# On charge l'objet contenant la session WAMP définie dans la vue
from votreapp.views import wapp
 
class VotreAppConfig(AppConfig):
    name = 'votreapp'
    def ready(self):
        # On dit a crochet de faire tourner notre app wamp dans sa popote qui
        # isole le reactor de Twisted
        @crochet.run_in_reactor
        def start_wamp():
           # On démarre la session WAMP en se connectant au serveur
           # publique de test
           wapp.run("wws://demo.crossbar.io/ws", "realm1", start_reactor=False)
        start_wamp()

On passe à urls.py dans lequel on se rajoute des vues de démo :

    url(r'^ping/', 'votreapp.views.ping'),
    url(r'^$', 'votreapp.views.index')

Puis dans notre fichier views.py, on met :

# -*- coding: utf-8 -*-
 
import uuid
 
from django.shortcuts import render
 
import crochet
 
# Crochet se démerde pour faire tourner le reactor twisted de
# manière invisible. A lancer avant d'importer autobahn
crochet.setup()
 
from autobahn.twisted.wamp import Application
 
# un objet qui contient une session WAMP
wapp = Application()
 
# On enrobe les primitives de WAMP pour les rendre synchrones
@crochet.wait_for(timeout=1)
def publish(topic, *args, **kwargs):
   return wapp.session.publish(topic, *args, **kwargs)
 
@crochet.wait_for(timeout=1)
def call(name, *args, **kwargs):
   return wapp.session.call(name, *args, **kwargs)
 
def register(name, *args, **kwargs):
    @crochet.run_in_reactor
    def decorator(func):
        wapp.register(name, *args, **kwargs)(func)
    return decorator
 
def subscribe(name, *args, **kwargs):
    @crochet.run_in_reactor
    def decorator(func):
        wapp.subscribe(name, *args, **kwargs)(func)
    return decorator
 
# Et hop, on peut utiliser nos outils WAMP !
 
@register('uuid')
def get_uuid():
    return uuid.uuid4().hex
 
@subscribe('ping')
def onping():
    with open('test', 'w') as f:
        f.write('ping')
 
# Et à côté, quelques vues django normales
 
def index(request):
    # pub et RPC en action côté Python
    publish('ping')
    print call('uuid')
 
    with open('test') as f:
        print(f.read())
    return render(request, 'index.html')
 
def ping(request):
    return render(request, 'ping.html')

Après, un peu de templating pour que ça marche…

Index.html :

{% load staticfiles %}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>
       UUID
    </title>
 
    <script src="{% static 'autobahn.min.js' %}"></script>
    <script type="text/javascript">
      var connection = new autobahn.Connection({
         url: "ws://localhost:8080/ws",
         realm: "realm1"
      });
 
     connection.onopen = function (session) {
 
        session.call("uuid").then(function (uuid) {
          var p = document.getElementById('uuid');
          p.innerHTML = uuid;
        });
     };
 
     connection.open();
    </script>
</head>
<body>
<h2>UUID</h2>
<p id="uuid"></p>
</body>
</html>

ping.html :

{% load staticfiles %}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>
       Ping
    </title>
 
    <script src="{% static 'autobahn.min.js' %}"></script>
    <script type="text/javascript">
      var connection = new autobahn.Connection({
         url: "ws://localhost:8080/ws",
         realm: "realm1"
      });
 
     connection.onopen = function (session) {
 
        session.subscribe("ping", function () {
          var ul = document.getElementById('ping');
          var li = document.createElement('li');
          li.innerHTML = 'Ping !'
          ul.appendChild(li);
        });
     };
 
     connection.open();
    </script>
</head>
<body>
<h2>Ping me !</h2>
 
<ul id="ping">
</ul>
</body>
</html>

On ouvre la console, on lance son routeur :

    crossbar init
    crossbar start

On lance dans une autre console son serveur Django :

./manage.py runserver

Et si on navigue sur http://127.0.0.1:8000, on récupère un UUID tout frais via RCP.

On peut aussi voir dans le shell que ça marche côté Python :

94cfccf0899d4c42950788fa655b65ed
ping

D’ailleurs un fichier nommé “test” est créé à la racine du projet.

Et si on navigue sur http://127.0.0.1:8000/ping/ et qu’on refresh http://127.0.0.1:8000 plusieurs fois, on voit la page se mettre à jour.

Achievement unlock : use WAMP and Django code in the same file.

A partir de là

Il y a plein de choses à faire.

On pourrait faire une lib qui wrap tout ça pour pas à avoir à le mettre dans son fichier de vue et qui utilise settings.py pour la configuration.

Il faut tester ça avec des setups plus gros pour voir comment ça se comporte avec gunicorn, plusieurs workers, le logging de Django, etc. Je suis à peu près sûr que les callbacks vont être registrés plusieurs fois et ça devrait faire des erreurs dans les logs (rien de grave ceci dit).

On pourrait aussi adapter le RPC pour qu’il utilise les cookies d’authentification Django, et pouvoir les protéger avec @login_required.

Mais un monde d’opportunités s’offrent à vous à partir de là.

Moi, ça fait 6 h que je taffe dessus, je vais me pieuter.


Télécharger le code de l’article

S’assurer que SELinux ne bloque pas un programme 7

samedi 3 janvier 2015 à 12:48

Les distros Linux récentes viennent souvent avec des surcouches de protections type policykit, apparmor, SELinux, etc.

Tout ça est bien loin de la simplicité de la notion user/group et leurs permissions, et peut mener à un arrachage de cheveux en règle.

Cette après midi par exemple, on avait notre backend qui ne répondait pas sur le serveur tout neuf. On a changé toutes les permissions, passé de sock:// à http://, tweaké les fichiers de settings de iptables, varnish, nginx, gunicorn, supervisor, django… Puis en dernier recours, on a viré toutes les couches une par une, desinstaller les programmes, réinstaller à neuf, reset les fichiers de config en valeur par défaut, créé des fichiers de config minimaux pour toute la stack, nada. La journée frustrante et foutue en l’air.

On a quand même isolé le point de blocage sur varnish, et après avoir fait tous les tests possibles et des sacrifices humains, Max a eu l’intuition SELinux.

Voici maintenant ce qui se passe généralement dans cette situation.

Dans un élan de colère, l’utilisateur impatient fera un rapide test pour savoir si SELinux le protège comme les USA ont protégé les irakiens :

echo 0 >/selinux/enforce

Ça désactive tout le bouzin jusqu’au prochain reboot, mettre 1 inversant la tendance.

Lançant son programme et voyant que ça marche, notre dev échaudé se fend de l’équivalent du chmod 777 -R pour SELinux, à savoir éditer le fichier de config (/etc/sysconfig/selinux sur Centos) et changer rageusement SELINUX=enforcing pour SELINUX=disabled. Reboot and forget.

Une solution radicale et efficace, certes, mais qui laisse quand même un petit poids sur le conscience : diantre, il y a sûrement un moyen propre d’essuyer toute cette merde. Où sont les 3 coquillages ?

La solution est dans un petit programme appelé audit2allow, qui fait parti d’un package plus gros qu’il vous faudra trouver pour votre OS. Sous CentOs, puisque c’était notre config ce jour là, on a du faire un petit :

yum install policycoreutils-python

Ensuite, on lance notre audit :

audit2allow -a -w

Et après un peu de moulinage, il va vous sortir tout un listing de blocage, et quoi faire pour s’en sortir. Un petit grep votre_prog sur la sortie et vous obtenez ceci :

type=AVC msg=audit(1331500393.450:25): avc:  denied  { name_connect } for  pid=1276 comm="varnishd" dest=81 scontext=unconfined_u:system_r:varnishd_t:s0 tcontext=system_u:object_r:reserved_port_t:s0 tclass=tcp_socket
        Was caused by:
        One of the following booleans was set incorrectly.
        Description:
        Allow varnishd to connect to all ports, not just HTTP.

        Allow access by executing:
        # setsebool -P varnishd_connect_any 1
        Description:
        Allow system to run with NIS

        Allow access by executing:
        # setsebool -P allow_ypbind 1

Nous, c’était varnish qui était bloqué, comme le confirme le discret denied dans ce gros blog d’informations illisibles. A croire que les gars de SELinux ont tout fait pour créer la techno la moins user friendly possible. Je pense que la ligne ne commence pas par “AVC” pour rien, chaque fois que SELinux bloque un truc, un devops stagiaire meurt quelque part.

Il suffit ensuite de suivre docilement les instructions. Pour ce cas, on rentre avec les droits admin la commande :

setsebool -P varnishd_connect_any 1

Et on est bon.

Enfin, jusqu’à la prochaine après midi de debug incompréhensible qui vous fera revenir à SELinux. Max est beaucoup tenté par la solution 1 pour éviter une nouvelle galère.