Le Blog de C-quad

Tester une restauration de sauvegarde sur un environnement vierge

C’est bien beau de sauvegarder votre blog par exemple, mais le jour ou la machine qui l’héberge rencontre un souci n’est pas le moment opportun pour vérifier que vos sauvegardes sont fonctionnelles.

Pour mon test, je pars donc du postulat que je n’ai perdu l’intégralité de mon serveur et que je désire remonter mes sauvegardes dans des conteneurs docker.

Commençons par remonter un environnement

Il nous faut une base de données  :

docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=mysecretpassword -d mysql

Et un conteneur wordpress :

docker run --name some-wordpress -e MYSQL_ROOT_PASSWORD=mysecretpassword --link some-mysql:mysql -p 80:80 -d wordpress

Le conteneur est maintenant accessible via http://host:80
On ajoute l’extension qui a permis les sauvegardes. Dans mon cas j’ai utilisé « UpdraftPlus – Backup/Restore »

Restaurons nos sauvegardes :

Charger les fichiers de sauvegardes et procéder à la restauration fournie par votre plugin.

S’il est nécessaire de modifier l’adresse du site web, nous éditons la configuration wordpress.
Pour lancer une commande sur un conteneur qui tourne :

docker exec -ti some-wordpress /bin/bash

Editer le fichier wp-config.php et insérer :

update_option('siteurl','http://example.com');
update_option('home','http://example.com');

example.com est bien entendu l’adresse de votre nouveau site. Il suffit ensuite de recharger une page pour que cela soit pris en compte. Et ces 2 lignes peuvent ensuite être supprimées.

Nous voila avec un site de nouveau fonctionnel. Cette méthode peut aussi s’appliquer pour tester une migration.

Docker – Les images et comment faire communiquer deux conteneurs

Nous avons vu comment utiliser les conteneurs dans docker dans l’article « Docker – une petite révolution pour moi », nous allons maintenant voir comment gérer les images qui servent à construire notre conteneur.

Voir les images présentes en local

Dès que l’on crée un conteneur à partir d’une image, celle ci est stockée en local et peut être ré-utilisée. Pour voir la liste des images disponibles en local :

$ docker images
REPOSITORY                  TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
training/webapp             latest              31fa814ba25a        3 months ago        278.6 MB

Si l’on désire télécharger une image :

$ docker pull centos

Créer ses propres images

Pour créer ses propres images, deux solutions sont possibles :

  • Partir d’une conteneur existant
  • Créer un Dockerfile

En partant d’un conteneur existant :

$ docker run -i -t centos /bin/bash
bash-4.2# yum install mysql
bash-4.2# exit
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
1c97b8bd7df9        centos:centos7      "/bin/bash"         5 minutes ago       Exited (0) 8 seconds ago                     drunk_perlman 
$ docker commit -m "Ajout de mysql" -a="Cedric OLIVIER" 1c97b8bd7df9 cquad/centos:mysql
64b17daafd4bfbf2f4af84efbb2d551c4565470a1c4e6d0150af6deb8d4735da

En partant d’un DockerFile :

Il s’agit dans ce cas de créer un fichier qui décrit ce que l’on veut et l’on construit ensuite le conteneur à partir de ce fichier.
Dans notre cas cela donne le Dockerfile suivant :

# Fichier Dockerfile à partir de l'image Centos, on ajoute mysql
FROM centos
MAINTAINER Cédric OLIVIER <mail@exemple.com>
RUN yum install -y mysql

On lance ensuite la construction à partir du fichier Dockerfile ci dessus :

$ docker build -t="cquad/centos:mysql" .
Sending build context to Docker daemon  2.56 kB
Sending build context to Docker daemon 
Step 0 : FROM centos
 ---> 70214e5d0a90
Step 1 : MAINTAINER Cédric OLIVIER <mail@exemple.com>
 ---> Running in 539bff6918b2
 ---> 3cfce923bb60
Removing intermediate container 539bff6918b2
Step 2 : RUN yum install -y mysql
 ---> Running in a7e711864bfc
Loaded plugins: fastestmirror
Determining fastest mirrors
 * base: centos.mirror.fr.planethoster.net
 * extras: centos.mirror.fr.planethoster.net
 * updates: centos.quelquesmots.fr
Resolving Dependencies
--> Running transaction check
...
Complete!
 ---> 8d3d8442cb55
Removing intermediate container a7e711864bfc
Successfully built 8d3d8442cb55

Faire communiquer deux conteneurs

Une petite option qui pourra vous être bien utile, il est possible de nommer les conteneurs au lancement avec l’option --name

Nous allons ici nommer web le conteneur qui heberge notre application python Flask

$ docker run -d -P --name web training/webapp python app.py
add2f97c63b01b79f3a4a9d6ba26525b2fe8bb358569e893e54f53b4fe302d02
$ docker ps 
CONTAINER ID        IMAGE                    COMMAND             CREATED             STATUS              PORTS                     NAMES
add2f97c63b0        training/webapp:latest   "python app.py"     6 minutes ago       Up 6 minutes        0.0.0.0:49154->5000/tcp   web       

Nous avons un conteneur web maintenant nous allons créer un conteneur db :

$ docker run -d --name db training/postgres

La base de données ne sera accessible que par le conteneur web, nous n’avons pas passé l’option -p pour « exposer » le conteneur au réseau.

Pour pouvoir créer le lien entre les 2 conteneurs, nous devons stopper et supprimer le conteneur web :

$ docker rm -f web
web
$ docker run -d -P --name web --link db:db training/webapp python app.py

Nous relançons le conteneur avec l’option --link pour lier les 2 conteneurs ensemble. l’option --link recoit pour argument nom:alias.
Le nom correspond au nom que l’on a donné au conteneur, l’alias permet de donner un autre nom pour le conteneur web si l’on veut.

$ docker ps
CONTAINER ID        IMAGE                      COMMAND                CREATED             STATUS              PORTS                     NAMES
4bdbc71d428f        training/webapp:latest     "python app.py"        15 seconds ago      Up 13 seconds       0.0.0.0:49155->5000/tcp   web                 
02ca8208e75d        training/postgres:latest   "su postgres -c '/us   9 minutes ago       Up 9 minutes        5432/tcp                  db,web/db           

On constate dans le champs NAMES le lien entre les deux conteneurs.
 

Docker – Une petite révolution pour moi

Docker est une application open source qui se base sur les conteneurs. Elle permet de faire la même chose qu’avec de la virtualisation (uniquement si l’os de la machine virtualisée est basée sur du Linux) mais en économisant la virtualisation.  Extrait de wikipedia :

Contrairement aux machines virtuelles traditionnelles, un conteneur Docker n’inclut pas de système d’exploitation, à la place il s’appuie sur les fonctionnalités du système d’exploitation fourni par l’infrastructure sous-jacente

Cela permet donc d’avoir des environnements léger et peu consommateur de ressources. Docker permet aussi d’automatiser le déploiement. Avant d’aborder le déploiement, commençons par le début et voyons comment l’utiliser.

Débuter avec Docker

Pour l’installer sous Fedora, un simple appel à yum permet de l’installer :

yum install docker

Pour ceux qui veulent pouvoir en profiter sous Windows, il existe boot2docker, qui lui par contre fonctionne à base d’une machine virtuelle Linux qui hébergera docker.

Lancement simple

Docker permet de lancer des applications dans un conteneur via la commande docker run

Par exemple :

$ docker run fedora:20 echo "Hello World"
Hello World

docker run lance un conteneur, ensuite nous lui avons passé en argument une image fedora version 20. docker cherche l’image en premier lieu en local et si elle n’est pas présente, il va chercher sur le repository dockerHub.

Ensuite docker lance la commande echo « Hello World » dans le conteneur. Par contre dès que la commande est terminée le conteneur est terminé.

Lancement interactif

Lançons maintenant :

$ docker run -t -i fedora:20 /bin/bash
bash-4.2# 

Nous avons lancé la même image (fedora:20) sauf que cette fois ci nous avons demandé un terminal tty avec l’option -t dans le conteneur et l’option -i permet d’interagir avec ce terminal.
Nous avons lancé un shell avec /bin/bash qui va nous permettre de lancer des commandes dans le conteneur.

Pour quitter, de la même façon que le conteneur est stoppé quand le echo « Hello World » a terminé, le conteneur sera fermé quand le process /bin/bash sera terminé. Il suffit donc de le quitter avec un exit.

bash-4.2# exit
exit
$

Lancement d’un daemon (en tâche de fond)

Nous avons vu comment lancer une commande dans un conteneur, comment interagir avec un conteneur, mais il peut aussi être utile de pouvoir lancer un processus en tache de fond.

$ docker run -d fedora:20 sh -c "while true; do echo hello world; sleep 1; done"
5b1507abe7ed9094e0ef15bf4f90c86764c1b38fced6feb2d699e4dae5eaa79d

L’option -d nous permet de lancer le conteneur en tache de fond. Par contre, comme vous pouvez le constater par de traces de nos « hello world » toutes les secondes…
Quand on lance un conteneur en tache de fond, docker retourne un identifiant dans notre cas : 5b1507abe7ed9094e0ef15bf4f90c86764c1b38fced6feb2d699e4dae5eaa79d

On peut via la commande docker ps voir la liste des conteneurs en cours d’executions :

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
5b1507abe7ed        fedora:20           "sh -c 'while true;    5 minutes ago       Up 5 minutes                            jolly_leakey        

On retrouve bien notre conteneur avec un nom défini aléatoirement par docker jolly_leakey.

Si on veut voir la sortie du conteneur, on utilise docker logs avec le nom du conteneur:

$ docker logs jolly_leakey
hello world
hello world
hello world

Nous allons maintenant arreter le conteneur avec la commande docker stop :

$ docker stop jolly_leakey
jolly_leakey
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

Notre conteneur est bien terminé.

Lancement d’un daemon sur le réseau

Nous allons maintenant lancer une application web qui tourne avec python Flask :

$ docker run -d -P training/webapp python app.py

L’option -P permet de pouvoir accéder à l’ensemble des ports ouverts à l’intérieur du conteneur à l’exterieur. L’application python Flask tourne sur le port 5000, mais ce n’est pas ce port qui sera utilisé en local pour accéder au conteneur. Pour connaitre l’association, nous lancons :

$ docker ps -l
CONTAINER ID        IMAGE                    COMMAND             CREATED             STATUS              PORTS                     NAMES
d749f39a560b        training/webapp:latest   "python app.py"     9 seconds ago       Up 7 seconds        0.0.0.0:49153->5000/tcp   boring_albattani    

Notre application est disponible sur le port 49153. Si on veut que le port local soit le même que le port interne 5000 dans notre cas, il faut lancer la commande suivante :

$ docker run -d -p 5000:5000 training/webapp python app.py
$ docker ps -l
CONTAINER ID        IMAGE                    COMMAND             CREATED             STATUS              PORTS                    NAMES
522394e84c05        training/webapp:latest   "python app.py"     5 seconds ago       Up 3 seconds        0.0.0.0:5000->5000/tcp   pensive_fermi    

Pour connaitre le port local sans faire appel à docker ps -l il existe une commande qui nous donne directement le mapping : docker port conteneur_name 5000

$ docker run -d -P training/webapp python app.py
26b92cf6f4aec2d59232dd664e5508423155426a487010b3c61d201ab2b80400
$ docker port 26b92 5000
0.0.0.0:49154

Vous constaterez qu’à la place du nom du conteneur, il est possible de passer les 4 premiers caractères de l’identifiant retourné au moment du lancement.

Relancer un conteneur arretté

Nous avons vu qu’a chaque fois que nous lancions docker run un nouveau conteneur est créé. Nous allons voir ici comment reprendre un conteneur qui a déjà été lancé.

$ docker start pensive_fermi
pensive_fermi
$ docker ps
CONTAINER ID        IMAGE                    COMMAND             CREATED             STATUS              PORTS                    NAMES
522394e84c05        training/webapp:latest   "python app.py"     About an hour ago   Up 18 seconds       0.0.0.0:5000->5000/tcp   pensive_fermi       

Suppression d’un conteneur

Une fois que l’on a fini avec un conteneur et que le décide de le supprimer, un simple docker rm permettra de la supprimer. Mais attention, la suppression est définitive.

$ docker stop pensive_fermi 
pensive_fermi
$ docker rm pensive_fermi 
pensive_fermi
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

 

Domotiser un poêle à granulés ( partie 2 : Le code )

Nous avons vu dans un précédent article le montage électronique pour contrôler le poêle grâce à un raspberry et un potentiomètre digital. Nous allons voir maintenant le code utilisé pour cela.

Nous allons avoir 2 processus qui tournent en tache de fond :

  • L’alimentation en continu de la température en faisant varier la résistance du potentiomètre digital.
  • L’interface web qui me permettra de piloter tout cela.

J’ai choisi de tout programmer en python pour diverses raisons. La principale étant que je ne connais pas et que le meilleur moyen d’apprendre est d’avoir quelque chose de concret à réaliser. Il se peut donc qu’il y ai des améliorations à apporter au code qui vous sera présenté ci dessous.

Communication avec le potentiomètre digital

Le potentiomètre digital choisi est le MCP4162. Voici la datasheet de ce potentiomètre.

Comme vous pourrez le constater, ce potentiomètre communique via SPI. J’ai choisi d’utiliser la librairie python spidev.

import spidev

# Ouverture du bus SPI
spi = spidev.SpiDev()
spi.open(0,0) # car j'utilise la pin CE0: serait spi.open(0,1) si j'utilisais la pin CE1

# Transfert de la temperature (plus exactement un pas sur le digipot)
resp = spi.xfer2([0, index])

Dans notre cas, le potentiomètre digital possède 256 valeur de 0 à 255, index prendra l’une de ces 256 valeurs.

Ce qui donne le code complet :

!/usr/bin/python
# -*- coding: utf-8 -*-
 
import spidev
import time
import urllib2
import json

# Ouverture du bus SPI
spi = spidev.SpiDev()
spi.open(0,0) # car j'utilise la pin CE0: serait spi.open(0,1) si j'utilisais la pin CE1

coeff = 0.087
TEMP_OFF = 25
CONSIGNE_POELE = 19

try:
    while True :
        content = urllib2.urlopen("http://ip_raspberry:5000/status").read()
        d = json.loads(content)    
        print d
        if d[u'Etat'] == u'off' :
            print "poele eteint"
            index = 0
        else :
            print "poele allume"
            print (d[u'Temperature']- d[u'Consigne'] + CONSIGNE_POELE)
            index = int((TEMP_OFF - (d[u'Temperature']- d[u'Consigne'] + CONSIGNE_POELE))/coeff)
            if index < 0 :
                index = 0
            if index > 255 :
                index = 255
        resp = spi.xfer2([0, index])
        time.sleep(10)

finally:
    spi.close()

Voici le code, celui ci n’est pas indépendant du service web. C’est grâce au service web que cette boucle déduit la température à donner au poêle.

L’appel au « status » retourne le json suivant :

{
  "Consigne": 19.0, 
  "Etat": "on", 
  "Exterieur": 17.6, 
  "Temperature": 23
}

Ce json est ensuite converti en dict pour être utilisé simplement dans la suite du traitement.

On demande au poêle de se mettre en route si la température est inférieure à 19°C. La température dans la pièce est actuellement de 23°C. La température extérieure n’est actuellement pas utilisée, elle le sera peut-être plus tard si je trouve comment l’utiliser convenablement.

L’interface web

Pour l’interface web, j’ai fait le choix d’utiliser Flask.

L’objectif est de pouvoir piloter le poêle directement depuis un smartphone, ou de pouvoir l’inclure dans ma solution domotique existante (Zibase pour le moment).

Récupération de la température

La température de la pièce est fournie par la station météo Netatmo, qui fournit une API. j’utilise une API python déjà existante.

Ce qui se traduit par le bout de code suivant:

# Connexion à la Netatmo
authorization = lnetatmo.ClientAuth()
devList = lnetatmo.DeviceList(authorization)

#Récupération de la température
temperature_interieur = devList.lastData()[u'Intérieur']['Temperature']
temperature_exterieur = devList.lastData()[u'Extérieur']['Temperature']

Communication avec la zibase

Je veux pouvoir connecter le poêle à la zibase, parce que celle-ci fournit directement une fonctionnalité de thermostat.

En réalité ce dont j’ai besoin que la zibase connaisse, c’est la température relevée par la station météo Netatmo.

La Zibase peut peut effectuer des commandes http, mais elle est plutôt limitée de ce coté. Elle sait lire du XML. Le service web fourni donc une page qui transforme le dict retourné par l’API netatmo en XML compréhensible par la Zibase.

#Appel par la zibase pour récupérer les infos de la Netatmo    
@app.route("/netatmo")
def netatmo():
    global devList
    xml = unicode(dicttoxml(devList.lastData()), "utf-8")
    xml = re.sub(r" type=(.*?)>",">",xml)
    xml = unicodedata.normalize('NFKD', xml).encode('ascii','ignore')
    return Response(xml, mimetype='text/xml')

Pour simplifier la configuration coté Zibase, j’ai retiré les accents par exemple « Extérieur » devient « Exterieur » et simplifier au maximum le xml produit en retirant les types.

La zibase ne permet que d’allumer/éteindre, j’ai donc un service qui recoit le on/off :

#Appel par la zibase pour allumer/eteindre le thermostat    
@app.route("/thermostat/<consigne>")
def thermostat(consigne):
    global VAR_ZIBASE_TEMP_INT
    global VAR_ZIBASE_CONSIGNE
    global zibase_control
    global Statut
    if zibase_control :
        Statut['Etat'] = consigne
        content = urllib2.urlopen("http://ip_zibase/sensors.xml").read()
        root = ET.fromstring(content)
        for var in root.iter('var'):
            if var.attrib['num'] == VAR_ZIBASE_TEMP_INT :
                Statut['Temperature'] = float(var.attrib['val'])/10
            if var.attrib['num'] == VAR_ZIBASE_CONSIGNE :
                Statut['Consigne'] = float(var.attrib['val'])/10
    return "OK"

Par contre, heureusement pour moi, les variables qui servent au thermostat de la zibase, sont accessibles dans sensors.xml. J’utilise ElementTree pour lire ce XML et affecter mes variables gloables qui sont retournées dans la page status (utilisée par l’autre processus).

Le statut global

C’est donc dans les variables globales du service Web que l’état du poêle est connu : température de la pièce, température demandée, thermostat activé ou non, température extérieure.

@app.route(« /status »)
def status():
global Statut
Statut[‘Temperature’] = devList.lastData()[u’Intérieur’][‘Temperature’]
Statut[‘Exterieur’] = devList.lastData()[u’Extérieur’][‘Temperature’]
return jsonify(**Statut)

Cette page status se contente donc juste d’afficher un json du dict global qui contient toutes les infos nécessaires.

Interface indépendante de la zibase

Parce que je n’ai pas envie de dépendre uniquement de la zibase, j’ai aussi réalisé une petite interface web. J’utilise Jquery Mobile pour que celle ci soit accessible de n’importe quel type de matériel que ce soit mon ordinateur, smartphone ou tablette.

Voici un petit aperçu de cette page :

Capture d'écran de 2014-06-19 14:13:14

Comme vous pouvez le constater, l’interface web prend en compte le niveau de granulés dans la réserve. Cela sera réalisé via un émetteur à ultra-son HC-SR04.

Mais au même titre qu’il me manque un boitier pour le raspberry qui accueille la Pi Plate, il me manque aussi une mise en place discrète sous le couvercle des granulés de l’émetteur à ultra-son.

Il est bien souvent plus simple de faire les prototypes et de programmer que d’intégrer cela proprement chez soi.

 

Domotiser un poêle à granulés ( partie 1 : Le montage )

Ma maison est une maison récente, mais dont la seule source d’énergie est l’électricité. Le chauffage était donc un plafond rayonnant au rez de chaussée et des « grilles pains » à l’étage.

Je n’ai aucun reproche à faire à la chaleur et le confort procuré par le plafond rayonnant, par contre, d’après nos estimations nous pensons être environ à 900€ de chauffage par an (comparaison de la consommation hiver/été). Nous avons donc pris la décision d’installer un poêle à pellets pour réduire cette facture et le plaisir de voir une flamme.

Notre choix s’est porté sur le CMG Dual :

Poêle à granulés CMG Dual

Ce poêle présente l’avantage d’être plutôt silencieux. Il était hors de question qu’il nous réveille en se mettant en route le matin.

Ce poêle a néanmoins 2 défauts (qui sont liés) :

  1. La sonde de température est filaire. De ce fait, elle est perturbée par la chaleur dégagée par le poêle en fonctionnement.
  2. La programmation est hebdomadaire, il n’est pas possible de programmer un allumage à une date précise ou dans X jours (exemple retour de vacances etc).

Voila donc pourquoi domotiser le poêle !

Domotiser le poêle

Les objectifs

Objectif numéro 1 : Faire en sorte que la température remontée au poêle ne soit plus perturbée par le fonctionnement du poêle.

Objectif numéro 2 : Pouvoir programmer le poêle plus finement.

Comment ?

CMG ne fourni pas à ce jour de solutions pour rendre connecté son matériel. Il ne me reste donc qu’a bidouiller avec ce que j’ai et mes connaissances.

Le gros point noir que j’ai c’est une sonde filaire.

Comment faire pour remplacer une sonde filaire par une sonde distante ?

Il existe deux types de sondes filaires, les sonde numériques et les sondes analogiques. La mienne ne possède que 2 files et a une résistance variable en fonction de la température constatée.

La première étape est donc de débrancher cette sonde et de prendre un ampèremètre pour mesurer la résistance de celle-ci. Cela me donne une résistance d’environ 12kohm.

Deuxième étape : je décide de remplacer cette sonde par un potentiomètre classique de 10 kohm avec en série une résistance de 10kohm. Il me reste maintenant à faire varier le potentiomètre, noter la température donnée par le poêle, débrancher et mesurer pour avoir la résistance équivalente.

Ce qui donne ces quelques mesures :

8°C       = 19,7 kohm
18,1 °C = 13    kohm
18,4 °C = 12.9 kohm
20 °C    = 12   kohm
25 °C    = 10   kohm

La solution : remplacer ce potentiomètre analogique, par un potentiomètre digital.

Le matériel

L’objectif est de pouvoir connecter le poêle pour qu’il soit contrôlable à la fois via un PC ou Smartphone. Il me fallait donc une connexion sans fil. Mon choix s’est donc porté sur :

  • Un raspberry PI
  • Une clé USB wifi
  • Un potentiomètre digital MCP 4162

Et pour les tests :

  • Un ampèremetre
  • Un Pi Cobler
  • Une breadbord (platine d’essai)
  • des fils 😉

Au départ je n’était pas parti sur ce potentiomètre, mais sur un AD5175 qui présente l’avantage d’avoir un pas de 1024 alors que celui-ci n’a que 256 valeurs possibles. Mais il présente l’avantage d’être dans un format qui me permet de le tester facilement sur une platine d’essai. Et surtout n’ayant jamais soudé, au plus simple au mieux.

Le montage

Voila donc une photo du prototype.

Le montage d’essai

Ce qui donne les schémas suivants :

raspberry_digipot_bb

 

raspberry_digipot_schéma

Voila pour le montage.

Résumé

Donc pour résumer, nous utilisons un potentiomètre digital connecté via SPI au Raspberry PI. Ce potentiomètre remplace la sonde filaire du poêle et lui indique la température. Nous allons jouer sur cette température pour alluer ou non le poêle.

Au niveau du poêle, il sera toujours allumé avec une température demandée à 19°C. Si on veut que le poêle se coupe, on fait croire à celui-ci qu’il fait 25°C dans la pièce sinon on lui envoie la température relevée par une sonde déportée. Dans mon cas je récupère la température via la station netatmo.

Dans le cas d’un poêle à granulé, il est impossible de le faire fonctionner en on/off parce que la quantité de granulés consommée par le poêle est variable en fonction de l’écart entre la température constatée et la température demandée. Il est donc nécessaire de prendre cela en compte.

Reste à faire

Comme vous pouvez le constater, actuellement le WAF n’est pas au rendez vous 😉

J’ai donc acheté une Pi plate :

http://www.adafruit.com/products/801

http://www.adafruit.com/products/801

Je cherche encore un boitier qui peut accueillir le raspberry et la Pi plate que je puisse facilement attacher derrière le poêle.

A suivre : Le code utilisé pour que tout cela fonctionne 😉