---
# System prepended metadata

title: Cellaserv
tags: [EVOLUTEK]

---

---
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())
```