wamp – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Nouvelle release de crossbar: historique des events et crypto http://sametmax.com/nouvelle-release-de-crossbar-historique-des-events-et-crypto/ http://sametmax.com/nouvelle-release-de-crossbar-historique-des-events-et-crypto/#comments Sun, 31 Jan 2016 18:25:33 +0000 http://sametmax.com/?p=18018 Crossbar, le routeur WAMP, passe en 0.12.]]> Je suis méga à la bourre. Entre les tickets github qui s’accumulent, les comments auxquels j’ai toujours pas répondu et la liste d’articles à écrire qui augmente au lieu de diminuer (mais comment, bordel, comment ?), j’ai vraiment du mal à suivre.

Je dois toujours un article sur Polymer + Crossbar à Tavendo. Et il faut que je fasse un tuto sur l’authentification également (en attendant, y a des exemples plus à jour).

Fichtre.

En attendant, je vais en profiter pour faire un article vite fait sur la dernière release, puisque Crossbar, le routeur WAMP, passe en 0.12.

Comme d’hab, correction de bugs, amélioration du support de Python 3, plus de docs et d’exemples, blablabla…

Mais ce qui est vraiment intéressant, c’est l’historique des évènements.

Normalement un évènement est éphémère, dans le sens où une fois qu’il a été propagé, vous ne pouvez plus le recevoir. Si vous arrivez après la fête, c’est terminé.

C’est un problème, par exemple si vous redémarrez un client qui a besoin de ces évènements. Ou si vous implémentez un client qui veut savoir ce qui vient de se passer avant de se pointer, comme dans le cas d’un chat : on veut avoir les derniers messages postés.

Par défaut, l’historique n’est pas activé, puisqu’il y un coût pour chaque pub/sub. On doit explicitement le demander pour chaque event dans le fichier de config :

{
   "name": "realm1",
   "roles": [
   ],
   "store": {
      "type": "memory", # ou stocker l'historique
      "event-history": [
         {
            "uri": "mon.uri.pour.un.event", # quel type event
            "limit": 10000 # combien d’events stocker
         }
      ]
   }
}

type n’accepte pour le moment que memory, qui est une simple liste en mémoire dans crossbar, et bouffe donc de la RAM. On perd aussi l’historique au redémarrage du routeur, mais ça a l’avantage d’être très rapide.

Pour la prochaine version, Tavendo va implémenter un stockage dans une base lmdb et si ils font une belle API, on peut s’attendre à voir fleurir des backends pour SQLAlchemy, Django, Redis, etc.

event-history prend un liste d’events (les URIs peuvent utiliser les jokers introduits dans la version précédente), on met la limite du nombre total d’events à stocker pour cet URI en particulier.

Pour profiter de l’historique côté client, il faut obligatoirement avoir un abonnement à un event dont les messages sont sauvegardés. On ne peut pas récupérer l’historique de messages auxquels on n’est pas abonnés : forcer l’abonnement oblige en effet le client à passer le check des permissions.

Par exemple, en JS, d’abord on s’inscrit:

var promise = session.subscribe('mon.uri.pour.un.event',
   function (args, kwargs, details) {
      // bon là vous faites bien ce que vous voulez avec les nouveaux events
      //, car ça n’a rien à voir avec l’historique
   }
)

Puis on demande les events:

promise = promise.then(function (sub) {
      // L’abonnement retourne un objet "subcription" qui possède l’id
      // dont on a besoin pour demander l’historique des events.
      // On fait un petit RPC sur la meta API 'wamp.subscription.get_events'
      // qui demande aux routeurs tous les X derniers events qui matchent
      // notre abo. Ici x = 10
      return session.call('wamp.subscription.get_events', [sub.id, 10]);
)

Et enfin, on a droit à l’histo:

promise.then(function (history) {
    console.log(history.length, " events:");
    history.forEach(function(event){
        console.log(event.timestamp, event.publication, event.args[0]);
    })
 });

En Python, le code pour récupérer l’histo est logiquement:

import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner


class Component(ApplicationSession):

    async def onJoin(self, details):


        def on_event(i):
            print("Got: {}".format(i))

        # pareil on chope la souscription
        sub = await self.subscribe(on_event, u'mon.uri.pour.un.event')
        # et on demande la liste des 10 derniers events pour cet abo
        events = await self.call('wamp.subscription.get_events', sub.id, 10)
        # et on boucle. Et on kiff await parceque putain c’est pratique.
        for event in events:
            print(event['timestamp'], event['publication'], event['args'][0])

    def onDisconnect(self):
        asyncio.get_event_loop().stop()


if __name__ == '__main__':
    runner = ApplicationRunner("ws://127.0.0.1:8080/ws", 'realm1')
    runner.run(Component)

L’autre point phare de cette release, c’est la dépréciation de Mozilla Persona comme méthode d’authentification (le projet est dead) et la promotion de deux nouvelles méthodes: les certificats TLS et les paires de clés publiques/privées (Curve25519 pour le moment).

C’est une très bonne nouvelle, car ça veut dire plus de mots de passe dans les fichiers de configuration en production pour identifier vos propres clients qui ont forcément des privilèges supplémentaires.

Je reviendrais là dessus en faisant le tuto sur l’authentification.

Une release chouette donc. Mais qui introduit plein de dépendances à des extensions en C qui pourraient être optionnelles, ce qui rend l’installation plus compliquée. Je suis en train de discuter avec la team pour voir si on peut arranger ça, mais Tobias à l’air plutôt pour les garder. Si vous aussi vous voulez garder la simplicité de la base pure Python, rejoignez la discussion.

Enfin, on a pu voir l’annonce d’une feature très intéressante : le chiffrement end-to-end des messages WAMP. Ça, c’est chouette. C’est pas encore implémenté, mais ça veut dire que la prochaine release, vous pourrez probablement envoyer des messages à travers le serveur WAMP sans que celui-ci puisse les lire.

]]>
http://sametmax.com/nouvelle-release-de-crossbar-historique-des-events-et-crypto/feed/ 8 18018
Jouons un peu avec Python 3.5 http://sametmax.com/jouons-un-peu-avec-python-3-5/ http://sametmax.com/jouons-un-peu-avec-python-3-5/#comments Wed, 16 Sep 2015 16:31:38 +0000 http://sametmax.com/?p=16918 fantastique article présentant Python 3.5, je ne vais donc pas pas répéter inutilement ce qu’ils ont dit. Le but de ce post est plutôt de faire mumuse avec le nouveau joujou.]]> Zeste de savoir a fait un fantastique article présentant Python 3.5, je ne vais donc pas répéter inutilement ce qu’ils ont dit. Le but de ce post est plutôt de faire mumuse avec le nouveau joujou.

La release est récente, mais fort heureusement on peut facilement l’installer. Sous Windows et Mac, il y a des builds tout chauds.

Pour linux, en attendant un repo tierce partie ou l’upgrade du système, on peut l’installer quelques commandes depuis les sources. Par exemple pour les distros basées sur Debian comme Ubuntu, ça ressemble à :

$ # dependances pour compiler python
$ sudo apt-get install build-essential libreadline-dev tk8.4-dev libsqlite3-dev libgdbm-dev libreadline6-dev liblzma-dev libbz2-dev libncurses5-dev libssl-dev python3-dev tk-dev
$ sudo apt-get build-dep python3 # juste pour etre sur :)

$ # téléchargement des sources
$ cd /tmp
$ wget https://www.python.org/ftp/python/3.5.0/Python-3.5.0.tar.xz
$ tar -xvf Python-3.5.0.tar.xz
$ cd Python-3.5.0

$ # et on build
$ ./configure
$ make
$ sudo make altinstall 
# pas 'make install' qui écrase le python du système !

$ python3.5 # ahhhhhh
Python 3.5.0 (default, Sep 16 2015, 10:44:14) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

Sur les centos-likes, c’est grosso merdo la même chose, sans le build-dep (mais plutôt un truc genre sudo yum groupinstall 'Development Tools'), et en remplaçant les -dev par -devel.

Nouvel opérateur

@ est maintenant le nouvel opérateur de produit matriciel, mais il ne fait officiellement rien.

Comprenez par là que Python implémente l’opérateur, mais pas le produit en lui-même, la feature ayant été spécialement incluse pour faire plaisir aux utilisateurs de libs scientifiques type numpy.

On va donc tester ça sur le terrain. On se fait un petit env temporaire avec pew et on s’installe numpy :

pew mktmpenv -p python3.5
pip install pip setuptools --upgrade
pip install numpy 
# encore un peu de compilation

Testons mon bon. L’ancienne manière de faire :

>>> a = np.array([[1, 0], [0, 1]])
>>> b = np.array([[4, 1], [2, 2]])
>>> np.dot(a, b)
array([[4, 1],
       [2, 2]])

Et la nouvelle :

>>> a @ b
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in ()
----> 1 a @ b

TypeError: unsupported operand type(s) for @: 'numpy.ndarray' and 'numpy.ndarray'

Woops, apparemment numpy n’a pas encore implémenté le truc.

Bon. Bon, bon, bon. Comment on va tester alors… Ah, oui, y a une magic method :

class Array(np.ndarray):
    def __matmul__(self, other):
        return np.dot(self, other)

>>> a = a.view(Array)
>>> b = b.view(Array)
>>> a @ b
Array([[4, 1],
       [2, 2]])

Bon, voilà ce que ça donnera quand les devs de numpy auront implémenté le bouzin (la dernière ligne hein, pas tout le bordel avant).

Apparemment ça fait bander les matheux, donc je suppose que c’est une super nouvelle.

% is back on bytes

En python 2, on pouvait faire "truc %s" % "bidule" et u"truc %s" % u"bidule" et b"truc %s" % u"bidule" et ça a été viré en python 3 qui ne garde % que pour str et pas pour bytes.

Ca n’aurait pas été un problème si ce n’est que Python est très utilisé pour le réseau, et que construire un paquet qui mélange de la sémantique binaire et textuelle devient soudainement une grosse soupe de decode() et encode().

Jour 1, test 3, suspense…

>>> bytearray([66, 108, 117, 101, 32, 112, 114, 105, 101, 115, 116, 32, 115, 97, 121, 115, 58, 32, 37, 115]) % b"wololo"
bytearray(b'Blue priest says: wololo')

Voilà ça c’est fait !

os.scandir()

os.path.walk() est dans mon top 10 des APIs que je déteste le plus en Python, juste à côté de la gestion timezone. Avoir os.walk() en Python 3 qui retourne un générateur me ravit. Avoir une version 10 X plus rapide avec scandir, n’est-ce pas choupinet ?

>>> import os
>>> list(os.scandir('/tmp/'))
                       [,
 ,
 ,
 ,
 ,
 ]

C’est très dommage que ça ne retourne pas des objets Path de pathlib, mais bon, les perfs, tout ça…

Zipapp, le grand inaperçu

Le saviez-vous ? Python peut exécuter un zip, ce qui permet de créer un script en plusieurs fichiers et de le partager comme un seul fichier. Non vous ne le saviez-vous-te-pas car personne n’en parle jamais.

La 3.5 vient avec un outil en ligne de commande pour faciliter la création de tels zip et une nouvelle extension (que l’installeur fera reconnaitre à Windows) pour cesdits fichiers : .pyz.

Je fais mon script :

foo
├── bar.py
├── __init__.py
└── __main__.py 

__main__.py est obligatoire, c’est ce qui sera lancé quand on exécutera notre script. Dedans je mets import bar et dans bar print('wololo again').

Ensuite je fusionne tout ça :

python -m zipapp foo

Et pouf, j’ai mon fichier foo.pyz :

$ python3.5  foo.pyz
wololo again

Attention aux imports dedans, ils sont assez chiants à gérer.

L’unpacking généralisé

J’adore cette feature. J’adore toutes les features de la 3.5. Cette release est fantastique. Depuis la 3.3 chaque release est fantastique.

Mais bon, zeste de savoir l’a traité en long et en large donc rien à dire de plus, si ce n’est que j’avais raté un GROS truc :

  • On peut faire de l’unpacking sur n’importe quel itérable.
  • On peut faire de l’unpacking dans les tuples.
  • Les parenthèses des tuples sont facultatives.

Donc ces syntaxes sont valides :

>>> *range(2), *[1, 3], *'ea'
(0, 1, 1, 3, 'e', 'a')
>>> *[x * x for x in range(3)], *{"a": 1}.values()
(0, 1, 4, 1)

Ce qui peut être très chouette et aussi la porte ouverte à l’implémentation d’un sous-ensemble de Perl en Python. C’est selon l’abruti qui code.

Type hints

Ce qu’il faut bien comprendre avec les types hints, c’est que Python ne s’en sert pas. Il n’en fait rien. Que dalle. Nada. Peau de balle. Zob. Niet. Zero. La bulle. Néant. Null. None. Réforme gouvernementale.

Les types hints sont disponibles, mais Python ne va pas les traiter différemment d’autres annotations. Le but est de permettre à des outils externes (linter, IDE, etc) de se baser sur ces informations pour ajouter des fonctionnalités.

Pour l’instant, un seul le fait : mypy.

Et là on sent bien que tout ça est tout neuf car si on fait pip install mypy-lang, on tombe sur une version buggée. Il faut donc l’installer directement depuis le repo, soit :

pip install https://github.com/JukkaL/mypy/archive/master.zip

Puis écriture d’une fonction annotée avec des types hints :


from typing import Iterable, Tuple

PixelArray = Iterable[Tuple[int, int, int]]

def rgb2hex(pixels: PixelArray) -> list:
    pattern = "#{0:02x}{1:02x}{2:02x}"
    return [pattern.format(r, g, b) for r, g, b in pixels]


# ça marche :
rgb2hex([(1, 2, 3), (1, 2, 3)])
# ['#010203', '#010203']

La preuve que Python n’en fait rien :

>>> hex("fjdkls")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in ()
----> 1 hex("fjdkls")

TypeError: 'str' object cannot be interpreted as an integer

Même la doc profite peu du typage :

Help on function rgb2hex in module __main__:

rgb2hex(pixels:typing.Iterable) -> list

Mais si on met ça dans un fichier foo.py :

from essai import rgb2hex

print(rgb2hex("fdjksl"))
res = rgb2hex([(1, 2, 3), (3, 4, 5)])
print(res + 1)

Et qu’on le passe à la moulinette :

$ mypy foo.py 
foo.py:3: error: Argument 1 to "rgb2hex" has incompatible type "str"; expected Iterable[...]
foo.py:5: error: Unsupported operand types for + (List[Any] and "int")

Ensuite j’ai essayé de créer un stub file, c’est-à-dire de mettre les hints dans un fichier à part plutôt que directement dans le code. Ma fonction redevient :

def rgb2hex(pixels):
    pattern = "#{0:02x}{1:02x}{2:02x}"
    return [pattern.format(r, g, b) for r, g, b in pixels]

Et mon fichier stub (même nom, mais avec extension .pyi) contient :

from typing import Iterable, Tuple

PixelArray = Iterable[Tuple[int, int, int]]

def rgb2hex(pixels: PixelArray) -> list:...

Les stubs sont donc bien des fichiers Python valides, mais avec une extension différente, et juste les signatures des fonctions (le corps du bloc est une Ellipsis).

Et poof, ça marche pareil :

$ mypy foo.py 
foo.py:3: error: Argument 1 to "rgb2hex" has incompatible type "str"; expected Iterable[...]
foo.py:5: error: Unsupported operand types for + (List[Any] and "int")

Il y a un repo qui contient des fichiers stubs pour la stdlib. Vous pouvez y participer, c’est un moyen simple de contribuer à Python.

Bref, pour le moment ça demande encore un peu de maturité, mais je pense que d’ici quelques mois on aura des outils bien rodés pour faire tout ça automatiquement.

Async/await

La feature pub. Techniquement le truc qui a fait dire à tous ceux qui voulaient de l’asyncrone que Python en fait, c’était trop cool. Sauf que Python pouvait faire ça avec yield from avant, mais c’est sur que c’était super confusionant.

Maintenant on a un truc propre : pas de décorateur @coroutine, pas de syntaxe semblable aux générateurs, mais des méthodes magiques comme __await__ et de jolis mots-clés async et await.

Vu que Crossbar est maintenant compatible Python 3, et qu’il supporte asyncio pour les clients… Si on s’implémentait un petit wrapper WAMP pour s’amuser à voir ce que ressemblerait une API moderne pour du Websocket en Python ?

pip install crossbar
crossbar init
crossbar start

(Ouhhhh, plein de zolies couleurs apparaissent dans ma console ! Ils ont fait des efforts cosmétiques chez Tavendo)

Bien, voici maintenant l’exemple d’un client WAMP de base codé avec asyncio selon l’ancienne API :

import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner

class MyComponent(ApplicationSession):

    @asyncio.coroutine
    def onJoin(self, details):
        
        # on marque cette fonction comme appelable
        # a distance en RPC
        def add(a, b):
            return a + b
        self.register(add, "add")

        # et on triche en l'appelant cash. J'ai
        # la flemme de coder un deuxième client
        # et ça passe quand même par le routeur
        # donc merde
        res = yield from self.call("add", 2, 3)
        print("Got result: {}".format(res))


if __name__ == '__main__':
    runner = ApplicationRunner("ws://127.0.0.1:8080/ws",
        u"crossbardemo",
        debug_wamp=False,  # optional; log many WAMP details
        debug=False,  # optional; log even more details
    )
    runner.run(MyComponent)

Et ça marche nickel en 3.5. Mais qu’est-ce que c’est moche !

On est en train de bosser sur l’amélioration de l’API, mais je pense que ça va reste plus bas niveau que je le voudrais.

Donc, amusons-nous un peu à coder un truc plus sexy. Je vous préviens, le code du wrapper est velu, j’avais envie de me marrer un peu après les exemples ballots plus haut :

import asyncio
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner

class App:

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.procedures = []
        self.subscriptions = []
        self.event_handlers  = {}

    def run(self, url="ws://127.0.0.1:8080/ws",
                realm="realm1", debug_wamp=False, debug=False):
        runner = ApplicationRunner(url, realm,
                                                     debug_wamp=debug_wamp,
                                                     debug=debug)
        runner.run(self)

    def run_cmd(self, *args, **kwargs):
        # et on pourrait même ici mettre du parsing d'argument
        # et de os.environ, mais j'ai la flemme
        if __name__ == '__main__':
            self.run(*args, **kwargs)

    # quelques décorateurs pour faire du déclaratif
    # et remettre les paramètres dans le bon ordre
    def register(self, name, *args, **kwargs):
            def wrapper(proc):
                self.procedures.append([name, proc, args, kwargs])
                return proc
            return wrapper

    def subscribe(self, topic, *args, **kwargs):
            def wrapper(callback):
                self.procedures.append([topic, callback, args, kwargs])
                return callback
            return wrapper

    # un système d'event interne
    def on(self, event):
            def wrapper(callback):
                self.event_handlers.setdefault(event, []).append(callback)
                return callback
            return wrapper

    async def trigger(self, event):
        for callback in self.event_handlers.get(event, ()):
            await callback(self.session)

    # un peu de code de compatibilité avec l'API initiale
    def __call__(self, *args):
        class CustomeSession(ApplicationSession):
            async def onJoin(session_self, details):

                # on joint on fait tous les registers et tous les
                # subscribes
                for name, proc, args, kwargs in self.procedures:
                     session_self.register(proc, name, *args, **kwargs)

                for topic, callback, args, kwargs in self.subscriptions:
                     session_self.subscribe(proc, topic, *args, **kwargs)

                # on appelle les handlers de notre event
                await self.trigger('joined')
        self.session = CustomeSession(*args)
        return self.session

Évidement la coloration syntaxique ne suit pas sur nos async/await.

Bon, vous allez me dire, mais ça quoi ça sert tout ça ? Et bien, c’est une version tronquée et codée à l’arrache de l’API Application pour Twisted… mais version asyncio.

C’est-à-dire que c’est une lib qui permet de faire le même exemple que le tout premier qu’on a vu dans cette partie – qui souvenez-vous était fort moche -, mais comme ça :

app = App()

@app.register('add')
async def add(a, b):
    return a + b

@app.on('joined')
async def _(session):
    res = await session.call("add", 2, 3)
    print("Got result: {}".format(res))

app.run_cmd()

Des jolis décorateurs ! Des jolis async ! Des jolis await !

Et tout ça tourne parfaitement sur 3.5 messieurs-dames.

Bref, on peut faire du WAMP avec une syntaxe claire et belle, il faut juste se bouger le cul pour coder une abstraction un peu propre.

Je pense que autobahn restera toujours un peu bas niveau. Donc il va falloir que quelqu’un se colle à faire une lib pour wrapper tout ça.

Des volontaires ?

Arf, je savais bien que ça allait me retomber sur la gueule.

]]>
http://sametmax.com/jouons-un-peu-avec-python-3-5/feed/ 27 16918
Nouvelle release de crossbar : support de Python 3 ! http://sametmax.com/nouvelle-release-de-crossbar-support-de-python-3/ http://sametmax.com/nouvelle-release-de-crossbar-support-de-python-3/#comments Wed, 09 Sep 2015 15:13:22 +0000 http://sametmax.com/?p=16908 WAMP en général, et je me suis fais un plaisir de leur rapporter toutes les merdes donc vous m'avez fait part.]]> L’équipe de Tavendo est à l’écoute de toutes les critiques de Crossbar et WAMP en général, et je me suis fait un plaisir de leur rapporter toutes les merdes dont vous m’avez fait part.

Cette nouvelle release contient beaucoup de choses qui corrigent ou pallient un paquet de trucs relou dans le routeur Crossbar (et par conséquent la lib client Autobahn) :

  • Support officiel de Python 3. Yes. Yes, yes yes !
  • Le debug a été complètement revu : meilleure console, meilleur login, meilleurs messages d’erreur et meilleur comportement en cas d’exceptions.
  • Un service dédié à l’upload de fichier intégré.
  • Un bridge HTTP complet qui permet d’utiliser Crossbar depuis n’importe quelle app qui peut faire des requêtes HTTP.

Pour la suite, ils travaillent sur la doc, et l’amélioration de l’API. En attendant, on peut pip install crossbar et profiter de ces nouveautés sans avoir à passer par github.

De mon côté j’ai un article sur l’authentification avec Crossbar dans les cartons. ETA dans les 10 prochains jours.

]]>
http://sametmax.com/nouvelle-release-de-crossbar-support-de-python-3/feed/ 18 16908
Today is a glorious day http://sametmax.com/today-is-a-glorious-day/ http://sametmax.com/today-is-a-glorious-day/#comments Sun, 12 Jul 2015 21:29:59 +0000 http://sametmax.com/?p=16600 >>> import crossbar >>> crossbar.__version__ '0.10.4' >>> import twisted >>> twisted.__version__ '15.2.1' >>> import sys >>> print('Wait for it...') Wait for it... >>> sys.version '3.4.0 (default, Apr 11 2014, 13:05:11) \n[GCC 4.8.2]' ]]> http://sametmax.com/today-is-a-glorious-day/feed/ 15 16600 Pendant ce temps, à Vera Cruz http://sametmax.com/pendant-ce-temps-a-vera-cruz/ http://sametmax.com/pendant-ce-temps-a-vera-cruz/#comments Sun, 10 May 2015 09:29:24 +0000 http://sametmax.com/?p=16198 Pour une fois, ce n’est pas un article payé par Tavendo, mais bien un truc que je ponds par enthousiasme :)

Pendant qu’on en parle pas, la stack WAMP continue d’évoluer, des mises à jours significatives ayant été apportées à Crossbar.io, ainsi qu’aux libs Python et JS d’autobahn. Parmi les plus intéressantes :

  • Le code passe de la licence Apache 2 à la licence MIT, augmentant la compatibilité avec un tas d’autres licences.
  • On peut faire un SUB avec un joker, et donc lier un seul callback à plusieurs événements.
  • On peut faire un register avec un joker également.
  • On peut choisir la stratégie à appliquer si plusieurs registers sont faits sur le même nom.
  • Une meta API permet d’être prévenu quand un client fait quelque chose ou de demander l’état des nœuds en cours.

Inutile de dire que c’est trop cool.

Pour profiter de tout ça, il suffit de faire :

pip install crossbar autobahn --upgrade

Et de télécharger la nouvelle version de la dernière version de la lib JS.

Licence MIT

Auparavant le travail de Tavendo était essentiellement sous Licence Apache. Une licence libre, certes, mais qui pouvait poser problème quand on mélangeait tout ça avec d’autres licences (par exemple, elle n’est pas compatible avec la GPL2). Avec la version 0.10, le code est maintenant sous licence MIT, beaucoup plus permissive.

Joker pour les subs

Supposez que vous faites un système de jeu d’échec donc chaque coup déclenche un événement “chess.game.[id_de_partie]”. C’est pratique, car seuls les clients intéressés à cette partie vont recevoir les événements. Mais si votre serveur doit enregistrer un log de tous les coups d’une partie, il faut que chaque client envoie AUSSI les coups au serveur explicitement.

C’était en tout cas vrai avant cette mise à jour, puisque maintenant on peut spécifier des jokers dans les noms des topics au moment de l’abonnement.

Essentiellement il y a deux modes.

Le mode “prefix”, qui match tous les events qui commencent par ce nom :

session.subscribe("debut.du.nom.du.topic", callback, { match: "prefix" });
# matchera debut.du.nom.du.topic.genial et debut.du.nom.du.topic.trop.cool

Et le mode “wildcard” qui permet, un peu comme les glob Unix (mais on utilise “..” au lieu de “*””), de faire un texte à trou :

session.subscribe("nom.du.topic..general", callback, { match: "wildcard" });
# matchera "nom.du.topic.moins.general" et "nom.du.topic.oui.mon.general"

Tous les callbacks qui matchent un topic seront appelés.

Plusieurs clients pour la même procédure

On peut utiliser le même principe que pour les sub avec joker, mais pour les procédures.

session.register("debut.du.nom.de.la.procedure", callback, { match: "prefix" });    
session.register("nom.de.la.procedure..generale", callback, { match: "wildcard" });

La différence avec le subscribe, c’est que seule UNE procédure est appelée. Dans les cas simples, un match exact prend le dessus sur un prefix (et le plus long prefix gagne toujours), qui prend le dessus sur un wildcard. Crossbar n’implemente pas encore de résolution pour deux wildcards en conflits, et je ne sais pas ce qu’il fait dans ce cas.

Il est aussi possible de de définir des règles d’appels en faisant :

session.register("nom.de.la.procedure..generale", procedure1, { invoke: "regle"});

La règle peut être :

  • roundrobin: on prend la liste de clients, on regarde le dernier appelé, et on utilise le suivant.
  • random: on prend un client au hasard.
  • last: on prend le dernier client ajouté de la liste.
  • first: on prend premier client ajouté à la liste.

“roundrobin” et “random” sont pratiques pour faire du load balancing.

“last” et “first” sont pratique pour les mises à jour d’un client sans arrêter le serveur. En gros on rajoute un client, on attend un peu, “last” route tout sur le dernier client, donc le nouveau client prend les requêtes, et on peut arrêter le vieux clients sans souci.

Meta RPC

Crossbar met automatiquement à notre disposition des procédures distantes toutes faites qui donnent des informations sur l’état des clients et du routeur. Voici les RPC que vous pouvez maintenant faire :

  • wamp.session.list: lister les sessions des clients connectés au routeur.
  • wamp.session.get: obtenir les infos d’un session pour un ID en particulier.
  • wamp.session.count: obtenir le nombre de client connectés.
  • wamp.registration.lookup: absolument aucune idée.
  • wamp.registration.get: obtenir des infos sur une procédure distante enregistrée.
  • wamp.registration.list_callees: lister les clients ayant enregistré pour une procédure avec ce nom.
  • wamp.registration.count_callees: compter les clients ayant enregistré une procédure avec ce nom.
  • wamp.registration.list: lister toutes les procédures distantes disponibles.
  • wamp.registration.remove_callee: virer un client de la liste de des clients enregistrés pour cet procédure.
  • wamp.subscription.lookup: toujours aucune idée.
  • wamp.subscription.get: récupérer des infos sur l’abonnement avec cet ID.
  • wamp.subscription.list_subscribers: lister les clients qui sont abonnés à ce sujet.
  • wamp.subscription.count_subscribers: compter les clients abonnés à ce sujet.
  • wamp.subscription.match: aucune idée.
  • wamp.subscription.list: lister tous les sujets d’abonnement disponibles.
  • wamp.subscription.remove_subscriber:
  • virer un client de la liste des abonnés à ce sujet.

En gros, si vous voulez faire une admin qui vous permet de killer certains client ou rechercher si des events existent, vous utilisez ça.

Meta SUB

De même, le routeur envoie maintenant des publications sur des sujets concernant le cycle son cycle de vie et celui des clients. On peut donc s’abonner à ces meta topic pour réagir à l’activité de son système :

  • wamp.session.on_join : un client s’est connecté au routeur.
  • wamp.session.on_leave : un client s’est déconnecté du routeur.
  • wamp.subscription.on_create : un nouveau topic existe.
  • wamp.subscription.on_subscribe : un client s’est abonné à un topic.
  • wamp.subscription.on_unsubscribe : un client s’est désabonné à un topic.
  • wamp.subscription.on_delete : un topic est retiré de la liste des topics disponibles.
  • wamp.registration.on_create : une procédure distante porte ce nom pour la première fois.
  • wamp.registration.on_register : un client propose ajoute un callable pour ce nom de procédure distante..
  • wamp.registration.on_unregister : un client retire son callable pour ce nom de procédure distante.
  • wamp.registration.on_delete : le nom de cette procédure n’a plus aucun callable lié.
  • wamp.schema.on_define : aucune idée.
  • wamp.schema.on_undefine : kamolox.

Ce genre de truc est idéal pour faire un petit outil de monitoring pour son archi et voir ce qui se passe en temps réel.

Le HTTP bridge est complet

Le bridge HTTP propose maintenant PUB/SUB, et tout RPC. On peut donc maintenant utiliser crossbar depuis n’importe quel app qui peut faire du HTTP : flask, pyramid, ruby on rails, du PHP pur, wget en ligne de commande et tout le bordel. C’est plus verbeux, mais ça dépanne bien.

]]>
http://sametmax.com/pendant-ce-temps-a-vera-cruz/feed/ 8 16198
Un petit dashboard de monitoring avec Django et WAMP http://sametmax.com/un-petit-dashboard-de-monitoring-avec-django-et-wamp/ http://sametmax.com/un-petit-dashboard-de-monitoring-avec-django-et-wamp/#comments Sat, 07 Feb 2015 10:58:43 +0000 http://sametmax.com/?p=15872 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 :

  • De connaissances de base en JS.
  • De connaître le principe de WAMP.
  • De savoir installer des bibliothèques Python avec extensions sur votre machine. pip et virtualenv sont vos amis.
  • De connaître Django. Même si le concept peut s’appliquer à Flask, Pyramid, ou autre.

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 :)



  
    

    
    

    
    


    
    

     Monitoring


    

Monitoring

    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.

    ]]>
    http://sametmax.com/un-petit-dashboard-de-monitoring-avec-django-et-wamp/feed/ 16 15872
    Les managers le détestent : faites tourner WAMP dans Django avec cette astuce insolite http://sametmax.com/les-managers-le-detestent-faites-tourner-wamp-dans-django-avec-cette-astuce-insolite/ http://sametmax.com/les-managers-le-detestent-faites-tourner-wamp-dans-django-avec-cette-astuce-insolite/#comments Sun, 04 Jan 2015 19:45:07 +0000 http://sametmax.com/?p=15665 directement dans Django.]]> 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 %}
    
    
      
        
        
           UUID
        
    
        
        
    
    
    

    UUID

    ping.html :

    {% load staticfiles %}
    
    
      
        
        
           Ping
        
    
        
        
    
    
    

    Ping me !

    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

    ]]>
    http://sametmax.com/les-managers-le-detestent-faites-tourner-wamp-dans-django-avec-cette-astuce-insolite/feed/ 16 15665
    Bridge HTTP/WAMP http://sametmax.com/bridge-httpwamp/ http://sametmax.com/bridge-httpwamp/#comments Mon, 29 Dec 2014 10:07:59 +0000 http://sametmax.com/?p=13072 Il y a 4 gros freins à l’adoption de WAMP :

    1. L’incompréhension de la techno. Les gens ne voient pas à quoi ça sert, ce qu’ils peuvent faire avec, et ont peur. Logique. Je vais pas les blâmer, c’est pareil avec toute nouvelle techo. S’investir dans un nouveau truc a un coût, surtout un truc jeune.
    2. La doc. je vais travailler sur la question, mais c’est un travail de fond.
    3. L’API : ça va avec les deux points plus haut. Il faut quelque chose de plus haut niveau. L’API flaskesque est un bon début, il faut maintenant continuer dans ce sens.
    4. L’intégration avec les anciennes technos. Par exemple, un site Django. Personne n’a envie de mettre toute son ancienne stack à la poubelle.

    Toute les libs qui introduisent une nouvelle façon de travailler rencontrent ce problème. Quand les ORM sont sortis, c’était pareil. Quand les Django et Rails, et Symfony sont sortis, c’était pareil.

    Mais puisqu’on le sait, on peut agir.

    Les 3 premiers points ont déjà un début de solution, ce qui nous intéresse c’est donc le 4ème point.

    Il y a de nombreuses choses à faire pour l’intégration : authentification, actions bloquantes, communications…

    On ne peut pas tout résoudre d’un coup, mais une solution qui ratisse large serait de créer un bridge HTTP/WAMP.

    Le principe : faire un client WAMP qui soit aussi client HTTP avec une API REST.

    Fonctionnement

    En envoyant des requêtes HTTP avec un webtoken pour s’authentifier, on peut faire un register/subscribe/call/publish sur le bridge en spécifiant une URL de callback. Le bridge transmet tout ça à routeur WAMP. Quand un événement arrive sur le bridge qui concerne une des URLs de callback, il fait une requête sur l’URL avec les infos arrivées via WAMP.

    API :

    POST /register/

    {
        // token d'authentification
        token: "fdjsklfqsdjm",
        // On enregistre des urls de callbacks pour chaque "function" exposées en RPC
        // Quand le bridge reçoit un appel RPC via WAMP, il fera une requête POST
        // sur la bonne URL. Votre app récupère les données via POST, et retourne
        // du JSON que le bridge va transmettre via WAMP.
        endpoints: {
            "nom_fonction_1":  "http://localhost:8080/wamp/rpc/nom_fonction_1/",
            "nom_fonction_2":  "http://localhost:8080/wamp/rpc/nom_fonction_2/"
        }
    }

    POST /subscribe/

    {
        token: "fdjsklfqsdjm",
        // On enregistre des urls de callbacks chaque abonnement à un topic.
        // Quand le bridge reçoit un message PUB via WAMP, il fera une requête POST
        // sur la bonne URL. Votre app récupère les données via POST.
        endpoints: {
            "nom_fonction_1":  "http://localhost:8080/wamp/pub/nom_fonction_1/",
            "nom_fonction_2":  "http://localhost:8080/wamp/pub/nom_fonction_2/"
        }
    }

    POST /call/nom_fonction/

    {
        token: "fdjsklfqsdjm",
        // Le bridge fera l'appel RPC, récupère la valeur de retour, et la renvoie
        // à cette URL via POST ou une erreur 500 en cas d'exception.
        callback: "http://localhost:8080/wamp/callback/nom_fonction",
        // Les params à passer à l'appel RPC
        params: ['param1', 'param2']
    }

    POST /publish/nom_sujet/

    {
        token: "fdjsklfqsdjm",
        // Le bridge fera la publication WAMP
        // Les params à passer lors de la publication
        params: ['param1', 'param2']
    }

    On peut rajouter des fioritures : recharger le fichier de config qui contient les API keys, se désabonner, désinscrire un callback RPC, etc.

    Bien entendu, du fait d’avoir un intermédiaire, c’est peu performant, mais suffisant pour permettre une communication entre des apps existantes et vos apps WAMP et mettre un pied dedans.

    Intégration poussée

    Ceci permet une intégration générique, de telle sorte que tout le monde puisse intégrer son app facilement avec quelques requêtes HTTP. Néanmoins, avoir un plugin pour son framework plug and play faciliterait la vie de beaucoup de gens.

    Donc la partie 2, c’est de faire des apps pour les frameworks les plus courants qui wrappent tout ça.

    Par exemple, pour Django, ça permettrait de faire :

    settings.py

    WAMP_BRIDGE_URL = "http://localhost:8181/"

    urls.py

    from wamp_bridge.adapters.django import dispatcher
    
    urlpatterns += ('',
        url('/wamp/, dispatcher),
        ...
    )

    views.py

    from wamp_bridge.adapters.django import publish, call, rpc, sub
    
    @rpc()
    def nom_fonction_1(val1, val2):
        # faire un truc
    
    @sub()
    def nom_topic_1(val1, val2):
        # faire un truc
    
    def vue_normale(request):
        publish('sujet', ['arg1'])
        resultat = call('function', ['arg1'])

    Ca répond pas à des questions du genre : “comment je fais pour garder mon authentification Django” mais c’est déjà super glucose.

    Implémentation

    On peut créer un bridge dans plein de langages, mais je pense que Python est le plus adapté.

    Les clients JS utiliseraientt de toute façon nodeJS, qui n’a pas besoin de bridge, puisque déjà asynchrone. Un routeur en C demanderait de la compilation. PHP est trop moche. Java, c’est tout un bordel à setuper à chaque fois. C#, si il faut se taper Mono sous Linux…

    En prime, le routeur crossbar est déjà en Python, donc a déjà tout sous la main pour le faire. On peut même penser à l’intégrer à crossbar plus tard histoire que ce soit batteries included.

    Il faut une implémentation Python 2 et Python 3, donc une avec Twisted, et une avec asyncio.

    Pour le client twisted, on peut fusionner le client WAMP ordinaire avec treq.

    Pour asyncio, il y a aiohttp qui fait client et serveur.

    On met les clés API dans un fichier de conf ou une variable d’env, une auth via token, et yala.

    C’est le genre de projet intéressant car pas trop gros, mais suffisamment complexe pour être un challenge surtout qu’il faudra des tests unitaires partout, de la doc, bref un truc propre.

    Je laisse les specs là, des fois qu’il y ait quelqu’un qui ait des envies de code cet hiver et cherche un projet open source dans lequel se lancer.

    Si on se sent un peut foufou, on peut même transformer ça en bridget HTTP <=> anything, avec des backends pour ce qu’on veut : IRC, XMPP, Redis, Trigger Happy…

    ]]>
    http://sametmax.com/bridge-httpwamp/feed/ 5 13072
    Corrections des slides WAMP http://sametmax.com/corrections-des-slides-wamp/ http://sametmax.com/corrections-des-slides-wamp/#comments Thu, 25 Dec 2014 09:58:47 +0000 http://sametmax.com/?p=13011 Suite aux commentaires, j’ai fais une refonte des dispos :

    • Plus d’insistance sur la différence entre RPC et PUB/SUB.
    • Les exemples sont amenés plus tôt, et les schémas sont en premier.
    • Des lourdeurs et des redondances sont supprimées.
    • J’ai ajouté des réponses à quelques questions posées : perf, sécu, etc.

    Histoire d’éviter d’éparpiller des versions partout, je l’ai juste réup au même endroit.

    Merci, donc, pour toutes les remarques qui ont significativement permises d’améliorer la prez.

    ]]>
    http://sametmax.com/corrections-des-slides-wamp/feed/ 8 13011
    Full disclosure http://sametmax.com/full-disclosure/ http://sametmax.com/full-disclosure/#comments Tue, 16 Dec 2014 15:32:26 +0000 http://sametmax.com/?p=12883 Depuis quelques jours je suis en discussion avec Tobias de Tavendo. Comme vous avez pu le remarquer avec mes précédents articles sur WAMP et Crossbar :

    • Ils sont bons techniquement, et nuls pour expliquer ce qu’ils ont techniqué.
    • Cette techno est une techno de rêve pour moi. J’y crois à mort.
    • Je suis le seul à avoir pondu des explications décentes sur WAMP et Crossbar. Et ça n’a pas suffit à faire battre un cil.

    Bref, ils ont embauché des mecs de haute voltige pour la technique (du genre un contributeur PyPy). Et ils m’ont contacté pour me demander si je n’étais pas chaud pour faire de l’évangélisme, rémunéré, autour de WAMP, Autobahn et Crossbar.

    L’idée : écrire des tutos, des articles, améliorer la doc, répondre sur le chan IRC, etc.

    J’adore le concept, vu que j’aime leur projet et que je le faisais gratos avant, surtout qu’ils sont pas trop contraignants sur le temps que je vais passer dessus.

    Donc voilà le deal : quand je vais pondre des tutos et des articles sur WAMP et Co, je vais d’abord les faire en français ici. Comme ça j’aurai les retours des lecteurs du blog qui pourront, comme d’habitude, me faire part de leurs douces remarques sur à quel point on ne pige rien.

    Une fois la prose aiguisée, je traduis et je publie chez Tavendo.

    Je disclose donc ici que vous verrez peut-être des prochaines rédactions qui seront attachées à une activité pro. Pas impartial donc. Mais bon, depuis quand je suis impartial ? Javascript c’est de la merde, et je préfère les rousses.

    Par saucisse d’honnêteté, je signalerai chaque choucroute concernée avec un lien vers ce post.

    Enfin, le contrat est pas signé encore, mais vu que je vais commencer à taffer dessus aujourd’hui, je pense à une première publication demain sous la forme d’un slide show expliquant avec de jolies diapos ce que sont WAMP, Autobahn et Crossbar. À quoi ça sert et ce qu’on peut faire avec.

    ]]>
    http://sametmax.com/full-disclosure/feed/ 28 12883