Utilisez Docker pour vos environnements de développement

Magazine
Marque
Hackable
Numéro
41
Mois de parution
mars 2022
Spécialité(s)


Résumé

Docker et les conteneurs logiciels font maintenant partie intégrante de la vie du sysadmin. Rappelons que les conteneurs permettent généralement d'isoler les processus, souvent des services réseau, afin de les détacher du système et de les faire s'exécuter dans des environnements distincts. Ainsi la ou les applications en question « vivent » dans leur petit monde, ignorant tout de la réalité du système faisant fonctionner le conteneur, mais aussi des autres applications dans leur propre environnement contenu. Cette mécanique peut être également très intéressante et avantageuse pour le développeur embarqué...


Body

Avant toute chose, précisons ici que nous parlons de GNU/Linux, et bien que les mécanismes de conteneurs et Docker existent dans d'autres systèmes, nous nous limiterons à cet environnement. La distribution n'a pas réellement d'importance, et ceci est valable également pour WSL, du moment que vous disposez d'une installation fonctionnelle de Docker (comprendre « que vous avez installé le paquet qui va bien »).

Je ne m'étendrai pas non plus sur les principes de fonctionnement, les différentes déclinaisons des architectures, l'orchestration ou tout autre contexte qui nous écarterait de notre sujet principal. L'objet de cet article est simplement de voir ce que Docker peut nous permettre de simplifier dans nos activités de développement et de construction de firmware.

À ce propos justement. Si vous êtes comme moi, à intervalles réguliers, vous êtes coincé entre deux options lorsque votre distribution (Debian en particulier, mais pas seulement) commence à prendre des rides. De plus en plus de paquets affichent des numéros de version qui paraissent venir d'un autre âge et de plus en plus d'outils, compilés localement, demandent des bibliothèques dont vous ne disposez pas ou plus matures que celles actuellement installées. Là, deux choix s'offrent à vous, un dist-upgrade à vos risques et périls, soit une réinstallation bien propre assortie d'une sympathique période de reconfiguration et de copies de fichiers. Ne nous voilons pas la face, j'utilise GNU/Linux depuis 25 ans et l'expérience m'a prouvé que, peu importe le choix qu'on fera, ceci n'est jamais exempt de problèmes, de frayeurs, de larmes et d'énervement en tous genres.

Mais voilà, il y a toujours un projet qui demande un CMake plus récent, une libc qui n'a pas 5 ans de retard ou un utilitaire que vous n'avez vraiment pas envie d'installer avec un horrible ./configure && make && make install qui ne ferait qu'empirer les choses. Idéalement, vous auriez besoin d'un système dans le système, facile à installer et à utiliser, qui ne repose pas sur une machine virtuelle pour des raisons de performance, mais assez souple pour vous permettre de jongler joyeusement ou d'essayer des choses sans rien casser. Ça tombe bien, parce que Docker permet exactement de faire cela !

Note : L'utilisation de conteneurs ne doit pas être confondue avec l'émulation ou la virtualisation. L'émulation consiste à simuler un matériel (comme une Pi sur un PC avec QEMU par exemple) pour y faire fonctionner tout un système. La virtualisation repose sur un partage de ressources matérielles entre plusieurs systèmes, mais le ou les systèmes virtualisés utilisent leur noyau et leurs pilotes. Avec un conteneur Docker, l'environnement exécuté n'a pas de noyau, mais uniquement un système de fichiers propre, séparé de celui de l'hôte faisant fonctionner Docker.

dock esp8266-s

Les devkits ESP8266 se programment en démarrant le microcontrôleur sur son bootloader qui utilise alors une liaison série pour dialoguer avec l'outil de programmation (esptool.py). L'accès au port série depuis l'environnement en conteneur est donc indispensable pour pouvoir développer correctement.

1. Construire son ou ses images

La logique derrière Docker est importante et la comprendre le plus rapidement possible vous permettra de ne pas perdre votre latin. Tout est une question de terminologie :

  • un Dockerfile est une description permettant de créer une image du système qui sera utilisée dans un conteneur ;
  • une image est une représentation du système tel qu'il prendra place dans un conteneur. Voyez cela comme un modèle, un template ou une classe qui sera instanciée ;
  • et un conteneur est une instance d'une image qui peut être créée, démarrée et stoppée.

En d'autres termes, vous utilisez un Dockerfile pour créer une image, mais chassez de votre esprit que cette image est le système, comme le serait une image d'un système de fichiers. C'est un simple point de départ et vous n'utiliserez pas directement l'image en vous servant du système qui s'exécutera dans le conteneur. Pourquoi faire aussi compliqué ? C'est simple, imaginez que vous construisiez le système qui vous convient, avec tous les éléments qui vous intéressent et même les configurations dont vous avez l'habitude. Mais vous avez la ferme intention d'utiliser cette base pour plus d'une expérimentation. Vous allez donc créer plusieurs conteneurs, utilisant tous cette même image comme point de départ, mais vivant leurs vies bien à eux. Un peu comme utiliser une bonne recette de cupcake, bien peaufinée et au point, et décliner le toping à loisir ensuite.

Exactement comme pour des cupcakes, vous allez réaliser votre appareil (le terme qui fait « pro » en pâtisserie pour désigner un mélange homogène de plusieurs ingrédients) à partir d'une recette. Avec Docker, cette recette, c'est le Dockerfile et comme en cuisine, ceci s'accompagne d'un contexte, en l'occurrence ce qui est mis à disposition sur le plan de travail, à savoir ce qu'on trouve dans le répertoire courant (c'est important, nous allons le voir dans un instant avec la copie de fichiers).

Ce Dockerfile (par défaut Dockerfile) se présente comme un simple fichier texte, mi-description, mi-script. Il doit commencer par l'instruction FROM permettant de préciser une image de base. Celle-ci peut être n'importe quelle image valide, mais sera généralement tirée du dépôt public (https://hub.docker.com/search?type=image). Pour cet exemple, nous utiliserons une image de base Debian Bullseye (Debian 11.1) :

FROM debian:bullseye

N'hésitez pas à visiter la page web du dépôt, on y trouve de tout, et rien ne vous empêchera d'utiliser une image OpenSuse dans un conteneur sur une Debian ou encore Ubuntu sur une Fedora. Il existe des images très basiques comme celle-ci avec un système de fichiers minimal, mais également d'autres orientées langages de programmation (Python, Go, Ruby, etc.), bases de données (MySQL, PostgreSQL, InfluxDB, etc.) ou encore frameworks applicatifs (Drupal, Gradle, Couchbase, .NET, etc.).

L'image Debian Bullseye nous fournit un environnement simple que nous pouvons personnaliser en exécutant une série de commandes, à commencer par une installation de paquets :

RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y \
    build-essential file g++ git libelf-dev \
    libncurses5-dev libncursesw5-dev libssl-dev \
    python python2.7-dev python3 python3-distutils \
    python3-setuptools python3-dev rsync time \
    unzip wget zlib1g-dev sudo mc bc vim screen \
    exuberant-ctags bash-completion

RUN est l'instruction permettant d'exécuter les commandes dans l'environnement et donc dans le « système » qui constituera l'image. Ceci n'est pas très différent de ce que vous feriez vous-même juste après l'installation d'une distribution, mais sera forcément spécifique à l'image de base. Ici, DEBIAN_FRONTEND nous permet de préciser à apt-get qu'il ne s'agit pas d'une session interactive et que tout devra se dérouler automatiquement. Ainsi après un petit update, nous spécifions simplement les paquets selon nos envies et préférences.

D'autres commandes peuvent (doivent) également être spécifiées. Pour créer un nouvel utilisateur par exemple :

RUN useradd denis -m -k /dev/null -d /home/denis \
    && echo "denis:mot2passe" | chpasswd \
    && adduser denis sudo \
    && adduser denis dialout \
    && adduser denis plugdev

Ceci créera denis, définira son mot de passe et le placera dans les groupes sudo (pour passer root) et dialout (pour l'accès aux /dev/TTYUSB* et /dev/ttyACM*) ainsi que plugdev pour accéder aux périphériques plug'n'play (mon Dieu, que ce terme fait vieux). Nous pouvons également ajuster la localisation :

RUN apt-get update && apt-get install -y locales \
    && rm -rf /var/lib/apt/lists/* \
    && localedef -i fr_FR -c -f UTF-8 -A \
    /usr/share/locale/locale.alias fr_FR.UTF-8 \
    && apt-get update
ENV LANG fr_FR.utf8

Nous aurons ainsi directement un système localisé avec des dates et un jeu de caractères adéquats (comprendre « français »). Notez l'utilisation de ENV permettant de définir une variable d'environnement (LANG) valable pour toutes les instructions suivantes dans le Dockerfile.

Nous basculons ensuite sur l'utilisateur défini précédemment pour toutes les autres instructions du fichier :

USER denis

Puis nous personnalisons son compte en copiant quelques fichiers avec COPY :

COPY --chown=denis:denis home/denis/.vimrc /home/denis
COPY --chown=denis:denis home/denis/.vim/ /home/denis/.vim/
COPY --chown=denis:denis home/denis/.inputrc /home/denis
COPY --chown=denis:denis home/denis/.bashrc /home/denis
COPY --chown=denis:denis home/denis/.screenrc /home/denis

Attention ! L'instruction COPY ne peut agir QUE sur le répertoire courant, qui est le fameux contexte de construction. Ceci signifie que home/denis/.vimrc n'est pas le .vimrc présent dans le répertoire personnel de l'utilisateur qui exécutera la construction, mais dans un sous-répertoire home/denis/ du répertoire courant. En d'autres termes, il convient de copier, dans le répertoire où vous créerez le Dockerfile, tous les éléments que vous désirerez placer dans l'image. Ceci est non seulement plus propre, mais permet également de ne pas se mélanger les pinceaux entre ses fichiers *rc de l'hôte qui exécute Docker et ceux à destination de l'image.

Enfin, une autre instruction pourra être utile :

WORKDIR /home/denis

Celle-ci a pour effet de définir, pour toutes les instructions qui suivent, le répertoire courant. J'ai pour habitude de laisser cette ligne dans mes Dockerfiles, même s'il n'y a rien ensuite. L'intérêt est de pouvoir ajouter des RUN pour ajouter des commandes qui devront être exécutées dans ce répertoire comme, typiquement, un git clone ou un téléchargement de fichiers.

Vous pouvez stocker tout cela dans un fichier Dockerfile dans le répertoire courant (idéalement prévu à cet effet), mais personnellement, je préfère nommer mes Dockerfiles en fonction de ce à quoi ils servent, ici simplement mydebian.dock puisque c'est une configuration très générique.

On pourra ensuite créer l'image avec :

$ docker build -t articledocker -f mydebian.dock .
Sending build context to Docker daemon 15.15MB
Step 1/12 : FROM debian:bullseye
bullseye: Pulling from library/debian
[...]
Step 11/12 : COPY home/denis/.screenrc /home/denis
---> 9b0482742f88
Step 12/12 : WORKDIR /home/denis
---> Running in c18182bc487d
Removing intermediate container c18182bc487d
---> 8d4751d0c8ce
Successfully built 8d4751d0c8ce
Successfully tagged articledocker:latest

Note : si docker vous refuse l'exécution sous prétexte que vous n'avez pas les permissions adéquates, placez simplement l'utilisateur dans le groupe docker, déconnectez-vous et reconnectez-vous. N'utilisez pas sudo, ce n'est ni nécessaire ni souhaitable.

Cette opération dure un certain temps, car l'image de base doit être téléchargée, les paquets installés et la configuration effectuée. Mais construire une autre image identique (ce qui présente peu d'intérêt), avec un autre nom (-t) ira beaucoup plus vite. docker build -t autre_image -f mydebian.dock . sera quasiment instantané, puisque le travail a déjà été fait. La commande docker build prend en argument un nom d'image, un nom de Dockerfile (-f) et un répertoire de construction, ici le répertoire courant (.).

Vous pouvez lister toutes les images construites sur votre système avec :

$ docker image ls
REPOSITORY     TAG       IMAGE ID      CREATED        SIZE
articledocker  latest    8d4751d0c8ce  4 minutes ago  791MB
autre_image    latest    8d4751d0c8ce  4 minutes ago  791MB
debian         bullseye  05d2318291e3  7 days ago     124MB

Remarquez que nous n'avons créé ici que deux images, articledocker et autre_image, mais que debian figure également dans la liste. De plus, nos deux images ont la même ID. Ceci vient du fait que Docker travaille avec des couches ou layers. Il n'est pas nécessaire de dupliquer l'ensemble des éléments des images. Les tailles données ne correspondent pas non plus à l'espace disque effectivement occupé, et c'est précisément là l'intérêt du système. debian a été téléchargée comme image de base et pourra être utilisée plus rapidement avec une déclinaison de notre Dockerfile. Ou supprimée avec docker rmi ou docker image rm suivie du nom de l'image, si vous le souhaitez.

dock stlink-s

Les interfaces ST-LINK équipant la totalité des cartes d'évaluation STMicroelectronics, comme cette Nucleo-F411RE, nécessitent un accès direct au périphérique USB. Cependant, avec Docker, il ne s'agit pas simplement de jouer avec les règles udev...

2. Création, exécution, connexion...

À ce stade, nous avons deux images parfaitement identiques, basées sur le même Dockerfile, mais nous n'avons pas de conteneur « vivant ». Il est donc temps d'utiliser ces images pour profiter des bienfaits de Docker. Il est important avant cela de comprendre qu'un conteneur doit être vu comme l’instanciation d'une image et que celui-ci peut être créé/exécuté (run), arrêté (stop) et (re)démarré (start). La distinction est importante, car la première exécution va créer le conteneur, mais quand vous le quitterez, celui-ci sera arrêté et non détruit. Vous n'allez donc pas l'exécuter à nouveau, mais le redémarrer pour l'utiliser une nouvelle fois.

Mais commençons par le début, en créant le premier conteneur :

$ docker run -ti --name deb1 articledocker
denis@b709d79a9bfe:~$

Nous utilisons la commande run qui s'appelle ainsi, car elle prévue pour « exécuter une commande dans un conteneur ». Mais comme nous demandons un fonctionnement interactif (-i) et l'utilisation d'un pseudo-TTY (-t) sans préciser de commande, nous nous retrouvons avec un shell. --name nous permet de donner un nom au conteneur et articledocker est tout simplement le nom de l'image à utiliser.

Nous nous retrouvons donc avec une ligne de commandes d'un Bash exécuté dans le conteneur, sous Debian Bullseye. Mais ce n'est pas un « vrai » système, /boot est vide, pas de /var/log/kern.log et presque rien dans /dev.

Plions-nous d'un touch TOTO pour créer rapidement un fichier dans le répertoire courant puis quittons le shell avec exit ou Ctrl+D, nous revenons à notre ligne de commandes précédente de la machine locale. Là, nous pouvons exécuter à nouveau docker run en spécifiant un nouveau nom de conteneur :

$ docker run -ti --name deb2 articledocker
denis@4855de3275c3:~$ ls TOTO
ls: impossible d'accéder à 'TOTO':
Aucun fichier ou dossier de ce type

Remarquez le nom d'hôte différent dans le prompt, et pour cause, ce n'est pas le même « système » et notre fichier TOTO n'est plus là. Nous venons de créer un second conteneur, à partir de la même image et, après avoir quitté avec exit, nous pouvons lister les conteneurs avec :

$ docker container ls -a
CONTAINER ID  IMAGE          COMMAND  CREATED         STATUS                          NAMES
4855de3275c3  articledocker  "bash"   32 minutes ago  Exited (2) About a minute ago   deb2
b709d79a9bfe  articledocker  "bash"   40 minutes ago  Exited (0) 34 minutes ago       deb1

Nous avons deux conteneurs, deb1 et deb2, utilisant l'image articledocker et dans un état « quitté ». Notez bien qu'ils utilisent articledocker comme base, mais que cette image n'est pas un support de stockage pour le « système ». J'insiste, car c'est une source de confusion, ceci n'a rien à voir avec une image de système de fichiers comme le .img d'une Raspberry Pi qu'on pourrait monter « en loopback » (losetup) pour y faire des modifications.

Pour reaccéder à un conteneur, nous pourrions être tentés de faire :

$ docker run -ti --name deb1 articledocker
docker: Error response from daemon: Conflict.
The container name "/deb1" is already in use by container
"b709d79a9bfec4cf973f37e532bdd31bb46aef21c4219b265ccab97459f62a31".
You have to remove (or rename) that container to be able to reuse that name.

Mais run ne peut être réutilisé, c'est l'autre point de confusion avec Docker. Le conteneur existe et est simplement arrêté. Pour le redémarrer, il faut utiliser :

$ docker start -ai deb1
denis@b709d79a9bfe:~ ls -l TOTO
-rw-r--r-- 1 denis denis 0 9 déc. 18:10 TOTO

Nous retrouvons, bien entendu, notre TOTO et le conteneur est à nouveau en marche. Les options -a et -i (combinées en -ai) correspondent respectivement au fait de s'attacher automatiquement et de travailler de façon interactive. Autrement dit, les sorties STDOUT/STDERR du conteneur sont envoyées sur notre terminal et notre STDIN devient également celui du conteneur.

Pour laisser fonctionner le conteneur tout en retournant à notre shell d'origine, vous pouvez utiliser la séquence Ctrl+P puis Ctrl+Q. Un docker container ls (l'option -a pour all permet d'afficher aussi les conteneurs à l'arrêt) vous montrera votre conteneur actif. Pour y retourner, ou plutôt vous y rattacher, utilisez simplement docker attach deb1. Vous pouvez également stopper un conteneur détaché avec docker stop deb1. Remarquez que l'opération peut paraître lente, mais en réalité, c'est un délai de 10 secondes qui est automatiquement appliqué. Vous pouvez préciser un autre délai avec l'option -t, dont 0.

Enfin, il peut vous venir l'envie de faire un brin de ménage. Vous pouvez donc supprimer un conteneur avec :

$ docker container rm deb1
deb1

Et une image avec :

$ docker image rm artdock:latest
Untagged: artdock:latest

Mais si vous essayez de supprimer une image utilisée par un conteneur, Docker vous rappellera à l'ordre sans ménagement :

$ docker image rm articledocker
Error response from daemon: conflict: unable to
remove repository reference "articledocker" (must force)
- container 4855de3275c3 is using its referenced
image ee3e2adaf1b9

Voici qui conclut la partie sur l'utilisation de base de Docker, pour rapidement créer des conteneurs pour des manipulations sans risques avec un environnement sur mesure, par projet. Ici, on peut se permettre des sudo make install sans remords et autres légèretés honteuses du même genre. Notez au passage qu'il est même possible de créer des images à partir d'un conteneur (docker commit) et donc de se servir de cette mécanique pour créer des « points de sauvegarde ».

dock fpgaDE0-s

Cette adorable carte de développement pour le FPGA Altera/Intel Cyclone IV (obsolète, mais toujours intéressante) intègre un programmeur USB-Blaster qui nécessite, lui aussi, un accès direct au périphérique USB. Avec ce type de plateforme et l'installation d'applicatifs tiers propriétaires, on comprend aisément l'intérêt de mettre en conteneur ce type d'environnement de développement.

3. Accès au matériel

Pouvoir disposer d'un environnement de développement personnalisé à souhait et ne plus être dépendant de la distribution ou de la version de la distribution qu'on utilise est une bonne chose, tout comme la possibilité de temporairement et impunément saccager tout ce qui nous chante. Mais le développement sur microcontrôleur ou à destination d'un système embarqué n'est pas qu'une affaire de SDK, de chaînes de compilation et de BSP. Il faut pouvoir, à un moment, accéder au matériel pour charger un firmware, déboguer ou même tout simplement communiquer avec la plateforme via une application ou un outil spécifique.

Les conteneurs existent principalement pour répondre aux problématiques d'isolation d'applications. Il n'est donc guère étonnant qu'accéder au matériel ne soit pas possible par défaut, et passer outre n'est pas aussi souple d'utilisation que le reste des fonctionnalités. De base, si vous essayer d'accéder à un périphérique, comme une carte STM32 Nucleo avec la suite open source des ST-Link tools, on vous remettra promptement à votre place ainsi :

denis@ed1df6bb77df:~$ st-info --descr
libusb: error [get_usbfs_fd] File doesn't exist,
  wait 10 ms and try again
libusb: error [get_usbfs_fd] libusb couldn't open
  USB device /dev/bus/usb/002/009, errno=2

Il existe plusieurs solutions vous permettant de contourner le problème et donc d'utiliser un périphérique USB, comme une carte de développement accessible avec des outils spécifiques (st-util, OpenOCD, PicoTool, etc.) par exemple, ou un port série comme celui proposé par une carte Arduino ou ESP8266/ESP32. La plus simple et brutale d'entre elles consiste à tout simplement oublier une partie de la sécurité [1] et de l'isolation fournies par les conteneurs, en utilisant l'option --privileged de docker run. Celle-ci permet de donner à un conteneur la permission d'accéder à n'importe quel périphérique de la machine local. Et le résultat est, bien entendu, celui espéré :

denis@a7c903647f46:~$ st-info --descr
stm32f411re

Depuis notre conteneur, l'outil st-info des ST-Link tools voit effectivement la carte Nucleo et son STM32F411 sans le moindre problème. Bien que nous ne soyons pas dans une logique d'isolation de service ou d'application, mais dans une simple démarche pratique, ceci pourrait être toléré. Mais ce n'en est pas moins une mauvaise idée et une très mauvaise habitude. Mieux vaut ne pas se comporter comme un homme de Cro-Magnon et ne permettre que l'accès au périphérique que nous utilisons.

Ceci est possible en utilisant l'option --device= suivie d'un chemin vers le périphérique en question. Sur l'hôte, et toujours avec notre exemple de STM32 Nucleo, nous commençons par identifier le périphérique avec lsusb :

$ lsusb
[...]
Bus 002 Device 009: ID 0483:374b
   STMicroelectronics ST-LINK/V2.1
[...]

Notre carte est sur le bus USB 002 et est le périphérique 009. Nous utilisons ces éléments avec cette nouvelle option :

$ docker run -ti --device=/dev/bus/usb/002/009 \
  --name deb_DEV articledocker
 
denis@04e70e2cebbe:~$ st-info --descr
stm32f411re

Magnifique, nous avons accès à la Nucleo sans compromettre notre sécurité. Ceci fonctionne même avec OpenOCD qui ne trouve rien à redire :

denis@04e70e2cebbe:~$ openocd -f board/st_nucleo_f4.cfg
Open On-Chip Debugger 0.11.0-rc2
Licensed under GNU GPL v2
[...]
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : clock speed 2000 kHz
Info : STLINK V2J22M5 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.261782
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f4x.cpu on 3333
Info : Listening on port 3333 for gdb connections

Ceci s'appliquera également aux cartes communiquant via un port série comme les Arduino, apparaissant sous GNU/Linux via des entrées comme /dev/ttyUSB0 et dev/ttyACM0. Il suffira d'utiliser --device=/dev/ttyUSB0 et le tour sera joué. Seul petit problème, si nous débranchons la carte et la rebranchons, elle apparaîtra probablement sous un autre numéro (et possiblement sur un autre bus selon où elle sera reconnectée). Pire encore, si c'est une Raspberry Pi Pico, elle apparaît et disparaît en fonction du démarrage en mode BOOTSEL ou non. Cette technique fonctionne en jonglant avec les suppressions et exécutions de conteneurs, mais elle est relativement rigide et ne se prête que très mal à un cycle habituel de développement sur microcontrôleur ou SBC. Nous pouvons faire mieux !

Une autre option envisageable est -v permettant de lier (bind) un volume ou, en d'autres termes, faire apparaître une partie du système de fichiers « réel » (local) dans le conteneur. On pourra donc utiliser quelque chose comme -v /dev/bus/usb:/dev/bus/usb, ou plus généralement -v /dev:/dev, pour se retrouver avec un /dev dans le conteneur, identique à celui de la machine elle-même. Sauf que :

libusb: error [get_usbfs_fd] libusb couldn't
open USB device /dev/bus/usb/002/009, errno=1

Avoir accès aux pseudofichiers de /dev est une chose, mais avoir la permission d'y lire et écrire en est une autre. Le fait que l'utilisateur, dans et hors du conteneur, fasse partie de dialout ou plugdev importe peu, la restriction vient de cgroups (pour control groups), une fonctionnalité du noyau Linux, utilisée par Docker afin de limiter, gérer et isoler l'utilisation des ressources. Il nous faut donc spécifier une option --device-cgroup-rule, en plus de -v, pour changer les permissions accordées. Pour les périphériques USB, nous pouvons utiliser :

$ docker run -ti \
  --device-cgroup-rule='c 189:* rmw' \
  -v /dev:/dev --name deb_DEV1 articledocker

La règle cgroups est « c 189:* rmw » où :

  • c désigne un périphérique de type « caractère » ;
  • 189 est le numéro majeur du périphérique Linux ;
  • :* désigne tous les numéros mineurs ;
  • et rmw indique des permissions de lecture (r), création de nœuds (m pour mknod) et d'écriture (w).

La notion de numéro de périphérique majeur et mineur est inhérente au noyau Linux et à la façon dont les périphériques sont associés aux pilotes. Ces numéros sont fixes et définis dans les sources du noyau. Mais vous pouvez également les trouver en listant les informations complètes des entrées (ou nœuds) dans /dev :

$ ls -l /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 0 déc. 10 11:07 /dev/ttyUSB0

c indique un périphérique en mode caractère, 188 le numéro majeur et 0 le mineur. Nous pouvons également consulter le contenu de /proc/devices pour obtenir une liste complète :

$ cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
[...]
136 pts
166 ttyACM
180 usb
188 ttyUSB
189 usb_device
226 drm
[...]

Ainsi, en liant /dev et en autorisant les accès aux périphériques en mode caractère dont le majeur est 166, 188 ou 189, nous obtenons un conteneur capable de dialoguer avec les périphériques USB (via la libUSB), ainsi que ceux apparaissant sous la forme de /dev/ttyUSB* et /dev/ttyACM*. Notre commande docker run devient donc :

$ docker run -ti \
  --device-cgroup-rule='c 189:* rmw' \
  --device-cgroup-rule='c 166:* rmw' \
  --device-cgroup-rule='c 188:* rmw' \
  -v /dev:/dev --name deb_DEV2 articledocker

Ceci règle notre problème de permissions et nous permet donc de développer, programmer et déboguer pour n'importe quelle cible depuis nos conteneurs.

Pour conclure sur cette partie liée au matériel et au développement, sans doute souhaiterez-vous également transférer des fichiers (sources) vers et depuis vos conteneurs. Ceci n'est largement pas aussi compliqué que l'accès au matériel USB et sera l'affaire d'une simple commande.

Pour copier depuis le système de fichiers local vers le conteneur :

$ docker cp ~/SRC/C/base deb2:/home/denis

deb2 est le nom du conteneur. Et dans le sens opposé :

$ docker cp deb2:/home/denis ~/SRC/C/truc

Notez qu'il n'y a pas d'options permettant une copie récursive de répertoire puisque c'est automatiquement le cas.

dock jlink-s

Notre approche consistant à donner accès en écriture à tous les périphériques ayant un majeur 189 (usb_device) nous permet également d'utiliser des outils de mise au point comme cette sonde J-Link BASE Classic.

4. M'sieur ? J'ai plus de place !

Jouer avec Docker et se créer tout un tas d'environnements et de systèmes à torturer dans des conteneurs c'est bien, c'est pratique et c'est amusant. Ce qui l'est moins, en revanche, c'est l'espace occupé par toutes ces données. Arrivera donc le moment fatidique où l'espace disque viendra à faire défaut. Vous serez alors tenté de déplacer tous ces conteneurs dans un autre emplacement que /var/lib/docker, sur un SSD supplémentaire monté, par exemple dans /mnt/SSD2T.

Déplacer ses « petites » affaires n'est pas très difficile, si l'on s'en réfère à la documentation officielle et non à des posts de blogs faits par des utilisateurs qui sont, passez-moi l'expression, rien de moins que de gros gorets. L'utilisateur Debian bien rôdé aura tendance à aller voir dans /etc/default/docker, mais là, une triste surprise l'y attend : « THIS FILE DOES NOT APPLY TO SYSTEMD ». C'est amusant comme certaines choses qui vous tapent sur les nerfs ont tendance à revenir vous taquiner à la moindre occasion... Merci Lennart de nous éloigner chaque jour un peu plus de la philosophie UNIX ! Vraiment merci (sarcasmes) !

Mais ce dont je parle, ce sont plutôt les documentations vous invitant joyeusement à éditer /lib/systemd/system/docker.service, histoire d'y glisser une option -g obsolète suivi d'un chemin. Non ! Non, re-non, et encore NON ! On ne touche pas à un fichier de configuration du système, géré par le gestionnaire de paquets, et qui se fera écraser à la moindre mise à jour. Et on n'entre pas non plus de chemin en dur dans un tel script. Systemd est déjà suffisamment irritant sans qu'on se sente obligé d'empirer le supplice. Je ne parle même pas de l'aspect assez présomptueux consistant à se dire que les développeurs de Docker n'ont pas pensé au problème...

Il y a une façon de faire cela proprement et cela commence par stopper le/les service(s) Docker :

$ sudo systemctl stop docker
$ sudo systemctl stop docker.socket
$ sudo systemctl stop containerd

On peut ensuite créer le fichier /etc/docker/daemon.json, contenant :

{
    "data-root": "/mnt/SSD2T/DOCKER"
}

Nous précisons ainsi, proprement, où se trouve la racine de l'arborescence de données et nous n'avons plus qu'à la déplacer à l'endroit désigné, avant de redémarrer le service :

$ sudo mv /var/lib/docker /mnt/SSD2T/DOCKER
 
$ sudo systemctl start docker
 
$ docker image ls
REPOSITORY    TAG       IMAGE ID      CREATED         SIZE
mydebian      latest    fd1681b3f43a  45 minutes ago  791MB
openwrt_test  latest    b0d9230ed355  7 hours ago     823MB
debian        bullseye  05d2318291e3  7 days ago      124MB
ubuntu        20.04     ba6acccedd29  7 weeks ago     72.8MB

À toutes fins utiles, un petit docker image inspect suivi du nom d'une image vous permettra de vous assurer que l'emplacement est bien référencé (WorkDir), ou en utilisant simplement la commande docker info -f '{{ .DockerRootDir}}'.

5. Pour conclure

Pour être tout à fait honnête avec vous, nous n'avons fait qu'effleurer le sujet ici. Docker permet de faire énormément de choses, comme placer des limitations (CPU, mémoire, etc.) sur les images et les conteneurs, sans compter toutes les options et solutions d'orchestration disponibles pour automatiser vos processus. Cette solution s'est démocratisée auprès des administrateurs système depuis très longtemps, mais il est encore rare de voir cette technologie utilisée dans le milieu du développement embarqué. Pourtant, ceci constituerait une solution très intéressante et pratique pour la diffusion des SDK comme celui du Raspberry Pi Pico, et certains projets l'ont d'ores et déjà compris (comme le firmware Proxmark de RFID Research Group [2]).

J'ai longtemps ignoré Docker, n'y voyant initialement que peu d'intérêt dans mon domaine de prédilection, mais prendre le temps de se pencher sur le sujet s'est avéré être un investissement très rentable. Je dispose de conteneurs pour mes développements ARM baremetal, Raspberry Pi Pico, ESP-IDF, STM32H7 sur Game & Watch, etc., et tout ce petit monde est rangé, ordonné et incroyablement facile à maintenir. Aujourd'hui, je pense que j'aurai bien du mal à m'en passer...

Références

[1] https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/

[2] https://hub.docker.com/r/iceman1001/proxmark3/



Article rédigé par

Par le(s) même(s) auteur(s)

Gestion des périphériques USB sous [Free|Net|Open]BSD

Magazine
Marque
GNU/Linux Magazine
Numéro
268
Mois de parution
mars 2024
Spécialité(s)
Résumé

Lorsqu'on fait connaissance avec l'un des systèmes héritiers du BSD d'origine, à savoir FreeBSD, NetBSD ou encore OpenBSD, tout en ayant une certaine expérience de GNU/Linux, il est relativement facile de retrouver ses petits. Certes, depuis quelques années, le système d'init et la gestion des services de GNU/Linux se sont drastiquement écartés des principes propres à la philosophie Unix, jusqu'alors respectés par GNU/Linux. Dans l'ensemble, la transition est relativement aisée, mais il y a cependant un point sur lequel les différences sont telles que quelques explications s'avèrent nécessaires : la gestion des périphériques « hotplug » (USB) et de leurs permissions.

Continuons notre exploration des Java Cards : jckit 3.0.4, NFC et code PIN

Magazine
Marque
Hackable
Numéro
53
Mois de parution
mars 2024
Spécialité(s)
Résumé

Dans un précédent article, nous avons découvert le monde des smartcards et des Java Cards en particulier, en prenant en main un modèle, certes toujours utilisable, mais relativement ancien (NXP J2A081). Il est temps aujourd'hui de nous mettre à jour, de goûter à davantage de modernité en nous penchant sur une version plus récente des spécifications et aussi d'explorer les aspects « sans contact » de cette formidable technologie. Depuis la dernière fois, une certaine expérience a été acquise et nous approfondirons également ce qu'il est possible de faire, autant avec des smartcards « anciennes » qu'avec des déclinaisons plus actuelles...

Hydrabus : un outil pour tous les bus

Magazine
Marque
Hackable
Numéro
53
Mois de parution
mars 2024
Spécialité(s)
Résumé

Vous connaissez la routine : un convertisseur USB/série, un analyseur logique, un petit bout de code sur un Arduino ou un ESP8266 pour vérifier un capteur i2c, une sonde JTAG, un programmeur de flash CH341A ou TL866A, un adaptateur CAN/USB, un programmeur SWD... Et forcément, le matériel qu'il vous faut n'est pas sous la main. Hydrabus est un projet open hardware et open source, cousin du Bus Pirate, qui règle ce problème. Un seul outil pour plein d'usages, un vrai couteau suisse des bus et interfaces en tous genres.

Les derniers articles Premiums

Les derniers articles Premium

Les nouvelles menaces liées à l’intelligence artificielle

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Sommes-nous proches de la singularité technologique ? Peu probable. Même si l’intelligence artificielle a fait un bond ces dernières années (elle est étudiée depuis des dizaines d’années), nous sommes loin d’en perdre le contrôle. Et pourtant, une partie de l’utilisation de l’intelligence artificielle échappe aux analystes. Eh oui ! Comme tout système, elle est utilisée par des acteurs malveillants essayant d’en tirer profit pécuniairement. Cet article met en exergue quelques-unes des applications de l’intelligence artificielle par des acteurs malveillants et décrit succinctement comment parer à leurs attaques.

Migration d’une collection Ansible à l’aide de fqcn_migration

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Distribuer du contenu Ansible réutilisable (rôle, playbooks) par l’intermédiaire d’une collection est devenu le standard dans l’écosystème de l’outil d’automatisation. Pour éviter tout conflit de noms, ces collections sont caractérisées par un nom unique, formé d’une espace de nom, qui peut-être employé par plusieurs collections (tel qu'ansible ou community) et d’un nom plus spécifique à la fonction de la collection en elle-même. Cependant, il arrive parfois qu’il faille migrer une collection d’un espace de noms à un autre, par exemple une collection personnelle ou communautaire qui passe à un espace de noms plus connus ou certifiés. De même, le nom même de la collection peut être amené à changer, si elle dépasse son périmètre d’origine ou que le produit qu’elle concerne est lui-même renommé.

Mise en place d'Overleaf Community pour l’écriture collaborative au sein de votre équipe

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Si vous utilisez LaTeX pour vos documents, vous connaissez vraisemblablement Overleaf qui vous permet de rédiger de manière collaborative depuis n’importe quel poste informatique connecté à Internet. Cependant, la version gratuite en ligne souffre de quelques limitations et le stockage de vos projets est externalisé chez l’éditeur du logiciel. Si vous désirez maîtriser vos données et avoir une installation locale de ce bel outil, cet article est fait pour vous.

Les listes de lecture

7 article(s) - ajoutée le 01/07/2020
La SDR permet désormais de toucher du doigt un domaine qui était jusqu'alors inaccessible : la réception et l'interprétation de signaux venus de l'espace. Découvrez ici différentes techniques utilisables, de la plus simple à la plus avancée...
8 article(s) - ajoutée le 01/07/2020
Au-delà de l'aspect nostalgique, le rétrocomputing est l'opportunité unique de renouer avec les concepts de base dans leur plus simple expression. Vous trouverez ici quelques-unes des technologies qui ont fait de l'informatique ce qu'elle est aujourd'hui.
9 article(s) - ajoutée le 01/07/2020
S'initier à la SDR est une activité financièrement très accessible, mais devant l'offre matérielle il est parfois difficile de faire ses premiers pas. Découvrez ici les options à votre disposition et les bases pour aborder cette thématique sereinement.
Voir les 31 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous