Compare commits

...

4 commits

Author SHA1 Message Date
0446c9084e
Merge branch 'docs' 2026-09-05 23:17:33 +02:00
45f8e43de8
docs: configuration for orchestrator
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-08-27 23:56:47 +02:00
cb904c4744
docs: premiere creation de documentation
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-08-26 23:41:50 +02:00
9a46b5cde7
docs: add first doc version
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-08-26 22:36:46 +02:00
34 changed files with 2343 additions and 0 deletions

37
docs/README.md Normal file
View file

@ -0,0 +1,37 @@
# Documentation `two`
Construction locale, avec rechargement automatique :
```bash
TMPDIR=$(mktemp -d)
python3 -m venv "${TMPDIR}/venv"
source "${TMPDIR}/venv/bin/activate"
pip install --upgrade pip
pip install -r requirements.txt
sphinx-autobuild . "${TMPDIR}/build"
```
Construction simple :
```bash
sphinx-build -b html . _build/html
```
La référence de l'API est générée depuis `../api/agent.yaml` : c'est la source unique du
contrat, la documentation ne la recopie pas.
## Schémas
Les schémas simples sont en mermaid, directement dans les pages. Les schémas travaillés sont des
SVG dans `schemas/`, en deux variantes :
```
schemas/<nom>.svg thème clair → .. figure:: /schemas/<nom>.svg :class: only-light
schemas/<nom>-dark.svg thème sombre → .. figure:: /schemas/<nom>-dark.svg :class: only-dark
```
`schemas/` est dans `exclude_patterns` : Sphinx n'y cherche pas de pages, mais copie les fichiers
référencés par une directive `figure` ou `image`.
Un export draw.io se dépose tel quel sous ce nom. Exporter en **SVG éditable** (« Include a copy
of my diagram ») pour pouvoir rouvrir le fichier dans draw.io ensuite.

View file

@ -0,0 +1,86 @@
Invariants et pièges
====================
Contraintes découvertes en production ou en corrigeant des bugs. Les enfreindre casse quelque
chose qui fonctionne, souvent en silence.
.. note::
Cette page reprend la section « Invariants et pièges » de ``CLAUDE.md``, qui reste la
référence de développement et fait foi en cas d'écart.
Réseau
------
* **Ne pas retirer la route ``/32`` vers ``169.254.169.254``** de l'option 121, même quand elle
paraît redondante avec la route par défaut. La DNAT vers le serveur de metadata est posée dans
le netns du VPC en ``PREROUTING`` : le paquet n'y est traité en L3 que si son next-hop est
``interface_ip``. Avec un autre next-hop, la trame est commutée en L2 sans traverser
``PREROUTING``, et le provisionnement cloud-init échoue.
* **RFC 3442** : un client qui lit l'option 121 **ignore l'option 3**. Toute route par défaut
doit donc figurer dans la 121 ; l'option 3 ne sert que les clients qui n'implémentent pas la
121.
* **La route vers le CIDR du VPC garde ``interface_ip`` comme next-hop** dans tous les modes sauf
``bridge`` : sur un subnet à IP publique, le trafic interne ne doit pas sortir par la gateway
publique.
* ``169.254.169.254`` est centralisé dans ``metadata.ServiceIP`` — ne pas le réécrire en dur.
QEMU et VM
----------
* **Un seul disque ``vdX`` par VM** ; les disques additionnels passent par le SCSI (``sdX``). La
carte PCI en dépend : NIC en ``0x03``, contrôleur SCSI en ``0x1e``, virtio-blk en ``0x1f``.
* ``bus=pci.0`` est explicite sur les trois ``-device`` : un passage de la machine en **q35**
casserait le démarrage (``Bus 'pci.0' not found``).
* QEMU est lancé par ``systemd-run --scope``, **jamais** en unit transitoire : le scope est
exécuté par le processus appelant et hérite du netns posé par ``netns.Call``. Une unit
transitoire, forkée par PID 1, démarrerait dans le netns racine et ne verrait pas le tap.
* L'arrêt d'une VM ne touche **jamais** aux fichiers disque. En revanche, un ``quit`` brutal est
envoyé à l'expiration de ``dispatcher.timeout_seconds``.
Arrêt de l'agent
----------------
* Ordre imposé : serveurs HTTP → drainage des workers → fermeture de la base. L'inverser crée une
course.
* Budget d'arrêt dépassé ⇒ **la base n'est pas fermée** : fermer Badger sous un écrivain
concurrent est pire qu'un rejeu du journal au démarrage suivant.
* Jamais de ``log.Fatal`` dans une goroutine : ``os.Exit`` n'exécute aucun ``defer``.
cloud-init
----------
* ``network-config.tmpl`` cible ``eth0`` alors que les guests sont en ``ens3`` : il ne s'applique
donc à rien, et le réseau vient du DHCP. **Ne pas le « corriger » ni le supprimer** — le rendre
opérant ferait remplacer par cloud-init la configuration réseau de l'image sur toutes les VM.
* Un document fourni par l'appelant est servi **verbatim** ; un document absent retombe sur le
template ; un document explicitement vide est servi vide. Les trois cas sont distincts.
* ``metadata.password`` est un **hash**, pas un mot de passe en clair.
* ``instance-id`` vaut le nom de la VM : recréer une VM du même nom sur le même disque fait que
cloud-init la reconnaît et **n'applique pas** le user-data.
Configuration
-------------
* Le chargement se fait par **viper** : tags ``mapstructure``, jamais ``yaml``.
* Un chemin configurable se propage par les signatures de fonction, jamais par une variable ou un
setter de paquet.
Sécurité connue et acceptée
---------------------------
Ces points sont documentés parce qu'ils sont **assumés en l'état**, pas parce qu'ils sont sans
conséquence. Ils doivent être réévalués avant toute exposition élargie de l'API.
* **L'API n'a aucune authentification** et l'exemple de configuration l'expose sur
``0.0.0.0:8080``. Quiconque atteint ce port pilote le host KVM.
* ``vm/<name>/password`` est stocké tel quel (c'est un hash) et restitué par ``/db?prefix=vm/``
du serveur d'administration — contenu par ``admin.enabled: false`` et l'écoute en boucle
locale.
* ``/run/two/metadata/<vm>/vendor-data`` est en ``0644`` et contient ce hash : tout compte local
du host peut le lire.
* ``pkg/systemd.New()`` n'a **pas de timeout** : si le socket D-Bus accepte sans répondre,
l'appelant se fige. Concerne le watchdog, la création et la suppression de subnets, et le
serveur de metadata.
* Il n'y a **pas de rollback** : un échec partiel de création laisse des objets réseau orphelins
jusqu'à un ``DELETE`` explicite.

View file

@ -0,0 +1,13 @@
Architecture
============
L'organisation interne de l'agent : découpage en paquets, stockage, et contraintes à ne pas
enfreindre. Ces pages s'adressent à qui modifie le code ; ``CLAUDE.md``, à la racine du dépôt,
reste la référence de développement et fait foi en cas d'écart.
.. toctree::
:maxdepth: 1
vue-densemble
stockage
contraintes

View file

@ -0,0 +1,51 @@
Schéma des clés
===============
Toutes les valeurs stockées dans Badger sont des **chaînes plates** : une clé, une valeur, pas
de sérialisation structurée.
.. code-block:: text
vpc/<name>/state → creating | running | error | deleting | deleted
vpc/<name>/cidr → <cidr>
subnet/<name>/state → creating | running | error | deleting | deleted
subnet/<name>/vpc → <vpc-name>
subnet/<name>/mode → vxlan | bridge | public_ip
subnet/<name>/vxlan_id → <id> (mode vxlan uniquement)
subnet/<name>/cidr → <cidr>
subnet/<name>/interface_ip → <ip> (gateway, portée par br-<subnetID>)
subnet/<name>/local_iface → <bridge-name>
subnet/<name>/default_route → "true" | "false"
subnet/<name>/gateway → <ip> (optionnel)
subnet/<name>/dhcp/<ip> → <mac>
vm/<name>/state → creating | running | error | deleting | deleted
vm/<name>/subnet → <subnet-name>
vm/<name>/tap_id → <int>
vm/<name>/ip → <ip>
vm/<name>/metadata_port → <port>
vm/<name>/disk/<dev> → <path> (une clé par disque : sda, vda, …)
vm/<name>/memory → <int> (Mo)
vm/<name>/cpus → <int>
vm/<name>/uefi → "true" (absent si SeaBIOS)
vm/<name>/password → <hash> (optionnel — un hash, pas un mot de passe)
vm/<name>/sshkey → <pubkey> (optionnel)
vm/<name>/metadata/<document> → <contenu brut> (optionnel : user-data, vendor-data, …)
Règles
------
**Pas de duplication.** Une ressource ne stocke que ce qui lui est propre. Une VM garde le lien
``vm/<name>/subnet`` ; le VPC, le bridge et l'``interface_ip`` sont lus depuis le subnet, leur
source canonique.
**Les états passent par ``state``.** Toujours ``state.Set`` / ``state.Get`` : ``Set`` refuse une
valeur hors énumération, ``Get`` refuse de retourner une valeur non reconnue. ``error`` n'est
écrit que par ``Dispatcher.Dispatch``, via ``cmd.Key()``.
**Tout entier lu depuis la base peut être corrompu.** Les erreurs de conversion sont retournées,
jamais ignorées : une valeur absente ou illisible est un état d'erreur réel.
**Un seul ouvreur.** L'agent est le seul processus à ouvrir la base. Le serveur de metadata lit
des fichiers écrits par l'agent sous ``metadata.run_dir``, jamais Badger.

View file

@ -0,0 +1,90 @@
Vue d'ensemble
==============
Cycle d'une requête
-------------------
.. code-block:: text
HTTP → internal/api/agent → Dispatcher.Prepare() → Dispatcher.Dispatch() → worker.Queue → Command.Execute()
**Prepare** (synchrone, dans le handler HTTP)
valide l'état, écrit l'état initial (``creating`` / ``deleting``) en base, et retourne 202 ou
une erreur.
**Dispatch** (asynchrone)
place la commande sur un canal bufferisé ; une goroutine worker appelle ``Execute``. C'est
``Dispatch``, et lui seul, qui marque la ressource en ``error`` si ``Execute`` échoue.
**Execute**
effectue le travail réseau (netns, netif, VXLAN, veth, bridge, DHCP), puis met l'état à
``running`` / ``deleted``.
Paquets
-------
.. list-table::
:header-rows: 1
:widths: 32 68
* - Chemin
- Rôle
* - ``internal/api/agent``
- handlers HTTP de ``/vpcs``, ``/subnets`` et ``/vms``
* - ``internal/dispatcher/agent``
- interface ``Command`` (``Prepare``/``Execute``/``Key``) et commandes concrètes
* - ``internal/state``
- énumération des états, ``CanDelete``/``IsTransient``, seul point d'écriture des états
* - ``internal/migration``
- migrations idempotentes jouées au démarrage de l'agent
* - ``internal/vpc``, ``internal/subnet``
- création et suppression bas niveau (netns + netif)
* - ``internal/netns``
- network namespaces : create/enter/delete/call
* - ``internal/netif``
- netlink : bridge, veth, vxlan, tap, routes, adresses
* - ``internal/ebtables``, ``internal/iptables``
- wrappers dédiés ; ne pas appeler ces binaires ailleurs
* - ``internal/qemu``, ``internal/qmp``
- lancement de QEMU et client QMP sur socket Unix
* - ``internal/vm``
- cycle de vie d'une VM : tap, iptables, metadata, qemu
* - ``internal/dhcp``
- génération des configurations dnsmasq et entrées ip → mac
* - ``internal/metadata``
- serveur de metadata cloud-init et ses templates
* - ``internal/watchdog``
- vérification périodique en lecture seule
* - ``internal/config/agent``
- chargement par viper — tags ``mapstructure``, jamais ``yaml``
* - ``internal/prometheus/agent``
- collector des métriques ``syonad_*``
* - ``pkg/db/kv``
- wrapper Badger ; toutes les valeurs sont des chaînes plates
* - ``pkg/worker``
- pool de goroutines sur canal
* - ``pkg/systemd``
- client D-Bus systemd
* - ``pkg/logger``, ``pkg/prometheus``
- journalisation ``slog`` et serveur de métriques
Ajouter un type de ressource
----------------------------
#. ajouter les helpers KV dans ``pkg/db/kv`` si nécessaire ;
#. définir ``Create<X>`` / ``Delete<X>`` dans un nouveau paquet ``internal/<x>/`` ;
#. ajouter ``Create<X>Command`` / ``Delete<X>Command`` dans ``internal/dispatcher/agent/``, dont
``Key()`` qui retourne ``<x>/<name>`` et le contrôle ``state.CanDelete`` dans
``Delete<X>Command.Prepare`` ;
#. ajouter les handlers HTTP dans ``internal/api/agent/`` et les routes dans ``server.go``.
Stubs de plateforme
-------------------
Les fichiers ``_linux.go`` portent l'implémentation netlink/netns réelle ; les ``_other.go``
correspondants retournent une erreur « not supported on this platform ». Tous les paquets
**compilent** sur macOS, ce qui permet d'y tester la logique qui ne touche ni netlink ni netns.
Deux exceptions à connaître : les stubs de ``netns`` exécutent ``fn`` **sans changer de
namespace** — ``netns.Call`` réussit donc hors Linux — et ``netif`` compile partout parce que
netlink fournit une implémentation « unspecified ».

View file

@ -0,0 +1,63 @@
Cycle de vie des ressources
===========================
VPC, subnets et VM partagent le même jeu d'états.
.. mermaid::
stateDiagram-v2
[*] --> creating
creating --> running
creating --> error
running --> deleting
running --> error
deleting --> deleted
deleting --> error
error --> deleting
deleted --> [*]
.. list-table::
:header-rows: 1
:widths: 15 85
* - État
- Signification
* - ``creating``
- la demande est acceptée et enregistrée ; ``Execute`` n'a pas encore abouti
* - ``running``
- la ressource existe sur le système
* - ``error``
- ``Execute`` a échoué ; état **terminal**, il n'y a pas de reprise automatique
* - ``deleting``
- suppression en cours
* - ``deleted``
- suppression terminée
Suppression
-----------
Elle n'est autorisée que depuis ``running`` ou ``error`` — sinon **409**. Depuis ``error``, elle
est **best-effort** : les ressources système peuvent n'avoir été créées que partiellement.
Un VPC ne peut être supprimé qu'une fois tous ses subnets supprimés.
Pas de rollback
---------------
En cas d'échec partiel pendant une création, les ressources réseau déjà créées **ne sont pas
nettoyées**. C'est un choix délibéré : le nettoyage est déclenché explicitement par une
suppression, qui est justement autorisée depuis ``error``.
Conséquence pour l'appelant : après un passage en ``error``, émettre un ``DELETE`` avant toute
tentative de recréation, faute de quoi la recréation butera sur des objets système résiduels.
États transitoires au redémarrage de l'agent
--------------------------------------------
La file d'attente des workers est **en mémoire**. Une ressource restée en ``creating`` ou
``deleting`` au moment d'un arrêt de l'agent est donc nécessairement orpheline : plus personne
ne la traite.
Au démarrage, une migration idempotente bascule ces ressources en ``error``, et traduit
l'ancien vocabulaire d'états. Une ressource retrouvée en ``error`` après un redémarrage n'a donc
pas forcément échoué techniquement — elle peut simplement avoir été interrompue.

14
docs/concepts/index.rst Normal file
View file

@ -0,0 +1,14 @@
Concepts
========
Le modèle de données, les modes réseau et le cycle de vie des ressources : comment les éléments
fonctionnent entre eux. Le contrat HTTP correspondant est dans
:doc:`/exploitation/api-agent/index`.
.. toctree::
:maxdepth: 1
vpc-subnet-vm
modes-reseau
cycle-de-vie
metadata-cloud-init

View file

@ -0,0 +1,71 @@
Metadata et cloud-init
======================
Chaque VM dispose d'un serveur de metadata NoCloud, servi sur ``169.254.169.254`` dans le netns
de son VPC, sous la forme d'une instance systemd ``metadata@<vm>``.
Chaîne de production
--------------------
.. mermaid::
graph LR
A["agent<br/>WriteNoCloudFiles"] -->|"/run/two/metadata/&lt;vm&gt;/"| M["binaire metadata"]
M -->|HTTP 169.254.169.254| G["guest<br/>cloud-init"]
L'agent écrit les fichiers cloud-init sur disque **avant** de démarrer le service ; le binaire
``metadata`` les lit et les sert. Le processus ``metadata`` n'ouvre **jamais** la base Badger :
deux processus ne doivent pas partager une même instance.
Documents servis
----------------
Chaque document suit la même règle :
* fourni par l'appelant → servi **verbatim**, l'agent n'interprète rien ;
* absent → le template par défaut est rendu ;
* fourni **vide** → servi vide.
Les deux derniers cas sont distincts, et c'est délibéré : fournir une chaîne vide est une façon
explicite de neutraliser un document.
Champs de ``metadata``
----------------------
``sshkey``
Clé publique ajoutée au compte ``syonad``. Transmise telle quelle, non encodée.
``password``
Un **hash**, tel qu'attendu par la clé ``passwd`` de cloud-config (``$6$…``) — jamais un mot
de passe en clair. Omis, le compte est créé verrouillé ; sans ``password`` ni ``sshkey``,
aucun compte n'est créé.
``user_data``
Le user-data cloud-init, **encodé en base64**. L'encodage évite l'échappement JSON des
documents multi-lignes et autorise les charges ``gzip+base64``. Un base64 invalide est rejeté
en 400 plutôt que servi vide.
``instance-id``
------------------
``instance-id`` vaut le **nom de la VM**. Recréer une VM du même nom sur le même disque fait que
cloud-init la reconnaît comme déjà provisionnée et **n'applique pas** le user-data. Pour rejouer
un provisionnement : changer de nom, repartir d'un disque neuf, ou exécuter
``cloud-init clean --logs`` dans le guest avant l'extinction.
Configuration réseau
--------------------
.. warning::
``network-config.tmpl`` cible ``eth0`` alors que les guests utilisent ``ens3`` : il ne
s'applique donc à rien, et le réseau des VM vient du DHCP. **Ne pas le « corriger » ni le
supprimer.** Le rendre opérant ferait remplacer par cloud-init la configuration réseau de
l'image, sur toutes les VM.
Sécurité
--------
``/run/two/metadata/<vm>/vendor-data`` est en ``0644`` et contient le hash de mot de passe. Tout
compte local du host peut le lire. C'est une exposition connue et acceptée en l'état ; elle
disqualifie l'usage de hashs faibles ou réutilisés.

View file

@ -0,0 +1,99 @@
Modes réseau
============
Le champ ``mode`` d'un subnet détermine la façon dont il est raccordé à l'host, et les routes
annoncées aux VM.
.. list-table::
:header-rows: 1
:widths: 15 45 40
* - Mode
- Raccordement
- État
* - ``vxlan``
- tunnel VXLAN (``vxlan_id``) + bridge dans le netns du VPC
- défaut
* - ``bridge``
- rattachement direct à un bridge existant de l'host, résolu depuis ``iface_type``
- disponible
* - ``public_ip``
- routé comme ``vxlan`` côté DHCP
- **mise en place host non implémentée** — la création échoue à l'exécution
* - ``vlan``
- —
- réservé, non implémenté
.. warning::
``public_ip`` est accepté par l'API et traité comme ``vxlan`` pour le DHCP, mais sa
configuration réseau côté host n'existe pas encore : la création part en ``error`` dans
``Execute``. Ne pas s'appuyer dessus en production.
vxlan
-----
.. mermaid::
graph LR
VM --- TAP[tap] --- BR["br-&lt;subnet&gt;<br/>interface_ip"]
BR --- VX["vxlan&lt;vni&gt;"] --- HBR["bridge host<br/>(iface_type)"] --- UP[uplink]
Le subnet vit dans le netns du VPC. La VM n'est donc **pas joignable depuis l'host** sans route
explicite — point à connaître avant de câbler un outil externe dessus.
bridge
------
Le subnet est rattaché directement à un bridge existant de l'host. Pas de tunnel, pas de route
VPC : le trafic sort par le bridge, et la VM est joignable depuis l'host.
Routes annoncées aux VM
-----------------------
Les routes sont poussées par DHCP, dans l'**option 121** (routes statiques sans classe,
RFC 3442). Trois entrées y figurent :
#. la route ``/32`` vers ``169.254.169.254``, le serveur de metadata ;
#. la route vers le CIDR du VPC ;
#. la route par défaut ``0.0.0.0/0``.
.. important::
**Un client qui lit l'option 121 ignore l'option 3.** Toute route par défaut doit donc figurer
dans l'option 121 ; l'option 3 ne sert que les clients qui n'implémentent pas la 121.
Route par défaut : ``default_route`` et ``gateway``
----------------------------------------------------------
Une route par défaut est **toujours** annoncée. Le champ ``default_route`` ne choisit que son
next-hop :
``default_route: false`` (défaut)
next-hop = ``interface_ip`` du subnet.
``default_route: true``
next-hop = le champ ``gateway`` s'il est fourni, sinon la gateway lue dans la table de routage
de l'host.
``gateway`` n'est **pas validé** par l'agent : sa joignabilité et sa cohérence avec le CIDR du
subnet relèvent de l'appelant. Fourni avec ``default_route: false``, il est ignoré.
Dans tous les modes sauf ``bridge``, la route vers le CIDR du VPC garde ``interface_ip`` comme
next-hop : sur un subnet à IP publique, le trafic interne ne doit pas sortir par la gateway
publique.
Pourquoi la route ``/32`` vers le serveur de metadata est indispensable
----------------------------------------------------------------------------
Elle paraît redondante avec la route par défaut. Elle ne l'est pas.
La DNAT vers le serveur de metadata est posée dans le netns du VPC, en ``PREROUTING``. Le paquet
n'y est traité en L3 que si son next-hop est ``interface_ip``, portée par le bridge du netns.
Avec un autre next-hop, la trame est commutée en **L2** sans traverser ``PREROUTING`` : le
serveur de metadata devient injoignable et tout le provisionnement cloud-init échoue,
silencieusement.
.. danger::
Ne jamais retirer cette route de l'option 121, quelle que soit l'apparence de redondance.

View file

@ -0,0 +1,63 @@
VPC, subnet et VM
=================
Trois types de ressources, une hiérarchie stricte.
.. mermaid::
graph TD
VPC["VPC<br/><i>network namespace</i><br/>cidr"] --> SN1["Subnet<br/><i>bridge + VXLAN</i><br/>interface_ip, cidr"]
VPC --> SN2["Subnet"]
SN1 --> VM1["VM<br/><i>QEMU/KVM</i>"]
SN1 --> VM2["VM"]
SN2 --> VM2
VPC
---
Un VPC est un **network namespace** portant un espace d'adressage (``cidr``). C'est l'unité
d'isolation : deux VPC ne se voient pas, et peuvent réutiliser les mêmes plages d'adresses.
Un VPC ne peut être supprimé que si tous ses subnets le sont déjà — sinon 409.
Subnet
------
Un subnet appartient à un VPC et pose, dans son netns, un bridge qui porte ``interface_ip`` — la
gateway vue par les VM. Il fournit aussi le DHCP (dnsmasq) et les routes annoncées aux guests.
``iface_type`` est une clé **logique** (``vms``, ``internet``, ``admin``…), traduite en nom de
bridge physique par la configuration de l'agent. Une clé absente ou inconnue retombe sur
``default_interface``. Ce niveau d'indirection permet au même appel d'API de fonctionner sur des
hosts dont le nommage réseau diffère.
Le comportement réseau dépend du :doc:`mode </concepts/modes-reseau>`.
VM
--
Une VM est un processus QEMU/KVM raccordé à un ou plusieurs subnets par des taps.
**Interfaces.** L'ordre du tableau ``interfaces`` détermine le slot PCI (``0x03 + index``), donc
le nom de l'interface dans le guest. Exactement une interface doit être ``primary`` : elle porte
la route par défaut et le serveur de metadata. Tous les subnets d'une VM doivent appartenir au
**même VPC**.
**Stockage.** Un seul disque ``vdX`` (virtio-blk) par VM ; les disques supplémentaires passent
par le contrôleur SCSI (``sdX``). Cette contrainte vient de la carte PCI figée — voir
:doc:`/architecture/contraintes`.
Ce qui est stocké, et où
------------------------
Une ressource ne porte en base que ce qui lui est propre. Une VM stocke le **lien** vers son
subnet (``vm/<name>/subnet``), pas le VPC ni le bridge ni la gateway : ces valeurs sont lues
depuis le subnet, leur source canonique. Le schéma complet des clés est dans
:doc:`/architecture/stockage`.
Nommage
-------
L'API est machine-to-machine : elle **ne valide pas** les conventions de nommage, à l'exception
du motif documenté pour les VPC (``vp-…``). Les exemples de cette documentation suivent la
convention ``vp-`` / ``sn-`` / ``i-``, mais c'est à l'appelant de la faire respecter.

48
docs/conf.py Normal file
View file

@ -0,0 +1,48 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'two'
copyright = '2026, Nicolas Boufidjeline'
author = 'Nicolas Boufidjeline'
version = '0.1'
release = '0.1.0'
# -- General configuration ---------------------------------------------------
templates_path = ['_templates']
exclude_patterns = ['_build', 'README.md', 'requirements.txt', 'schemas']
language = 'fr'
extensions = [
'myst_parser',
'sphinxcontrib.mermaid',
'sphinxcontrib.openapi',
]
myst_enable_extensions = [
'colon_fence',
'deflist',
]
source_suffix = {
'.rst': 'restructuredtext',
'.md': 'markdown',
}
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_book_theme'
html_static_path = []
html_show_sphinx = False
html_theme_options = {
'home_page_in_toc': True,
}

14
docs/demarrage/index.rst Normal file
View file

@ -0,0 +1,14 @@
Démarrage
=========
Le parcours court : un hyperviseur, un VPC, un subnet, une VM qui démarre. Tout reste sur le
même nœud — c'est suffisant pour valider une installation et pour découvrir le modèle, pas pour
faire fonctionner un parc.
Pour un cluster, poursuivre avec :doc:`/deploiement/index`.
.. toctree::
:maxdepth: 1
installation
premier-vpc

View file

@ -0,0 +1,147 @@
Installation d'un hyperviseur
=============================
Cette page installe l'agent sur **un** hyperviseur. Le réseau du cluster — routage entre nœuds,
plan de contrôle — est traité à part : voir :doc:`/deploiement/index`.
Prérequis
---------
Un host Linux avec KVM, sur lequel vous avez ``root``. Les opérations réseau (network
namespaces, netlink, VXLAN, ebtables, iptables) et QEMU ne fonctionnent que sous Linux.
Déploiement
-----------
.. code-block:: bash
curl -O https://git.g3e.fr/syonad/two/raw/branch/main/scripts/deploy.sh
bash ./deploy.sh -t 0.1.0 -i
``deploy.sh`` se met à jour lui-même depuis la branche avant toute action — s'il diffère, il se
réécrit et demande d'être relancé. Il télécharge ensuite binaires, units systemd et scripts
depuis la release, et les vérifie contre le manifeste ``SHA256SUMS``.
.. list-table::
:header-rows: 1
:widths: 26 54 20
* - Option
- Effet
- Défaut
* - ``-t <tag>``
- déployer une release donnée
- dernière
* - ``-b <branche>``
- branche utilisée pour l'auto-mise à jour du script
- ``main``
* - ``-p <profil>``
- profil d'host ; seul ``kvm`` installe les units de l'agent
- ``kvm``
* - ``-i``
- préparer l'host : paquets, noyau, réseau
- désactivé
* - ``-u <iface>``
- interface physique d'uplink
- ``eno1``
* - ``-B <bridge>``
- bridge principal, auquel l'uplink est rattaché
- ``br-000000``
* - ``-P <bridge>``
- bridge supplémentaire, créé vide et réservé
- ``br-public``
* - ``-R <secondes>``
- délai avant le redémarrage de secours pendant la migration réseau
- ``120``
* - ``-d``
- dry-run : affiche les commandes sans les exécuter
- désactivé
Les options booléennes actives par défaut se **désactivent** par leur forme longue négative :
``--nopackages``, ``--nonetwork``, ``--noverify``, ``--noup_script``.
.. warning::
``--noverify`` désactive la seule vérification d'intégrité des artefacts téléchargés. Ne
l'utiliser que pour diagnostiquer un manifeste cassé, jamais en déploiement courant.
Ce que fait ``-i``
------------------
**Paquets** — ``qemu-system-x86``, ``ovmf``, ``dnsmasq``, ``ebtables``, ``iptables``,
``nfs-common``, ``jq``, ``curl``. Le service ``dnsmasq`` du système est ensuite désactivé et
**masqué** : il prendrait le port 53 en concurrence des instances ``dnsmasq@`` que l'agent lance
dans les netns.
**Noyau** — chargement de ``br_netfilter``, puis ``net.ipv4.ip_forward = 1`` et
``net.bridge.bridge-nf-call-iptables = 1``. Cette dernière clé est **requise** par la DNAT vers
le serveur de metadata : sans elle, iptables ne voit pas le trafic bridgé des VM et cloud-init
ne se provisionne pas. Contrepartie assumée : tout le trafic inter-VM traverse les tables NAT.
**Réseau** — création du bridge réservé, puis rattachement de l'uplink au bridge principal,
l'adresse et la route par défaut étant déplacées de l'interface physique vers le bridge.
.. danger::
La migration réseau **coupe le réseau de l'host si elle échoue à mi-parcours**, sans console
de secours. Deux garde-fous sont en place : un redémarrage de secours armé avant l'opération
(``-R``, 120 s par défaut) qui ramène la configuration d'origine puisque rien n'est écrit sur
disque, et l'exécution de la séquence sous systemd plutôt que dans la session SSH, pour
qu'une coupure de SSH ne l'interrompe pas.
Le désarmement n'a lieu **qu'après** un ping réussi vers la passerelle. Prévoir un accès
physique ou console avant de lancer un ``-i`` à distance sur un host de production.
Host sans état
--------------
L'hyperviseur est **stateless** : sa racine est en tmpfs, rien de ce que pose ``-i`` ne survit à
un redémarrage. ``deploy.sh --bootstrap`` est donc rejoué à chaque démarrage — c'est le
mécanisme normal, pas une réparation.
Binaires installés
------------------
.. list-table::
:header-rows: 1
:widths: 20 60 20
* - Binaire
- Rôle
- Drapeau de config
* - ``agent``
- processus principal : API, dispatcher, exécution, watchdog
- ``-config``
* - ``metadata``
- serveur de metadata cloud-init, une instance par VM dans le netns du VPC
- ``-conf``
* - ``db``
- inspection de la base clé-valeur en ligne de commande
- ``-conf``
Les trois partagent le même fichier, ``/etc/two/agent.yml`` — voir
:doc:`/exploitation/configuration`.
Mise à jour
-----------
``deploy.sh`` relève les instances ``dnsmasq@`` et ``metadata@`` actives **avant** d'arrêter les
services, et les redémarre ensuite : c'est la seule façon de savoir lesquelles relancer. Arrêter
les services à la main avant de lancer le script fait perdre cette liste.
Vérifier l'installation
-----------------------
.. code-block:: bash
systemctl status agent
curl -s http://127.0.0.1:8080/vpcs
Une liste JSON — vide au premier démarrage — signifie que l'API répond. Passez à
:doc:`/demarrage/premier-vpc`.
.. important::
L'API de l'agent **n'a aucune authentification**. Avant d'ouvrir le port au-delà de la boucle
locale, lisez l'avertissement de :doc:`/exploitation/configuration` : quiconque atteint ce
port pilote la totalité de l'hyperviseur.

View file

@ -0,0 +1,155 @@
Premier VPC, premier subnet, première VM
========================================
Ce tutoriel crée de bout en bout une VM joignable, sur un hyperviseur où l'agent est installé et
répond. Il suppose l'API sur ``127.0.0.1:8080`` et une image disque déjà présente sur l'host.
Tout se passe sur un **seul nœud** : un subnet ne s'étend à d'autres hyperviseurs qu'une fois le
plan de contrôle du cluster en place, cf. :doc:`/deploiement/architecture-cluster`.
Ce que l'on construit
---------------------
.. mermaid::
graph LR
subgraph netns vp-admin
BR["br-sn000001<br/>10.1.1.1"]
MD["metadata@i-web<br/>169.254.169.254"]
end
VM["VM i-web<br/>10.1.1.2"] --- BR
BR --- MD
BR --- VXLAN["VXLAN vni 1<br/>br-000000"]
Le VPC est un network namespace ; le subnet y pose un bridge porteur de la gateway ; la VM s'y
raccroche par un tap, reçoit son adresse en DHCP et son cloud-init depuis le serveur de metadata
du netns.
1. Le VPC
---------
.. code-block:: bash
curl -X POST http://127.0.0.1:8080/vpcs \
-H 'Content-Type: application/json' \
-d '{"name": "vp-admin", "cidr": "192.168.0.0/16"}'
Le ``cidr`` est l'espace d'adressage global du VPC : c'est lui qui sera annoncé aux VM comme
route interne, quel que soit le mode du subnet.
La réponse est un **202** : la création est acceptée, pas terminée.
.. code-block:: bash
curl -s http://127.0.0.1:8080/vpcs/vp-admin
Attendez ``"state": "running"`` avant l'étape suivante — un subnet dont le VPC parent n'est pas
prêt est refusé en **422**. Le modèle d'attente est décrit dans :doc:`/exploitation/api-agent/asynchronisme`.
2. Le subnet
------------
.. code-block:: bash
curl -X POST http://127.0.0.1:8080/subnets \
-H 'Content-Type: application/json' \
-d '{"name": "sn-000001",
"vpc": "vp-admin",
"mode": "vxlan",
"vxlan_id": 1,
"iface_type": "vms",
"interface_ip": "10.1.1.1",
"cidr": "10.1.0.0/23"}'
``iface_type`` est une clé **logique** résolue dans la configuration de l'agent (section
``interfaces``) vers un bridge physique de l'host ; une clé inconnue retombe sur
``default_interface``. ``interface_ip`` est la gateway du subnet, portée par le bridge créé dans
le netns.
Les modes disponibles et leurs conséquences sur le routage sont détaillés dans
:doc:`/concepts/modes-reseau`.
Là encore, attendez ``running`` :
.. code-block:: bash
curl -s http://127.0.0.1:8080/subnets/sn-000001
3. La VM
--------
.. code-block:: bash
curl -X POST http://127.0.0.1:8080/vms \
-H 'Content-Type: application/json' \
-d '{"name": "i-web",
"memory": 2048,
"cpus": 2,
"uefi": true,
"metadata": {"sshkey": "ssh-ed25519 AAAA…",
"user_data": "'"$(base64 < user-data.yml | tr -d '\n')"'"},
"interfaces": [{"subnet": "sn-000001", "ip": "10.1.1.2", "primary": true}],
"storage": [{"path": "/var/lib/two/volumes/i-web.qcow2", "dev": "vda"}]}'
Quatre points qui coûtent du temps quand on les découvre en production :
``user_data`` est **encodé en base64**
Un base64 invalide est rejeté en 400 plutôt que servi vide. L'agent n'interprète jamais ce
contenu.
``password`` est un **hash**, pas un mot de passe
Le champ attend la valeur de la clé ``passwd`` de cloud-config (``$6$…``). Sans ``password``
ni ``sshkey``, aucun compte n'est créé.
Exactement une interface est ``primary``
Elle porte la route par défaut et le serveur de metadata. L'ordre du tableau détermine le
slot PCI (``0x03 + index``), donc le nom de l'interface dans le guest. Tous les subnets d'une
VM doivent appartenir au même VPC.
Un seul disque ``vdX``
Les disques supplémentaires passent par ``sdX``. La carte PCI en dépend — voir
:doc:`/architecture/contraintes`.
4. Vérifier
-----------
.. code-block:: bash
curl -s http://127.0.0.1:8080/vms/i-web
En ``running``, la VM est démarrée et le serveur de metadata est en place. Le provisionnement
cloud-init, lui, se déroule dans le guest ; on l'observe par la console série :
.. code-block:: bash
socat -,raw,echo=0 UNIX-CONNECT:/run/two/vms/serial/i-web.sock
Puis, depuis l'host :
.. code-block:: bash
ssh syonad@10.1.1.2
.. note::
En mode ``vxlan``, la VM vit dans le netns du VPC : elle n'est pas joignable depuis l'host
sans route explicite. En mode ``bridge``, elle l'est directement.
5. Supprimer
------------
Dans l'ordre inverse — un VPC dont il reste des subnets est refusé en **409** :
.. code-block:: bash
curl -X DELETE http://127.0.0.1:8080/vms/i-web
curl -X DELETE http://127.0.0.1:8080/subnets/sn-000001
curl -X DELETE http://127.0.0.1:8080/vpcs/vp-admin
La suppression d'une VM ne touche **jamais** aux fichiers disque.
.. warning::
``instance-id`` vaut le nom de la VM. Recréer une VM du même nom sur le même disque fait que
cloud-init la reconnaît et **n'applique pas** le user-data. Pour rejouer un provisionnement,
changez de nom ou repartez d'un disque neuf.

View file

@ -0,0 +1,98 @@
Architecture du cluster
=======================
Topologie
---------
.. figure:: /schemas/topologie-cluster.svg
:alt: Topologie du cluster : routeurs, route reflector et hyperviseurs
:align: center
:width: 100%
:class: only-light
Topologie cible. Trait plein : plan de données. Trait pointillé : plan de contrôle.
.. figure:: /schemas/topologie-cluster-dark.svg
:alt: Topologie du cluster : routeurs, route reflector et hyperviseurs
:align: center
:width: 100%
:class: only-dark
Topologie cible. Trait plein : plan de données. Trait pointillé : plan de contrôle.
Deux plans distincts, à ne pas confondre au moment du diagnostic :
Plan de données
les tunnels VXLAN entre hyperviseurs, encapsulés sur le réseau qui les relie.
Plan de contrôle
ce qui dit à chaque hyperviseur où se trouvent les adresses MAC des autres. C'est le rôle de
FRR et du route reflector.
Ce que l'agent suppose déjà en place
------------------------------------
L'agent ne configure **que** son propre hyperviseur, et seulement à partir du bridge d'uplink.
Tout ce qui est en amont — adressage des hyperviseurs, routage entre eux, plan de contrôle — lui
préexiste et n'est jamais créé ni vérifié par lui.
Concrètement, il attend :
* le bridge d'uplink de la configuration (``br-000000`` par défaut), avec l'interface physique
esclave et l'adresse de l'hyperviseur portée par le bridge — c'est ce que fait
``deploy.sh --bootstrap``, voir :doc:`/demarrage/installation` ;
* une connectivité IP entre hyperviseurs sur cette adresse, port UDP **4789** ouvert dans les
deux sens ;
* un plan de contrôle qui peuple la table de transfert VXLAN — voir ci-dessous.
Pourquoi un plan de contrôle est nécessaire
-------------------------------------------
L'agent crée les interfaces VXLAN sur le port 4789 **sans groupe multicast et avec
l'apprentissage désactivé** (``Learning: false``). Il n'y a donc ni inondation multicast, ni
apprentissage des adresses MAC depuis le trafic, ni voisin statique configuré.
.. important::
Conséquence directe : sur un même VNI, **rien ne traverse d'un hyperviseur à l'autre** tant
qu'un composant externe n'a pas peuplé la table de transfert (FDB) du VXLAN. Sur un nœud
isolé le trafic reste sur le bridge local et cette absence ne se voit pas ; elle apparaît dès
le deuxième nœud.
C'est exactement le rôle que remplissent FRR sur chaque hyperviseur et le route reflector qui
les fait converger.
MTU
---
.. warning::
L'agent crée bridges, veth et interfaces VXLAN avec un **MTU figé à 1500**. VXLAN ajoute 50
octets d'encapsulation : le réseau qui relie les hyperviseurs doit donc accepter au moins
**1550 octets** de MTU, sinon les paquets pleine taille des VM sont perdus.
Le symptôme est trompeur : le ping passe, les petites requêtes passent, les transferts
volumineux et les poignées de main TLS échouent.
Hyperviseurs sans état
----------------------
L'hyperviseur est **stateless** — sa racine est en tmpfs, rien de ce que pose
``deploy.sh --bootstrap`` ne survit à un redémarrage, et le script est rejoué à chaque démarrage.
Toute configuration ajoutée à un hyperviseur — FRR compris — doit donc être posée par un
mécanisme rejouable au démarrage, jamais par une modification manuelle d'un fichier sous
``/etc``.
Adressage
---------
.. note::
**À rédiger** — cette page ne décrit pas encore le plan d'adressage du cluster. À documenter :
* la plage utilisée pour les adresses d'hyperviseurs, et son rapport avec ``br-000000`` ;
* l'allocation des VNI VXLAN : qui la tient, et comment on évite les collisions, puisque
l'agent ne valide pas ``vxlan_id`` ;
* l'usage prévu de ``br-public``, créé vide et réservé par le bootstrap ;
* le plan d'adressage des VPC, et ce qui garantit qu'ils ne se recouvrent pas entre clients.

View file

@ -0,0 +1,337 @@
Construction de l'image qcow2
=============================
Toutes les VM du cluster — ``intel``, PostgreSQL, route reflector et les suivantes — partent
d'une même image qcow2 « golden », construite une fois puis réutilisée. Cette page décrit la
procédure en service.
.. important::
Cette image est un **artefact redistribuable** : tout ce qui s'y trouve se retrouve dans
chaque VM qui en dérive. Les étapes de nettoyage de la fin ne sont pas une commodité, ce sont
des exigences.
Principe
--------
La construction se fait dans une **VM jetable**, et non par montage de l'image sur l'host : le
chroot a besoin d'un noyau et d'un espace utilisateur cohérents avec la distribution cible, ce
que l'host ne fournit pas nécessairement.
Cette VM de construction démarre sur un overlay de l'image du fournisseur et voit deux disques
supplémentaires : le futur disque « golden », et un espace de travail.
.. mermaid::
graph LR
ISO["seed.iso<br/>cloud-init NoCloud"] --> BVM
OVL["&lt;os&gt;-tmp.qcow2<br/><i>overlay, jetable</i>"] --> BVM["VM de construction"]
BVM --> ROOT["&lt;os&gt;-root.qcow2<br/><b>image golden</b>"]
BVM --> WORK["tmp.qcow2<br/><i>espace de travail</i>"]
BASE["image du fournisseur<br/>(qcow2)"] -.backing file.-> OVL
.. list-table::
:header-rows: 1
:widths: 26 20 54
* - Disque
- Vu dans la VM
- Rôle
* - ``<os>-tmp.qcow2``
- ``vda`` (virtio-blk)
- système de la VM de construction ; overlay de l'image du fournisseur, jeté à la fin
* - ``<os>-root.qcow2``
- ``sda`` (SCSI)
- **le résultat** : l'image golden, écrite en brut depuis la VM
* - ``tmp.qcow2``
- ``sdb`` (SCSI)
- espace de travail : téléchargement et conversion
Variables
---------
.. code-block:: bash
export os=<nom_os>
export os_link=<url_du_qcow2_fournisseur>
export os_file=<nom_du_fichier_qcow2>
export os_dir=<repertoire_de_telechargement>
export disk_dir=<repertoire_des_disques>
Étape 1 — Le seed cloud-init de la VM de construction
------------------------------------------------------
Ce seed ne concerne **que la VM de construction**. Il n'a aucun rapport avec la configuration
cloud-init de l'image produite, qui est posée plus loin en chroot. Son seul rôle est de donner
un accès à la VM le temps du build.
.. code-block:: bash
mkdir -p "${os_dir}" && cd "${os_dir}"
mkdir -p /opt/seed/${os}
cat << 'ENDFILE' > /opt/seed/${os}/meta-data
instance-id: iid-local01
local-hostname: my-vm-01
ENDFILE
cat << 'ENDFILE' > /opt/seed/${os}/network-config
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: true
ENDFILE
cat << 'ENDFILE' > /opt/seed/${os}/user-data
#cloud-config
users:
- name: <utilisateur>
lock_passwd: false
passwd: "<hash du mot de passe>"
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- <clé publique ssh>
ENDFILE
mkisofs -o /opt/seed/${os}_seed.iso -V cidata -J -r /opt/seed/${os}/
Le label de volume ``cidata`` n'est pas décoratif : c'est ce qui fait reconnaître l'ISO comme une
source NoCloud par cloud-init.
.. warning::
``passwd`` attend un **hash**, et ``ssh_authorized_keys`` une clé publique personnelle : ces
deux valeurs sont des données à ne pas recopier hors de l'host de construction. Elles ne
figurent volontairement pas dans cette documentation.
``openssl passwd -5`` pour generer un hash
Étape 2 — Les disques
---------------------
.. code-block:: bash
curl "${os_link}" -O
qemu-img create -f qcow2 "${disk_dir}/${os}-root.qcow2" 10G
qemu-img create -f qcow2 "${disk_dir}/tmp.qcow2" 50G
qemu-img create -f qcow2 -b "${os_dir}/${os_file}" -F qcow2 "${disk_dir}/${os}-tmp.qcow2" 10G
.. important::
``-F qcow2`` est **obligatoire** sur qemu récent : sans lui, le format du backing file n'est
pas figé dans l'en-tête de l'overlay.
La taille de ``<os>-root.qcow2`` (10 Gio ici) borne l'image produite : elle doit être au moins
égale à la taille **virtuelle** de l'image du fournisseur, pas à la taille de son fichier.
Étape 3 — Lancer la VM de construction
--------------------------------------
.. code-block:: bash
qemu-system-x86_64 \
-enable-kvm \
-cpu host \
-m 2048 \
-smp 2 \
-nographic \
-serial mon:stdio \
-monitor unix:/tmp/vm-build.mon-sock,server,nowait \
-drive file=/opt/seed/${os}_seed.iso,media=cdrom,if=ide \
\
-drive file=${disk_dir}/${os}-tmp.qcow2,format=qcow2,if=none,id=vda \
-device virtio-blk-pci,drive=vda,bootindex=0 \
\
-device virtio-scsi-pci,id=scsi0 \
\
-drive file=${disk_dir}/${os}-root.qcow2,if=none,id=hd0 \
-device scsi-hd,drive=hd0,bus=scsi0.0 \
\
-drive file=${disk_dir}/tmp.qcow2,if=none,id=hd1 \
-device scsi-hd,drive=hd1,bus=scsi0.0 \
\
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-device virtio-net-pci,netdev=net0,mac=00:22:33:00:00:01
La répartition virtio-blk pour le système / SCSI pour les disques supplémentaires est la même que
celle qu'impose l'agent — voir :doc:`/architecture/contraintes`. Le tap ``tap0`` doit exister et
être raccordé à un réseau qui donne un accès sortant : la suite télécharge l'image du
fournisseur depuis la VM.
Étape 4 — Écrire l'image du fournisseur sur le disque cible
------------------------------------------------------------
Les commandes suivantes s'exécutent **dans la VM de construction**. Identifier d'abord les
disques : le disque de travail et le disque cible ne doivent pas être confondus.
.. danger::
``qemu-img convert`` écrase intégralement le disque cible. Vérifier les noms avant, avec
``lsblk``, plutôt que de supposer l'ordre d'énumération.
.. code-block:: bash
work_disk=/dev/sdb
os_disk=/dev/sda
mkdir /work
mkfs.xfs ${work_disk}
mount ${work_disk} /work
cd /work
curl "${os_link}" -O
qemu-img convert ./*.qcow2 -O raw ${os_disk}
L'image du fournisseur est écrite **en brut** directement sur le disque cible : le qcow2 obtenu
côté host contient donc une image disque complète et amorçable, sans backing file.
.. code-block:: bash
partprobe
echo 1 > /sys/block/sda/device/rescan
sleep 2
# La partition racine est la plus grande du disque
root_partition=$(fdisk -lo device,size /dev/sda | grep -E '^\/dev\/' | tr -s ' ' \
| sort -rhk2 | head -n1 | cut -d ' ' -f1)
mount -o nouuid $root_partition /mnt
mount -o bind /dev /mnt/dev
mount -o bind /proc /mnt/proc
mount -o bind /sys /mnt/sys
cp /etc/resolv.conf /mnt/etc/resolv.conf
``-o nouuid`` est nécessaire parce que le système de fichiers qui vient d'être écrit porte le
même UUID que celui déjà monté par la VM de construction. Le ``resolv.conf`` est copié pour que
les commandes en chroot aient la résolution DNS ; il est supprimé au nettoyage.
Étape 5 — Personnaliser l'image
-------------------------------
**Accès SSH**
.. code-block:: bash
yum install -y augeas
echo "The default user for Syonad VMs is 'syonad'." > /mnt/etc/banner
augtool -r /mnt -s <<'EOF'
set /files/etc/ssh/sshd_config/X11Forwarding no
set /files/etc/ssh/sshd_config/PermitTunnel no
set /files/etc/ssh/sshd_config/PermitRootLogin no
set /files/etc/ssh/sshd_config/RSAAuthentication yes
set /files/etc/ssh/sshd_config/PubkeyAuthentication yes
set /files/etc/ssh/sshd_config/PasswordAuthentication no
set /files/etc/ssh/sshd_config/UseDNS no
set /files/etc/ssh/sshd_config/ChallengeResponseAuthentication no
set /files/etc/ssh/sshd_config/GSSAPIAuthentication no
set /files/etc/ssh/sshd_config/Match[1]/Condition/User "root,centos,ubuntu,debian,ec2-user"
set /files/etc/ssh/sshd_config/Match[1]/Settings/Banner "/etc/banner"
EOF
``PasswordAuthentication no`` vaut pour toutes les VM dérivées : l'accès se fait par clé, et le
champ ``password`` de l'API de l'agent ne sert donc **pas** à ouvrir une session SSH.
**Utilisateur par défaut et source de metadata**
.. code-block:: bash
cat << 'ENDFILE' > /mnt/etc/cloud/cloud.cfg.d/20_user.cfg
system_info:
default_user:
name: syonad
ENDFILE
cat << 'ENDFILE' > /mnt/etc/cloud/cloud.cfg.d/99_metadata.cfg
datasource_list: [ NoCloud ]
datasource:
NoCloud:
seedfrom: 'http://169.254.169.254:80'
timeout: 5
max_wait: 10
ENDFILE
C'est ce second fichier qui raccorde l'image au serveur de metadata de l'agent : ``NoCloud`` est
la seule source retenue, et elle pointe sur ``169.254.169.254``. La route ``/32`` vers cette
adresse est indispensable côté agent — voir :doc:`/concepts/modes-reseau`.
**Services et durcissement**
.. code-block:: bash
chroot /mnt/ systemctl enable fstrim.timer
chroot /mnt/ systemctl disable rpcbind.service
chroot /mnt/ systemctl disable rpcbind.socket
augtool -r /mnt -s set /files/etc/selinux/config/SELINUX disabled
chroot /mnt/ dnf remove -y 'cockpit*'
chroot /mnt/ rm -rf /run/cockpit
Étape 6 — Nettoyer, puis éteindre
---------------------------------
.. code-block:: bash
rm -f /mnt/etc/resolv.conf
rm -rf /mnt/var/cache/yum
rm -rf /mnt/root/.ssh
rm -rf /mnt/root/.bash_history
rm -rf /mnt/tmp/*
rm -rf /mnt/var/lib/dhcp/*
rm -rf /mnt/var/tmp/*
find /mnt/var/log ! -type d -exec rm '{}' \;
rm -rf /mnt/var/lib/cloud/*
poweroff
``/mnt/var/lib/cloud/*`` est le nettoyage le plus important : c'est lui qui fait que cloud-init
considère chaque VM dérivée comme une instance neuve. Sans lui, l'image embarque l'identité de
l'instance de construction et le user-data n'est pas appliqué — même mécanisme que la
recréation d'une VM sous un nom déjà utilisé, cf. :doc:`/concepts/metadata-cloud-init`.
Une fois la VM éteinte, ``${disk_dir}/${os}-root.qcow2`` est l'image golden.
``${os}-tmp.qcow2`` et ``tmp.qcow2`` sont jetables.
Points de vigilance
-------------------
.. warning::
**SELinux est désactivé** dans l'image. C'est une couche de protection en moins sur toutes les
VM qui en dérivent, y compris celles qui portent des fonctions sensibles comme le route
reflector ou la base de données. Décision à assumer explicitement, et à réévaluer : le mode
``permissive`` permettrait au minimum de savoir ce qui serait bloqué.
.. note::
``fstrim.timer`` est activé dans l'image, mais l'agent lance QEMU **sans** ``discard=unmap``
ni ``detect-zeroes=unmap`` sur les disques. Le ``fstrim`` du guest ne rend donc aujourd'hui
aucun espace à l'host : les qcow2 ne se rétractent pas. L'activation reste utile pour le jour
où l'option sera ajoutée côté agent, mais ne pas compter dessus pour la place disque.
.. note::
**À vérifier** — ``seedfrom`` sans barre oblique finale. cloud-init construit l'URL des
documents en concaténant ``seedfrom`` avec ``meta-data`` et ``user-data``. Confirmer sur une
VM réelle que les deux documents sont bien récupérés, et corriger en
``http://169.254.169.254:80/`` si ce n'est pas le cas.
.. note::
**Provenance de l'image du fournisseur** — tout ce qui est construit en hérite. Vérifier la
somme de contrôle, et la signature quand elle existe, avant de construire dessus. La
procédure actuelle télécharge l'image deux fois, une fois sur l'host et une fois dans la VM,
sans vérification.
.. note::
**À rédiger** — la procédure ne dit pas encore comment l'image produite est nommée, versionnée
et distribuée aux hyperviseurs, ni quelles variantes existent par rôle (``intel``, PostgreSQL,
route reflector) : image unique personnalisée au démarrage par cloud-init, ou images
dérivées ?

View file

@ -0,0 +1,32 @@
Déploiement d'un cluster
========================
Le :doc:`/demarrage/index` couvre un hyperviseur isolé : un VPC, un subnet, une VM, tout sur le
même nœud. Cette section couvre la mise en place d'un **cluster** complet, dans l'ordre où les
étapes se font.
Cet ordre n'est pas indifférent : chaque étape a besoin de la précédente. L'image doit exister
avant qu'on puisse démarrer quoi que ce soit ; le réseau doit être en place avant le premier
hyperviseur ; le route reflector est lui-même une VM, il lui faut donc un hyperviseur qui
fonctionne.
Étapes
------
#. :doc:`architecture-cluster` — la topologie cible, à lire avant tout le reste
#. :doc:`image-qcow2` — l'image golden dont dérivent toutes les VM du cluster
#. :doc:`routeurs` — le matériel : routeurs de cluster, de datacentre et de bordure
#. :doc:`premier-hyperviseur` — le premier nœud, agent et plan de contrôle
#. :doc:`route-reflector` — les VM route reflector
D'autres étapes viendront à mesure que les composants d'orchestration seront livrés.
.. toctree::
:hidden:
:maxdepth: 1
architecture-cluster
image-qcow2
routeurs
premier-hyperviseur
route-reflector

View file

@ -0,0 +1,74 @@
Premier hyperviseur
===================
Le premier nœud du cluster se déploie comme les suivants, mais il est le seul à devoir
fonctionner **avant** que le plan de contrôle existe : c'est lui qui hébergera la première VM
route reflector.
Installation
------------
L'installation de l'agent est identique à celle d'un nœud isolé et n'est pas reprise ici :
voir :doc:`/demarrage/installation` pour ``deploy.sh``, ses options, la préparation de l'host et
la migration réseau vers le bridge d'uplink.
Deux points à relire avant de lancer un ``-i`` sur un nœud de production : la migration réseau
coupe le réseau de l'host si elle échoue à mi-parcours, et l'hyperviseur est **sans état** —
tout ce qui est posé doit l'être par un mécanisme rejoué à chaque démarrage.
Plan de contrôle — FRR
----------------------
FRR tourne sur chaque hyperviseur et peuple la table de transfert (FDB) des interfaces VXLAN
créées par l'agent. C'est ce qui rend un subnet utilisable au-delà d'un seul nœud, puisque
l'agent désactive l'apprentissage et ne configure aucun voisin — cf.
:doc:`architecture-cluster`.
.. note::
**À rédiger.** À documenter :
* la version de FRR de référence et son mode d'installation, sachant que l'hyperviseur est
sans état : le paquet et la configuration doivent être posés à chaque démarrage, par le
bootstrap ou par un mécanisme équivalent ;
* les démons activés dans ``/etc/frr/daemons`` ;
* la configuration de référence : numéro d'AS, session vers le route reflector, famille
d'adresses utilisée pour annoncer les MAC et les VNI ;
* l'articulation avec les interfaces créées par l'agent : comment FRR découvre une interface
VXLAN qui apparaît à la création d'un subnet, et si une action est nécessaire ensuite ;
* ce qui se passe au démarrage à froid, quand FRR démarre avant ou après l'agent ;
* le cas particulier du **premier** hyperviseur, dont la session ne peut pas s'établir tant
que le route reflector n'existe pas.
Vérifier le plan de données
---------------------------
Ces deux vérifications restent valables quelle que soit la configuration retenue, et méritent
d'être dans toute procédure de diagnostic :
.. code-block:: bash
# La FDB du VXLAN doit contenir des entrées vers les autres hyperviseurs.
# Vide, c'est le plan de contrôle qui ne fonctionne pas, pas l'agent.
ip netns exec <vpc> bridge fdb show dev <interface-vxlan>
# L'interface VXLAN telle que l'agent l'a créée : port 4789, learning off,
# aucun groupe multicast, aucun remote.
ip netns exec <vpc> ip -d link show <interface-vxlan>
.. important::
Une FDB vide alors que le subnet est en ``running`` n'est **pas** un défaut de l'agent : il
crée délibérément l'interface sans apprentissage ni voisin, et laisse le peuplement au plan
de contrôle.
Valider le nœud
---------------
Avant de passer à la suite, le nœud doit savoir créer une VM de bout en bout à partir de l'image
golden — c'est exactement le parcours de :doc:`/demarrage/premier-vpc`, avec
``storage[0].path`` pointant sur une copie de l'image produite par :doc:`image-qcow2`.
Une VM qui démarre, obtient son adresse en DHCP et applique son user-data valide d'un coup
l'agent, le DHCP, la route vers le serveur de metadata et l'image. C'est le prérequis de
:doc:`route-reflector`.

View file

@ -0,0 +1,42 @@
VM route reflector
==================
Le route reflector est le point de rendez-vous du plan de contrôle : plutôt que de maintenir une
session entre chaque paire d'hyperviseurs, chaque hyperviseur ouvre une session vers le route
reflector, qui redistribue.
Il tourne lui-même en machine virtuelle, ce qui crée une dépendance circulaire à traiter
explicitement : la VM qui porte le plan de contrôle du cluster est hébergée par le cluster.
.. note::
**À rédiger.** À documenter :
* la création de la VM : ressources, subnet et mode utilisés, et s'il s'agit d'une VM créée
par l'agent comme les autres ou d'un cas particulier — l'image, elle, est l'image golden de
:doc:`image-qcow2` ;
* son adressage, et comment les hyperviseurs le connaissent ;
* la configuration du démon de routage qu'elle héberge ;
* la redondance : une seule VM route reflector, ou deux, et sur quels hyperviseurs ;
* la procédure de reconstruction, et l'état du cluster pendant que le route reflector est
absent — les tunnels déjà établis continuent-ils de fonctionner, et pendant combien de
temps ;
* la procédure d'amorçage : ce qui fonctionne, et dans quel ordre, quand on démarre un cluster
entier depuis zéro — le premier hyperviseur n'a pas de session FRR établie tant que cette VM
n'existe pas, cf. :doc:`premier-hyperviseur`.
Points de vigilance
-------------------
.. warning::
L'agent **ne réattache pas** les VM existantes à son démarrage, et les processus QEMU ne
survivent pas à un redémarrage de l'hyperviseur. Le redémarrage de l'hyperviseur qui héberge
le route reflector est donc un événement à part entière : la procédure de remise en service
doit être écrite, et testée.
.. warning::
``instance-id`` valant le nom de la VM, recréer la VM route reflector sous le même nom sur le
même disque fait que cloud-init **ne rejoue pas** le user-data. Cf.
:doc:`/concepts/metadata-cloud-init`.

View file

@ -0,0 +1,51 @@
Routeurs
========
Trois niveaux de routage entourent le cluster, du plus proche des hyperviseurs au plus proche de
l'extérieur. Tous préexistent à l'agent : celui-ci ne les configure pas et n'en a aucune
connaissance.
Routeurs de cluster
raccordent les hyperviseurs entre eux. C'est le niveau dont dépend directement le plan de
données VXLAN.
Routeurs de datacentre
agrègent les clusters d'un même site.
Routeurs de bordure
terminent le routage vers l'extérieur.
.. note::
**À rédiger.** Cette page attend les éléments de terrain. Pour chacun des trois niveaux :
* le matériel ou le logiciel employé, et la version de référence ;
* la configuration de référence : interfaces, adressage, protocole de routage et numéros
d'AS ;
* la redondance : combien d'équipements, quel mécanisme de bascule, quel comportement attendu
pendant une bascule ;
* ce qui est annoncé et ce qui est filtré à chaque niveau ;
* l'ordre de mise en service, et ce qui doit être opérationnel avant de préparer le premier
hyperviseur.
Contraintes imposées par le reste du cluster
---------------------------------------------
Indépendamment des choix d'équipement, deux contraintes viennent de ce que fait l'agent.
.. warning::
**MTU** — l'agent crée bridges, veth et interfaces VXLAN avec un MTU figé à 1500, et VXLAN
ajoute 50 octets d'encapsulation. Les liens entre hyperviseurs doivent donc accepter au moins
**1550 octets**. Cf. :doc:`architecture-cluster`.
.. warning::
**UDP 4789** doit passer entre hyperviseurs, dans les deux sens : c'est le port des tunnels
VXLAN.
.. warning::
**L'API de l'agent n'a aucune authentification.** Le filtrage réalisé ici est aujourd'hui
l'une des rares barrières entre cette API et le reste du réseau : son port ne doit être
joignable que depuis le réseau d'administration. Cf. :doc:`/exploitation/configuration`.

View file

@ -0,0 +1,91 @@
Modèle asynchrone et codes de retour
====================================
Toute création et toute suppression sont **asynchrones**. C'est le point qui surprend le plus
souvent à l'intégration : un ``202`` ne dit pas que la ressource existe, il dit que la demande
a été acceptée et enregistrée.
Les deux temps d'une requête
----------------------------
.. mermaid::
sequenceDiagram
participant C as Appelant
participant A as API
participant W as Worker
C->>A: POST /subnets
A->>A: Prepare — valide, écrit "creating"
A-->>C: 202 + ressource en creating
A->>W: Dispatch (file d'attente)
W->>W: Execute — netns, netif, dnsmasq
W->>W: état → running (ou error)
C->>A: GET /subnets/<name>
A-->>C: 200 + state
**Prepare** est synchrone, dans le handler HTTP : il valide l'état, écrit l'état initial en base
et répond. **Execute** est asynchrone : il fait le travail réseau réel, puis positionne l'état
final.
Conséquence directe : un échec d'``Execute`` ne peut pas être remonté dans la réponse HTTP. Il
se lit dans l'état de la ressource, qui passe à ``error``.
Attendre correctement
---------------------
Il n'y a pas de webhook ni de long-polling : l'appelant interroge ``GET /<type>/<name>`` jusqu'à
un état stable.
.. code-block:: bash
until [ "$(curl -sf http://127.0.0.1:8080/subnets/sn-000001 | jq -r .state)" = running ]; do
sleep 2
done
Trois règles pour un appelant robuste :
* **Toujours borner l'attente.** Côté agent, ``dispatcher.timeout_seconds`` (300 s par défaut)
borne les opérations qui attendent une transition ; l'appelant doit avoir sa propre borne.
* **Traiter ``error`` comme terminal, pas comme un échec transitoire.** Un ``Execute`` en échec
ne se rejoue pas tout seul.
* **Ne pas enchaîner sans vérifier.** Créer un subnet dont le VPC est encore en ``creating``
échoue en 422 ; démarrer une VM sur un subnet en ``creating`` ou ``running`` est en revanche
accepté.
Codes de retour
---------------
.. list-table::
:header-rows: 1
:widths: 12 88
* - Code
- Signification
* - ``202``
- demande acceptée et enregistrée ; l'état passera à ``running`` ou ``error``
* - ``200``
- lecture réussie (``GET``)
* - ``400``
- champ obligatoire manquant, corps invalide, ``iface_type`` inconnu, base64 invalide
* - ``404``
- ressource inexistante
* - ``409``
- conflit d'existence ou d'état : ressource déjà créée, ou suppression depuis un état qui
ne l'autorise pas ; pour un VPC, subnets encore présents
* - ``422``
- dépendance absente ou pas prête : VPC parent d'un subnet, subnet d'une VM
* - ``500``
- erreur interne
Le corps d'erreur est uniforme : ``{"error": "…"}``.
Idempotence
-----------
Les créations ne sont **pas** idempotentes : recréer une ressource existante donne 409, pas 202.
Un appelant qui rejoue une requête après un timeout réseau doit donc traiter 409 comme
« déjà fait », après avoir vérifié l'état par un ``GET``.
Les suppressions depuis l'état ``error`` sont acceptées mais **best-effort** : les ressources
système peuvent n'avoir été créées que partiellement, et il n'y a pas de rollback — voir
:doc:`/concepts/cycle-de-vie`.

View file

@ -0,0 +1,21 @@
API de l'agent
==============
L'agent expose une API HTTP par hyperviseur. C'est aujourd'hui la seule API de ``two`` ; les
API de niveau supérieur viendront avec les composants d'orchestration, et seront documentées
à part.
Elle est **machine-to-machine** : elle est consommée par un autre logiciel, pas par un humain.
La validation de cohérence des entrées (format des CIDR, plage des VXLAN ID, convention de
nommage) est à la charge de l'appelant — l'agent ne la refait pas.
.. warning::
Cette API **n'a aucune authentification**. Voir :doc:`/exploitation/configuration` avant de
l'exposer au-delà de la boucle locale.
.. toctree::
:maxdepth: 1
asynchronisme
reference

View file

@ -0,0 +1,8 @@
Référence
=========
Cette page est générée depuis ``api/agent.yaml``, à la racine du dépôt. C'est la source unique
du contrat : en cas d'écart avec le reste de la documentation, c'est elle qui fait foi.
.. openapi:: ../../../api/agent.yaml
:examples:

View file

@ -0,0 +1,138 @@
Configuration
=============
Un seul fichier, ``/etc/two/agent.yml``, partagé par les trois binaires : ``agent -config``,
``metadata -conf`` et ``db -conf``. Le fichier de référence commenté est
``conf/agent/config.exemple.yml`` dans le dépôt.
Le chargement se fait par **viper** : les clés sont celles ci-dessous, en YAML.
.. danger::
**L'API de l'agent n'a aucune authentification.** L'exemple livré écoute sur
``0.0.0.0:8080`` : quiconque atteint ce port peut créer et détruire des VM et des réseaux sur
l'host KVM, c'est-à-dire en prendre le contrôle.
Sur tout déploiement réel : restreindre ``api.address`` à une adresse d'administration, ou
filtrer le port en amont (pare-feu, réseau dédié). Traiter l'ouverture de ce port comme une
décision d'architecture, pas comme un réglage.
Base de données
---------------
.. code-block:: yaml
database:
path: "/var/lib/two/data/"
Répertoire de la base clé-valeur Badger. **Un seul processus l'ouvre** : l'agent. Ni le serveur
de metadata ni aucun autre outil ne doit être configuré pour ouvrir le même répertoire pendant
que l'agent tourne.
Serveurs
--------
.. code-block:: yaml
api:
address: "0.0.0.0"
port: 8080
prometheus:
address: "0.0.0.0"
port: 9090
admin:
enabled: false
address: "127.0.0.1"
port: 9091
``admin`` expose une inspection en lecture seule de la base (``/db?prefix=…``). Elle est
désactivée par défaut et prévue pour la boucle locale uniquement.
.. warning::
Le contenu de la base inclut ``vm/<name>/password``, qui est un hash de mot de passe.
L'activation de l'API d'administration rend ces valeurs lisibles par tout ce qui atteint le
port. Ne pas l'exposer hors de la boucle locale.
Exécution des commandes
-----------------------
.. code-block:: yaml
worker:
count: 4
buffer_size: 100
dispatcher:
timeout_seconds: 300
poll_seconds: 2
``worker.count`` est le nombre de goroutines qui exécutent les commandes ; ``buffer_size`` le
nombre de commandes en attente au-delà duquel ``Dispatch`` bloque.
``dispatcher.timeout_seconds`` borne les opérations qui attendent une transition d'état, dont
l'extinction d'une VM.
.. warning::
À l'expiration de ce délai, une VM qui ne s'est pas éteinte reçoit un ``quit`` QMP — un arrêt
**brutal**. Pour des charges dont l'extinction est lente (bases de données, construction
d'images), une valeur confortable évite un système de fichiers invité incohérent.
Correspondance des interfaces
-----------------------------
.. code-block:: yaml
default_interface: br-000000
interfaces:
vms: br-000000
internet: br-000000
admin: br-000000
Traduit les clés logiques ``iface_type`` de l'API vers les bridges physiques de l'host. Une clé
inconnue ou omise retombe silencieusement sur ``default_interface`` — ce n'est pas une erreur,
mais c'est une source de subnets branchés au mauvais endroit sans le dire.
Metadata et QEMU
----------------
.. code-block:: yaml
metadata:
run_dir: "/run/two/metadata"
qemu:
ovmf_code_path: "/usr/share/OVMF/OVMF_CODE.fd"
ovmf_vars_template: "/usr/share/OVMF/OVMF_VARS.fd"
uefi_vars_dir: "/run/two/vms/efi"
serial_dir: "/run/two/vms/serial"
monitor_dir: "/run/two/vms/monitor"
qmp_dir: "/run/two/vms/qmp"
Les chemins OVMF sont nécessaires aux VM démarrées avec ``uefi: true`` (paquet ``ovmf`` sur
Debian et Ubuntu). ``uefi_vars_dir`` reçoit une copie inscriptible des variables UEFI par VM,
créée au démarrage et supprimée à l'arrêt.
Watchdog
--------
.. code-block:: yaml
watchdog:
enabled: true
interval_seconds: 60
Vérification périodique **en lecture seule** — voir :doc:`/exploitation/observabilite`.
Journalisation
--------------
.. code-block:: yaml
logger:
level: info # debug, info, warn, error
debug: false # force le niveau debug quel que soit level

View file

@ -0,0 +1,122 @@
Diagnostic
==========
Symptôme, cause probable, vérification. Les causes listées sont celles réellement rencontrées.
La ressource part en ``error`` juste après le 202
-------------------------------------------------
``Execute`` a échoué : la cause est dans le journal de l'agent, pas dans la réponse HTTP.
.. code-block:: bash
journalctl -u agent -n 100
Cas fréquents :
* subnet en mode ``public_ip`` — la mise en place host n'est pas implémentée, l'échec est attendu ;
* ``vxlan_id`` déjà utilisé sur l'host ;
* bridge cible absent : ``iface_type`` inconnu retombé sur ``default_interface``, lui-même
inexistant.
Avant toute recréation, émettre un ``DELETE`` : il n'y a pas de rollback, les objets système
partiellement créés subsistent.
La VM démarre mais n'a pas d'adresse
------------------------------------
Le DHCP est servi par l'instance ``dnsmasq@`` du subnet.
.. code-block:: bash
systemctl status 'dnsmasq@<netns>_<bridge>'
tail -50 /var/log/dnsmasq-<netns>_<bridge>.log
cat /run/dnsmasq-<netns>_<bridge>.leases
cat /etc/dnsmasq.d/<netns>_<bridge>.conf
Si dnsmasq ne voit passer aucune requête, le problème est en amont : tap absent, bridge non
raccordé, VM dans le mauvais netns.
La VM a une adresse mais cloud-init n'applique rien
---------------------------------------------------
Deux causes distinctes, à écarter dans cet ordre.
**1. La VM porte déjà cet ``instance-id``.** ``instance-id`` vaut le nom de la VM : sur un disque
déjà provisionné sous le même nom, cloud-init considère l'instance connue et ne rejoue pas le
user-data. Vérification dans le guest :
.. code-block:: bash
cloud-init query instance-id
ls /var/lib/cloud/instances/
**2. Le serveur de metadata est injoignable.** Depuis le guest :
.. code-block:: bash
ip route
curl -s http://169.254.169.254/latest/meta-data/
La route ``169.254.169.254/32`` doit être présente, avec l'``interface_ip`` du subnet comme
next-hop. Si elle est absente ou pointe ailleurs, la DNAT posée en ``PREROUTING`` dans le netns
n'est jamais traversée : la trame est commutée en L2 et le serveur reste injoignable. Voir
:doc:`/concepts/modes-reseau`.
Depuis l'host, l'instance correspondante :
.. code-block:: bash
systemctl status 'metadata@<vm>'
ls -l /run/two/metadata/<vm>/
Le user-data est servi vide
---------------------------
Un document fourni explicitement vide est servi vide — ce n'est pas la même chose qu'un document
absent, qui retombe sur le template. Vérifier le contenu réellement écrit :
.. code-block:: bash
cat /run/two/metadata/<vm>/user-data
Un base64 invalide, lui, aurait été rejeté en 400 à la création.
La VM ne démarre pas (UEFI)
---------------------------
``uefi: true`` exige les fichiers OVMF déclarés dans la configuration :
.. code-block:: bash
ls -l /usr/share/OVMF/OVMF_CODE.fd /usr/share/OVMF/OVMF_VARS.fd
ls -l /run/two/vms/efi/
Sur Debian et Ubuntu, le paquet est ``ovmf``.
Le ``DELETE`` renvoie 409
-------------------------
La suppression n'est autorisée que depuis ``running`` ou ``error``. Depuis ``creating`` ou
``deleting``, attendre l'état stable. Pour un VPC, tous les subnets doivent être supprimés
d'abord.
La base et le système ont divergé
---------------------------------
Le watchdog signale une ressource ``running`` absente du système. Il ne répare rien : la
correction est un ``DELETE`` explicite suivi d'une recréation. Après un redémarrage de l'agent,
les ressources restées transitoires sont basculées en ``error`` par la migration de démarrage —
elles n'ont pas forcément échoué, elles ont été interrompues.
L'agent ne redémarre pas après un arrêt brutal
----------------------------------------------
Badger rejoue son journal au démarrage : c'est normal et attendu, notamment si le budget d'arrêt
précédent a été dépassé et que la base n'a pas été fermée. Si le démarrage échoue vraiment, le
message se trouve dans ``journalctl -u agent``.
.. note::
Les VM ne sont pas réattachées au redémarrage de l'agent : un processus QEMU survivant à
l'agent n'est plus piloté par lui.

View file

@ -0,0 +1,14 @@
Exploitation
============
Faire tourner un hyperviseur en service : configuration, services systemd, API de l'agent,
observabilité et diagnostic.
.. toctree::
:maxdepth: 1
configuration
services
api-agent/index
observabilite
diagnostic

View file

@ -0,0 +1,83 @@
Observabilité
=============
Métriques Prometheus
--------------------
Exposées sur le port ``prometheus.port`` (9090 par défaut), alimentées par l'état lu en base :
.. list-table::
:header-rows: 1
:widths: 40 60
* - Métrique
- Description
* - ``syonad_vpcs_total``
- nombre de VPC, par état
* - ``syonad_subnets_total``
- nombre de subnets, par état
* - ``syonad_vms_total``
- nombre de VM, par état
Les états sont ceux du :doc:`cycle de vie </concepts/cycle-de-vie>`. Une valeur non nulle et
durable sur ``error`` est l'alerte la plus utile à poser ; une valeur durable sur ``creating``
ou ``deleting`` signale une opération qui n'aboutit pas.
Watchdog
--------
Une goroutine périodique vérifie que les ressources marquées ``running`` en base existent
toujours sur le système, et **notifie les écarts sans jamais réparer**.
.. code-block:: yaml
watchdog:
enabled: true
interval_seconds: 60
Il contrôle notamment l'existence des network namespaces, des liens réseau des subnets, des taps
de VM et la réponse des units systemd associées.
.. note::
Un écart persistant est signalé **à chaque tick**, sans déduplication. Le volume de
notifications est donc proportionnel à la durée de l'anomalie : c'est voulu, mais cela veut
dire qu'une alerte doit agréger, pas compter.
Le watchdog étant strictement en lecture seule, une divergence entre la base et le système
subsiste jusqu'à une action explicite (``DELETE`` puis recréation).
Journaux
--------
``slog`` structuré, niveau réglé par ``logger.level`` (``debug``, ``info``, ``warn``, ``error``),
``logger.debug: true`` forçant ``debug``.
.. code-block:: bash
journalctl -u agent -f
journalctl -u 'metadata@i-web' -n 50
tail -f /var/log/dnsmasq-vp-admin_br-sn000001.log
Inspection de la base
---------------------
En ligne de commande, sur l'host :
.. code-block:: bash
/opt/two/bin/db -conf /etc/two/agent.yml
.. warning::
``db`` ouvre directement la base Badger. **Ne pas l'utiliser pendant que l'agent tourne** :
deux processus ne doivent pas ouvrir la même instance.
L'API d'administration donne la même lecture sans ce risque, quand elle est activée :
.. code-block:: bash
curl -s 'http://127.0.0.1:9091/db?prefix=vm/'
Elle expose l'intégralité des valeurs, **y compris les hashs de mot de passe** — cf.
:doc:`/exploitation/configuration`.

View file

@ -0,0 +1,91 @@
Services systemd
================
Trois units, installées sous ``/opt/two/bin`` par ``deploy.sh``.
.. list-table::
:header-rows: 1
:widths: 28 32 40
* - Unit
- Instance ``%i``
- Rôle
* - ``agent.service``
- —
- processus principal : API, dispatcher, exécution, watchdog
* - ``dnsmasq@.service``
- ``<netns>_<bridge>``
- dnsmasq lancé dans le netns du VPC, un par subnet
* - ``metadata@.service``
- ``<nom de la VM>``
- serveur de metadata cloud-init, un par VM
Les instances sont créées et pilotées par l'agent au fil des créations de subnets et de VM : il
n'y a pas à les démarrer à la main en fonctionnement normal.
.. code-block:: bash
systemctl status agent
systemctl status 'dnsmasq@vp-admin_br-sn000001'
systemctl status 'metadata@i-web'
dnsmasq
-------
Le script ``run-dnsmasq-in-netns.sh`` entre dans le netns puis exécute dnsmasq avec un fichier
de configuration par subnet, généré par l'agent :
.. list-table::
:widths: 40 60
* - Configuration
- ``/etc/dnsmasq.d/<netns>_<bridge>.conf``
* - Baux
- ``/run/dnsmasq-<netns>_<bridge>.leases``
* - Journal
- ``/var/log/dnsmasq-<netns>_<bridge>.log``
* - PID
- ``/run/dnsmasq-<netns>_<bridge>.pid``
Le fichier de baux et le journal sont les deux premiers endroits à regarder quand une VM n'obtient
pas d'adresse.
QEMU n'est pas une unit
-----------------------
Les processus QEMU sont lancés par ``systemd-run --scope``, **jamais** en unit transitoire. Un
scope est exécuté par le processus appelant et hérite donc du network namespace posé par
l'agent ; une unit transitoire, forkée par PID 1, démarrerait dans le netns racine et ne verrait
pas le tap de la VM.
Conséquence pratique : les VM n'apparaissent pas dans ``systemctl list-units`` mais dans
``systemd-cgls``, et elles ne survivent pas à un ``systemctl stop agent`` suivi d'un
redémarrage — l'agent ne réattache pas les VM existantes.
Sockets par VM
--------------
.. code-block:: text
/run/two/vms/serial/<vm>.sock console série
/run/two/vms/monitor/<vm>.sock monitor QEMU
/run/two/vms/qmp/<vm>.sock QMP (utilisé par l'agent)
.. code-block:: bash
socat -,raw,echo=0 UNIX-CONNECT:/run/two/vms/serial/i-web.sock
Arrêt de l'agent
----------------
L'ordre d'arrêt est imposé : serveurs HTTP, puis drainage des workers, puis fermeture de la base.
Si le budget d'arrêt est dépassé, la base **n'est pas fermée** — fermer Badger sous un écrivain
concurrent est pire qu'un rejeu du journal au démarrage suivant. Un message à ce sujet dans le
journal au moment d'un ``stop`` n'est donc pas une anomalie.
Mise à jour
-----------
``deploy.sh`` relève les instances ``dnsmasq@`` et ``metadata@`` actives **avant** d'arrêter les
services, et les redémarre ensuite : c'est la seule façon de savoir lesquelles relancer. Arrêter
les services à la main avant de lancer le script fait perdre cette liste.

66
docs/index.rst Normal file
View file

@ -0,0 +1,66 @@
two
===
**two** est un orchestrateur de virtualisation et de réseau : il pilote un parc d'hyperviseurs,
le réseau qui les relie, et les machines virtuelles qui y tournent.
Il se compose de plusieurs éléments, déployés et versionnés séparément.
.. list-table::
:header-rows: 1
:widths: 22 58 20
* - Composant
- Rôle
- État
* - **agent**
- un par hyperviseur : expose une API HTTP qui crée des VPC — isolés par network
namespace —, des subnets — VXLAN ou bridge — et des VM QEMU/KVM raccordées à ces
subnets, avec DHCP, routage et metadata cloud-init
- livré (0.1.0)
* - *à venir*
- les composants de niveau supérieur — ordonnancement sur le parc, API d'orchestration,
interface d'administration — sont à documenter au fur et à mesure de leur livraison
- à venir
À ce stade, la totalité de cette documentation porte donc sur l'**agent** et sur le réseau du
cluster qui l'entoure.
Par où commencer
----------------
:doc:`/demarrage/index`
Installer l'agent sur un hyperviseur et créer un premier VPC, un subnet et une VM. C'est le
parcours court, sur un nœud isolé.
:doc:`/deploiement/index`
L'architecture complète : réseau du cluster, routage, et ce qu'il faut mettre en place avant
qu'un parc d'hyperviseurs fonctionne ensemble.
:doc:`/exploitation/index`
Configuration, services, API de l'agent, métriques et diagnostic sur un nœud en service.
:doc:`/concepts/index`
Comment les éléments fonctionnent entre eux : modèle de données, modes réseau, cycle de vie,
metadata. À lire avant de diagnostiquer un comportement inattendu.
.. toctree::
:hidden:
:caption: Mise en œuvre
demarrage/index
deploiement/index
.. toctree::
:hidden:
:caption: Exploitation
exploitation/index
.. toctree::
:hidden:
:caption: Interne
concepts/index
architecture/index
versions/index

6
docs/requirements.txt Normal file
View file

@ -0,0 +1,6 @@
sphinx==9.1.0
sphinx-autobuild==2025.8.25
sphinx-book-theme==1.2.0
myst-parser==5.1.0
sphinxcontrib.mermaid==2.0.2
sphinxcontrib-openapi==0.9.0

View file

@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 500" width="900" height="500" role="img" aria-label="Topologie du cluster">
<style>
.lbl { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 14px; fill: #e6edf3; }
.sub { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 11px; fill: #9198a1; }
.grp { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 12px; font-weight: 600; fill: #9198a1; letter-spacing: .04em; }
.box { fill: #22272e; stroke: #6e7781; stroke-width: 1.5; }
.group { fill: #1c2128; stroke: #444c56; stroke-width: 1.5; stroke-dasharray: 0; }
.link { stroke: #9198a1; stroke-width: 1.8; fill: none; }
.ctrl { stroke: #c297ff; stroke-width: 1.6; fill: none; stroke-dasharray: 5 4; }
</style>
<rect class="group" x="250" y="24" width="400" height="96" rx="8"/>
<text class="grp" x="270" y="46">ROUTEURS DE CLUSTER</text>
<rect class="box" x="275" y="58" width="160" height="42" rx="6"/>
<text class="lbl" x="355" y="84" text-anchor="middle">routeur A</text>
<rect class="box" x="465" y="58" width="160" height="42" rx="6"/>
<text class="lbl" x="545" y="84" text-anchor="middle">routeur B</text>
<rect class="box" x="350" y="168" width="200" height="56" rx="6"/>
<text class="lbl" x="450" y="191" text-anchor="middle">VM route reflector</text>
<text class="sub" x="450" y="210" text-anchor="middle">plan de contrôle</text>
<rect class="group" x="40" y="278" width="370" height="196" rx="8"/>
<text class="grp" x="60" y="300">HYPERVISEUR 1</text>
<rect class="box" x="145" y="312" width="160" height="40" rx="6"/>
<text class="lbl" x="225" y="337" text-anchor="middle">br-000000</text>
<rect class="box" x="145" y="372" width="160" height="40" rx="6"/>
<text class="lbl" x="225" y="397" text-anchor="middle">FRR</text>
<rect class="box" x="145" y="424" width="160" height="40" rx="6"/>
<text class="lbl" x="225" y="449" text-anchor="middle">agent</text>
<path class="link" d="M225 352 L225 372"/>
<path class="link" d="M225 412 L225 424"/>
<rect class="group" x="490" y="278" width="370" height="196" rx="8"/>
<text class="grp" x="510" y="300">HYPERVISEUR 2</text>
<rect class="box" x="595" y="312" width="160" height="40" rx="6"/>
<text class="lbl" x="675" y="337" text-anchor="middle">br-000000</text>
<rect class="box" x="595" y="372" width="160" height="40" rx="6"/>
<text class="lbl" x="675" y="397" text-anchor="middle">FRR</text>
<rect class="box" x="595" y="424" width="160" height="40" rx="6"/>
<text class="lbl" x="675" y="449" text-anchor="middle">agent</text>
<path class="link" d="M675 352 L675 372"/>
<path class="link" d="M675 412 L675 424"/>
<path class="link" d="M225 312 L225 148 L330 148 L330 120"/>
<path class="link" d="M675 312 L675 148 L570 148 L570 120"/>
<text class="sub" x="232" y="266">uplink</text>
<text class="sub" x="682" y="266">uplink</text>
<path class="ctrl" d="M350 196 L322 196 L322 392 L305 392"/>
<path class="ctrl" d="M550 196 L578 196 L578 392 L595 392"/>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 500" width="900" height="500" role="img" aria-label="Topologie du cluster">
<style>
.lbl { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 14px; fill: #1f2328; }
.sub { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 11px; fill: #57606a; }
.grp { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 12px; font-weight: 600; fill: #57606a; letter-spacing: .04em; }
.box { fill: #ffffff; stroke: #8c959f; stroke-width: 1.5; }
.group { fill: #f6f8fa; stroke: #d0d7de; stroke-width: 1.5; stroke-dasharray: 0; }
.link { stroke: #57606a; stroke-width: 1.8; fill: none; }
.ctrl { stroke: #8250df; stroke-width: 1.6; fill: none; stroke-dasharray: 5 4; }
</style>
<rect class="group" x="250" y="24" width="400" height="96" rx="8"/>
<text class="grp" x="270" y="46">ROUTEURS DE CLUSTER</text>
<rect class="box" x="275" y="58" width="160" height="42" rx="6"/>
<text class="lbl" x="355" y="84" text-anchor="middle">routeur A</text>
<rect class="box" x="465" y="58" width="160" height="42" rx="6"/>
<text class="lbl" x="545" y="84" text-anchor="middle">routeur B</text>
<rect class="box" x="350" y="168" width="200" height="56" rx="6"/>
<text class="lbl" x="450" y="191" text-anchor="middle">VM route reflector</text>
<text class="sub" x="450" y="210" text-anchor="middle">plan de contrôle</text>
<rect class="group" x="40" y="278" width="370" height="196" rx="8"/>
<text class="grp" x="60" y="300">HYPERVISEUR 1</text>
<rect class="box" x="145" y="312" width="160" height="40" rx="6"/>
<text class="lbl" x="225" y="337" text-anchor="middle">br-000000</text>
<rect class="box" x="145" y="372" width="160" height="40" rx="6"/>
<text class="lbl" x="225" y="397" text-anchor="middle">FRR</text>
<rect class="box" x="145" y="424" width="160" height="40" rx="6"/>
<text class="lbl" x="225" y="449" text-anchor="middle">agent</text>
<path class="link" d="M225 352 L225 372"/>
<path class="link" d="M225 412 L225 424"/>
<rect class="group" x="490" y="278" width="370" height="196" rx="8"/>
<text class="grp" x="510" y="300">HYPERVISEUR 2</text>
<rect class="box" x="595" y="312" width="160" height="40" rx="6"/>
<text class="lbl" x="675" y="337" text-anchor="middle">br-000000</text>
<rect class="box" x="595" y="372" width="160" height="40" rx="6"/>
<text class="lbl" x="675" y="397" text-anchor="middle">FRR</text>
<rect class="box" x="595" y="424" width="160" height="40" rx="6"/>
<text class="lbl" x="675" y="449" text-anchor="middle">agent</text>
<path class="link" d="M675 352 L675 372"/>
<path class="link" d="M675 412 L675 424"/>
<path class="link" d="M225 312 L225 148 L330 148 L330 120"/>
<path class="link" d="M675 312 L675 148 L570 148 L570 120"/>
<text class="sub" x="232" y="266">uplink</text>
<text class="sub" x="682" y="266">uplink</text>
<path class="ctrl" d="M350 196 L322 196 L322 392 L305 392"/>
<path class="ctrl" d="M550 196 L578 196 L578 392 L595 392"/>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

2
docs/versions/0.1.0.md Normal file
View file

@ -0,0 +1,2 @@
```{include} ../../release_notes/0.1.0.md
```

12
docs/versions/index.rst Normal file
View file

@ -0,0 +1,12 @@
Versions
========
Chaque version porte un nom de code dérivé du rang de sa publication : anges et démons alternés.
.. toctree::
:maxdepth: 1
0.1.0
.. include:: ../../release_notes/codenames.md
:parser: myst_parser.sphinx_