📕 Logiciel et exécution

L’icône rouge 📕 signifie que ce chapitre ne sera pas présenté en séance de CM, et sera donc entièrement à travailler en autonomie. Il est important de l’avoir lu et préparé avant la séance sur le chapitre suivant.

Avant d’apprendre à déployer un logiciel, il faut d’abord comprendre ce qu’implique l’exécution d’un logiciel dans un environnement donné. Ce chapitre présente succinctement quelques notions de base.

1. Exécution d’un logiciel

layers no execution.drawio
Figure 1. Schéma d’un environnement simple sans logiciel en cours d’exécution

Un logiciel est toujours exécuté dans un environnement. À sa base, un environnement comporte généralement du matériel (processeur, mémoire, stockage, réseau, etc.), ainsi qu’un système d’exploitation (Linux, Windows, etc.). Le système d’exploitation est responsable de toutes les interactions avec le matériel, et expose aux logiciels une représentation abstraite de ce dernier, que ce soit le stockage représenté sous la forme d’un système de fichiers, ou le réseau représenté par un adressage IP.

Au dessus du système d’exploitation se trouve tout un ensemble de fichiers, comportant notamment :

  • Un ensemble de logiciels que l’on souhaite exécuter pour accomplir des tâches. Un logiciel peut prendre la forme d’un binaire exécutable nativement (par exemple un programme C compilé), d’un binaire exécutable par une plateforme d’exécution (par exemple un programme Java compilé en .jar) ou encore d’un script interprétable (par exemple un programme Python .py ou shell .sh).

  • Un ensemble de dépendances requises par les logiciels exécutés. Ces dépendances comportent principalement des bibliothèques logicielles (bibliothèque C, Java, Python, etc.), mais aussi des exécutables (utilitaires, interpréteurs, etc.).

  • Un ensemble de données manipulées par les logiciels exécutés. Ces données incluent à la fois des fichiers de configuration (par exemple un fichier .json ou .yml) et des données de travail (par exemple une base de données).

Un environnement peut prendre la forme d’un système informatique physique (c’est à dire avec du vrai matériel), ou bien d’un système informatique virtuel (c’est à dire avec du matériel virtuel "simulé"), aussi appelé machine virtuelle. Dans ce cours, on ne fera pas de distinction explicite entre les deux situations.
layers process.drawio
Figure 2. Exécution d’un logiciel, donnant un processus qui accède à dépendances et données

L’exécution d’un logiciel va générer un processus dans l’environnement, à savoir une représentation en memoire du logiciel exécuté et de son état à l’exécution. Une fois en exécution, le processus va aller chercher et utiliser les dépendances dont il a besoin, et va écrire et/ou lire les données avec lesquelles il doit travailler.

Il est possible d’exécuter simultanément le même logiciel plusieurs fois, ce qui crée alors plusieurs processus à partir du même logiciel.
Exemple 1. Exécution de Simplestblog

On exécute Simplestblog ainsi qu’une bases de données MariaDB associée :

simplestblog execution.drawio

On notera que cela nécessite de préparer l’environnement pour y avoir :

  • Un interpréteur Python permettant d’exécuter Simplestblog. On choisit pour cela l’implémentation officielle Python, appelée CPython, en version 3.

  • La bibliothèque logicielle Python Flask.

  • Un système de gestion de base de données compatible MySQL. On choisit d’utiliser le logiciel MariaDB.

  • MariaDB nécessite d’utiliser un répertoire où stocker tous les fichiers de stockage de la base de données.

Pour le moment le schéma ne montre pas la communication entre les deux processus, ce qui sera détaillé plus loin dans le cours lorsqu’on parlera de réseau.
Il est crucial qu’un processus ait bien toujours accès à tout ce dont il a besoin. En cas de dépendances ou de données absentes, le comportement obtenu risque d’être incorrect (par exemple, un plantage) !

2. Configuration d’un logiciel

La majorité des logiciels peuvent être configurés au moment de leur lancement, que ce soit pour leur confier les informations qu’ils requièrent (par exemple indiquer à une base de données où stocker ses fichiers) ou pour influencer leur mode de fonctionnement (par exemple indiquer à une base de données comment optimiser ses tables).

On compte au moins trois manières courantes de configurer un logiciel à son lancement : la ligne de commande, les variables d’environnement, et les fichiers de configuration.

Parmi ces trois stratégies, certains logiciels choisissent de n’en utiliser une seule, tandis que d’autres choisissent d’en combiner plusieurs (voire les trois !). Il n’y a pas de règle universelle !

2.1. Ligne de commande

Lorsqu’un logiciel s’exécute à l’aide d’une commande, il peut souvent recevoir un ensemble d'arguments passés avec cette commande. Ces arguments sont généralement passés de deux manières :

  • Soit à l’aide d’une option, à savoir un paramètre avec un nom explicite. Une option peut être soit:

    • sans valeur (--option) pour simplement activer un comportement (par exemple ls --reverse indique à ls de lister les fichiers dans l’ordre inverse),

    • avec valeur (--option=valeur ou --option valeur) pour passer une donnée spécifique (par exemple ls --sort=size pour passer la valeur size à l’option --sort)

  • Soit à l’aide d’un paramètre positionnel, à savoir un paramètre sans nom explicite devant être à une position précise dans la commande (par exemple dans la commande cp fichier1 fichier2, fichier1 est la valeur donnée à un premier paramètre positionnel correspondant au fichier source, fichier2 est la valeur donnée à un second paramètre positionnel correspondant à la destination).

Exemple 2. Commande pour exécuter Simplestblog

Le cas d’étude Simplestblog est un logiciel Python qui doit être exécuté à l’aide d’un autre logiciel Python appelé Flask. Pour cela, Flask fournit une commande appelée waitress-serve, que nous utilisons ainsi :

$ waitress-serve --call simplestblog:create_app

Dans cette commande, on passe la valeur simplestblog:create_app à l’option --call, pour indiquer à Flask quel fichier Python et quelle fonction doivent être exécutés pour démarrer Simplestblog.

2.2. Variables d’environnement

Une variable d’environnement est une manière universelle de stocker une valeur au sein d’un environnement ou au sein d’un processus. Par convention elle est écrite en majuscules sous la forme VARIABLE=valeur.

Une variable d’environnement peut être définie à plusieurs niveaux :

  • Elle peut-être définie par le système d’exploitation au niveau de l’environnement d’exécution. Cela permet de définir des valeurs qui sont transmises à l’intégralité des processus qui s’exécutent dans cet environnement. Par exemple le système d’exploitation définit typiquement la variable HOME qui contient l’emplacement du répertoire utilisateur, ou bien la variable PATH qui contient la liste des répertoires contenant des programmes exécutables.

  • Elle peut-être définie au niveau d’un processus spécifique, soit au moment du lancement du processus, soit durant son exécution. Cela permet non seulement de confier des valeurs au processus, mais cela permet aussi à un processus de définir des variables qui seront transmises à tous ses processus enfants.

Une variables d’environnement peut donc soit servir à transmettre des "informations générales" à tout un ensemble de processus, soit transmettre des "informations spécifiques" à un processus particulier. Dans le second cas, le rôle joué par une variable d’environnement est très similaire à celui d’une option avec valeur définie dans une ligne de commande.

Définir et manipuler des variables d’environnement

Dans un interpréteur de commandes Linux classique comme bash, supposons qu’on souhaite lancer un processus à partir d’un exécutable appelé monprogramme, et qu’on souhaite confier à ce processus la variable d’environnement MAVARIABLE avec pour valeur mavaleur.

Une première solution est de définir la variable dans l’interpréteur de commande à l’aide de la commande export, puis de créer le processus qui héritera de cette variable :

$ export MAVARIABLE=valeur
$ ./monprogramme
MAVARIABLE sera ici accessible à tous les processus démarrés dans l’interpréteur de commandes après le export.

Une seconde solution est de définir la variable d’environnement directement lors de la création du processus donné de la manière suivante :

$ MAVARIABLE=valeur ./monprogramme
MAVARIABLE ne sera ici accessible que par le processus monprogramme ainsi lancé.

Dans un interpréteur de commandes, on peut également lire le contenu d’une variable d’environnement à l’aide de la syntaxe $VARIABLE. Par exemple pour définir le contenu d’une variable puis afficher son contenu à l’aide de la commande echo (similaire à un print) :

$ export MAVARIABLE=valeur
$ echo $MAVARIABLE
valeur
Attention le symbole $ utilisé en début de ligne est une convention pour indiquer que nous sommes dans un interpréteur de commandes, et n’a aucun lien avec l’opérateur $ utilisé pour lire une variable d’environnement.

Pour connaître l’ensemble des variables d’environnement définies dans une session, on peut utiliser la commande printenv.

$ export MAVARIABLE=valeur
$ printenv

DISPLAY=:0
SESSION_MANAGER=local/unix:@/tmp/.ICE-unix/2812,unix/unix:/tmp/.ICE-unix/2812
SSH_AUTH_SOCK=/run/user/1000/keyring/ssh
DEBUGINFOD_IMA_CERT_PATH=/etc/keys/ima
DESKTOP_SESSION=gnome
LOGNAME=bousse-e
…
…
MAVARIABLE=valeur
…
…
Il y a toujours une quantité importante de variables d’environnements pré-définies dans un environnement Linux, donc la liste peut être très longue !

Enfin, on notera que tout langage de programmation est capable d’accéder aux variables d’environnements qui sont présentes dans le processus où il s’exécute.

  • En C++ :

    getenv("MAVARIABLE")
  • En Java :

    System.getenv("MAVARIABLE")
  • En R :

    Sys.getenv("MAVARIABLE")
Exemple 3. Variable d’environnement utilisée par Simplestblog

Le logiciel Simplestblog utilise la variable d’environnement SIMPLESTBLOG_SETTINGS_FILE pour savoir à quel emplacement se situe le fichier de configuration qu’il doit utiliser pour son exécution.

Ainsi, si on prépare un fichier de configuration à l’emplacement /monrepertoire/config.py, on peut donner cette information au processus en démarrant le logiciel avec les commandes suivante :

$ export SIMPLESTBLOG_SETTINGS_FILE=/monrepertoire/config.py
$ waitress-serve --call simplestblog:create_app

2.3. Fichiers de configuration

Un logiciel peut choisir de recevoir ses valeurs de configuration en lisant un fichier de configuration écrit dans un langage informatique qu’il comprend. On utilise généralement pour cela un langage spécialisé dans la représentation de données, comme les langages JSON, YAML, TOML ou INI. Certains logiciels possèdent également leur propre langage de configuration, et d’autres vont parfois directement utiliser le langage de programmation dans lequel ils sont écrits.

Un fichier de configuration comporte toujours des paramètres auxquels sont associés des valeurs. En fonction du langage de configuration, il peut être également par exemple possible de répartir les paramètres dans différentes sections, ou bien de définir des paramètres structurés chacun composé de plusieurs sous-paramètres.

Exemple 4. Fichier de configuration de Simplestblog

Simplestblog doit être configuré à l’aide d’un fichier de configuration écrit directement dans le langage Python. Ce fichier doit uniquement comporter une liste de déclaration des variables suivantes :

  • SECRET_KEY est une clé secrète unique interne au logiciel, qui lui permet notamment de sécuriser les comptes utilisateurs. Cette clé doit être différente et aléatoire pour chaque déploiement de Simplestblog.

  • DB_TYPE permet de choisir entre une base de données MySQL (mysql) ou SQLite (sqlite).

  • MYSQL_DATABASE_HOST est le nom d’hôte (ou, à défaut, l’adresse IP) pour se connecter à un serveur compatible MySQL existant contenant les données de l’instance SimplestBlog.

  • MYSQL_DATABASE_USER est le nom d’utilisateur à utiliser pour se connecter au serveur de base de données.

  • MYSQL_DATABASE_PASSWORD est le mot de passe à utiliser pour se connecter au serveur de base de données.

  • MYSQL_DATABASE_DB est le nom de la base de données à utiliser au sein du serveur de base de données.

  • MYSQL_DATABASE_PORT est le nom du port réseau sur lequel se connecter sur MYSQL_DATABASE_HOST.

Un exemple de fichier config.py peut donc être :

SECRET_KEY = "LGGQWzcR2F9Gz9E3LseiV7VqJUY"

DB_TYPE = "mysql"

MYSQL_DATABASE_HOST = "172.0.0.10"
MYSQL_DATABASE_USER = "simplestblog"
MYSQL_DATABASE_PASSWORD = "CqbVx4G5fG7wm9VdgexJLNB5PWZ"
MYSQL_DATABASE_DB = "simplestblog"
MYSQL_DATABASE_PORT = "3306"

3. Comptes utilisateurs et droits d’accès

Bien que les notions présentées ici concernent tout système d’exploitation (Linux, Windows, Mac, etc), les exemples évoqués sont spécifiques à un environnement de type Linux.

Au sein d’un environnement, le système exploitation exige que chaque action sur l’environnement soit réalisée à l’aide d’un compte utilisateur. Un compte utilisateur possède un ensemble de droits qui définissent quels logiciels, périphériques, et fichiers peuvent être exécuté, lus ou modifiés par ce compte utilisateur.

Chaque processus exécuté dans un environnement est exécuté par un unique compte utilisateur, qui va définir quels sont les droits accordés à ce processus. Ainsi, si un compte utilisateur a le droit de modifier le contenu du répertoire /etc, alors un processus exécuté par ce compte utilisateur pourra modifier le répertoire /etc. Bien utilisé, ce système permet d’éviter à un processus de lire une information qu’il n’est pas en droit de lire (comme une clé de chiffrement), ou de commettre des ravages parmi des fichiers qu’il n’est pas censé modifier ou supprimer.

Un compte utilisateur peut correspondre :

  • soit à une personne humaine qui utilise un environnement − par exemple chaque étudiant⋅e possède un compte utilisateur permettant de se connecter à une machine de TP ;

  • soit à un rôle abstrait, qui ne correspond à aucune personne humaine spécifique, et qui n’existe que pour restreindre de manière précise les droits accordés à certains processus, par exemple :

    • www-data est un compte utilisateur de ce type souvent utilisé pour exécuter les serveurs web (Apache, Nginx, etc), et qui permet de restreindre les droits de ces processus pour limiter les attaques provenant d’acteurs malicieux.

    • root, le plus connu, est le compte utilisateur de ce type qui possède des droits absolus sur l’intégralité de l’environnement. C’est généralement le seul compte utilisateur qui a le droit de modifier les fichiers du système d’exploitation, et par conséquent le seul compte qui peut facilement installer (ou désinstaller) des logiciels systèmes dans l’environnement.

      Un compte utilisateur non-root peut se voir confier le droit d’utiliser une commande appelée sudo, qui permet de déclencher un processus comme s’il avait été lancé par le compte root. L’administrateur système d’un environnement possède habituellement le droit d’utiliser sudo.
Affichage du compte utilisateur dans l’invite de commande

Dans un environnement Linux, il est courant d’interagir avec le système à l’aide d’un interpréteur de commande comme bash. Dans un terminal, cet interpréteur de commandes affiche traditionnellement une courte chaîne de caractères appelée l’invite de commande qui signifie que l’interpréteur est prêt à recevoir une commande.

En fonction de l’environnement, un invite de commande va fournir un certain nombre d’informations : compte utilisateur, nom de l’environnement, répertoire courant, etc. On trouvera notamment toujours une information concernant la nature du compte utilisateur en cours d’utilisation dans l’interpréteur de commande, à l’aide de la convention très simple suivante :

  • Le symbole $ à la fin de l’invite signifie que le compte utilisateur ne possède pas les droits root,

  • Le symbole # à la fin de l’invite signifie que le compte utilisateur possède les droits root.

Ainsi si l’invite de commande est :

obiwan@alderaan:~$

on peut lire que l’utilisateur est obiwan, que l’environnement s’appelle alderaan, que le répertoire courant est ~, et que le compte utilisateur n’est pas root ($).

Si l’invite de commande est juste :

$

on sait uniquement que le compte utilisateur n’est pas root ($).

Si l’invite de commande est :

root@alderaan:/#

on peut lire que le compte utilisateur est root, sur l’environnement alderaan, dans le répertoire /, et avec les droits root (#)

Et enfin si l’invite de commande est :

#

on sait uniquement que le compte utilisateur est root (#).

Bien que très courante, cette convention n’est pas utilisée dans 100% des cas. Par exemple elle n’est pas appliquée par des interpréteurs de commandes plus récents comme fish.
Exemple 5. Droits d’accès dans un environnement Ubuntu

Supposons un interpréteur de commandes dans un environnement Ubuntu, lancé par un compte utilisateur obiwan qui ne possède par les droits root :

obiwan@alderaan:~$

Si on essaie de créer un fichier dans le répertoire utilisateur du compte obiwan, tout fonctionne sans erreur :

obiwan@alderaan:~$ echo "Bonjour !" > ~/message.txt
obiwan@alderaan:~$ cat ~/message.txt
Bonjour !

Par contre si on essaie de créer un fichier dans un répertoire système comme /etc, une erreur apparait car le compte utilisateur utilisé n’a pas le droit de modifier le répertoire /etc :

obiwan@alderaan:~$ echo "Bonjour !" > /etc/message.txt
bash: /etc/message.txt: Permission denied

En revanche, si on est connecté avec le compte utilisateur root :

root@alderaan:/#

On peut essayer à nouveau de créer un fichier dans un répertoire système comme /etc, et constater qu’il n’y a pas d’erreur, car root possède tous les droits dans l’environnement :

root@alderaan:/ echo "Bonjour !" > /etc/message.txt
root@alderaan:/ cat /etc/message.txt
Bonjour !