PROJET AUTOBLOG


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

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

⇐ retour index

Un petit dashboard de monitoring avec Django et WAMP 1

samedi 7 février 2015 à 11:58

Cet article est écrit dans le cadre de ma collaboration avec Tavendo.

On a déjà vu que WAMP c’est cool, mais c’est asynchrone et nos frameworks Web chéris WSGI sont synchrones.

J’ai donné une solution de contournement avec la lib crochet qui permet de faire tourner du twisted de manière synchrone dans son projet.

Néanmoins, beaucoup sont, j’en suis certain, à la recherche d’un truc plus simple. En effet, le bénéfice le plus immédiat de WAMP sont les notifications en temps réel. Et pour ça, crossbar vient avec le HTTP PUSHER service : quelques lignes de JSON dans le fichier de config de crossbar et zou, on peut publier sur un topic WAMP avec une simple requête POST :

 "transports": [
    {
       "type": "web",
       "endpoint": {
          "type": "tcp",
          "port": 8080
       },
       "paths": {
          ...
          "notify": {
             "type": "pusher",
             "realm": "realm1",
             "role": "anonymous"
          }
       }
    }
 ]

Et derrière, pour publier un event sur le sujet “super_sujet”, on peut faire :

import requets
requests.post("http://ip_du_router/pusher",
                  json={
                      'topic': 'super_sujet'
                      'args': [queques, params, a, passer, si, on veut]
                  })

Ceci va envoyer une requête POST à un service de crossbar qui va transformer ça en véritable publish WAMP.

Histoire d’illustrer tout ça, je vais vous montrer comment construire un petit service de monitoring avec Crossbar.io et Django. Pour suivre le tuto vous aurez besoin :

Premiers pas

Le but du jeu est d’avoir un petit client WAMP qu’on lance sur chaque machine qu’on veut monitorer. Celui-ci va, toutes les x secondes, récupérer l’usage CPU, RAM et disque et faire un publish WAMP.

Chaque machine possède un client WAMP

Chaque machine possède un client WAMP

A l’autre bout, on a un site Django qui a un modèle pour chaque machine monitorée, avec des valeurs pour dire si on est intéressé par le CPU, la RAM ou le disque et la valeur de x.

Une page affiche en temps réel tous les relevés pour toutes les machines. Si dans l’admin de Django on change un modèle, la page reflète ce changement.

Si je déclique "CPU" dans l'admin Django, les CPUs ne sont plus affichés

Si je déclique “CPU” dans l’admin Django, les CPUs ne sont plus affichés

On aura donc besoin de django (pip install Django, ça c’est pas trop dur), requests (pip install requests, jusqu’ici tout va bien), et psutil.

psutil est la lib Python qui va nous permettre de récupérer toutes le valeurs pour la RAM, le disque et le CPU. Elle utilise des extensions en C, il faut donc un compilateur et les headers Python. Sous Ubuntu, il faut donc faire :

sudo apt-get install gcc python-dev

Sous CentOS ça donne :

yum groupinstall "Development tools"
yum install python-devel

Sous Mac, les headers Python devraient être inclus, mais il vous faut aussi GCC. Si vous avez xcode, vous avez déjà un compilateur, sinon, il existe un installeur plus léger.

Sous windows, c’est un wheel donc rien à faire normalement.

Et reste plus qu’à pip install psutil.

Enfin il nous faudra, logique, installer crossbar. pip install crossbar, sachant que sous Windows vous aurez besoin de PyWin32 et comme toujours, d’avoir les dossiers C:\Python27\ and C:\Python27\Scripts dans votre PATH.

Le HTML

On a besoin que d’une page. Afin de rendre le tuto agnostique, je l’ai fait en pur JS, pas de jQuery, pas d’Angular. Donc c’est verbeux :)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
 
    <!-- De quoi cacher un bloc facilement -->
    <style type="text/css">
        .hide {display:none;}
    </style>
 
    <!--
        La lib JS qui permet de parler WAMP .
 
        Ici je suppose qu'on utilise un navigateur qui support websocket.
        Il est possible de faire du fallback sur flash ou long poll, mais
        ce sont des dépendances en plus.
    -->
    <script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"
           type="text/javascript"></script>
 
 
    <!-- Tout notre code client, inline pour faciliter votre lecture -->
    <script type="text/javascript">
 
      /* Connexion à notre serveur WAMP */
      window.addEventListener("load", function(){
        var connection = new autobahn.Connection({
           url: 'ws://127.0.0.1:8080/ws',
           realm: 'realm1'
        });
 
        /* Quand la connexion est ouverte, exécuter ce code */
        connection.onopen = function(session) {
 
          var clients = document.getElementById("clients");
 
          /* Quand on reçoit l'événement clientstats, lancer cette fonction */
          session.subscribe('clientstats', function(args){
            var stats = args[0];
            var serverNode = document.getElementById(stats.ip);
 
            /*
                 Créer un li contenant un h2 et un dl pour ce client si
                 il n'est pas encore dans la page.
            */
            if (!serverNode){
                serverNode = document.createElement("li");
                serverNode.id = stats.ip;
                serverNode.appendChild(document.createElement("h2"));
                serverNode.appendChild(document.createElement("dl"));
                serverNode.firstChild.innerHTML = stats.name + " (" + stats.ip + ")";
                clients.appendChild(serverNode);
 
                // Cacher les infos du serveur si il est désactivé.
                session.subscribe('clientconfig.' + stats.ip, function(args){
                    var config = args[0];
                    if (config.disabled){
                        var serverNode = document.getElementById(config.ip);
                        serverNode.className = "hide";
                    }
                });
 
            }
 
            // Remettre à zéro le contenu du li du serveur.
            serverNode.className = "";
            var dl = serverNode.lastChild;
            while (dl.hasChildNodes()) {
                dl.removeChild(dl.lastChild);
            }
 
            // Si on a des infos sur le CPU, les afficher
            if (stats.cpus){
                var cpus = document.createElement("dt");
                cpus.innerHTML = "CPUs:";
                dl.appendChild(cpus);
                for (var i = 0; i < stats.cpus.length; i++) {
                    var cpu = document.createElement("dd");
                    cpu.innerHTML = stats.cpus[i];
                    dl.appendChild(cpu);
                };
            }
 
            // Si on a des infos sur l'espace disque, les afficher
            if (stats.disks){
                var disks = document.createElement("dt");
                disks.innerHTML = "Disk usage:";
                dl.appendChild(disks);
                for (key in stats.disks) {
                    var disk = document.createElement("dd");
                    disk.innerHTML = "<strong>" + key + "</strong>: " + stats.disks[key];
                    dl.appendChild(disk);
                };
            }
 
            // Si on a des infos sur l'usage mémoire, les afficher.
            if (stats.memory){
                var memory = document.createElement("dt");
                memory.innerHTML = "Memory:";
                dl.appendChild(memory);
                var memVal = document.createElement("dd");
                memVal.innerHTML = stats.memory;
                dl.appendChild(memVal);
            }
 
          });
 
        };
 
        // Ouvrir la connexion avec le routeur WAMP.
        connection.open();
 
      });
    </script>
 
    <title> Monitoring</title>
</head>
<body>
    <h1> Monitoring </h1>
    <ul id="clients"></ul>
</body>
 
</html>

Comme vous pouvez le voir, c’est beaucoup de JS ordinaire et du DOM. Les seules parties spécifiques à WAMP sont :

var connection = new autobahn.Connection({
           url: 'ws://127.0.0.1:8080/ws',
           realm: 'realm1'
        });
connection.onopen = function(session) {
...
}
connection.open();

Pour se connecter au serveur.

Et :

session.subscribe('nom_du_sujet', function(args){
...
}

Pour réagir à la publication d’un sujet WAMP.

Le client de monitoring

C’est la partie qui va aller sur chaque machine qu’on veut surveiller.

# -*- coding: utf-8 -*-
 
from __future__ import division
 
import socket
 
import requests
import psutil
 
from autobahn.twisted.wamp import Application
from autobahn.twisted.util import sleep
 
from twisted.internet.defer import inlineCallbacks
 
def to_gib(bytes, factor=2**30, suffix="GiB"):
    """ Converti un nombre d'octets en gibioctets.
 
        Ex : 1073741824 octets = 1073741824/2**30 = 1GiO
    """
    return "%0.2f%s" % (bytes / factor, suffix)
 
def get_infos(filters={}):
    """ Retourne la valeur actuelle de l'usage CPU, mémoire et disque.
 
        Ces valeurs sont retournées sous la forme d'un dictionnaire :
 
            {
                'cpus': ['x%', 'y%', etc],
                'memory': "z%",
                'disk':{
                    '/partition/1': 'x/y (z%)',
                    '/partition/2': 'x/y (z%)',
                    etc
                }
            }
 
        Le paramètre filter est un dico de la forme :
 
            {'cpus': bool, 'memory':bool, 'disk':bool}
 
        Il est utilisé pour décider d'inclure ou non les résultats des mesures
        pour les 3 types de ressource.
 
    """
 
    results = {}
 
    if (filters.get('show_cpus', True)):
        results['cpus'] = tuple("%s%%" % x for x in psutil.cpu_percent(percpu=True))
 
    if (filters.get('show_memory', True)):
        memory = psutil.phymem_usage()
        results['memory'] = '{used}/{total} ({percent}%)'.format(
            used=to_gib(memory.active),
            total=to_gib(memory.total),
            percent=memory.percent
        )
 
    if (filters.get('show_disk', True)):
        disks = {}
        for device in psutil.disk_partitions():
            usage = psutil.disk_usage(device.mountpoint)
            disks[device.mountpoint] = '{used}/{total} ({percent}%)'.format(
                used=to_gib(usage.used),
                total=to_gib(usage.total),
                percent=usage.percent
            )
        results['disks'] = disks
 
    return results
 
# On créé le client WAMP.
app = Application('monitoring')
 
# Ceci est l'IP publique de ma machine puisque
# ce client doit pouvoir accéder à mon serveur
# depuis l'extérieur.
SERVER = '172.17.42.1'
 
# D'abord on utilise une astuce pour connaître l'IP publique de cette
# machine.
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
# On attache un dictionnaire à l'app, ainsi
# sa référence sera accessible partout.
app._params = {'name': socket.gethostname(), 'ip': s.getsockname()[0]}
s.close()
 
@app.signal('onjoined')
@inlineCallbacks
def called_on_joinded():
    """ Boucle envoyant l'état de cette machine avec WAMP toutes les x secondes.
 
        Cette fonction est exécutée quand le client "joins" le router, c'est
        à dire qu'il est connecté et authentifié, prêt à envoyer des messages
        WAMP.
    """
    # Ensuite on fait une requête post au serveur pour dire qu'on est
    # actif et récupérer les valeurs de configuration de notre client.
    app._params.update(requests.post('http://' + SERVER + ':8080/clients/',
                                    data={'ip': app._params['ip']}).json())
 
 
    # Puis on boucle indéfiniment
    while True:
        # Chaque tour de boucle, on récupère les infos de notre machine
        infos = {'ip': app._params['ip'], 'name': app._params['name']}
        infos.update(get_infos(app._params))
 
        # Si les stats sont a envoyer, on fait une publication WAMP.
        if not app._params['disabled']:
            app.session.publish('clientstats', infos)
 
        # Et on attend. Grâce à @inlineCallbacks, utiliser yield indique
        # qu'on ne bloque pas ici, donc pendant ce temps notre client
        # peut écouter les événements WAMP et y réagir.
        yield sleep(app._params['frequency'])
 
 
# On dit qu'on est intéressé par les événements concernant clientconfig
@app.subscribe('clientconfig.' + app._params['ip'])
def update_configuration(args):
    """ Met à jour la configuration du client quand Django nous le demande. """
    app._params.update(args)
 
# On démarre notre client.
if __name__ == '__main__':
    app.run(url="ws://%s:8080/ws" % SERVER)

Le plus gros du code est get_infos() qui n’a rien à voir avec WAMP. C’est nous, manipulant psutil pour obtenir les relevés de cette machine. Je ne recommande bien évidement pas de faire ça en prod : une grosse fonction monolithique qui prend un dico en param. Mais c’est pour une démo, et ça me permet de grouper les instructions qui vont ensemble pour faciliter votre compréhension.

La partie qui concerne WAMP :

app = Application('monitoring')
 
@app.signal('onjoined')
@inlineCallbacks
def called_on_joinded():
    ...
 
    while True:
 
        ...
        app.session.publish('clientstats', infos)
        ...
        yield sleep(app._params['frequency'])

app = Application('monitoring') créé un client WAMP, et @app.signal('onjoined') nous dit de lancer la fonction quand notre client est connecté et prêt à envoyer des événements. @inlineCallbacks est une spécificité de Twisted qui nous permet d’écrire du code asynchrone sans avoir à mettre des callback partout : à la place on met des yield.

Tout le boulot de notre client a lieu dans la boucle : app.session.publish('clientstats', infos) publie les nouvelles mesures de CPU/RAM/Disque via WAMP, puis attend un certain temps (yield sleep(app._params['frequency'])) avant de le faire à nouveau. L’attente n’est pas bloquante car elle se fait avec le sleep de Twisted.

N’oublions pas :

@app.subscribe('clientconfig.' + app._params['ip'])
def update_configuration(args):
    app._params.update(args)

La fonction update_configuration() sera appelée à chaque fois qu’une publication WAMP sera faite sur le sujet clientconfig.<ip_du_client>. Notre fonction ne fait que mettre à jour la configuration du client, qui est un dico de la forme :

    {'cpus': True,
    'memory': False,
    'disk': True,
    'disabled': False,
    'frequency': 1}

C’est ce dico qui est utilisé par get_infos() pour choisir quelles mesures récupérer, et aussi par sleep() pour savoir combien de secondes attendre avant la prochaine mesure.

La valeur initiale de ce dico est récupérée au lancement du client, en faisant :

app._params.update(requests.post('http://' + SERVER + ':8080/clients/',
                                    data={'ip': app._params['ip']}).json())

requests.post(url_du_serveur, data={'ip': app._params['ip']}).json() fait en effet une requête POST vers une URL de django qui nous allons voir plus loin, et qui retourne la configuration du client portant cette IP sous forme de JSON.

On utilise donc une fois HTTP pour obtenir les valeurs de départs, et ensuite WAMP pour les mises à jours des futures valeurs. WAMP et HTTP ne s’excluent pas : ils sont complémentaires.

Petite parenthèse sur :

SERVER = '172.17.42.1'
 
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
app._params = {'name': socket.gethostname(), 'ip': s.getsockname()[0]}
s.close()

D’une part, j’ai mis l’IP du serveur qui va contenir Crossbar.io et Django en dur car je suis, je pense que maintenant vous le savez, une grosse feignasse. Mais en prod, vous me faites un paramètre, on est d’accord ? Ensuite, il faut que j’identifie mon client, ce que je fais avec l’adresse IP. Il me faut donc son adresse IP externe, et je l’obtiens avec une astuce consistant à me connecter à l’IP 8.8.8.8 (les DNS google \o/) et en fermant la connexion juste derrière. Ce me permet de voir comment les autres machines me voit depuis l’extérieur.

Le site Django

Puisque le prérequis de l’article et de connaître Django, ça va pas être trop dur.

On créé son projet et son app :

django-admin startproject django_project
./manage.py startapp django_app

On se rajoute un petit modèle qui contient la configuration de chaque client (vous vous souvenez, le fameux dico) :

# -*- coding: utf-8 -*-
 
import requests
 
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.forms.models import model_to_dict
 
 
class Client(models.Model):
    """ Configuration de notre client. """
 
    # Pour l'identifier.
    ip = models.GenericIPAddressField()
 
    # Quelles données envoyer à notre dashboard
    show_cpus = models.BooleanField(default=True)
    show_memory = models.BooleanField(default=True)
    show_disk = models.BooleanField(default=True)
 
    # Arrêter d'envoyer les données
    disabled = models.BooleanField(default=False)
 
    # Fréquence de rafraîchissement des données
    frequency = models.IntegerField(default=1)
 
    def __unicode__(self):
        return self.ip
 
 
@receiver(post_save, sender=Client, dispatch_uid="server_post_save")
def notify_server_config_changed(sender, instance, **kwargs):
    """ Notifie un client que sa configuration a changé.
 
        Cette fonction est lancée quand on sauvegarde un modèle Client,
        et fait une requête POST sur le bridge WAMP-HTTP, nous permettant
        de faire un publish depuis Django.
    """
    requests.post("http://127.0.0.1:8080/notify",
                  json={
                      'topic': 'clientconfig.' + instance.ip,
                      'args': [model_to_dict(instance)]
                  })

La partie modèle est connue. L’astuce est dans :

@receiver(post_save, sender=Client, dispatch_uid="server_post_save")
def notify_server_config_changed(sender, instance, **kwargs):
    requests.post("http://127.0.0.1:8080/notify",
                  json={
                      'topic': 'clientconfig.' + instance.ip,
                      'args': [model_to_dict(instance)]
                  })

On utilise ici les signaux Django, une fonctionnalité du framework qui nous permet de lancer une fonction quand quelque chose se passe. Ici on dit “lance cette fonction quand le modèle Client est modifié”.

Donc notify_server_config_changed va se lancer quand la config d’un client est modifiée, par exemple dans l’admin, et recevoir l’objet modifié via son paramètre instance.

On fait alors une petite requête POST sur http://127.0.0.1:8080/notify, l’URL sur laquelle on configurera plus loin notre service de push. En faisant une requête dessus, on va demander à Crossbar.io de transformer la requête HTTP en message publish WAMP, ici sur le sujet ‘clientconfig.<ip_du_client>’. On publie donc un message WAMP, depuis Django.

Ca marche depuis n’importe où, pas juste Django. Depuis le shell, depuis Flask, n’importe où on peut faire une requête HTTP vers le service de push de crossbar.

Ce message va être récupéré par notre client, où qu’il soit, puisqu’il est aussi connecté au routeur WAMP. Comme, je vous le rappelle, notre client fait ça :

@app.subscribe('clientconfig.' + app._params['ip'])
def update_configuration(args):
    app._params.update(args)

Il va recevoir ce message, et donc le contenu de 'args': [model_to_dict(instance)], c’est à dire la nouvelle configuration qu’on a changé en base de donnée. Il se met ainsi à jour immédiatement. La boucle est bouclée.

Comme on veut profiter de notre boucle toute bouclée, on rajoute le modèle dans l’admin :

from django.contrib import admin
 
# Register your models here.
 
from django_app.models import Client
 
admin.site.register(Client)

Ainsi, les configs des clients seront éditables dans l’admin, et quand on cliquera sur “save”, ça va lancer notre publish WAMP qui mettra à jour le bon client.

Le reste, c’est du fignolage. Une petite vue pour créer ou récupérer notre configuration de client au démarrage :

# -*- coding: utf-8 -*-
 
import json
 
from django.http import HttpResponse
from django_app.models import Client
from django.views.decorators.csrf import csrf_exempt
from django.forms.models import model_to_dict
 
 
@csrf_exempt
def clients(request):
    """ Récupère la config d'un client en base de donnée et lui envoie."""
    client, created = Client.objects.get_or_create(ip=request.POST['ip'])
    return HttpResponse(json.dumps(model_to_dict(client)), content_type='application/json')

On désactive la protection CSRF pour la démo, mais encore une fois, en prod, faites ça proprement, avec une jolie authentification pour protéger la vue, et tout, et tout.

Donc, cette vue récupère la configuration d’un client avec cette IP (la créant au besoin), et la retourne en JSON. Souvenez-vous, cela permet à notre client de faire :

    app._params.update(requests.post('http://' + SERVER + ':8080/clients/',
                                    data={'ip': app._params['ip']}).json())

Au démarrage et se déclarer dans la base de données, tout en récupérant sa config.

On branche tout ça via urls.py :

from django.conf.urls import patterns, include, url
from django.contrib import admin
from django.views.generic import TemplateView
 
urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
    url(r'^clients/', 'django_app.views.clients'),
    url(r'^$', TemplateView.as_view(template_name='dashboard.html')),
)

L’admin, notre vue toute fraiche, et de quoi servir le HTML du début de l’article.

Y plus qu’à :

./manage.py syncdb

Crossbar.io

Finalement, tout ce qu’il reste, c’est notre bon crossbar :

crossbar init

Ceci nous pond le dossier .crossbar dans lequel on a le fichier config.json qu’on édite pour qu’il ressemble à ça :

{
   "workers": [
      {
         "type": "router",
         "realms": [
            {
               "name": "realm1",
               "roles": [
                  {
                     "name": "anonymous",
                     "permissions": [
                        {
                           "uri": "*",
                           "publish": true,
                           "subscribe": true,
                           "call": true,
                           "register": true
                        }
                     ]
                  }
               ]
            }
         ],
         "transports": [
            {
               "type": "web",
               "endpoint": {
                  "type": "tcp",
                  "port": 8080
               },
               "paths": {
                  "/": {
                     "type": "wsgi",
                     "module": "django_project.wsgi",
                     "object": "application"
                  },
                  "ws": {
                     "type": "websocket"
                  },
                  "notify": {
                     "type": "pusher",
                     "realm": "realm1",
                     "role": "anonymous"
                  },
                  "static": {
                     "type": "static",
                     "directory": "../static"
                  }
               }
            }
         ]
      }
   ]
}

La partie du haut c’est un peu l’équivalent du chmod 777 de crossbar :

         "type": "router",
         "realms": [
            {
               "name": "realm1",
               "roles": [
                  {
                     "name": "anonymous",
                     "permissions": [
                        {
                           "uri": "*",
                           "publish": true,
                           "subscribe": true,
                           "call": true,
                           "register": true
                        }
                     ]
                  }
               ]
            }
         ],

“Met moi en place un router avec un accès nommé realm1 qui autorise à tous les anonymes de tout faire”. Un realm est une notion de sécurité dans Crossbar.io qui permet de cloisonner les clients connectés, nous on va tout mettre sur le même realm, c’est pour une démo je vous dis.

Ensuite on rajoute les transports pour chaque techno qui nous intéresse. On va tout regrouper sur le port 8080 car Twisted peut écouter en HTTP et Websocket sur le même port :

"transports": [
{
   "type": "web",
   "endpoint": {
      "type": "tcp",
      "port": 8080
   },

A la racine, on sert notre app Django :

  "/": {
     "type": "wsgi",
     "module": "django_project.wsgi",
     "object": "application"
  },

Car oui, crossbar peut servir votre app django en prod. Pas besoin de gunicorn. En fait même pas besoin d’nginx pour un site simple, car ça tient très bien la charge. On a juste à lui indiquer quelle variable (application) de quel fichier WSGI (django_project/wsgi.py) charger, et il s’occupe du reste.

Sur ‘/ws’, on écoute en Websocket :

"ws": {
 "type": "websocket"
},

WAMP passe par là, et c’est pour ça que nos clients se connectent en faisant app.run(url="ws://%s:8080/ws" % SERVER) et autobahn.Connection({url: 'ws://127.0.0.1:8080/ws', realm: 'realm1'});.

‘/notify’ va recevoir le bridge WAMP-HTTP :

"notify": {
     "type": "pusher",
     "realm": "realm1",
     "role": "anonymous"
  }

Tous les anonymes du realm1 peuvent l’utiliser. Grâce à ça, on a pu faire depuis notre signal Django :

    requests.post("http://127.0.0.1:8080/notify",
                  json={
                      'topic': 'clientconfig.' + instance.ip,
                      'args': [model_to_dict(instance)]
                  })

Et donc publier un message WAMP, via un POST HTTP.

Enfin, on sert les fichiers statiques Django avec Crossbar (oui, il fait aussi ça :):

 "static": {
    "type": "static",
    "directory": "../static"
}

N’oubliez pas le de spécifier STATIC_ROOT dans le fichier settings et lancer ./manage.py collecstatic.

Tout ça en place, on lance notre routeur :

export PYTHONPATH=/chemin/vers/votre/project
crossbar start

(Remplacer export par set sous Windows>

La modification de PYTHONPATH est nécessaire pour que crossbar trouve votre fichier WSGI.

On visite http:127.0.0.1:8080/, qui va charger notre template Django dashboard.html.

Chaque machine qui lance un client via python client.py va déclencher l’apparition des stats sur notre dashboard, qui seront mises à jour en temps réel.

Si on va sur http:127.0.0.1:8080/admin/ et qu’on change la config d’un client, notre client s’adapte, et notre dashboard se met à jour automatiquement.

Conclusion

Notre projet ressemble à ceci au final :

.
├── client.py
├── .crossbar
│   ├── config.json
├── db.sqlite3
├── django_app
│   ├── admin.py
│   ├── __init__.py
│   ├── models.py
│   ├── templates
│   │   └── dashboard.html
│   └── views.py
├── django_project
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── static
└── manage.py

Vous pouvez récupérer le code ici.

Finalement, très peu de code WAMP : un peu dans le JS, un peu dans le client. Et la seule chose qui lie WAMP à Django est la config crossbar qui ajoute le service HTTP PUSHER et notre requête POST dans models.py

Cette technique n’est pas limitée à Django, et fonctionne bien pour toutes techno synchrones qui ne peut pas lancer un client WAMP directement en son sein. Pour le moment, le bridge HTTP-WAMP ne propose que PUB, pas de SUB, de pas de RPC. C’est déjà assez sympa pour avoir les notifications en temps réel un peu partout, et ça Tobias m’a dit qu’il ajoutera les autres actions dans un future proche.

En attendant, vous voyez le deal : on peut mélanger allègrement HTTP, WAMP, Python, JS, Client, Serveur, et monter sa petite architecture comme on le souhaite. Crossbar permet de démarrer du WSGI, mais aussi les clients WAMP sur la même machine et même n’importe quel process en ligne de commande (par exemple NodeJS) si besoin. C’est Mac Gyver ce truc.

On aurait pu écrire le client en Python 3 puisqu’il est sur une autre machine. Et en fait, si on lance Django en dehors de crossbar, aussi la partie Django en Python 3. Le code de crossbar n’est jamais modifié, on touche juste la configuration JSON.

Personnellement j’ai lancé plusieurs images dockers avec un client dedans à chaque fois, et c’est vraiment sympas de voir les machines se rajouter sur le dashboard en temps réel. On a une super sensation d’interactivité quand on change une valeur dans l’admin et qu’on voit le dashboard bouger.

Recentrer les efforts dans l’open source 17

jeudi 5 février 2015 à 11:25

Je râle souvent en ce moment contre les gens qui réinventent la roue. Dernièrement, c’était pour les gens qui sortaient chaque semaine une nouvelle lib pour accéder à un dico avec la syntaxe dico.cle.cle.cle.

Cela m’énerve doublement.

D’abord parce c’est de l’énergie perdue : le temps passé à développer cette nouvelle lib aurait pu être consacré à améliorer l’ancienne, la maintenir, la corriger, etc. C’est pas comme si on avait trop de bras dans le monde de l’open source.

Ensuite, parce que ça créé plein de confusion.

Dans un monde où il y a 40000 libs, l’utilisateur qui doit choisir ne sait que faire. Créer une énième solution lui ajoute un choix, qu’il doit évaluer. Ca divise aussi l’impact de la publicité qu’on peut faire à la ou les meilleurs solutions. Et on sait que faire connaître quelque chose est difficile.

Bref, réinventer la roue est un pur gachi.

Certes, pas pour l’auteur. Il a amélioré ses capacités techniques, il s’est peut être même amusé, et il a mis un coup de polish a son égo. Je le sais, moi aussi je le fais. J’aime ça.

Mais le monde du libre ne fonctionne que si on peut mettre son cul de côté de temps à autre. Déjà qu’on utilise gratos tout le boulot des autres, c’est le minimum de se sentir un peu concerné, non ?

Alors, évidement, je ne parle pas de recréer une lib pour le même usage, mais qui le fait différemment. Où alors un fork du fait de mésentente avec la direction précédente du projet. Il y a des raisons acceptables pour faire une autre roue.

Il y a néanmoins des cas évidents où ce n’est pas le cas.

Par exemple, j’avais pondu requests-twisted pour me faciliter la vie, n’ayant pas trouver quelque chose de similaire.

Ce matin, l’auteur de txrequest me notifie qu’il a lui aussi une lib qui fait pareil, mais un poil plus avancée, et me demande si je peux fermer mon projet et pointer vers le sien.

Je regarde son projet, je l’évalue. Effectivement, le sien est plus intéressant. Le mien n’apporte rien par rapport à lui. Et sa lib a clairement besoin de visibilité puisque je ne l’avais pas trouvée.

J’ai donc édité mon projet pour pointer vers son projet. Si j’ai des améliorations futures, je ferai un PR sur son repo.

La communauté, ce n’est pas 2 ou 3 gros exploits qui brillent. C’est la somme de milliers petites choses qui amène le mouvement dans la bonne direction.

Le don du mois : Python requests 4

mercredi 4 février 2015 à 12:55

Il n’y aucune cohérence dans ma manière de donner. Je le fais au fil de l’eau, anarchiquement. Parfois j’oublie pendant des mois et des mois.

Là, je me baladais sur l’excellente documentation de la non moins excellente lib requests, à la recherche du one-liner qui allait, encore une fois, me faire gagner un bon quart d’heure.

Et j’ai vu un bouton “Buy request pro”.

Curieux. Il y a plus mieux que le meilleur de requests ?

Je clique, et en fait le bouton est juste là pour proposer de supporter requests. On “achète” la lib gratuite :)

Le minimum est $12, ce qui est fort raisonnable considérant que ce petit bout de code m’a sauvé la mise un million de fois :

Bref, 10 euros, c’est le prix de mon amour en février. Je suis un mec facile.

Des pastes mystérieux sur 0bin 13

vendredi 30 janvier 2015 à 12:19

J’ai reçu un email étrange nous signalant des pastes sur 0bin.net comme étant pédophiles et nous demandant de les retirer.

Le contenu en question ressemble à ceci. Vous pouvez cliquer, c’est safe.

Vu les abus de DMCA ces temps-ci, je suis plutôt méfiant sur ce genre d’allégation, et j’ai donc demandé un peu plus de détails. La personne, contrairement à pas mal de mecs de boites avec des avocats, a pris le temps de quelques échanges avec moi et, malgré mon ton clairement sceptique, a fini par m’expliquer le principe.

Certains forums hébergeant des photos sexualisant des mineurs sont accessibles au public. Néanmoins, pour y accéder, il faut obtenir des indices qui changent régulièrement. Ces indices sont postés sur des boards jetables, comme cette page facebook, qui pointe sur des pastebin, dans notre cas 0bin.

Ensuite, il faut aller sur le site connu pour son contenu, ajouter les filtres adblocks, faire quelques manipulations JS (dans l’exemple, un flag localstorage), et l’entrée est possible.

Je vire donc ces pastes quand on me les signale, et je fais circuler l’info, au cas où vous tombiez sur ce genre de trucs, que vous sachiez de quoi il retourne.

0bin n’étant pas modérable (c’est le principe), il est logique que des usages non souhaités en soit fait. J’aimerais donc profiter de cet article pour rappeler que le chiffrement n’est pas que pour les terroristes et les pédophiles. Tout comme les couteaux ne sont pas uniquement pour les serial killers : la plupart des gens découpent des carottes avec.

Il y a, en proportion, peu de pédophiles, et peu de serial killers. Ils existent, il ne faut pas les ignorer. Mais ne paniquons pas à l’idée qu’ils utilisent des outils utiles pour les citoyens lambda également. Nous créons un monde pour ces citoyens, pas pour les autres.

Écouter sur le port 80 sans être root 16

mercredi 28 janvier 2015 à 14:47

Sous beaucoup d’OS, tous les ports d’un nombre inférieur à 1024 ne peuvent pas être utilisés par des processus sans avoir les privilèges administrateurs. Néanmoins, on a pas vraiment envie que son app bricolée un lendemain de cuite soit lancée en root, pour que la moindre faille de sécurité donne l’accès total à son système.

Beaucoup de logiciels se lancent en root, et relâchent leurs privilèges a posteriori. C’est ce que faisait Apache a une époque (peut être le fait-il toujours, j’ai pas cheché). Nginx lui, lance un processus racine en root, et spawn des enfants avec un utilisateur normal.

Mais nous, on a pas la foi de se faire chier à faire ça, donc généralement, on met nginx en front et il gère ça pour nous.

Sauf que, parfois, on a pas envie de mettre 40 couches devant notre app. Par exemple, si on utilise crossbar.io (ouai, j’ai encore réussi à le placer \o/), le logiciel est clairement capable d’être en front tout seul.

Bonne nouvelle, sur les Linux modernes, les exécutables peuvent avoir des permissions avancées, comme “pourvoir changer l’horloge système” ou “empêcher la mise en veille”. Ces permissions sont changeables avec l’outil setcap.

Sous Ubuntu, ça s’installe avec :

sudo apt-get install libcap2-bin

Puis on choisit l’exécutable à qui on veut donner nos nouvelles permissions. Dans mon cas, le Python du virtualenv de mon projet :

$ pew workon super_projet
$ which python
/home/sam/.local/share/virtualenvs/super_projet/bin/python

Je check si il a pas déjà des permissions (normalement non) :

$ sudo getcap `which python`

Nada. Good.

Un petit coup de man setcap nous liste les permissions utilisables, et on peut voire que CAP_NET_BIND_SERVICE est la permission qui permet d’autoriser un exécutable à binder n’importe quel port.

On rajoute les permissions :

sudo setcap cap_net_bind_service=+ep `which python`

On check que les permissions ont bien été ajoutées :

$ sudo getcap `which python`
/home/sam/.local/share/virtualenvs/super_projet/bin/python = cap_net_bind_service+eip

On a rajouté avec le + la permission cap_net_bind_service pour les cas e et p qui correspondent aux premières lettres de ces définitions :

Permitted (formerly known as forced):
    These capabilities are automatically permitted to the thread, regardless of the thread's inheritable capabilities. 

Inheritable (formerly known as allowed):
    This set is ANDed with the thread's inheritable set to determine which inheritable capabilities are enabled in the permitted set of the thread after the execve(2). 

Effective:
    This is not a set, but rather just a single bit. If this bit is set, then during an execve(2) all of the new permitted capabilities for the thread are also raised in the effective set. If this bit is not set, then after an execve(2), none of the new permitted capabilities is in the new effective set.

Et je n’ai absolument rien compris à celles-ci, je sais juste que ça marche.

Voilà, maintenant tout ce que vous lancez avec le Python de ce virtualenv peut se binder au port 80.

Si vous n’aimez pas l’idée de donner cette permission à tout un Python, il existe une alternative : rediriger tout ce qui rentre sur le port 80 vers un autre port.

Pour ça on peut utiliser un autre soft :

sudo apt-get install socat

Par exemple, balancer tout le trafic du port 80 vers le port 8080 :

socat tcp-listen:80,fork,reuseaddr tcp:localhost:8080

Et pouf, votre appli qui écoute sur le port 8080 reçoit le trafic du port 80.

C’est plus simple, mais il faut le faire pour chaque port, et s’assurer que la commande est bien lancée au démarrage du serveur.