Créer son propre cloud : installer proxmox et une première VM connectée au net sur un serveur kimsuffi

A priori ce tutoriel est valable pour n’importe quel serveur (kimsuffi ou autre) du moment que vous pouvez intaller proxmox VE6 dessus, et pour du proxmox 5 il ne devrait pas y avoir beaucoup de différences

En ce moment je travaille pour deux projets différents, une startup qui a besoin d’un site web, tout ce qu’il y a de plus basique, et pour moi, je souhaite avoir mon propre cloud pour pouvoir tester rapidement des applications auto-hébergées. Dans les deux cas j’ai besoin d’avoir une architecture évolutive. Quand je teste des applications je veux pouvoir créer une VM temporaire, et pour héberger un site web la, des VMs on en a besoin pour tout un tas de choses quand on développe. J’ai besoin d’une solution pour pouvoir installer des VMs sur un serveur distant. Il existe plein de solutions (Xen server, VMWare ESXi, et d’autres … j’avais déjà utilisé vSphere par le passé avec un client en flash ! lol). J’ai choisi proxmox VE 6 (PVE6), gratuit, open source, basé sur KVM et disposant de plein de fonctionnalités interressantes : cluster, snapshot, migration live, interface de gestion web, CLI, et plein d’autres choses (comme Ceph que je ne connais pas encore mais que je vais bientôt étudier).

Kimsuffi, ce sont des serveurs OVH un peu datés avec une interface de gestion minimaliste. En gros vous pouvez installer une distribution dessus et c’est à peu près tout. C’est parfait pour moi, ils proposent PVE6 justement. Je lance, je reçois le mot de passe root et en avant. Il n’y a aucune configuration à faire, je me loggue sur Proxmox et j’arrive sur l’interface de gestion :

Parfait. Maitenant pour créer sa première VM c’est la que les problèmes commencent. D’abord, il me demande un ISO, mais la liste est vide !! Après quelques recherches, je trouve, il faut la télécharger soi même en ligne de commande (Boutton « Shell »). Je prends la dernière LTS ubuntu server :

cd /var/lib/vz/template/iso; wget http://releases.ubuntu.com/18.04/ubuntu-18.04.3-live-server-amd64.iso

Je vous invites à faire un upgrade en passant de proxmox. Kimsuffi installe la v6.0 mais la v6.1 apporte des choses intéressantes, notamment le rechargement de la conf réseau.

apt update; apt upgrade

Maintenant je peux créer ma VM :

Le reste du wizard est simplissime, j’ai mis 2 coeurs, 2Go de RAM, 32Go de disque. Par contre la partie réseau, c’est la que ça s’est compliqué !! Première tentative ma VM s’installe correctement mais je n’ai pas d’accès au réseau. Ouch ! Et configurer un réseau sous linux ce n’est pas ma tasse thé. J’ai galéré. Je suis habitué sous Virtual Box à faire du NAT ou un Bridge avec mes VM et basta. Proxmox propose les même choses mais …

J’ai passé 2 jours entiers à chercher sur les forums et à tester des configurations. Le problème c’est que si vous mettez en l’air le réseau d’un serveur distant … et ben vous êtes bon pour le ré-installer from scratch.

Il y a plusieurs possibilités, installer une VM avec DHCP, utiliser « cloud init » mais le problème c’est que si je n’arrive pas à faire le truc le plus simple, je me vois mal m’engager dans des manoeuvres beaucoup plus compliquées. La plupart des tutos font des suppositions sur votre architecture et les choses évoluent vite (et malheureusement ça sera un peu pareil avec le mien). Bref, voici la solution que j’ai trouvée et qui corresponds à ce qu’on a dans le manuel proxmox. Je vous l’aie condensée en une seule ligne de commande :

cat >> /etc/network/interfaces <<EOF

auto vmbr1
iface vmbr1 inet static
        address  192.168.1.1
        netmask  24
        bridge-ports none
        bridge-stp off
        bridge-fd 0

        post-up echo 1 > /proc/sys/net/ipv4/ip_forward
        post-up   iptables -t nat -A POSTROUTING -s '192.168.1.0/24' -o vmbr0 -j MASQUERADE
        post-down iptables -t nat -D POSTROUTING -s '192.168.1.0/24' -o vmbr0 -j MASQUERADE
EOF

Et voila !

Ha non, n’oubliez pas de rebooter proxmox, sinon il ne prendra pas en compte votre nouvelle interface réseau.

On rajouter un réseau NAT « vmbr1 » qui utilises le réseau pré-configuré vmbr0 par défaut (ça peut être différent dans votre configuration) et ensuite il faut configurer la VM en manuel pour utiliser ce réseau. Le sous-réseau 192.168.1.0/24 sera utilisé (c’est à dire toutes les adresses entre 0 et 254). Les règles iptable servent à transférer de vmbr1 à vmbr0.

Par défaut dans mon fichier de configuration des interfaces j’ai ceci (et on n’y touche pas, il y a plein de tutos qui démarrent avec des IP statiques dans cette partie) :

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

iface enp1s0 inet manual

auto vmbr0
iface vmbr0 inet dhcp
  bridge-ports enp1s0
  bridge-stp off
  bridge-fd 0

D’abord à la création de la VM pensez bien à mettre vmbr1 :

Ensuite, lors de la configuration d’ubuntu server, il va falloir mettre les bon paramètres (je passe les étapes bidons de choix de la langue) :

Continuer sans le réseau ? ha ben non, pas question, je fais quoi de mon ubuntu server si je n’ai pas le réseau ? Proxmox fournit un accès VNC, soit, mais je ne vais pas aller bien loin avec ça. Donc … à ce stade ma seule solution c’est la configuration manuelle :

C’est la qu’il faut un minimum de connaissance réseaux pour remplir ces cases :

  • subnet: c’est le masque de sous-réseau, ça indique les adresses réseau autorisées
  • address: c’est l’IP de la nouvelle machine dans ce sous-réseau
  • gateway : la gateway c’est un peu comme votre box internet, c’est elle qui fait le routage des requettes internet. Au début je pensais qu’il fallait que je prenne cette de proxmox (donc celle de kimsuffi) avant que je ne réalise que la gateway c’est « proxmox » lui même, c’est à dire 192.168.1.1, l’adresse virtuelle que j’ai définie dans la conf réseau de proxmox.
  • name server : on met généralement l’IP du serveur de google

A partir de la on fini le wizard d’installation d’ubuntu server (moi j’installe sur tout le disque avec les options par défaut). On se retrouve avec un fichier netplan comme suit (/etc/netplan/50-cloud-init.yaml) sur la VM, qui reprends la configuration qu’on a entré à la main. A la rigueur vous pourriez installer ubuntu sans réseau et le rajouter après coup en modifiant le fichier :


network:
	ethernets:
		ens18:
			addresses:
			- 192.168.1.2/24
			gateway4: 192.168.1.1
			nameservers:
				addresses:
				- 8.8.8.8
	version: 2

La, vous avez une machine qui peut se connecter au net, faire ses updates, toussa toussa. Super, mais … est-ce que le net peut communiquer avec votre machine pour autant ? Non car vous êtes en NAT, c’est un réseau privé. Donc si vous installez (comme moi) un site dessus, internet ne pourra pas le voir.

Il faut donc modifier la table de routage pour que le traffic entrant des ports 80 et 443 (les ports généralements utilisés pour server un site web) soit redirigé vers la nouvelle VM. Et pour ça il faut encore aller modifier l’interface réseau de proxmox en rajoutant ces lignes dans vmbr1 :

	post-up iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 80 -j DNAT --to 192.168.1.2:80
	post-down iptables -t nat -D PREROUTING -i vmbr0 -p tcp --dport 80 -j DNAT --to 192.168.1.2:80
	post-up iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 443 -j DNAT --to 192.168.1.2:443
	post-down iptables -t nat -D PREROUTING -i vmbr0 -p tcp --dport 443 -j DNAT --to 192.168.1.2:443

Cette fois on reboote proxmox et on est bon. Pour tester rapidement si votre serveur réponds bien, petite astuce, utilisé python :

python -m SimpleHTTPServer 80

Si ça réponds sur l’IP ou le nom de domaine de votre serveur, c’est que la configuration de votre VM est correcte et que vous pouvez passer à la suite !

Etape par étape.

Les prochaines fois je prévoies justement d’installer un DHCP ou un « cloud init » parce que devoir modifier la configuration du réseau et rebooter à chaque nouvelle VM ça va vite devenir ennuyeux. Mais, je compte surtout faire un article bientôt sur « rancher », pour faire du SaaS, c’est dire installer des applications web en self hosted très rapidement.

Pour terminer quelques lignes de commande très utiles pour debugger quand ça ne marche pas, parce que oui, quand je lis les tutos personne n’explique jamais quoi faire si leur tuto ne s’adapte pas bien :

Pour tester si votre machine à accès à internet et si un carte réseau fonctionne:

ping -I vmbr0 www.google.fr

Pour avoir votre table de routage (et la gateway actuelle) :

route -n

Si vous modifié (sous ubuntu server) la configuration réseau, pour le redémarrer :

sudo netplan apply

Ce n’est pas grand chose mais quand on ne connait pas (comme moi) c’est bien pratique.

Faire un backup automatique de son wordpress sur son disque dur via FTP

Ce blog est hébergé chez OVH, sous debian 9. Par défaut les plugins de backup de wordpress (j’ai pré-sélectionné UpdraftPlus et BackWPup) me proposent de faire une sauvegarde sur disque. Bien sûr je pourrais louer de l’espace sur un cloud pour une somme modique (par exemple amazon S3 ça ferait du 24€ par mois pour 1To) mais je n’aime pas trop cette idée car je ne vais pas maîtriser mes coûts à priori. Donc, pourquoi ne pas utiliser un espace dédié sur un disque dur de ma machine, sur laquelle je vais installer un protocole qui permet le transfert de fichier ?

Dans cette série nous allons voir comment installer ce bon vieux FTP, puis comment le sécuriser avec FTPS et comment installer un S3 plus moderne avec minio. On démarre avec FTP (inspiré de ce tuto).

Attention, cette méthode n’est pas sécurisée.

Nous verrons plus tard comment faire du FTPS (FTP encrypté).

Tout dabor on va installer vsftpd et un user « remotebackup », dont on va changer le répertoire « home » pour ‘/media/yba/Working/FTP’ (le répertoire dédié aux backups sur mon disque dur « Working »):

sudo apt-get install vsftpd
sudo useradd -m remotebackup
sudo passwd remotebackup
sudo usermod -d /media/yba/Working/FTP remotebackup

Ensuite on va configurer vsftd en modifiant le fichier /etc/vsftpd.conf. On refuse les connections anonymes (il faudra donc un user/mdp pour se connecter, et on arrive directement dans le répertoire local du user) :

listen=YES
listen_ipv6=NO
connect_from_port_20=YES

anonymous_enable=NO
local_enable=YES
write_enable=YES
chroot_local_user=YES
allow_writeable_chroot=YES
secure_chroot_dir=/var/run/vsftpd/empty

pam_service_name=vsftpd

userlist_enable=YES
userlist_file=/etc/vsftpd.userlist
userlist_deny=NO

Comme on précise que la liste des users est dans le fichier /etc/vsftd.userlist il va falloir remplir ce fichier avec :

remotebackup

Enfin on lance le démon :

sudo service vsftd restart

Si votre disque dur est en FAT32 (le mien est formaté en ext4) alors pour que remotebackup puisse en faire sa home il vous faut faire dans /etc/fstab :

/dev/sdc1       /media/yba/HDD  vfat    user,umask=0000   0   0

Notez que vous devez ouvrir le port 21 et 20. Si vous êtes dans mon cas, ça se fait dans le panneau de configuration de la freebox. Sinon avec ufw :

sudo ufw allow 21/tcp
sudo ufw allow 01/tcp

Pour aller plus loin avec ufw (par exemple pour n’ouvrir qu’un port pour une seule appli ou une seule machine) je vous conseille ce tuto.

Kit autonomie forestière électrique

Si comme moi vous faîtes votre propre bois, vous serez peut-être intéressé par ce « kit forestier ». Il suppose que vous ayez des panneaux solaire car il est tout électrique, et l’idée est fonctionner (le plus possible) hors réseau, en autonomie donc.

Du coup vous n’allez pas abattre des forêts entière avec, on est d’accord. L’intérêt ici c’est que ce kit fonctionne qu’il y ai de l’essence à portée de main ou pas. Il émet moins de CO2 (du moins sur le lieu d’utilisation, il faudrait vérifier en terme d’énergie grise). Pour une cabane ou une petite maison bien isolée ça devrait suffire.

J’ai choisis les outils de la game « greenwork tools » que j’utilises depuis plusieurs mois et dont je suis content. J’ai pris ces outils car, du moins à l’époque ou j’avais regardé, greenworks étaient les seuls à proposer une tronçonneuse 40cm, qui en plus existe en version filaire (pour économiser les batteries en finissant de tronçonner à la maison). Les pièces étant interchangeables.

L’avantage c’est qu’on peut utiliser les même batteries pour tous les outils. Il existe d’autres gammes possible bien sûr, mais l’ensemble que je vous proposes est cohérent.

  • L’outil de départ, c’est bien sûr la tronçonneuse. Auquel il faut rajouter les outils de protection, casque (du coup la protection des oreilles est inutile avec les outils électriques, mais, il vaut mieux avoir un casque qui fait les deux si jamais vous manipulez des outils thermiques), gants et combinaison anti-coupure et l’affûteuse ainsi qu’une chaîne de remplacement et de l’huile.
  • Ensuite il faut de quoi accéder au bois, d’ou la débroussailleuse, et l’élagueuse qui permet de couper les branches et de prendre moins de risque quand on coupe le tronc.  La débroussailleuse peut faire aussi coupe bordure, mais attention à prendre des fils biodégradable sinon (et c’est un vrai problème) vous allez saturer votre forêt ou jardin de micro-plastiques (déjà qu’il y en a suffisamment qui tombent du ciel !)
  • Pour les plus experts il est possible de se faire un kit « escalade » à base de matériel de spéléo pour monter dans les arbres et les ébrancher de haut en bas. Avec un baudrier, deux pieds ascenseur gauche et droit (bien plus pratique que la méthode classique avec un pied et une main). Attention à ne pas prendre de la corde d’escalade qui est plus élastique que celle de spéléo mais qui n’est pas faite pour monter du coup.
  • Il faut un palan (ou tirefort) pour tirer l’arbre dans le bon sens si éventuellement sa chute pose problème ou alors un treuil électrique pour débarder. A l’époque j’avais pris un Timbertech dont je suis content.
  • La brouette électrique pour ramener le bois. On est d’accord ça ne remplace pas un bon vieux chenillard thermique, mais l’idée ici c’est qu’une fois qu’on a des batteries (certes elles ont une durée de vie limitée, mais la technologie s’améliore) on est autonome, plus besoin de dépendre du pétrole. Je dois préciser que la brouette greenwork tools à un gros défaut, elle n’a pas de frein. Pour ma part, je ne l’ai pas achetée car je me suis bricolé ma propre brouette électrique à base d’une roue électrique achetée sur AliExpress qui fonctionne en 48V avec la batterie de mon vélo électrique. Il y a d’autres brouettes sur amazon que je trouve meilleures, je ferais une revue plus tard. Mais la brouette que je vous proposes dans ce kit fonctionne avec les même batteries que les autres outils.
  • J’ai rajouté un fendoir électrique que personnellement je trouve très pratique car je n’ai pas d’endroit ou utiliser un merlin classique (qui fait très bien le job sinon). 

(si vous n’avez pas accès au widget, voici l’équivalent en liste amazon)

Kit autonomie off-grid solo

Je vous proposes de découvrir mon kit autonomie solo. Il est constitué des outils les plus polyvalents possible afin d’assurer les besoins de base : eau, électricité, communication, défense, chauffage. Vous pouvez l’utilisez de façon nomade, ou bien comme complément/assurance en appartement.

Les outils phare du kit :

  • La jerrican Lifesaver, outil de collecte, purification et mise sous pression de l’eau tout en un.
  • La mini centrale électrique Poweroak qui peut être chargée au solaire ou autre, et qui fournit du 220V ou de l’USB en sortie. Pour les panneaux solaires il y a un modèle 100W qui est intéressant mais je préfère le 200W pliant, parce que dans la pratique si l’ensoleillement ou l’angle du panneaux n’est pas parfait vous n’aurez pas les 200W plutôt 100 …
  • Le hamac-tente-moustiquaire Lawson
  • Le réchaud Biolite campstove 2 et le kettelpot pour chauffer la nourriture ou l’eau et qui dispose d’un petit générateur USB d’appoint (pour recharger les lampes ou talkie quand il n’y a pas de soleil)
  • Le Letherman Surge, multitool qui a fait ses preuves.

Plus un EDC (Every day carry) :

Plus quelques accessoires et surprises que je vous laisse découvrir.

(l’affichage du widget ne passe pas bien firefox, à défaut voici ma liste amazon équivalente)

Retex en tant que CTO d’une startup

Article posté initialement sur le forum pragmatic entrepreneur. (La rédaction a été améliorée)

J’ai été CTO pour une startup Web. Du point de vue technique c’est très enrichissant : on est seul chef à bord, il faut tout gérer donc on touche à tous les domaines du web au lieu d’être confiné à une certaine expertise.

Pour ma part, j’en ai profité pour enfin pouvoir travailler proprement avec un process qualité que je n’avais jamais pu mettre en place dans les sociétés de services (tests automatisés, politique de déploiement propre, backups, etc…).
Je ne jette pas la pierre sur les SSII, elles ne font pas le même job : souvent ce sont des sites jetables, à bas coûts, alors que dans mon cas je savais que le site allait de voir vivre et évoluer plusieurs années.

D’autre part, alors qu’en SSII je devenais de plus en plus  » spécialiste de la spécialité », c’est à dire confiné à un domaine d’expertise de plus en plus restreint, en tant que CTO j’ai pu m’occuper de tous les aspects d’un site web (SEO, backup, perf, versionning, deploiment, monitoring, marketing, architecture, hébergement, ABtesting, sécurité, etc…). En plus j’ai eu la chance de participer à une ICO (sur la blockchain ethereum) qui a réussie.

Mais …
C’est donc un boulot exigeant, il faut être sur tous les fronts, et bien souvent, à n’importe quelle heure, notamment pour gérer les urgences. Ensuite on est mal payé, c’est un fait. Si la startup décolle c’est intéressant c’est sûr, mais il y a beaucoup d’échecs et contrairement aux investisseurs qui répartissent le risque sur plusieurs entreprises, le CTO en général n’en gère qu’une. Si ça « plante », il a gagné de l’expérience, mais c’est à peu près tout.

Après, de l’expérience justement, je pense qu’il faut quand même beaucoup en préalable pour gérer une startup en tant que CTO : il faut être capable de faire les bons choix, ça va de soi, mais ce n’est pas tout, il faut être capable de les expliquer. Parce que souvent, ce qui se passe c’est que les bons choix coûtent plus cher à court terme, mais moins cher à long terme et une startup c’est parfois de la survie au jour le jour. Il faut savoir gérer les demandes du “patron” qui n’est pas toujours au fait des pratiques et de la culture du web, et il faut gérer les autres développeurs, plus ou moins compétents, plus ou moins motivés, dans un environnement ultra-compétitif.

Les outils professionnel open-source de collaboration (pour entreprise ou association)

Voici une liste des outils que j’utilise dans mon activité professionnelle ou associative et dont je suis pleinement satisfait. Tout est open-source. Leur installation peut donc se faire « gratuitement » si vous avez les compétences. Et on peut les utiliser en SaaS.

Mais avant de vous donner « ma » liste, il me faut parler du « pourquoi » utiliser ces outils. C’est une question de philosophie et de politique plus que de technique, parce que, c’est vrai, il faut le dire, les outils « classiques » font le job.

D’abord, il y a la propriétés des données. Pour une entreprise, il me semble important que ces dernières restent en son sein et n’aillent pas se balader chez d’autres. Pour une question de risques, mais aussi d’espionnage industriel (oui ça existe).

Ensuite il y a le fait de « partager », ne serait-ce que des remontées de bugs avec les communautés open-source.

L’inconvénient c’est qu’il y a un surcroît de travail, c’est vrai. Il faut installer ces outils, les mettre à jour, faire des backups, etc… Et on n’a pas toujours le temps de s’en occuper, et d’ailleurs, on ne sait pas toujours quels outils en valent le coup, alors, on fait comme les voisins !

  • Gitlab : github est très bien pour se donner une visibilité publique. J’utilise gitlab pour les projets privés. L’avantage c’est d’avoir une plus grande intégration avec les outils du système d’information de l’entreprise. Les outils de workflow, l’intégration continue, la gestion des merge request et des issues avec le dashboard étaient utilisés par mon équipe quotidiennement et nous rendaient de vrais services.
  • Mattermost : outil que j’ai découvert récemment, il remplace avantageusement slack. Les conversations vidéos en 1to1 sont possible via zoom (qui n’est pas open-source) d’entrée de jeu, mais il semble possible de configurer un WebRTC pour le remplacer (je n’ai pas encore eu le temps de le faire). Les webhook (ingoing et outgoing sont bien présent) et permettent d’avoir des alertes comme avec slack. Le gros point fort de mattermost, ce sont les Team qui sont très bien intégrées à l’outil et plus conviviales que dans slack.
  • Drupal / WordPress : on ne les présentes plus. Pour les projets pro, Drupal permet de construire toutes sortes de sites d’envergure. Pour les blogs perso ou associatifs, wordpress est le meilleur.
  • TICK : c’est un acronyme pour une suite d’outils de monitoring : Telegram (un démon qui récolte et envoies les données), InfluxDB (une base de donnée de série temporelles), Chronograf (un outil de visualisation de séries temporelles), Kapacitor (un outil de levée d’alertes en fonction de seuil). Cet outil permet de monitorer un ensemble de serveurs de manière fine et permet de détecter des problèmes en amont (comme par exemple, un disque dur qui se remplit, ce qui peut provoquer des erreurs très bizarres qu’on ne comprends pas de premier abord), ou des problèmes de performance. Je l’avais préféré aux outils traditionnels de monitoring comme Munin ou Nagios (ou tant d’autres) pour sa simplicité et sa capacité à mettre en graphes.
  • Piwik/Matomo : le google analytics open-source, qui disposes des fonctionnalités de base pour analyser le trafic mais aussi d’outils plus avancés comme les funnels et bien d’autres dans ses plugins.
  • Docker : alors que vagrant est très bon pour monter des machines virtuelles, docker est tout de même plus puissant avec ses images et conteneurs, la possibilité de versionner facilement ses environnements. Il est aussi moins gourmand et on peut donc l’utiliser pour déployer simplement des environnement de dev « iso » d’un serveur à l’autre.
  • Ansible : bien que n’ayant encore que peu d’expérience avec Ansible, je le rajoute à ma boite à outil pour gérer les déploiements de manière industrialisée (les classiques : site de dev, préprod, prod notamment).
  • Loomio : outil de prise de décision collective, qui permet de voter sur des sujets et qui est utilisé par certains mouvements politiques. J’y vois deux gros intérêts. D’abord le vote de base dispose de quatre boutons « favorable, abstention,défavorable,contre ». Je trouve ça très malin d’avoir introduit la nuance entre défavorable/contre. D’autre part il y a aussi un outil de prise de RDV un peu comme Doodle.
  • KeepassX : indispensable pour gérer ses mots de passe perso ou pro sans avoir à faire tourner 2 ou 3 mot de passes qu’on a mémorisé. De plus, couplé avec Nextcloud, il permet de partager un fichier de mot de passes admin et évite les trous de sécurités classique des mots de passes qui transitent par des canaux moins sécurisés.
  • Behat : j’exagère à peine en disant que Behat m’a sauvé la vie. C’est un outil de test de sites web automatisé (par script ecrits en « langage naturel » ou presque). Je l’ai utilisé principalement pour des sites Drupal et je n’ai pas creusé pour savoir s’il existe en version nodejs ou pour WordPress. Mais, un outil de test sémantique me semble indispensable pour mener à bien un projet web sur le long terme et avoir confiance dans son produit. De plus, Behat permet d’avoir des specs « utiles » et à jour puisqu’elles servent à lancer les tests. Behat s’insère très bien dans le workflow d’intégration continue de gitlab.
  • Nextcloud : il arrive en dernier mais c’est un poids lourd. Au départ, je ne l’ai installé qu’en tant que remplaçant de Dropbox. Mais il est tellement riche de fonctionnalités qu’on peut lui trouver bien plus d’utilités. Par exemple, la première extension que j’ai rajoutée est « Collabora », une version de google-docs issue de Libre-office.
  • Pencil : pour créer les wireframes, les mockups, les maquettes de votre site avant de faire appel à un graphiste.
  • Pour les performances et la scalabilité : xdebug, locust.io, xhprof, xhgui
  • backstopjs : pour les tests de régression visuel

Drupal 7 et ethereum, un hello world avec web3.js et parity : acheter un token

A supposer que vous soyez déjà enregistré en tant que user drupal reconnu sur la blockchain et que vous ayez déployé un contrat Token ERC20, nous allons voir comment faire pour :

  • afficher votre solde de token
  • acheter un token avec vos ethereums
  • modifier votre solde dès qu’ils change
  • valider la transaction automatiquement (2 méthodes)

Le code complet (et testé) est disponible sur github. Je ne reviens pas sur les principes de base : Drupal.settings, connexion à la blockchain, création de l’instance du contract dans JS. On va se concentrer sur les manipulations que l’on fait sur le contrat, en javascript, via web3.js.

Afficher votre solde en temps réel

A supposer que vous ayez initialisé votre contrat, afficher le solde en token de votre compte se fait avec un simple .call :

token_contract.methods.balanceOf(clientAddress).call().then(function(result){$("#client-token").html(result);});

Mais cet affichage sera fait une seule fois au chargement de la page. Hors, la blockchain peut enregistrer des transactions depuis n’importe quel client. Le solde peut donc changer à tout moment. Comment faire pour que votre soit mise à jour en temps réel le cas échéant ? Si votre smartcontract l’a prévu, vous pouvez utiliser les évènements à cet effet :

event Transfer(address indexed from, address indexed to, uint256 value);

    function _transfer(address _from, address _to, uint _value) internal {
        // Prevent transfer to 0x0 address. Use burn() instead
        require(_to != 0x0);
        // Check if the sender has enough
        require(balanceOf[_from] >= _value);
        // Check for overflows
        require(balanceOf[_to] + _value > balanceOf[_to]);
        // Save this for an assertion in the future
        uint previousBalances = balanceOf[_from] + balanceOf[_to];
        // Subtract from the sender
        balanceOf[_from] -= _value;
        // Add the same to the recipient
        balanceOf[_to] += _value;
        Transfer(_from, _to, _value);
        // Asserts are used to use static analysis to find bugs in your code. They should never fail
        assert(balanceOf[_from] + balanceOf[_to] == previousBalances);
}

Le smartcontract définit un évènement Transfer qui est appelé dans la fonction interne _transfer. Il déclenche un appel qui peut être intercepté coté javascript à l’aide du code suivant :

token_contract.events.Transfer().on('data', function(event){
  token_contract.methods.balanceOf(clientAddress).call().then(function(result){$("#client-token").html(result);});
});

On retrouve le même code d’affichage du solde, mais en tant résultat d’une promise d’un .events.Transfer().on('data', {}) . Sauf que si vous vous arrêtez la, vous allez avoir un petit problème. Le HttpProvider que l’on utilises pour web3 ne supporte pas les évènements. Il faut utiliser le WebsocketProvider :

window.web3 = new Web3(new Web3.providers.WebsocketProvider(fallback));

Et pour que ça fonctionne on doit saisir une url du style : ws://localhost:8546. Le port n’est plus 8545 mais 8546 et le http transformé en ws. A partir de ce moment la, votre appel d’évènement fonctionnera et le solde sera mis à jour en temps réel.

Acheter des tokens

L’achat de Token doit être prévu dans votre smartcontract. C’est une simple fonction qui augmente votre solde à une nuance près, elle est « payable » :

    function buy() payable public {
        uint amount = (msg.value / buyPrice) * unit;      // calculates the amount
        _transfer(this, msg.sender, amount);              // makes the transfers
}

L’appel se fait donc simplement par un .send :

token_contract.methods.buy().send({from:clientAddress, value:web3.utils.toWei(0.001, "ether")})

Notez que dans le cas présent (un token ERC20 donc) le solde n’est pas simplement augmenté, mais un transfert est réalisé depuis le contrat lui même vers le user. C’est la signification du _transfer(this, Ce qui peut sembler étrange au premier abord, mais qui permet en fait de fixer le nombre de Token à la création (selon la même astuce déflationniste utilisée par bitcoin qui consiste à ne créer que 21 millions de bitcoin au maxium). En fonction du prix d’achat du token et du taux de conversion, vos ethers seront convertis en token.

Valider la transaction via parity

Avec le code tel qu’il est présenté précédemment, l’utilisateur devra obligatoirement basculer sur son wallet (Mist, Partiy, Jaxx, MetaMask ou autre) pour valider la transaction en saisissant son mot de passe. Pour un utilisateur expérimenté habitué à ethereum c’est acceptable, mais pour le grand public, il y a un risque que l’utilisateur attende sans comprendre qu’il doit valider sa transaction « de l’autre coté ». Mais nous pouvons l’aider. Il y a un 2ème cas ou la validation de transaction peut-être intéressante : pour les développeurs. C’est en effet pénible de devoir aller valider à chaque fois qu’on teste. Enfin, c’est carrément rédhibitoire pour les tests automatisés. Dans tous ces cas, il peut être utile de savoir comment faire.

La première méthode consiste à appeler parity nous même pour lui dire de valider la dernière transaction de son pipe. Ce n’est pas la méthode la plus sûre dans la mesure ou quelqu’un d’autre pourrait avoir rajouté une transaction dans l’intervalle. Mais ça peut suffire (notamment pour les développeurs).

Nous allons utiliser l’API JsonRPC de parity, qui s’utilise comme un appel AJAX. Cela se fait en 2 étapes, d’abord signer_requestsToConfirm pour récupérer les transactions en attente de signature, puis, nous allons demander à l’utilisateur son mot de passe et l’envoyer à parity avec un signer_confirmRequest.  Ce qui nous donne le code suivant :

        autoSign = function() {
          $.ajax({
            type:"POST",  url: fallback, Accept : "application/json", contentType: "application/json",  dataType: "json",
            data: JSON.stringify({"method":"signer_requestsToConfirm","params":[],"id":1,"jsonrpc":"2.0"}),
            success: function(result) { 
              if (result.result == []) alter('Could not sign');
              if (result.result[0] == undefined) alter('Could not sign');
              id = result.result[0].id;
              pass = $('#eth-password').val();
              $.ajax({
                type:"POST", url: fallback, Accept : "application/json", contentType: "application/json", dataType: "json",
                data: JSON.stringify({"method":"signer_confirmRequest","params":[id, {}, pass],"id":1,"jsonrpc":"2.0"}),
                success: function(result) { alert('transaction validated automatically'); }
              });
            }
          });
}

Mais ce n’est pas une solution entièrement satisfaisante dans la mesure ou : on passe par parity. Que faire si on souhaite utiliser geth ? Et s’il y a d’autres transactions dans le pipe ? Sans parler du fait qu’il est difficile de savoir « quand » faire l’appel vers parity (il faut lui laisser le temps de créer sa transaction). Vous aurez remarqué dans le code que l’appel se fait après un delai :

            setTimeout(function() {autoSign();}, 1000);

Nous allons maintenant voir comment signer la transaction proprement avec eth3. Mais c’est un peu plus compliqué (et surtout non documenté à l’heure ou j’écris).

Valider la transaction avec web3

Sans plus tarder voici le code « propre » d’un appel de transaction signé dans web3 :

        autoSignWeb3 = function (pass, onreceipt) {
          var walletContractAddress = Drupal.settings.blockchain.token_deployed_contract_address_fallback;
          var privateKey = new buffer.Buffer(pass, 'hex');
          var fromAccount = clientAddress;
          var signature = _.find(JSON.parse(Drupal.settings.blockchain.token_deployed_contract_ABI), { name: 'buy' });
          var payloadData = web3.eth.abi.encodeFunctionCall(signature, []);
          gasPrice = web3.eth.gasPrice;
          gasPriceHex = web3.utils.toHex(gasPrice);
          gasLimitHex = web3.utils.toHex(300000);
          web3.eth.getTransactionCount(fromAccount).then((nonce) => {
            nonceHex = web3.utils.toHex(nonce);
            var rawTx = {
              nonce: nonceHex,
              gasPrice: gasPriceHex,
              gasLimit: gasLimitHex,
              to: walletContractAddress,
              from: fromAccount,
              value: web3.utils.toHex(web3.utils.toWei(0.001, "ether")),
              data: payloadData
              };
            var tx = new ethereumjs.Tx(rawTx);
            tx.sign(privateKey);
            var serializedTx = tx.serialize();
            web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex')).then( () =>{ onreceipt(); return null; } );
            return null;
          });
}

Si vous regardez la documentation de web3, vous trouverez facilement cette fonction web3.eth.sendSignedTransaction qui permet d’envoyer des transactions signées. Mais le problème c’est qu’on doit envoyer une chaine représentant la transaction. Il faut donc construire cette chaine. On est loin du .methods.xxx.send() plutôt naturel. Construire une sorte de code hexadécimal pour faire appel … ce n’est pas si simple, surtout que les exemples sur le net concernent des versions différentes ou plus anciennes de web3.js.

Pour y parvenir je me suis inspiré du code trouvé ici. Sauf que sendRawTransaction est remplacé par sendSignedTransaction, mais surtout :

var solidityFunction = new SolidityFunction('', _.find(ABI, { name: 'sendCoin' }), '');
var payloadData = solidityFunction.toPayload([toAccount, 3]).data;

est remplacé par

 var payloadData = web3.eth.abi.encodeFunctionCall(signature, [toAccount, 1]);

C’est cette partie qui fût la plus compliquée à trouver dans le dédalle du net. On doit faire appel à 3 libraires js externes :

  • ehtereumjs-tx qui sert à sérialiser la transaction.
  • buffer qui sert à envoyer la clé à la transaction
  • lodash qui sert à récupérer la signature de la méthode qu’on veut appeler dans l’ABI

De plus, l’utilisateur ne doit pas saisir le mot de passe de son wallet parity mais sa clé privée. On pourrait tout à fait crypter sa clé à l’aide d’un mot de passe et ne lui demander que ce mot de passe (au final, c’est ce que fait parity), mais ça dépasse le cadre de cet article.

Drupal 7 et ethereum, un hello world avec web3.js et parity : s’enregistrer sur la blockchain

Cet article suppose que vous connaissez déjà la blockchain (BC). C’est une sorte de base de donnée (coûteuse) infalsifiable, avec, dans le cas d’ethereum (ETH) la possibilité d’exécuter des bouts de code certifiés (dans un langage de programmation proche du javascript : solidity). Si ces notions ne vous sont pas familières, passez votre chemin.

La BC est une technologie en pleine évolution (et en plein boom). Il y aurait beaucoup à dire, mais dans cet article, on va se limiter à ropsten, parity, web3.js 1.0 et … drupal (version 7 – oui c’est un peu vieillot) et comment coder un « hello world » en Drupal pour se connecter à la BC.

Nous allons réaliser 2 choses :

  • connexion d’un user drupal avec un compte de la BC
  • achat d’un token ERC20, avec validation automatique de la transaction (ce qui permet de se passer d’un wallet)

Dans ce premier article, on va déjà s’intéresser au premier cas.

Environnement de développement

A supposer que vous ayez un site Drupal quelconque. Par exemple, un que vous avez installé à l’aide d’ansible. Il va vous falloir en plus le module user_hash, et un champ « ethereum_address » de type string en plus sur votre profil utilisateur. Pour faire fonctionner la BC, pour les développeurs, il est conseillé d’utiliser parity (1.8.3 à l’heure ou j’écris). Une fois installé, vous lancez parity comme ceci :
parity ui --chain=dev --unsafe-expose --jsonrpc-apis=all
Cette ligne de commande permet d’avoir un parity « de dev » qui tourne sans aucune restriction. Quand on démarre et qu’on est dans une VM, j’estime que ce n’est pas encore l’heure de ses pré-occuper de la sécurité. Chaque chose en son temps. La, ce qu’on veut c’est voir la bête tourner.

Si vous souhaitez accéder à l’interface en ligne de parity (et je vous le conseille) : http://192.168.50.5:8180/#/accounts/

192.168.50.5 est l’IP fixe privée que j’ai donné à la VM vagrant (sinon, ça sera probablement sur localhost) :
config.vm.network "private_network", ip: "192.168.50.5
Vous devez obtenir quelque chose comme ceci (ce n’est pas un tuto parity, je n’irais donc pas plus loin à ce sujet) :

A partir de la, il vous faudra créer un compte pour chaque user drupal que vous souhaitez connecter à la BC.

Cette instance de parity fonctionne avec une blockchain de dev, locale, légère, rapide, mais une fois que votre application fonctionnera, il vous faudra basculer sur une version publique de test d’ethereum: par exemple ropsten. L’avantage de parity c’est qu’il le permet très simplement, et qu’il fait aussi office de wallet (porte-feuille ethereum) et aussi il dispose d’outils pour les développeurs (notamment pour les contrats). Notez (mais ce n’est pas le sujet de cet article) que Mist (un autre wallet ethereum) dispose d’une debugger de smart-contract assez bluffant (si jamais vous avez besoin d’en arriver la). Mais en dehors de ce cas, parity fait tout à fait l’affaire.

Préparation des données

[Attention, ce n’est pas un tuto drupal, donc je passe rapidement sur les étapes principales du code Drupal]

On peut communiquer avec le BC de différentes manières possibles, mais le plus simple et le plus répandu c’est d’utiliser la librairie web3.js en javascript, pour se connecter au noeud parity. On pourrait se connecter au noeud en PHP (à l’aide d’ethereum-php mais ce n’est pas une librairie officielle et le coté asynchrone des appels à la BC sont plus facile à gérer en JS).

Récupérez une web3.min.js. Qu’on va ensuite injecter avec un :

drupal_add_js(drupal_get_path('module', 'hellothereum') . '/js/web3.min.js', array('scope' => 'footer'));

De l’autre coté, il va nous falloir un champ pour accueillir l’adresse ethereum du user (à la rigueur on pourrait aussi se contenter de la rechercher le hash du user dans la blockchain pour récupérer son adresse. Il faudrait réfléchir aux conséquences en terme de performance et de sécurité des 2 choix, mais ça dépasse le cadre de ce simple article qui vise à découvrir les fonctions de base).

Cette partie se fait avec un field_create_field et un field_create_instance dans le hook_enable :

  if (!field_info_field('field_ethaddress')) {
    $field = array(
        'field_name' => 'field_ethaddress', 
        'type' => 'text', 
    );
    field_create_field($field);

    // Create the instance on the bundle.
    $instance = array(
        'field_name' => 'field_ethaddress', 
        'entity_type' => 'user', 
        'label' => 'Ethereum address', 
        'bundle' => 'user', 
        'settings' => array(
           // Here you inform either or not you want this field showing up on the registration form.
            'user_register_form' => 1,
        ),
        'widget' => array(
            'type' => 'textfield',
            'weight' => '1',
        ), 
    );
    field_create_instance($instance);
}

Avec le module user_hash, et le champ ethaddress, on est équipé pour aller taquiner la blockchain. Il ne nous reste plus qu’à envoyer les informations au javascript qui va passer par Web3 pour appeler les smartcontract. Les adresses des smartcontract et leur ABI sont stockés dans la table variable et envoyées au JS :

    drupal_add_js(array(
      'ethereum_user' => array(
        'contract' => array(
          'address' => variable_get('ethereum_user_register_drupal_deployed_contract_address'),
          'abi' => variable_get('ethereum_user_register_drupal_deployed_contract_abi'),
        ),
        'fallback_node' => variable_get('ethereum_user_register_drupal_fallback_node'),
        'token' => variable_get('ethereum_user_registry_list_token'),
        'user' => array(
          'hash' => $this_user->hash,
          'address' => $this_user_ethereum_address,
        ),
      ),
    ), 'setting');

Appel des smart-contracts en JS

On récupère d’abord toutes les infos nécessaires envoyées par Drupal via Drupal.settings :

        window.web3 = new Web3(new Web3.providers.HttpProvider(Drupal.settings.ethereum_user.fallback_node));
        var user_address = Drupal.settings.ethereum_user.user.address.toLowerCase();
        var contract_abi = JSON.parse(Drupal.settings.ethereum_user.contract.abi);
        var contract_address = Drupal.settings.ethereum_user.contract.address;
        var contract = new web3.eth.Contract(contract_abi, contract_address);
        var user_hash = Drupal.settings.ethereum_user.user.hash;

A partir de la, on va pouvoir enfin faire notre premier appel de smart-contract. Attention, c’est du code asynchrone qui utilises des « promises » ou des « callbacks » (au choix). On n’écrit donc pas du code séquentiel (comme c’était le cas avec les premières versions de web3.js). Dans les versions précédentes de web3.js on faisait un appel de méthode classique. Cette fois il faut rajouter .methods avant l’appel et .call après. Ce qui donne :

contract.methods.validateUserByHash(user_hash).call({from: user_address}, function (error, result) {

Voyons le code du contrat correspondant :

  function validateUserByHash (bytes32 drupalUserHash) constant returns (address result){
      return _accounts[drupalUserHash];
}

Il est très simple. Le contrat dispose d’une table de correspondance, comme un tableau associatif en php, et il se contente de renvoyer une entrée à l’indice indiqué, si elle existe.

Comme vous le voyez, le contrat ne définit qu’un seul paramètre qu’on retrouve dans le .validateUserByHash(user_hash), le reste de l’appel, contient les paramètres « génériques » d’un appel de contrat : .call({from: user_address}, function (error, result) { qui est le user qui fait l’appel de contrat, et quelle est la callback à déclencher en retour. Dans le cas des transactions payantes on pourrait aussi ficher un prix.

Le traitement du résultat de l’appel se fait donc dans la fonction de callback. S’il n’y a pas eu d’erreur, c’est la qu’on peut vérifier si l’utilisateur est déjà enregistré et, si c’est pas le cas, lui proposer une transaction d’enregistrement :

contract.methods.newUser(user_hash).send({from: user_address})

Cette fois-ci c’est un .send à la place du .call, j’y reviens un peu plus loin. Voici le code solidity correspondant :

  function newUser(bytes32 drupalUserHash) public {

    if (_accounts[drupalUserHash] == msg.sender) {
      // Hash allready registered to address.
      accountCreated(msg.sender, drupalUserHash, 4);
    }
    else if (_accounts[drupalUserHash] > 0) {
      // Hash allready registered to different address.
      accountCreated(msg.sender, drupalUserHash, 3);
    }
     else if (drupalUserHash.length > 32) {
      // Hash too long
      accountCreated(msg.sender, drupalUserHash, 2);
    }
    else if (_registrationDisabled){
      // Registry is disabled because a newer version is available
      accountCreated(msg.sender, drupalUserHash, 1);
    }
    else {
      _accounts[drupalUserHash] = msg.sender;
      accountCreated(msg.sender, drupalUserHash, 0);
    }
}

Le code est très simple en soi si on enlève les vérifications qui sécurisent le contrat : _accounts[drupalUserHash] = msg.sender;. La dernière ligne est un évènement, ce sera traité dans les prochains articles.

Cette fois, par contre, on va utiliser les « promises » pour réagir à la transaction plutôt qu’une callback. Dans ce cas précis les promisses permettent d’avoir une action à différents moments. Notez une différence majeure par rapport au .call précédent : elle doit être validée par l’utilisateur, dans son wallet, puis être ensuite « minée », c’est à dire confirmée dans la blockchain. Typiquement, on est dans de l’asynchrone la. Notre javascript, ni notre site, ne s’arrêtent de vivre parce que la blockchain fait son boulot de son coté.

              .on('transactionHash', function (transactionHash) {
                $('#ethereum_user_registry_sign').html('<p>Please wait between 10 seconds and a few minutes for the transaction to be mined on the Ethereum network. You can reload this page at any time to see if the transaction is confirmed. Or you can <a href="https://etherscan.io/tx/' + transactionHash + '" target="_blank">see the transaction status in live</a>.</p>');
              })
              .on('error', function (error) {
                // 0 is "success error" in RegisterDrupal.sol
                if (error != 0) {
                  console.error;
                }
              })
              .on('confirmation', function (confirmationNumber, receipt) {
                // Should be 0 because there's no block after this transaction yet.
                console.log('Number of confirmation blocks: ' + confirmationNumber);
               })

On peut réagir à plusieurs évènements. TransactionHash : c’est quand la transaction est crée, c’est à dire, quand l’utilisateur valide la transaction dans son wallet. Sinon, on déboule sur une erreur. Si la transaction est validée, en principe, elle va finir en confirmation, sauf s’il y a une error déclenchée dans le smart-contract. Si votre smartcontract est bien fait, il va vous remonter une erreur explicite. Sinon … c’est la qu’on commence à pleurer ! Mais il y a une solution.

Si jamais vous être coincés et que ne comprenez pas pourquoi votre smartcontract ne renvoies pas le résultat attendu, vous pouvez lancer Mist qui contient un debugger de smart-contract, tout à fait bluffant. Ce n’est pas le sujet de cet article, je ne m’étendrais pas, mais sachez simplement qu’il permet de dérouler votre code comme film, y compris de faire du rewind et qu’il est vraiment très bien fait. C’est le même debugger que « Remix IDE« , mais qui tourne sur votre BC locale.

Voila, vous avez les bases. Il nous restera à voir dans les prochains articles : les transactions payantes, les évènements, l’auto-signature, pour faire un premier tout d’horizon plus complet des possibilités principales de la blockchain.

Drupal 8 et React : block dynamique ou headless

2 tuto en 1, nous allons voir comment coupler Drupal avec react.js en headless et … avec Drupal lui même (pour rajouter un bloc react temps-réel).

D’abord nous activons les modules RESTful Web Services et Serialization.

Ensuite nous allons créer une vue sur les derniers commentaires postés avec un display REST export (j’ai mis plain text dans le format de display des champs par soucis de simplicité) :

Nous allons maintenant nous appuyer dessus pour faire des appels du webservice JSON via React. Notez pour plus tard le chemin d’appel du webservice : api/v1/comments.

Drupal Headless avec React

En mode headless, Drupal n’est pas utilisé pour le front-office, seulement pour le back-office. Nous avons besoin de charger les bibliothèques react (on peut le faire en ligne) dans une premier temps et de faire l’appel au webservice dans une second temps :

<!DOCTYPE html>
<html>
<head>
 <meta charset="utf-8" />
 <title>App</title>
 <link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
 <link rel="stylesheet" type="text/css" href="stylesheets/style.css">
</head>

<body>

<div id="container">test</div>

<script src="https://npmcdn.com/react@15.3.1/dist/react.js"> </script>
<script src="https://npmcdn.com/react-dom@15.3.1/dist/react-dom.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.24/browser.min.js"></script>
<script type="text/babel" src="js/app.jsx"></script>

</body>
</html>

react.js et react-dom.js sont les libraries react à proprement parler. Mais en plus nous incluons axios qui permet de faire des appels de webservices en JSON. Quand à babel-core c’est le compiler qui transforme le JSX en javascript. Le JSX permet (entre autres) d’avoir des tags HTML directement dans le code JavaScript, ils sont convertis en React.createElement() directement.

Le fichier app.jsx :

class App extends React.Component {

constructor() {
 super();
 // Setting up initial state
 this.state = { 
   data: []
 }
}

// calling the componentDidMount() method after a component is rendered for the first time

componentDidMount() {
 var th = this;
 this.serverRequest = axios.get(this.props.source)
 .then(function(event) {
   th.setState({
     data: event.data
   });
 })
}

// calling the componentWillUnMount() method immediately before a component is unmounted from the DOM

componentWillUnmount() {
 this.serverRequest.abort();
}

render() {
 var titles = []
 this.state.data.forEach(item => {
   titles.push(<h3 className="events">{item.subject}</h3> );
 })
 return (
   <div className="container">
     <div className="row">
       <div className="col-md-6 col-md-offset-5">
         <h1 className="title">All Comments</h1>
         {titles}
       </div>
     </div>
   </div>
 );
 }
}

// rendering into the DOM
ReactDOM.render(
 <App source="http://test.box.local/drupal/web/api/v1/comments" />,
 document.getElementById('container')
);

Ce n’est pas un tuto react, je ne m’étendrais donc pas trop sur le sujet. Si vous voulez en savoir plus allez ici ou ici. La base de react c’est essentiellement son DOM virtuel et son moteur de rendu optimisé qui détecte les différences : ReactDOM.render et React.CreateElement (ici masqué dans du JSX), ainsi que la possibilité de créer ses propres « tags HTML » grâce au système de classe. C’est pour cela que <App .../> est compris par JSX comme un React.CreateElement('App', ...); est compris comme l’instanciation de la classe App (classe qui doit contenir une methode render et hériter de React.Component pour être reconnue par React).

C’est très basique comme application, mais ça fonctionne. Mais si l’on ne souhaite pas faire tout un front en react il est possible de l’utiliser pour améliorer l’ergonomie de Drupal en rajoutant React sur certains blocs pour les rendre « temps réel ». C’est ce que nous allons voir dans la 2ème partie de ce tutoriel.

Drupal real-time avec React

Tiré d’un tuto portugais dont l’objectif est de réaliser l’équivalent de la version en drupal 7.

Avec la console nous créons un module react_comment et un bloc ReactComments :

drupal generate:module --module="react_comment" --machine-name="react_comment" --module-path="/modules/custom" --description="React real time comments" --core="8.x" --package="Custom" --composer --learning --uri="http://default" --no-interaction

drupal generate:plugin:block --module="react_comment" --class="ReactComments" --label="React comments" --plugin-id="react_comments" --learning --uri="http://default" --no-interaction

Voici le fichier react_comment.libraries.yml qui permet d’inclure le javascript :

recent.comments:
 version: VERSION
 js:
   js/react-comments.js: {}
 dependencies: 
   - react_comment/reactjs

reactjs:
 version: VERSION
 js:
   js/react.min.js: {}

Note: il faut inclure une version de react.min.js dans le répertoire js en le téléchargeant à la main (moi je l’ai pris ici).

Voici le fichier src/Plugin/Block/ReactComments.php qui sert à inclure l’application react et à créer un div par défaut dans un block que l’application react pourra modifier à son gré :

<?php

namespace Drupal\react_comment\Plugin\Block, il n'y a que 2 lignes qui changent :

use Drupal\Core\Block\BlockBase;

/**
  * Provides a 'ReactComments' block.
  *
  * @Block(
  * id = "react_comments",
  * admin_label = @Translation("React comments"),
  * )
  */
class ReactComments extends BlockBase {
 
  /** 
    * {@inheritdoc}
    */
  public function build() {
    $build = [];
    $build['react_comments']['#markup'] = '<div id="recent-comments"></div>';
    $build['#attached']['library'][] = 'react_comment/recent.comments';
    return $build;
  }

}

Enfin la pièce maîtresse : le fichier js/react-comments.js :

/**
 * @file
 * Main JS file for react functionality.
 *
 */
 
(function ($) {
 
  Drupal.behaviors.react_blocks = {
    attach: function (context) {
 
      // A div with some text in it
      var CommentBox = React.createClass({displayName: 'CommentBox',
 
      loadCommentsFromServer: function() {
        $.ajax({
          url: this.props.url,
          dataType: 'json',
          success: function(data) {
            this.setState({data: data});
          }.bind(this),
          error: function(xhr, status, err) {
            console.error(this.props.url, status, err.toString());
          }.bind(this)
        });
      },
 
      getInitialState: function() {
        return {data: []};
      },
 
      componentDidMount: function() {
        this.loadCommentsFromServer();
        setInterval(this.loadCommentsFromServer, this.props.pollInterval);
      },
 
      render: function() {
          return (
            React.createElement("div", {className: "commentBox"},
              React.createElement("h3", null, React.createElement("b", null, "Check them out!")),
              React.createElement(CommentList, {data: this.state.data})
            )
          );
        }
      });
 
      var CommentList = React.createClass({displayName: 'CommentList',
        render: function() {
          var commentNodes = this.props.data.map(function (comment) {
            return (
              React.createElement(Comment, {name: comment.name, subject: comment.subject},
                comment.subject
              )
            );
          });
          return (
            React.createElement("div", {className: "commentList"},
              commentNodes
            )
          );
        }
      });
 
      var Comment = React.createClass({displayName: 'Comment',
        render: function() {
          return (
            React.createElement("div", {className: "comment"},
              React.createElement("h2", {className: "commentAuthor"},
                this.props.name
              ),
              this.props.subject
            )
          );
        }
      });
 
      // Render our reactComponent
      React.render(
        React.createElement(CommentBox, {url: "api/v1/comments", pollInterval: 2000}),
        document.getElementById('recent-comments')
      );
 
    }
  }
 
})(jQuery);

Ce fichier est écrit en JS et non en JSX, il est donc un peu plus lourd à lire.

Et voila le résultat :

Tester l’envoi d’emails avec behat pour Drupal

Behat ne permet pas à priori de tester l’envoi d’email, ni de vérifier leur contenu. Mais, il est possible de rajouter cette fonctionnalité.

1ère étape on va rajouter un système de collecte des emails dans une variable qu’on pourra ensuite interroger. Il y a peu de nettoyage à faire car seul le title, send-to, send-from, et body nous intéressent vraiment :

<?php 

/** 
 * A mail sending implementation that captures sent messages to a variable. 
 * 
 * This class is for running tests or for development. Inspired from TestingMailSystem
 */ 
class EMHMailSystem extends DefaultMailSystem implements MailSystemInterface { 

  /** 
   * Save an e-mail message to a file, using Drupal variables and default settings. 
   * 
   * @see http://php.net/manual/en/function.mail.php * @see drupal_mail() 
   * 
   * @param $message * A message array, as described in hook_mail_alter(). 
   * @return 
   * TRUE if the mail was successfully accepted, otherwise FALSE. 
   */ 
  public function mail(array $message) { 
    $output = $this->composeMessage($message);
    unset($message['params']['context']['state']);
    unset($message['params']['context']['action']);
    $captured_emails = variable_get('drupal_test_email_collector', array());
    $captured_emails[] = $message;
    variable_set('drupal_test_email_collector', $captured_emails);
    return TRUE;
  }

}

2ème étape on rajoute le support pour behat. Attention, la recherche de pattern dans les emails (afin qu’on ne remonte pas une chaine trouvée dans un email qui n’a rien à voir) se fait via l’utilisation d’un email actif qui permet de chercher dans un email qu’on identifié au préalable, et aussi par la recherche dans le dernier email envoyé. Enfin il est possible de vérifier qu’un email n’existe pas.

  /**
   * @Given /^the test email system is enabled$/
   */
  public function theTestEmailSystemIsEnabled() {
    // Store the original system to restore after the scenario.
    $this->originalMailSystem = variable_get('mail_system', array('default-system' => 'DefaultMailSystem'));
    // Set the test system.
    variable_set('mail_system', array('default-system' => 'EMHMailSystem'));
    // Flush the email buffer, allowing us to reuse this step definition to
    // clear existing mail.
    variable_set('drupal_test_email_collector', array());
    // Delete queue from other test, can be overloaded if All Experts used.
    db_query("DELETE FROM queue WHERE name='emh_request_request_email_notification'");
    db_query('TRUNCATE TABLE {mail_logger}');
  }

  /**
   * @Then /^the email to "([^"]*)" should contain "([^"]*)"$/
   */
  public function theEmailToShouldContain($to, $contents) {
    // We cannot use variable_get() because $conf is only fetched once per
    // scenario.
    $variables = array_map('unserialize', db_query("SELECT name, value FROM {variable} WHERE name = 'drupal_test_email_collector'")->fetchAllKeyed());
    $this->activeEmail = FALSE;
    foreach ($variables['drupal_test_email_collector'] as $message) {
      if ($message['to'] == $to) {
        $this->activeEmail = $message;
        if (strpos($message['body'], $contents) !== FALSE ||
          strpos($message['subject'], $contents) !== FALSE) {
          return TRUE;
        }
        throw new \Exception('Did not find expected content in message body or subject.');
      }
    }
    throw new \Exception(sprintf('Did not find expected message to %s', $to));
  }

  /**
   * @Then /^the last email to "([^"]*)" should contain "([^"]*)"$/
   */
  public function theLastEmailToShouldContain($to, $contents) {
    $variables = array_map('unserialize', db_query("SELECT name, value FROM {variable} WHERE name = 'drupal_test_email_collector'")->fetchAllKeyed());
    $this->activeEmail = FALSE;
    foreach (array_reverse($variables['drupal_test_email_collector']) as $message) {
      if ($message['to'] == $to) {
        $this->activeEmail = $message;
        if (strpos($message['body'], $contents) !== FALSE ||
          strpos($message['subject'], $contents) !== FALSE) {
          return TRUE;
        }
        throw new \Exception('Did not find expected content in message body or subject.');
      }
    }
    throw new \Exception(sprintf('Did not find expected message to %s', $to));
  }

  /**
   * @Then /^the last email to "([^"]*)" should not contain "([^"]*)"$/
   */
  public function theLastEmailToShouldNotContain($to, $contents) {
    $variables = array_map('unserialize', db_query("SELECT name, value FROM {variable} WHERE name = 'drupal_test_email_collector'")->fetchAllKeyed());
    $this->activeEmail = FALSE;
    foreach (array_reverse($variables['drupal_test_email_collector']) as $message) {
      if ($message['to'] == $to) {
        $this->activeEmail = $message;
        if (strpos($message['body'], $contents) == FALSE ||
          strpos($message['subject'], $contents) == FALSE) {
          return TRUE;
        }
        throw new \Exception('Found expected content in message body or subject.');
      }
    }
    // Dont care if not found any email at all.
  }

  /**
   * @Then /^there should be no email to "([^"]*)" containing "([^"]*)"$/
   */
  public function thereIsNoEmailToContaining($to, $contents) {
    $recipient = FALSE;
    $not_contains = FALSE;
    $variables = array_map('unserialize', db_query("SELECT name, value FROM {variable} WHERE name = 'drupal_test_email_collector'")->fetchAllKeyed());
    foreach ($variables['drupal_test_email_collector'] as $message) {
      if ($message['to'] == $to) {
        $recipient = TRUE;
        if (strpos($message['body'], $contents) == FALSE && strpos($message['subject'], $contents) == FALSE) {
          $not_contains = TRUE;
        }
      }
    }
    if (($recipient == TRUE && $not_contains == TRUE) || $recipient == FALSE) {
      return TRUE;
    }
    else {
      throw new \Exception('Found email and expected content in message body or subject.');
    }
  }

  /**
   * @Given /^the email should contain "([^"]*)"$/
   */
  public function theEmailShouldContain($contents) {
    if (!$this->activeEmail) {
      throw new \Exception('No active email');
    }
    $message = $this->activeEmail;
    if (strpos($message['body'], $contents) !== FALSE ||
      strpos($message['subject'], $contents) !== FALSE) {
      return TRUE;
    }
    throw new \Exception('Did not find expected content in message body or subject.');
  }

On peut enfin tester nos emails:

@api @watchdog
Feature: Contact
  In order to test the contact mail
  As an user
  I want to send a mail to contact 

  @email @nodelay
  Scenario: Test if the contact mail was sent
    Given the test email system is enabled
    When I visit '/contact'
      And I fill in "Bruce" for "firstname"
      And I fill in "Wayne" for "lastname"
      And I fill in "emh.test+batman@gmail.com" for "mail"
      And I fill in "Gotham City" for "message"
      And I press "Send"
    Then I should see the text "Your message has been sent."

    Then the last email to "contact@emh.com" should contain "(emh.test+batman@gmail.com)"
      And the email should contain "From - Bruce Wayne"