marionnet.org — un laboratoire de réseaux virtuel

Piloter Marionnet par un script ou par une IA

Ce texte fait foi ; la version anglaise en est la traduction.

Depuis 2026, Marionnet peut recevoir ses ordres d’un programme, et non plus seulement de la souris. Un script peut ainsi construire le réseau d’un TP, le démarrer, attendre que les machines soient prêtes, leur faire exécuter des commandes, puis vérifier que le réseau est bien dans l’état voulu ; un agent conversationnel, c’est-à-dire une IA capable d’exécuter des commandes (un assistant de programmation en ligne de commande, par exemple), peut en faire autant à partir d’un énoncé rédigé en langage naturel. Notons que Marionnet n’embarque aucune IA : c’est la vôtre qui le pilote. Pendant tout ce temps, la fenêtre de Marionnet reste à l’écran : vous voyez le réseau se construire et les machines démarrer, et vous pouvez même intervenir à la souris en même temps que le programme.

Quelques usages, pour fixer les idées :

  • préparer le projet d’un TP sans cliquer, puis le reconstruire à l’identique d’une année sur l’autre, ou en plusieurs variantes ;
  • livrer aux étudiants un projet qui démarre tout seul ;
  • vérifier en une commande qu’un réseau répond à l’énoncé (adresses, routes, connectivité), plutôt que d’ouvrir chaque machine l’une après l’autre ;
  • faire construire, puis vérifier un TP par une IA, qui pilote Marionnet à votre place.

Ce guide présente ce mode de fonctionnement du point de vue de l’utilisateur. Les détails techniques, qui ne servent que dans des cas particuliers, sont repliés dans des encadrés qui s’ouvrent d’un clic.


1. Lancer Marionnet en mode piloté

Il suffit d’ajouter une option au lancement de Marionnet :

marionnet --control-socket /tmp/marionnet.sock

Le fichier /tmp/marionnet.sock est le point de rendez-vous entre Marionnet et le programme qui le pilote (techniquement, une socket Unix, autrement dit un fichier spécial par lequel deux programmes d’une même machine se parlent). Sans cette option, ce point de rendez-vous n’existe pas, et Marionnet ne peut donc pas être piloté : c’est vous qui décidez, à chaque lancement, s’il accepte des ordres venus d’ailleurs.

Les ordres s’envoient ensuite par mrnctl, le pilote, installé avec Marionnet (son nom complet est marionnet-ctl), auquel on indique une fois pour toutes le point de rendez-vous :

export MARIONNET_CONTROL_SOCKET=/tmp/marionnet.sock

mrnctl new /tmp/tp1.mar
mrnctl add machine m1
mrnctl start m1

Chaque ligne est un ordre, que Marionnet exécute comme s’il venait de la souris : la machine m1 apparaît sur le plan du réseau, puis elle démarre, et son terminal s’ouvre.

Le choix du chemin, et ce qu'il faut installer

Le chemin doit être absolu, et ne pas dépasser 107 octets (c’est une limite du système, et non de Marionnet). S’il est relatif, trop long, situé dans un répertoire où Marionnet ne peut pas écrire, ou déjà utilisé par un autre Marionnet en cours d’exécution, Marionnet refuse de démarrer et en donne la raison : un script échoue ainsi dès sa première ligne, plutôt que dix ordres plus loin.

Le pilote cherche le point de rendez-vous dans son option --socket=CHEMIN, à défaut dans la variable MARIONNET_CONTROL_SOCKET, et nulle part ailleurs : il ne part jamais à la recherche d’un Marionnet. Il a besoin du programme socat, et de jq pour ses options de mise en forme (--pretty, --query), deux programmes que les paquets de Marionnet installent avec lui.

Plusieurs pilotes peuvent se connecter au même Marionnet, huit au plus en même temps.

La touche TAB complète les ordres, leurs options, et surtout les noms réels du projet ouvert (composants, ports, champs), qu’elle demande au Marionnet en cours d’exécution :

$ mrnctl connect c2 s1:<TAB>
s1:port1  s1:port2  s1:port3  s1:port4

Si vous avez installé Marionnet par un paquet (.deb ou .rpm) ou par son programme d’installation, vous n’avez rien à faire pour en profiter : la complétion est installée avec le logiciel, et le terminal la charge de lui-même, dans tout terminal ouvert après l’installation.

Si la touche TAB ne complète rien

La complétion est chargée par le paquet bash-completion de votre distribution. Ce paquet est présent sur la plupart des postes de bureau, mais pas toujours sur une installation minimale ; au besoin, installez-le, puis ouvrez un nouveau terminal :

# Debian, Ubuntu :
sudo apt install bash-completion

# Fedora, Rocky Linux, AlmaLinux :
sudo dnf install bash-completion

# openSUSE :
sudo zypper install bash-completion

Si vous utilisez Marionnet depuis ses sources, sans l’avoir installé, chargez la complétion à la main dans chaque terminal (ou une fois pour toutes dans votre ~/.bashrc), depuis la racine des sources :

. bin/scripts/marionnet-completion.bash

Quand aucun Marionnet ne tourne, la complétion des ordres et des options peut s’appuyer sur un instantané du vocabulaire, pris une fois pour toutes (les noms du projet, eux, ne peuvent alors pas être complétés) :

mrnctl help > ~/.marionnet-grammar.json
export MARIONNET_CTL_GRAMMAR=~/.marionnet-grammar.json

Seul ou à deux : qui tient les commandes ?

Marionnet ouvre parfois de lui-même une fenêtre : la page de garde au démarrage, un avertissement, le récapitulatif des adaptations faites à un ancien projet, une erreur. Ce qu’il convient de faire de ces fenêtres dépend de qui se trouve devant l’écran, et c’est la première question à vous poser.

  • Le programme pilote seul. Personne n’est là pour lire ces fenêtres, ni pour les fermer, et une fenêtre qui attend une réponse bloquerait tout. Marionnet les affiche donc deux secondes, puis les ferme de lui-même, en conservant ce qu’elles disaient, que le programme relit par mrnctl notifications. C’est le comportement par défaut : il n’y a rien à ajouter à la commande de lancement.

  • Le programme et un humain pilotent ensemble. Vous construisez par exemple le réseau par un script, mais vous voulez ensuite cliquer vous-même pour l’explorer ; ou bien vous montrez Marionnet à des étudiants pendant qu’un script le fait travailler. Il faut alors que les fenêtres restent à l’écran, comme d’habitude, et c’est le rôle de l’option --keep-dialogs :

    marionnet --control-socket /tmp/marionnet.sock --keep-dialogs
    

    C’est donc l’humain qui lit et ferme les fenêtres, et qui répond aux questions que pose Marionnet, y compris quand un ordre du programme les a provoquées ; le programme attend alors sa réponse. En contrepartie, mrnctl notifications ne recueille plus rien.

Dans les deux cas, l’humain garde la main : rien ne l’empêche de cliquer pendant qu’un programme pilote, et Marionnet ne répond jamais à sa place à une question qu’il a lui-même déclenchée.

Le cas d’une IA se ramène à l’un des deux précédents, selon la façon dont elle agit. Une IA qui exécute des commandes envoie ses ordres par mrnctl, exactement comme un script : elle prend la place du programme, et vous choisissez de la laisser piloter seule ou de piloter avec elle. Mais une IA capable de voir l’écran et de cliquer peut aussi prendre la place de l’humain, et cela sans aucune option, puisque Marionnet la traite alors comme un utilisateur ordinaire. Pour apprendre à une IA à piloter Marionnet, donnez-lui à lire ../marionnet-driving-skill.md, qui a été écrit à son intention : il lui explique comment construire, jouer et vérifier un TP par des commandes. La conception du TP n’est pas son objet, même si l’IA peut bien sûr vous y aider par ailleurs.

Régler le délai de fermeture des fenêtres

Quand le programme pilote seul, l’option --dialog-timeout=N change le temps pendant lequel une fenêtre reste affichée avant de se fermer ; N s’exprime en millisecondes, et vaut 2000 par défaut. Ce délai n’est pas nul, afin qu’une personne qui regarde l’écran (ou une copie d’écran) voie encore ce qui s’est passé.


2. Un premier réseau piloté

Voici, en entier, un réseau semblable à celui de la page Premiers pas (deux machines reliées par un switch), construit, démarré et enregistré cette fois par un script :

export MARIONNET_CONTROL_SOCKET=/tmp/marionnet.sock

# Lancer Marionnet, puis attendre qu'il réponde :
marionnet --control-socket "$MARIONNET_CONTROL_SOCKET" &
until mrnctl status > /dev/null 2>&1; do sleep 0.5; done

# D'abord un projet, comme à la souris :
mrnctl new /tmp/demo.mar

# Les composants, puis les câbles et leurs deux extrémités :
mrnctl add machine m1
mrnctl add machine m2
mrnctl add switch  s1 --ports=4
mrnctl connect c1 m1:eth0 s1:port1
mrnctl connect c2 m2:eth0 s1:port2

# Les adresses :
mrnctl ifconfig-set m1 eth0 ipv4-address 10.0.0.1/24
mrnctl ifconfig-set m2 eth0 ipv4-address 10.0.0.2/24

# Démarrer, puis attendre que les machines tournent (cf. § 4) :
mrnctl start m1
mrnctl start m2
mrnctl wait m1 --state=on --timeout=120
mrnctl wait m2 --state=on --timeout=120

# Arrêter avant d'enregistrer (cf. § 5), puis quitter :
mrnctl stop m1
mrnctl stop m2
mrnctl wait-all --state=off --timeout=180
mrnctl save-as /tmp/demo.mar
mrnctl quit

On y retrouve, ligne à ligne, ce que l’on ferait à la souris. Les ports se désignent par leur nom, comme dans l’interface : une machine numérote ses cartes réseau à partir de 0 (eth0, eth1), un switch ses ports à partir de 1 (port1, port2). Une fois enregistré, ce projet se rejoue d’une seule ligne, l’option -r démarrant aussitôt tous ses composants :

marionnet --control-socket /tmp/marionnet.sock -r /tmp/demo.mar

Le répertoire examples/ contient des scripts complets de ce genre, prêts à être exécutés.


3. Ce que répond Marionnet

Chaque ordre reçoit une réponse d’une ligne. Quand l’ordre est accepté, elle commence par "ok":true ; quand il est refusé, par "ok":false, suivi d’un code et d’une explication en clair, ici « “m1” cannot start from state “on” » (la machine m1 est déjà allumée) :

{"ok":false,"error":"forbidden_transition","detail":"…"}

Un script n’a d’ailleurs pas besoin de lire la réponse pour savoir si l’ordre a réussi : comme toute commande Unix, mrnctl rend le code 0 en cas de succès, ce qui permet d’écrire mrnctl start m1 && echo démarrée.

Deux règles simples évitent la plupart des refus.

  • Un programme ne peut faire que ce que permet l’interface graphique. Une machine allumée ne se supprime pas plus par un ordre que par la souris, et une machine déjà démarrée ne se démarre pas une seconde fois. Pour savoir ce qui est permis sur un composant à un instant donné, il suffit de le demander. Pour une machine éteinte, par exemple :

    mrnctl can m1
    # {"ok":true,"name":"m1","kind":"machine","state":"off",
    #  "can":["set","del","start"],"beyond_gui":[]}
    

    Le champ can donne les actions permises en ce moment : modifier la machine (set), la supprimer (del) ou la démarrer (start). Une fois allumée, elle ne pourra plus être supprimée, mais elle pourra être arrêtée (stop). Le champ beyond_gui signale, parmi les actions permises, celles qui n’ont pas de bouton dans l’interface, comme l’extinction brutale d’une seule machine (poweroff). Les câbles, eux, se modifient et se suppriment même quand le réseau fonctionne, comme dans l’interface graphique, et pour la même raison : on peut débrancher un câble réel et le rebrancher ailleurs sans rien éteindre.

  • La liste des ordres se demande à Marionnet. mrnctl help donne tous les ordres avec leur syntaxe, et mrnctl help connect celle d’un seul. Cette liste s’enrichit d’une version à l’autre, de sorte que c’est la réponse du Marionnet que vous pilotez qui fait foi ; les deux encadrés qui suivent en donnent toutefois un aperçu.

Exemple : la liste des ordres de Marionnet 1.0.470 (novembre 2026)

Voici la syntaxe de chaque ordre, telle que l’a restituée, le 4 novembre 2026, la commande suivante :

mrnctl -q '.commands[].syntax' help

Les lignes trop longues ont été coupées pour tenir dans l’encadré, et les parties entre crochets sont facultatives. Cette liste ne dépend pas du projet ouvert : elle était identique sans projet et avec le projet de l’encadré suivant. Elle dépend en revanche de la version de Marionnet, et une version plus récente peut en offrir davantage.

help [<command>]
status
ls [--kind=<kind>] [--can=<action>]
can [<component>]
add <kind> <name> [--ports=<n>] [--<field>=<value>]…
del <component>
get <component> [<field>]
set <component> <field> <value>
rename <component> <new name>
connect <cable> <node>:<port> <node>:<port> [--crossover]
rc-get <component> [<field>|--field=<field>]
rc-set <component> [<one-line content>]
    [--from=<absolute path>] [--enable|--disable]
    [--select|--unselect] [--terminal|--no-terminal]
    [--field=<field>]
ifconfig [<node>]
defects [<node>]
history [<node>]
documents
ifconfig-set <node> <port> <field> [<value>]
    [--restart|--no-restart]
defects-set <node> <port> <inward|outward> <field> [<value>]
    [--restart|--no-restart]
defects-set <cable> <leftward|rightward> <field> [<value>]
history-start <cow file>
history-del <cow file> [--except]
history-set <cow file> <field> [<value>]
history-export <cow file> <variant name> [--force]
open <absolute path>
new <absolute path> [--save|--no-save]
save
save-as <absolute path>
close [--save|--no-save]
notifications [--since=<n>] [--clear]
log <component> [<file>|--file=<file>] [--tail=<n>]
switch-info <switch> [<table>|--table=<table>]
report <component> [--timeout=<s>]
exec <component> <command-line> [--timeout=<s>]
wait <component> (--state=on|off|sleeping | --ready)
    [--timeout=<s>]
wait-all --state=on|off|sleeping [--timeout=<s>]
quit
start <component>
stop <component>
suspend <component>
resume <component>
poweroff <component>
restart <component>
start-all
shutdown-all
poweroff-all
Exemple : ce qui est permis, dans un projet en marche

Ce que mrnctl can autorise dépend, au contraire, de l’état du projet à l’instant où on le demande. Voici sa réponse, mise en tableau, pour un projet de trois machines reliées à un switch par trois câbles, où m1, m2 et le switch s1 sont allumés, et m3 éteinte (Marionnet 1.0.470, 4 novembre 2026) :

Composant État Actions permises
m1, m2 (machines) allumées stop, suspend, poweroff, restart
m3 (machine) éteinte set, del, start
s1 (switch) allumé stop, suspend, poweroff, restart
c1, c2 (câbles de m1 et m2) branchés, en service set, del, suspend
c3 (câble de m3) branché, hors service set, del, suspend

On y lit qu’une machine allumée ne peut être ni modifiée ni supprimée, mais qu’elle peut être arrêtée, suspendue, éteinte brutalement ou redémarrée ; les deux dernières actions sont marquées beyond_gui, car l’interface graphique ne les propose pas pour une seule machine. Un câble, lui, reste modifiable et supprimable quel que soit l’état du réseau, et suspend le débranche. Notons enfin que le câble de m3 est hors service tant que m3 est éteinte.

Les codes de refus

Le champ error d’un refus est un code stable, sur lequel un script peut s’appuyer :

Code Signification
unknown_command aucun ordre de ce nom (la réponse donne la liste de ceux qui existent)
bad_argument nombre d’arguments erroné, option inconnue, valeur mal formée
no_active_project aucun projet n’est ouvert : ouvrez-en un ou créez-en un d’abord
unknown_node, unknown_port, unknown_field, unknown_direction, unknown_target, unknown_state, unknown_can ce qui est nommé n’existe pas
forbidden_transition l’interface graphique l’interdirait aussi en ce moment (cf. mrnctl can)
forbidden_in_exam_mode Marionnet a été lancé avec --exam, et l’ordre ferait perdre la copie de l’étudiant
constraint_violated la valeur est refusée par le même contrôle que dans l’interface graphique
restart_choice_required la modification exige de redémarrer un composant en marche : précisez --restart ou --no-restart
unsaved_changes le projet a été modifié : précisez --save ou --no-save
components_running un composant tourne, et le projet ne peut pas être enregistré dans cet état (cf. § 5)
timeout le délai a expiré (la réponse dit ce qui a été observé en dernier)
internal un bogue de Marionnet ; le détail contient ce qui a pu être capturé
Les codes de retour du pilote, et la mise en forme des réponses
Code Signification
0 Marionnet a accepté l’ordre
1 Marionnet a répondu, et il a refusé
2 l’erreur vient du pilote : pas de point de rendez-vous, outil manquant, option inconnue
3 rien n’est revenu : Marionnet a disparu, il est bloqué, ou il n’a jamais été là

La réponse est un objet JSON. Par défaut, mrnctl l’affiche telle quelle, sur une ligne ; --pretty l’indente, et -q en extrait une partie par un filtre jq :

mrnctl --pretty status

# Le nom de chaque composant :
mrnctl -q '.nodes[].name' ls

# true si m1 peut être supprimée :
mrnctl -q '.can | index("del") != null' can m1

Un ordre est formé d’un verbe, d’arguments, puis d’options (--option=valeur). Les noms des composants ne contenant jamais d’espace, seul le dernier argument d’un ordre qui prend un texte libre (un commentaire, une commande) peut en contenir.


4. Attendre qu’une machine soit prête

Démarrer une machine, c’est démarrer un véritable noyau Linux, ce qui prend quelques secondes. Or mrnctl start m1 répond tout de suite, car sa réponse signifie que l’ordre a été accepté, et non que la machine a fini de démarrer. Un script qui enchaînerait sans attendre s’adresserait donc à une machine qui n’est pas encore là. Il y a deux façons d’attendre, selon ce dont on a besoin.

  • Attendre que la machine tourne, c’est-à-dire que son processus existe :

    mrnctl wait m1 --state=on --timeout=120
    

    L’attente est brève (moins d’une seconde), mais le système de la machine n’est pas encore utilisable : sur une Debian, il lui faut encore environ cinq secondes.

  • Attendre que la machine se dise prête. Marionnet ne peut pas savoir quand une machine est prête pour le TP, c’est-à-dire quand elle a fini ce qu’on attend d’elle (poser ses adresses, lancer ses services…) : seule sa configuration de démarrage le sait (cf. § 6). C’est donc à celle-ci de le signaler, par sa dernière ligne, en créant un fichier convenu (un fichier témoin) dans /mnt/hostfs, un répertoire que Marionnet monte dans chaque machine et qu’il surveille depuis l’hôte :

    echo ready > /mnt/hostfs/marionnet-guest-ready
    

    Du côté du pilote, il ne reste alors qu’à attendre ce fichier témoin :

    mrnctl wait m1 --ready --timeout=300
    

    Le nom du fichier doit être recopié exactement, puisque c’est lui que Marionnet guette. Il est en revanche inutile de l’effacer d’une séance à l’autre : Marionnet ne tient compte que du fichier écrit pendant le démarrage en cours. Enfin, une machine sans configuration de démarrage n’écrit jamais ce fichier, et --ready attendrait donc jusqu’à l’expiration du délai : il faut alors s’en tenir à --state=on.

Transmettre un résultat dans le fichier témoin

Ce que la machine écrit dans le fichier revient dans la réponse de wait --ready. Au lieu du simple mot ready, la configuration de démarrage peut donc y écrire un résultat, par exemple celui d’un ping :

R="ping KO"
ping -c 3 10.0.0.2 > /dev/null && R="ping ok"
echo "$R" > /mnt/hostfs/marionnet-guest-ready

Le pilote le lit alors dans la réponse :

mrnctl -q .line wait m1 --ready --timeout=300
# ping ok
Enchaîner deux sessions : attendre la vraie fin de Marionnet

Si votre script quitte Marionnet pour en relancer un autre aussitôt, attendez que le premier soit réellement terminé : deux sessions qui coexistent se disputent en effet les mêmes ressources de l’hôte. Or mrnctl quit répond avant que Marionnet ait fini de s’arrêter (il faut bien que la réponse parte avant l’arrêt), et Marionnet survit environ une demi-seconde à cette réponse, avec ses machines.

Si c’est votre script qui a lancé Marionnet, comme au § 2, le plus simple est d’attendre la fin du processus par la commande wait du shell, $! désignant le dernier processus lancé en arrière-plan :

marionnet --control-socket "$MARIONNET_CONTROL_SOCKET" &
pid=$!
# … la session …
mrnctl quit
wait "$pid"

Si Marionnet a été lancé autrement (par vous, à la main, ou par un autre script), la réponse de quit donne le numéro du processus, qu’il suffit de surveiller :

pid=$(mrnctl -q .pid quit)
while kill -0 "$pid" 2>/dev/null; do sleep 0.05; done

5. Construire et enregistrer un TP

Le § 2 a montré l’essentiel. Voici les ordres qui servent le plus souvent, avec leur équivalent à la souris :

Ordre Équivalent dans l’interface graphique
new, open, save, save-as, close, quit les entrées du menu Projet
add machine m1, add switch s1 --ports=8… Ajouter, dans la palette
connect c1 m1:eth0 s1:port1 (--crossover pour un câble croisé) ajouter un câble
set m1 memory 256, rename m1 pc1, del m1 Modifier, Supprimer
start, stop, suspend, resume, start-all… les boutons de démarrage et d’arrêt
ifconfig-set, defects-set une case des onglets Interfaces et Anomalies

Deux précautions s’imposent, qui sont aussi celles de l’interface graphique.

  • Marionnet ne perd jamais votre travail sans qu’on le lui dise. Si le projet en cours a été modifié, new et close attendent que vous précisiez --save (enregistrer d’abord) ou --no-save (abandonner les modifications), là où l’interface vous poserait la question.
  • Un réseau en marche ne s’enregistre pas, de même que l’interface grise ses entrées d’enregistrement tant qu’une machine tourne. Arrêtez donc d’abord les machines, ou bien utilisez close --save, qui arrête tout, attend que tout soit arrêté, puis enregistre (comptez alors quelques dizaines de secondes).
Ce que Marionnet choisit, accepte ou refuse quand vous ajoutez un composant
  • Le noyau d’une machine est choisi pour vous. Comme la fenêtre Ajouter, l’ordre add machine prend un noyau qui convient au système de la machine : une machine ajoutée ainsi démarre sans autre réglage.
  • La distribution et le noyau demandés doivent être installés sur l’hôte. Une distribution (le système de fichiers de la machine virtuelle) qui n’y est pas installée, demandée par l’option --distrib= de add ou par set m1 distrib, est refusée, et le refus donne la liste des distributions installées ; il en va de même pour un noyau que cette distribution ne prévoit pas. Marionnet ne remplace donc jamais en silence ce que vous avez demandé.
  • Le nombre de ports suit les limites de l’interface : de 1 à 8 cartes réseau pour une machine, de 4 à 16 ports pour un hub, un switch, un routeur ou un Gateway, de 1 à 16 pour un NAT bridge ou un LAN bridge.
  • Un ajout refusé ne laisse rien derrière lui : le nom reste libre, et le script peut réessayer sous le même nom, une fois l’erreur corrigée.
  • Un câble ne se renomme pas, ni à la souris ni par un ordre : pour changer son nom, supprimez-le, puis reliez-en un nouveau.
Pourquoi il faut arrêter les machines avant d'enregistrer

Le fichier .mar contient, outre la description du réseau, le disque de chaque machine (ou plus exactement ce qui a changé par rapport à l’image d’origine). Enregistrer une machine en marche reviendrait donc à recopier un disque en pleine écriture, et rouvrir ensuite le projet reviendrait à rallumer une machine après une coupure de courant. D’où le refus, qui est aussi celui de l’interface graphique.

close --save règle la question, puisqu’il arrête les machines, attend qu’elles soient arrêtées, puis enregistre. Cela peut prendre plusieurs dizaines de secondes, que l’option --timeout ne borne pas : laissez donc au script le temps de l’arrêt.

Pour la même raison, un fichier .mar ne se fabrique pas à la main. La description du réseau y est certes écrite en texte (cf. ../project-format-v3.md), mais les disques ne peuvent être produits que par Marionnet : construisez donc le réseau par des ordres, et enregistrez-le par save-as.

Adresses et anomalies, case par case

Ces deux onglets se lisent et s’écrivent par deux paires d’ordres :

  • l’onglet Interfaces, par mrnctl ifconfig et mrnctl ifconfig-set ;
  • l’onglet Anomalies, par mrnctl defects et mrnctl defects-set.

L’écriture se fait une case à la fois, en désignant la colonne par un nom que donne le champ slugs de la lecture :

mrnctl -q '.slugs[]' ifconfig m1
# mac-address
# mtu
# ipv4-address
# …

mrnctl ifconfig-set m1 eth0 ipv4-address 10.0.0.1/24
mrnctl defects-set  m1 eth0 inward loss 5 --no-restart
mrnctl defects-set  c1 rightward maximum-delay-ms 50

La valeur est contrôlée exactement comme dans l’interface graphique. Quand une modification exige de redémarrer une machine en marche, l’interface vous demanderait s’il faut le faire tout de suite ; un script doit répondre d’avance, par --restart ou --no-restart. Un câble branché reçoit en revanche sa nouvelle anomalie à chaud, sans redémarrage.


6. Faire travailler les machines

Les ordres de Marionnet agissent sur le réseau, vu de l’extérieur. Pour ce qui se passe à l’intérieur d’une machine, il existe deux moyens.

  • La configuration de démarrage, la fonctionnalité que l’interface graphique propose sous ce nom, est un petit script bash que la machine exécute à la fin de son démarrage. Il peut configurer la machine, jouer sa part du TP (lancer un ping, par exemple), écrire des résultats, puis signaler que la machine est prête :

    mrnctl rc-set m1 --from=/tmp/scenario.sh --enable
    mrnctl start m1
    mrnctl wait  m1 --ready --timeout=300
    

    Ce script étant lu au démarrage, il ne prend effet qu’au prochain démarrage de la machine.

  • L’ordre exec fait exécuter une commande à une machine déjà en marche, et rend sa sortie :

    mrnctl exec m1 uname -r
    mrnctl -q .output exec m1 -- ping -c 1 -W 2 10.0.0.2
    

    Le -- isolé sépare les options du pilote de celles de la commande : sans lui, une option comme --all serait prise par le pilote pour l’une des siennes.

Pour récupérer ce qu’une machine a produit, le plus simple est qu’elle l’écrive dans /mnt/hostfs. Ce répertoire de la machine virtuelle est en réalité un répertoire de l’hôte, que votre script lit directement ; mrnctl rc-get m1 en donne le chemin, dans le champ hostfs.

Un exemple de configuration de démarrage
# Le script que m1 exécutera à la fin de son démarrage,
# écrit ici sur l'hôte :
cat > /tmp/scenario.sh <<'EOF'
ip addr add 10.0.0.1/24 dev eth0 2>/dev/null
ip link set eth0 up
ping -c 3 10.0.0.2 > /mnt/hostfs/ping.log 2>&1
echo "$?" > /mnt/hostfs/ping.status
# Dernière ligne : signaler que la machine est prête.
echo ready > /mnt/hostfs/marionnet-guest-ready
EOF

# Le confier à m1, la démarrer, attendre qu'elle soit prête :
mrnctl rc-set m1 --from=/tmp/scenario.sh --enable
mrnctl start m1
mrnctl wait  m1 --ready --timeout=300

# Lire, depuis l'hôte, ce que la machine a écrit :
hostfs=$(mrnctl -q '.hostfs' rc-get m1)
cat "$hostfs/ping.log"

Pour un script d’une seule ligne, le contenu peut aussi être donné directement dans l’ordre :

mrnctl rc-set m1 'echo bonjour'

Aujourd’hui, les machines, les switchs et les routeurs ont une configuration de démarrage ; les hubs, les nuages et les composants d’accès au réseau réel n’en ont pas.

Les démons de routage d'un routeur

Un routeur a, outre sa configuration Unix, une configuration par démon de routage, de même que sa fenêtre Modifier a un onglet par protocole. Demandez-lui lesquels il possède, puis nommez-en un par --field= :

mrnctl -q '.available | join(" ")' rc-get r1
# rc_config_unix zebra rip ripng ospf bgp ospf6 isis

mrnctl rc-set r1 --from=/tmp/zebra.conf --field=zebra

# Ce démon-ci ne démarrera pas :
mrnctl rc-set r1 --field=ospf --unselect

# Ouvrir le terminal de zebra, à la CISCO-IOS :
mrnctl rc-set r1 --field=zebra --terminal

Écrire la configuration d’un démon coche à la fois sa case activée (votre texte est utilisé) et sa case sélectionnée (le démon démarre), car une configuration écrite pour rien serait une mauvaise surprise ; --unselect et les options voisines permettent d’en décider autrement.

Les guillemets dans exec

Supposons que vous vouliez faire exécuter à m1 la commande sh -c 'exit 7', qui doit rendre le code 7. Avant d’atteindre la machine, cette commande passe par votre shell, qui retire ses propres guillemets : il faut donc la transmettre en un seul argument, entre guillemets doubles.

# CORRECT : la machine reçoit  sh -c 'exit 7'  (code 7).
mrnctl exec m1 "sh -c 'exit 7'"

# ERREUR : votre shell a retiré les guillemets,
# la machine reçoit  sh -c exit 7  (code 0).
mrnctl exec m1 -- sh -c 'exit 7'
Ce que Marionnet garde de chaque exec, et ses limites

Chaque commande exécutée par exec est notée, avec sa date et son code de retour, dans un journal propre à la machine, le journal exec (cf. § 7) : à la correction, elle ne se confond donc jamais avec ce que l’étudiant a tapé lui-même.

Deux limites protègent par ailleurs le script : la sortie rendue est limitée à 200 lignes, et l’option --timeout=<s> borne la durée de la commande. À l’expiration du délai, la commande est arrêtée dans la machine, et la réponse le signale.


7. Savoir ce qui s’est passé

Marionnet sait ce qu’il a construit, et les machines savent ce qui s’est passé en elles ; les uns comme les autres s’interrogent :

# Les composants et leur état, puis les adresses de m1 :
mrnctl ls
mrnctl ifconfig m1

# Ce qu'a fait la configuration de démarrage de m1 :
mrnctl log m1

# Ce qui a été tapé dans son terminal :
mrnctl log m1 commands

# Lui demander un rapport sur son état, puis le lire :
mrnctl report m1
mrnctl log m1 report

Le rapport mérite une explication. Les journaux sont des traces : ils disent quelles commandes ont été lancées, mais pas toujours l’état qui en résulte. Supposons par exemple que la configuration de démarrage d’une machine contienne la ligne suivante :

echo 1 > /proc/sys/net/ipv4/ip_forward

Le transfert de paquets est alors bien activé, mais le journal de la configuration de démarrage n’en garde que echo 1, car sa trace ne note pas les redirections. Le rapport, lui, est une photographie de la machine à un instant donné (ses adresses réelles, ses routes, ses voisins, son pare-feu) : c’est donc sur lui qu’il faut s’appuyer pour savoir si une machine est dans l’état voulu.

Enfin, l’onglet Historique se pilote lui aussi : mrnctl history m1 liste les états de disque d’une machine, et history-start redémarre la machine depuis l’un d’eux, comme l’entrée Démarrer dans cet état de l’interface, ce qui permet de rejouer un TP à partir d’un point connu.

Les sept journaux d'une machine
Journal Écrit par Contenu
rc_config la machine la trace de sa configuration de démarrage, ses erreurs, et la commande qui a échoué (un switch a aussi ce journal, écrit par Marionnet)
boot la machine ce qu’a fait son démarrage : messages du noyau, services en échec
commands la machine les commandes tapées, chacune avec sa date
report la machine, sur demande ou à son arrêt propre le rapport décrit ci-dessus
exec la machine les commandes exécutées par mrnctl exec, avec leur date et leur code de retour
console Marionnet, sur l’hôte la console de la machine, depuis la première ligne (option --console-log)
terminal Marionnet, sur l’hôte l’enregistrement du terminal, commandes et sorties (option --terminal-log)

Les deux derniers n’existent que si Marionnet a été lancé avec l’option correspondante, car enregistrer une session à l’insu de l’utilisateur relèverait de la surveillance plutôt que de l’enseignement ; --exam active les deux (cf. ../exam-mode.FR.md). Notons aussi que les cinq premiers sont écrits dans un répertoire où l’étudiant peut écrire, alors que la console et le terminal, écrits par l’hôte, sont hors de sa portée.

Un journal survit à ce qu’il décrit : une machine éteinte répond toujours à log. Quand un journal n’existe pas encore, le refus explique pourquoi (machine jamais démarrée, démarrage pas encore terminé, option absente au lancement). Une réponse porte au plus 400 lignes, dont --tail=<n> permet de choisir les dernières.

Ce que sait un switch en ce moment

mrnctl switch-info s1 demande à un switch en marche ce qu’il sait à l’instant même : ses ports, sa table d’adresses MAC, ses VLAN, son arbre couvrant. Ces informations ne sont écrites nulle part, et disparaissent avec le switch ; ce qu’il a dit à son démarrage reste en revanche dans son journal (mrnctl log s1).


8. Écrire les ordres dans un fichier

Plutôt qu’un script shell, un TP peut s’écrire comme une simple liste d’ordres, un par ligne, dans un fichier .mrn :

cat > tp.mrn <<'EOF'
new /tmp/tp.mar
add machine m1
add machine m2
add switch s1 --ports=4
connect c1 m1:eth0 s1:port1
connect c2 m2:eth0 s1:port2
save-as /tmp/tp.mar
EOF

mrnctl -f tp.mrn

Le fichier s’exécute ligne à ligne, et s’arrête au premier refus (--keep-going permet de continuer). Notons qu’un échec au milieu du fichier laisse le projet dans l’état où l’ont mis les lignes précédentes, puisque rien n’est annulé ; d’où l’intérêt de vérifier le fichier avant de l’envoyer, ce que fait mrn-check (installé, comme les deux outils qui suivent, avec Marionnet), sans rien envoyer à Marionnet :

mrn-check tp.mrn
# tp.mrn: 9 request(s), no error

Il signale les ordres inconnus, les ports qui ne peuvent pas exister (port0 sur un switch), les composants utilisés avant d’avoir été ajoutés, ou encore les ports déjà occupés.

Enfin, quand un TP a besoin de plus qu’une liste d’ordres (attendre une machine, lire un résultat, répéter une opération sur chaque machine), mrn2sh traduit le fichier en un script shell ordinaire, que vous complétez ensuite à la main :

mrn2sh tp.mrn > tp.sh
Vérifier sans Marionnet en marche, et détails de la traduction

mrn-check demande le vocabulaire au Marionnet en cours d’exécution. Pour vérifier un fichier sans Marionnet, prenez une fois un instantané du vocabulaire, que vous reprendrez quand Marionnet changera :

mrnctl help > grammar.json
mrn-check --grammar=grammar.json tp.mrn

Les vérifications portant sur les noms supposent que le fichier construise lui-même le projet, c’est-à-dire qu’il commence par new. Après un open, mrn-check ne connaît pas le projet : il ne vérifie alors que la syntaxe et les noms de ports, et le dit. Ce qui ne peut se décider qu’en marche (une machine peut-elle démarrer maintenant ?) reste l’affaire de Marionnet.

mrn2sh refuse de traduire un fichier qui contient une erreur. Le script qu’il produit affiche chaque ordre avant de l’envoyer, conserve vos commentaires, et accepte en argument un autre fichier de projet que celui du .mrn :

./tp.sh /tmp/autre.mar

La traduction est à sens unique : une fois le script modifié, c’est lui qui devient la source, et le régénérer l’écraserait.


9. Vérifier un TP

Un fichier .mrn dit de quoi un réseau est fait ; un fichier .mrv dit ce qui doit en être vrai, à raison d’une affirmation par ligne, et mrn-verify le confronte à la session en cours :

cat > tp.mrv <<'EOF'
state m1 is on
cable c1 m1:eth0 s1:port1
journal m1 rc_config ok
report m1 says ~ net[.]ipv4[.]ip_forward *= *1
reaches m1 m2
EOF

mrn-verify tp.mrv
# PASS  state m1 is on
# PASS  cable c1 m1:eth0 s1:port1
# …

Chaque ligne reçoit l’un de trois verdicts : PASS si l’affirmation est vraie, FAIL si elle est fausse, et SKIP quand Marionnet n’offre aucun moyen de le savoir, par exemple pour un ping vers une machine éteinte, dont l’adresse ne peut pas être lue. Notons que reaches m1 m2 lance un ping depuis m1 vers l’adresse de m2, lue dans le rapport de m2 : un pare-feu qui bloque ICMP donne donc FAIL, comme le ferait un ping tapé à la main. Ce troisième verdict a son importance, car un vérificateur qui répondrait FAIL dans ce cas sanctionnerait un étudiant pour une limite de l’outil ; l’option --strict permet toutefois de compter un SKIP comme un échec. La liste des affirmations possibles (neuf formes) est donnée par mrn-verify --help, et la vérification ne modifie rien dans le projet. Enfin, un motif précédé de ~, comme dans la ligne report de l’exemple, est une expression régulière étendue (celle de grep -E) ; sans ~, le motif est un texte cherché tel quel.

Les neuf formes d'affirmation (Marionnet 1.0.470)

Telles que les donne mrn-verify --help :

state <component> is on|off|sleeping
field <component> <field> is <value>
cable <cable> <node>:<port> <node>:<port>
switch <switch> <table> has <key>=<value> [<key>=<value>]…
journal <component> <journal> contains|lacks <pattern>
journal <component> <journal> ok
documents <component> has <pattern>
report <component> says|lacks <pattern>
reaches <component> <component>|<address>
La trace et l'état : deux affirmations qui se ressemblent
journal m1 rc_config contains ip_forward
report  m1 says     ~ net[.]ipv4[.]ip_forward *= *1

La première lit la trace de la configuration de démarrage, la seconde le rapport. Sur la machine de l’exemple du § 7, dont la configuration active le transfert de paquets par une redirection, la première échoue, puisque la trace ne contient que echo 1, alors que la seconde réussit. Un corrigé doit donc s’appuyer sur la trace pour prouver qu’une commande a été lancée, et sur le rapport pour prouver son effet, c’est-à-dire l’état dans lequel la machine se trouve réellement.

Pour un script ou une IA : codes de retour et sortie JSON

mrn-verify rend 0 si tout est vrai, 1 si une affirmation est fausse (ou si le fichier contient une erreur), et 2 si rien n’a pu être vérifié. --json donne un objet par affirmation, avec sa ligne et sa raison. Comme mrn-check, il peut vérifier le fichier lui-même hors session (--grammar=grammar.json), et dit alors d’avance ce qui ne pourra pas être vérifié. Enfin, --refresh=never lit le dernier rapport de chaque machine au lieu d’en demander un nouveau, ce qui convient pour noter une session terminée.


10. Quand ça ne marche pas

Ce que vous voyez Ce qu’il faut faire
Marionnet refuse de démarrer (--control-socket: …) le chemin est relatif, trop long, ou déjà utilisé par un autre Marionnet : le message précise lequel
no control socket indiquez le point de rendez-vous (export MARIONNET_CONTROL_SOCKET=…)
not a unix socket le chemin est faux, ou bien Marionnet a été lancé sans --control-socket
no answer from … Marionnet a disparu, ou une fenêtre attend une réponse à l’écran (avec --keep-dialogs, c’est à l’humain de la fermer)
no_active_project créez ou ouvrez d’abord un projet
unsaved_changes précisez --save ou --no-save
components_running à l’enregistrement arrêtez d’abord les machines, ou utilisez close --save
forbidden_transition l’interface graphique l’interdirait aussi : demandez mrnctl can <composant>
restart_choice_required ajoutez --restart ou --no-restart
timeout sur wait … --ready la machine n’a pas écrit le fichier convenu : vérifiez que sa configuration de démarrage est activée (--enable) et qu’elle a bien démarré
réponse tardive à un ordre long augmentez son --timeout= ; le pilote règle sa propre attente sur cette valeur

11. Pour aller plus loin

  • mrnctl help donne tous les ordres, toujours à jour.
  • mrnctl --help, mrn-check --help et mrn-verify --help donnent les options de chaque outil.
  • examples/ contient des scripts complets, prêts à être exécutés.
  • ../teacher-guide.FR.md présente le pilotage vu depuis une salle de TP (préparer un TP, mener la séance, corriger), avec un TP complet dans ../labs/session-7/.
  • ../exam-mode.FR.md décrit le mode examen.
  • ../marionnet-driving-skill.md est le guide à donner à une IA pour qu’elle pilote Marionnet, c’est-à-dire pour qu’elle construise, vérifie et note un TP par des commandes.
Pour les développeurs

Dans les sources de Marionnet :

  • docs/pilotage-par-script.md : la conception de l’interface de pilotage, ses motivations et le journal de sa construction ;
  • docs/journalisation-profonde.md : la conception des journaux ;
  • bin/scripts/marionnet-ctl.sh : le pilote.

Ce guide décrit la forme de l’interface et ses règles durables. Il ne recopie ni la liste des ordres ni leurs options, que le Marionnet en cours d’exécution publie lui-même par help : en cas de désaccord, c’est donc help qui a raison.