--- title: "Cellaserv" date: "18-02-2020" link: "https://hackmd.io/JhQ0mkRDQuGQKasJEBeDBQ" tags: EVOLUTEK --- ## TODO * Logs * Variables * Event * ConfigVariables Design patterns: * Lancer plusieurs actions en parallèle # Cellaserv ## Introduction **Cellaserv** est un serveur RPC développé par **Rémi Audebert (Halfr)** aka DIEU <3. Ce serveur codé en Go permet à des processus *services*, tournant sur des machines connectées sur le même réseau, de communiquer entre eux en *TCP/IP* via le serveur. Le serveur permet de: * Publier un événement *(Publish)* * Ecouter sur un événement *(Subscribe)* * Envoyer une *requête à un service* * Répondre a une *requête* Pour en savoir plus, je vous invite à voir la conférence de **Halfr** sur ce sujet : [LSE Week 2014](https://www.youtube.com/watch?v=zV1a-kmO1BU) ou encore : [NDI 2015](https://youtu.be/p4qXxmP_h34?t=2494) ### Installation & éxécution Le repo de cellaserv est ouvert en ligne ici : [Repo](https://github.com//evolutek/cellaserv3/src/master/) Il faut penser à installer go sur votre machine *(Golang)* et setup son *$GOPATH* Pour set son *$GOPATH*, il suffit de mettre dans son .bashrc : ```bash= export GOPATH=$HOME/go export PATH=$PATH:$GOROOT/bin:$GOPATH/bin ``` Pour installer **Cellaserv** : ```bash= go install github.com/evolutek/cellaserv3/cmd/cellaserv@latest sudo mkdir /var/log/cellaserv sudo chmod 777 /var/log/cellaserv ``` Vous pouvez aussi installer *cellaservctl*, un utilitaire qui permet d'intéragir avec le serveur : ```bash= go install github.com/evolutek/cellaserv3/cmd/cellaservctl@latest ``` Pour lancer **Cellaserv**, il suffit de : ```bash= cellaserv ``` Cellaserv utilise pour sa config le fichier */etc/conf.d/cellaserv* qui contient : ``` [cellaserv] debug = 1 # Niveau de Debug port = 4200 # Port du serveur ``` Pour utiliser *cellaservctl* : ```bash= cellaservctl <command> ```` Pour plus d'info, vous pouvez utiliser : ```bash= cellaserv --help ou cellaservctl --help ``` ### Interface web Une fois lancé, cellaserv expose un serveur web (port 4280 par défaut) qui permet d'inspecter l'état interne comme la liste des services, des clients, et d'envoyer des requêtes via un formulaire. ### Debug TODO ## Service Un *service* est un processus qui va venir se connecter au serveur via un client. Actuellement il existe plusieurs clients : * [Go](https://github.com/evolutek/cellaserv3/tree/master/client) * [Python](https://github.com/evolutek/python-cellaserv3) > Il est possible d'utiliser un client sans faire de service Un *service* va venir communiquer avec **Cellaserv** via le client choisi. Du coup un *service* peut : * *Subscribe* à un *event* * Publier un *event* * Faire des *requêtes* à d'autres *services* * Recevoir des *requêtes* Un service posséde un nom est un indentifiant sur le serveur. L'identifiant permet d'avoir plusieurs même *services* tournant sur le serveur. > Par défault, l'identifiant est *Null* A partir de maintenant, je vais parler exclusivement du client *Python*, le plus complet et celui que nous utilisons sur les robots. ### Installation Le client *Python* se trouve sur ce [Repo](https://github.com/evolutek/python-cellaserv3/). Tout d'abord il faut cloner le repo : ```bash= En https : git clone --recurse-submodules https://github.com/evolutek/python-cellaserv3.git En ssh : git clone --recurse-submodules git@github.com:evolutek/python-cellaserv3.git ``` Ensuite, il faut installer le client : ```bash= cd python-cellaserv3 sudo python3 setup.py develop ``` Comme pour **Cellaserv**, la config du client se trouve aussi dans */etc/conf.d/cellaserv*: ``` [client] debug = 1 # Niveau de debug host = localhost # Host du serveur Cellaserv port = 4200 # Port du serveur Cellaserv ``` ### Tester le client Ici on suppose que vous avez un **Cellaserv** qui tourne et que vous avez réussi à installer le client. Dans le repo, il y a un dossier *examples/* contenant des exemples d'utilisation du client. Prenons *cellaserv/examples/service/date_service.py*. Cet exemples contient le code d'un *service*, le service *Date* qui permet d'avoir l'heure. Si vous le lancez avec : ```bash= ./date_service.py ``` Le service devrait se lancer et se mettre à *log* la date : ```bash= INFO:cellaserv.client:[Publish] log.date(b'{"time": 1586611405.748847}') ``` Le *service* date propose aussi des actions que vous pouvez appeler avec *cellaservctl*, par example : ```bash= cellaservctl r date.time -> renvoie la date cellaservctl r date.print_time -> le service va afficher la date ``` Le *service subscribe* aussi à un *event*, l'*event* hello, que vous pouvez aussi tester avec *cellaservctl : ```bash= cellaservctl p hello -> le service va afficher "hello world" cellaservctl p hello what=me -> le service va afficher "hello me" ``` ### Mon permier service Pour avoir un *service* fonctionnel, il suffit d'écrire : ```python= import asyncio from cellaserv.service import Service # Crée une classe héritant de la classe Service class Dumb(Service): pass # Le main() est une coroutine, définie avec `async` async def main(): # Crée une instance de la classe date = Dumb() # Attend tant que le service n'est pas terminé await date.done() if __name__ == "__main__": # On doit lancer la coroutine `main()` en utilisant `asyncio.run()` asyncio.run(main()) ``` Ce service appelé *dumb* ne va rien faire mais il sera connecté à **Cellaserv**. > Pour voir la liste des clients connectés : `cellaservctl ls` ### Features Une grande partie de ces features sont accéssible via les *décorateurs* de **Python**, une feature très puissante. > Pour en savoir plus : https://www.python.org/dev/peps/pep-0318/ #### asyncio `python-cellaserv3` fonctionne grace à [asyncio](https://docs.python.org/3/library/asyncio.html), la bibliothèque python permettant de mettre en oeuvre des coroutines et plus généralement une logique asynchrone. Il est important de se familiariser avec les possiblités et les contraintes de asyncio. Les examples ce-dissous permettront de comprendre progressivement les fonctionalités offertes par asyncio. En ce qui concerne les contraintes, on peut en citer quelques unes à garder en tête : * Pas de possibilité de bloquer activement l'exécution, par example asyncio n'est pas compatible avec `time.sleep(1)` et il faudra utiliser à la place `await asyncio.sleep(1)` à la place. En effet, si on bloque l'execution, `asyncio` ne pourra traiter les autres tâches en cours. Celà vaut pour notre code, mais aussi pour les bibliothèques externes utilisées. #### Init Pour le moment, je n'ai montré qu'un exemple de *service* sans définition de la fonction ```__init__()``` qui est le constructeur d'une classe en **Python**. Lorsque l'on ne défini pas de constructeur, il en crée un vide par défault, qui appelle le constructeur de la classe parent si la classe hérite d'une autre classe. Du coup lorsque l'on redéfini soit même le constructeur, il est nécessaire d'appeller le constructeur de la classe *Service* : ```python= from cellaserv.service import Service class Dumb(Service): def __init__(self): super().__init__() ``` Comme annoncé plus haut, un *service* a un identifiant. Pour le choisir, il faut : ```python= from cellaserv.service import Service class Dumb(Service): def __init__(self, identifiant): super().__init__(identifiant) ``` #### Action Une *action* est une méthode qui est accessible depuis **Cellaserv**. Il suffira de faire une requête au *service* possédant la méthode. Il est possible d'envoyer des arguments à cette *action* et qu'elle renvoie quelque chose via **Cellaserv** Pour déclarer une *action*, il suffit de mettre dans son service : ```python= @Service.action # Indique que cette méthode est une action du service def test(self): #Do something pass ``` Le décorateur prend un argument qui permet de changer le nom de l'action : ```python= @Service.action('other_name') def current_name(self): pass ``` Du coup, cette *action* ne sera appelable que par sont *other_name*. Comme indiqué, il est possible de recevoir des parametres et de renvoyer avec une *action*, c'est le même fonction que du Python classique : ```python= @Service.action def action(self, arg1, arg2, ...): # Something return Something ``` Il faut cependant à veiller que les arguments et la valeur de retour soit sérialisable. De plus, une action est bloquante: c'est à dire que tant qu'elle n'a pas quitté ou retourné une valeur, on ne peut rien faire. Pour appeler la fonction d'un service avec *cellaservctl*, il faut : ```bash= cellaservctl r service/identifiant.action arg1=... ``` Si le service n'a pas d'identifiant : ```bash= cellaservctl r service.action ``` #### Event Les événements permettent de notifier tout le serveur. Tous les entités qui écoutent l'événement seront notifiées. Pour un *service*, pour publier un *event* il suffit de : ```python= self.publish('event') # Sans arguments self.publish('event', arg1=1, arg2=2, ...) # Avec des arguments ``` Pour subscribe à un *event*, il faut aussi utiliser un *décorateur* sur une fonction du service. Cette fonction appelée fonction de *callback* sera appelée avec les arguments de l'*event* dès qu'il sera publié : ```python= @Service.event def callback(self): pass Avec des arguments : @Service.event def callback(self, arg1, arg2, ...): pass ``` Comme pour les *actions*, il est aussi possible de choisir sur quel *event* une fonction de *callback* va être appelée via le décorateur sinon le nom de l'*event* sera celui de la fonction : ```python= @Service.event('event') def callback(self): pass ``` Pour publier un *event* avec *cellaservctl*, il faut : ```bash= cellaservctl p event ``` Avec des arguments : ```bash= cellaservctl p event arg1=1 arg2=2 ``` TODO : Class Event #### Coroutines Il est possible de lancer des *coroutines* automatiquement au lancement du *service* via un *décorateur* : ```python= @Service.coro async def loop(): while True: print('I am looping') await asyncio.sleep(1) ``` Toutes les fonctions ainsi définies seront lancées une fois que le service est initialisé (dépendance présentes, config à jour). #### ConfigVariable *ConfigVariable* est une classe pour récupérer une variable dans la config via le *service Config*. Example : ```python= from cellaserv.service import Service, ConfigVariable class Dumb(Service): var = ConfigVariable("section", "var", coerc=type) # Setup l'objet def func(self): self.var() # Récupére la valeur actuelle dans la config ``` Il est possible de configurer une fonction de *callback* pour être notifié quand la variable change : ```python= from cellaserv.service import Service, ConfigVariable class Dumb(Service): var = ConfigVariable("section", "var", coerc=type) # Setup l'objet def __init__(self): self.on_var_update() # set the self.color_coef self.var.add_update_cb(self.on_var_update) # Configure callback def on_var_update(self, value): pass ``` TODO check example #### Require Un *service* peut indiquer qu'il est dépendent d'autres *services* via encore un autre *décorateur* : ```python= @Service.require('required_event') class Dumb(Service): pass ``` Lorsque le constructeur de la classe parente est appellé (```super().__init__()```), il attendra que le service désiré se connecte avec le serveur. #### Log TODO ## Autres features du client Python Le client **Python** propose d'autres features intéressants : * Un proxy , *CellaservProxy* * Un client *asynchrone*; *AsynClient* * Un gestionnaire de paramètres, *Settings* ### CellaservProxy **CellaservProxy** est un proxy permettant d'interargir avec les *services* présents sur **Cellaserv**. Il permet de récupérer des instances de *services* existant : ```python= from cellaserv.proxy import CellaservProxy cs = CellaservProxy() # Crée le proxy dumb = cs.dumb # Récupére l'instance du service dumb qui tourne ``` Comme un *service* peut avoir un identifiant, il faut l'utiliser pour y avoir accès : ```python= from cellaserv.proxy import CellaservProxy cs = CellaservProxy() # Crée le proxy dumb = cs.dumb[identifiant] # Récupére l'instance du service dumb qui tourne ``` Evidemment il est possible d'utiliser cette instance pour appeler une de ces actions : ```python= await dumb.action() # Appelle l'action du service dunb ``` Il est aussi possible de ne pas stocker l'instance et d'appeler directement l'action : ```python= from cellaserv.proxy import CellaservProxy cs = CellaservProxy() # Crée le proxy await cs.dumb[identifiant].action() # Appelle une action du service dumb ``` Il est conseillé d'utiliser cette méthode qui permet de continuer à communiquer avec un *service* même s'il a redémarré entre-temps. ### Settings Les *Settings* permettent de créer des paramètres et de changer leur valeur sur un même client, ils ne sont locaux au client dans un processus. La fonction pour créer et modifier un paramètres est : ```python= make_setting(name, default, cfg_section, cfg_section, env, coerc=str) ``` * name : nom du paramètre * default : valeur du paramètre par défaut * cfg_section : section dans la config * cfg_section : option dans la section * env : environement du code Il est à noter que make_settings remplacera la valeur par défaut par la valeur dans la config de **Cellaserv** qui se trouve dans */etc/conf.d/cellaserv* si elle existe déjà. ## Excercices ### Premier Service #### Consignes * Objectif : Un service simple qui affiche son nom dans la console lorsqu'on lui demande * Features authorisées : * Service.action * Example de fonctionnement : ```bash= $ python3 first.py & [1] pid DEBUG:cellaserv.settings:DEBUG: 1 DEBUG:cellaserv.settings:HOST: localhost DEBUG:cellaserv.settings:PORT: 4200 INFO:cellaserv.service:[Dependencies] Service ready! $ cellaservctl r first.print_name DEBUG[2020-04-18T15:15:06+02:00] Sending request first[].print_name({}) module=client null ``` * tip : le nom du service est stocké dans la variable 'service_name' #### Correction ```python= import asyncio from cellaserv.service import Service class First(Service): @Service.action def print_name(self): print(self.service_name) async def main(): first = First() await first.done() if __name__ == "__main__": asyncio.run(main()) ``` ### Ping Pong #### Consignes * Objectif : Un service qui doit réagir à un ping par un pong * Features authorisées : * Service.publish * Exqmple d'utilisation : ```bash= $ python3 pong.py & [1] pid DEBUG:cellaserv.settings:DEBUG: 1 DEBUG:cellaserv.settings:HOST: localhost DEBUG:cellaserv.settings:PORT: 4200 INFO:cellaserv.client:[Subscribe] ping INFO:cellaserv.service:[Dependencies] Service ready! [...] $ cellaservctl p ping DEBU[2020-04-18T19:11:50+02:00] [Publish] Sending: ping(map[]) module=client [...] DEBUG:cellaserv.service:Publish callback: ping({}) INFO:cellaserv.client:[Publish] pong(None) ``` #### Correction ```python= import asyncio from cellaserv.service import Service class Pong(Service): @Service.event def ping(self): self.publish('pong') async def main(): pong = Pong() await pong.done() if __name__ == "__main__": asyncio.run(main()) ```