This commit is contained in:
forgejo-actions 2026-09-10 20:11:54 +00:00
commit 222d739a9d
382 changed files with 3 additions and 64867 deletions

View file

@ -1,52 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,52 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,86 +0,0 @@
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

@ -1,13 +0,0 @@
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

@ -1,51 +0,0 @@
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

@ -1,90 +0,0 @@
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

@ -1,63 +0,0 @@
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.

View file

@ -1,14 +0,0 @@
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

@ -1,71 +0,0 @@
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

@ -1,99 +0,0 @@
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

@ -1,63 +0,0 @@
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.

View file

@ -1,14 +0,0 @@
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

@ -1,147 +0,0 @@
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

@ -1,155 +0,0 @@
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

@ -1,98 +0,0 @@
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

@ -1,337 +0,0 @@
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

@ -1,32 +0,0 @@
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

@ -1,74 +0,0 @@
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

@ -1,42 +0,0 @@
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

@ -1,51 +0,0 @@
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

@ -1,91 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,8 +0,0 @@
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

@ -1,138 +0,0 @@
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

@ -1,122 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,83 +0,0 @@
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

@ -1,91 +0,0 @@
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.

View file

@ -1,66 +0,0 @@
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

View file

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

View file

@ -1,12 +0,0 @@
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_

View file

@ -1,476 +0,0 @@
// @ts-check
/**@constructor*/
BaseStemmer = function() {
/** @protected */
this.current = '';
this.cursor = 0;
this.limit = 0;
this.limit_backward = 0;
this.bra = 0;
this.ket = 0;
/**
* @param {string} value
*/
this.setCurrent = function(value) {
this.current = value;
this.cursor = 0;
this.limit = this.current.length;
this.limit_backward = 0;
this.bra = this.cursor;
this.ket = this.limit;
};
/**
* @return {string}
*/
this.getCurrent = function() {
return this.current;
};
/**
* @param {BaseStemmer} other
*/
this.copy_from = function(other) {
/** @protected */
this.current = other.current;
this.cursor = other.cursor;
this.limit = other.limit;
this.limit_backward = other.limit_backward;
this.bra = other.bra;
this.ket = other.ket;
};
/**
* @param {number[]} s
* @param {number} min
* @param {number} max
* @return {boolean}
*/
this.in_grouping = function(s, min, max) {
/** @protected */
if (this.cursor >= this.limit) return false;
var ch = this.current.charCodeAt(this.cursor);
if (ch > max || ch < min) return false;
ch -= min;
if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false;
this.cursor++;
return true;
};
/**
* @param {number[]} s
* @param {number} min
* @param {number} max
* @return {boolean}
*/
this.go_in_grouping = function(s, min, max) {
/** @protected */
while (this.cursor < this.limit) {
var ch = this.current.charCodeAt(this.cursor);
if (ch > max || ch < min)
return true;
ch -= min;
if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0)
return true;
this.cursor++;
}
return false;
};
/**
* @param {number[]} s
* @param {number} min
* @param {number} max
* @return {boolean}
*/
this.in_grouping_b = function(s, min, max) {
/** @protected */
if (this.cursor <= this.limit_backward) return false;
var ch = this.current.charCodeAt(this.cursor - 1);
if (ch > max || ch < min) return false;
ch -= min;
if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false;
this.cursor--;
return true;
};
/**
* @param {number[]} s
* @param {number} min
* @param {number} max
* @return {boolean}
*/
this.go_in_grouping_b = function(s, min, max) {
/** @protected */
while (this.cursor > this.limit_backward) {
var ch = this.current.charCodeAt(this.cursor - 1);
if (ch > max || ch < min) return true;
ch -= min;
if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return true;
this.cursor--;
}
return false;
};
/**
* @param {number[]} s
* @param {number} min
* @param {number} max
* @return {boolean}
*/
this.out_grouping = function(s, min, max) {
/** @protected */
if (this.cursor >= this.limit) return false;
var ch = this.current.charCodeAt(this.cursor);
if (ch > max || ch < min) {
this.cursor++;
return true;
}
ch -= min;
if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) == 0) {
this.cursor++;
return true;
}
return false;
};
/**
* @param {number[]} s
* @param {number} min
* @param {number} max
* @return {boolean}
*/
this.go_out_grouping = function(s, min, max) {
/** @protected */
while (this.cursor < this.limit) {
var ch = this.current.charCodeAt(this.cursor);
if (ch <= max && ch >= min) {
ch -= min;
if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) != 0) {
return true;
}
}
this.cursor++;
}
return false;
};
/**
* @param {number[]} s
* @param {number} min
* @param {number} max
* @return {boolean}
*/
this.out_grouping_b = function(s, min, max) {
/** @protected */
if (this.cursor <= this.limit_backward) return false;
var ch = this.current.charCodeAt(this.cursor - 1);
if (ch > max || ch < min) {
this.cursor--;
return true;
}
ch -= min;
if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) {
this.cursor--;
return true;
}
return false;
};
/**
* @param {number[]} s
* @param {number} min
* @param {number} max
* @return {boolean}
*/
this.go_out_grouping_b = function(s, min, max) {
/** @protected */
while (this.cursor > this.limit_backward) {
var ch = this.current.charCodeAt(this.cursor - 1);
if (ch <= max && ch >= min) {
ch -= min;
if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) != 0) {
return true;
}
}
this.cursor--;
}
return false;
};
/**
* @param {string} s
* @return {boolean}
*/
this.eq_s = function(s)
{
/** @protected */
if (this.limit - this.cursor < s.length) return false;
if (this.current.slice(this.cursor, this.cursor + s.length) != s)
{
return false;
}
this.cursor += s.length;
return true;
};
/**
* @param {string} s
* @return {boolean}
*/
this.eq_s_b = function(s)
{
/** @protected */
if (this.cursor - this.limit_backward < s.length) return false;
if (this.current.slice(this.cursor - s.length, this.cursor) != s)
{
return false;
}
this.cursor -= s.length;
return true;
};
/**
* @param {Among[]} v
* @return {number}
*/
this.find_among = function(v)
{
/** @protected */
var i = 0;
var j = v.length;
var c = this.cursor;
var l = this.limit;
var common_i = 0;
var common_j = 0;
var first_key_inspected = false;
while (true)
{
var k = i + ((j - i) >>> 1);
var diff = 0;
var common = common_i < common_j ? common_i : common_j; // smaller
// w[0]: string, w[1]: substring_i, w[2]: result, w[3]: function (optional)
var w = v[k];
var i2;
for (i2 = common; i2 < w[0].length; i2++)
{
if (c + common == l)
{
diff = -1;
break;
}
diff = this.current.charCodeAt(c + common) - w[0].charCodeAt(i2);
if (diff != 0) break;
common++;
}
if (diff < 0)
{
j = k;
common_j = common;
}
else
{
i = k;
common_i = common;
}
if (j - i <= 1)
{
if (i > 0) break; // v->s has been inspected
if (j == i) break; // only one item in v
// - but now we need to go round once more to get
// v->s inspected. This looks messy, but is actually
// the optimal approach.
if (first_key_inspected) break;
first_key_inspected = true;
}
}
do {
var w = v[i];
if (common_i >= w[0].length)
{
this.cursor = c + w[0].length;
if (w.length < 4) return w[2];
var res = w[3](this);
this.cursor = c + w[0].length;
if (res) return w[2];
}
i = w[1];
} while (i >= 0);
return 0;
};
// find_among_b is for backwards processing. Same comments apply
/**
* @param {Among[]} v
* @return {number}
*/
this.find_among_b = function(v)
{
/** @protected */
var i = 0;
var j = v.length
var c = this.cursor;
var lb = this.limit_backward;
var common_i = 0;
var common_j = 0;
var first_key_inspected = false;
while (true)
{
var k = i + ((j - i) >> 1);
var diff = 0;
var common = common_i < common_j ? common_i : common_j;
var w = v[k];
var i2;
for (i2 = w[0].length - 1 - common; i2 >= 0; i2--)
{
if (c - common == lb)
{
diff = -1;
break;
}
diff = this.current.charCodeAt(c - 1 - common) - w[0].charCodeAt(i2);
if (diff != 0) break;
common++;
}
if (diff < 0)
{
j = k;
common_j = common;
}
else
{
i = k;
common_i = common;
}
if (j - i <= 1)
{
if (i > 0) break;
if (j == i) break;
if (first_key_inspected) break;
first_key_inspected = true;
}
}
do {
var w = v[i];
if (common_i >= w[0].length)
{
this.cursor = c - w[0].length;
if (w.length < 4) return w[2];
var res = w[3](this);
this.cursor = c - w[0].length;
if (res) return w[2];
}
i = w[1];
} while (i >= 0);
return 0;
};
/* to replace chars between c_bra and c_ket in this.current by the
* chars in s.
*/
/**
* @param {number} c_bra
* @param {number} c_ket
* @param {string} s
* @return {number}
*/
this.replace_s = function(c_bra, c_ket, s)
{
/** @protected */
var adjustment = s.length - (c_ket - c_bra);
this.current = this.current.slice(0, c_bra) + s + this.current.slice(c_ket);
this.limit += adjustment;
if (this.cursor >= c_ket) this.cursor += adjustment;
else if (this.cursor > c_bra) this.cursor = c_bra;
return adjustment;
};
/**
* @return {boolean}
*/
this.slice_check = function()
{
/** @protected */
if (this.bra < 0 ||
this.bra > this.ket ||
this.ket > this.limit ||
this.limit > this.current.length)
{
return false;
}
return true;
};
/**
* @param {number} c_bra
* @return {boolean}
*/
this.slice_from = function(s)
{
/** @protected */
var result = false;
if (this.slice_check())
{
this.replace_s(this.bra, this.ket, s);
result = true;
}
return result;
};
/**
* @return {boolean}
*/
this.slice_del = function()
{
/** @protected */
return this.slice_from("");
};
/**
* @param {number} c_bra
* @param {number} c_ket
* @param {string} s
*/
this.insert = function(c_bra, c_ket, s)
{
/** @protected */
var adjustment = this.replace_s(c_bra, c_ket, s);
if (c_bra <= this.bra) this.bra += adjustment;
if (c_bra <= this.ket) this.ket += adjustment;
};
/**
* @return {string}
*/
this.slice_to = function()
{
/** @protected */
var result = '';
if (this.slice_check())
{
result = this.current.slice(this.bra, this.ket);
}
return result;
};
/**
* @return {string}
*/
this.assign_to = function()
{
/** @protected */
return this.current.slice(0, this.limit);
};
};

View file

@ -1,906 +0,0 @@
/*
* Sphinx stylesheet -- basic theme.
*/
/* -- main layout ----------------------------------------------------------- */
div.clearer {
clear: both;
}
div.section::after {
display: block;
content: '';
clear: left;
}
/* -- relbar ---------------------------------------------------------------- */
div.related {
width: 100%;
font-size: 90%;
}
div.related h3 {
display: none;
}
div.related ul {
margin: 0;
padding: 0 0 0 10px;
list-style: none;
}
div.related li {
display: inline;
}
div.related li.right {
float: right;
margin-right: 5px;
}
/* -- sidebar --------------------------------------------------------------- */
div.sphinxsidebarwrapper {
padding: 10px 5px 0 10px;
}
div.sphinxsidebar {
float: left;
width: 230px;
margin-left: -100%;
font-size: 90%;
word-wrap: break-word;
overflow-wrap : break-word;
}
div.sphinxsidebar ul {
list-style: none;
}
div.sphinxsidebar ul ul,
div.sphinxsidebar ul.want-points {
margin-left: 20px;
list-style: square;
}
div.sphinxsidebar ul ul {
margin-top: 0;
margin-bottom: 0;
}
div.sphinxsidebar form {
margin-top: 10px;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
div.sphinxsidebar #searchbox form.search {
overflow: hidden;
}
div.sphinxsidebar #searchbox input[type="text"] {
float: left;
width: 80%;
padding: 0.25em;
box-sizing: border-box;
}
div.sphinxsidebar #searchbox input[type="submit"] {
float: left;
width: 20%;
border-left: none;
padding: 0.25em;
box-sizing: border-box;
}
img {
border: 0;
max-width: 100%;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin-top: 10px;
}
ul.search li {
padding: 5px 0;
}
ul.search li a {
font-weight: bold;
}
ul.search li p.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
}
ul.keywordmatches li.goodmatch a {
font-weight: bold;
}
/* -- index page ------------------------------------------------------------ */
table.contentstable {
width: 90%;
margin-left: auto;
margin-right: auto;
}
table.contentstable p.biglink {
line-height: 150%;
}
a.biglink {
font-size: 1.3em;
}
span.linkdescr {
font-style: italic;
padding-top: 5px;
font-size: 90%;
}
/* -- general index --------------------------------------------------------- */
table.indextable {
width: 100%;
}
table.indextable td {
text-align: left;
vertical-align: top;
}
table.indextable ul {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
table.indextable > tbody > tr > td > ul {
padding-left: 0em;
}
table.indextable tr.pcap {
height: 10px;
}
table.indextable tr.cap {
margin-top: 10px;
background-color: #f2f2f2;
}
img.toggler {
margin-right: 3px;
margin-top: 3px;
cursor: pointer;
}
div.modindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
div.genindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
/* -- domain module index --------------------------------------------------- */
table.modindextable td {
padding: 2px;
border-collapse: collapse;
}
/* -- general body styles --------------------------------------------------- */
div.body {
min-width: 360px;
max-width: 800px;
}
div.body p, div.body dd, div.body li, div.body blockquote {
-moz-hyphens: auto;
-ms-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
a.headerlink {
visibility: hidden;
}
a:visited {
color: #551A8B;
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
h4:hover > a.headerlink,
h5:hover > a.headerlink,
h6:hover > a.headerlink,
dt:hover > a.headerlink,
caption:hover > a.headerlink,
p.caption:hover > a.headerlink,
div.code-block-caption:hover > a.headerlink {
visibility: visible;
}
div.body p.caption {
text-align: inherit;
}
div.body td {
text-align: left;
}
.first {
margin-top: 0 !important;
}
p.rubric {
margin-top: 30px;
font-weight: bold;
}
img.align-left, figure.align-left, .figure.align-left, object.align-left {
clear: left;
float: left;
margin-right: 1em;
}
img.align-right, figure.align-right, .figure.align-right, object.align-right {
clear: right;
float: right;
margin-left: 1em;
}
img.align-center, figure.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
img.align-default, figure.align-default, .figure.align-default {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-default {
text-align: center;
}
.align-right {
text-align: right;
}
/* -- sidebars -------------------------------------------------------------- */
div.sidebar,
aside.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px;
background-color: #ffe;
width: 40%;
float: right;
clear: right;
overflow-x: auto;
}
p.sidebar-title {
font-weight: bold;
}
nav.contents,
aside.topic,
div.admonition, div.topic, blockquote {
clear: left;
}
/* -- topics ---------------------------------------------------------------- */
nav.contents,
aside.topic,
div.topic {
border: 1px solid #ccc;
padding: 7px;
margin: 10px 0 10px 0;
}
p.topic-title {
font-size: 1.1em;
font-weight: bold;
margin-top: 10px;
}
/* -- admonitions ----------------------------------------------------------- */
div.admonition {
margin-top: 10px;
margin-bottom: 10px;
padding: 7px;
}
div.admonition dt {
font-weight: bold;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
}
div.body p.centered {
text-align: center;
margin-top: 25px;
}
/* -- content of sidebars/topics/admonitions -------------------------------- */
div.sidebar > :last-child,
aside.sidebar > :last-child,
nav.contents > :last-child,
aside.topic > :last-child,
div.topic > :last-child,
div.admonition > :last-child {
margin-bottom: 0;
}
div.sidebar::after,
aside.sidebar::after,
nav.contents::after,
aside.topic::after,
div.topic::after,
div.admonition::after,
blockquote::after {
display: block;
content: '';
clear: both;
}
/* -- tables ---------------------------------------------------------------- */
table.docutils {
margin-top: 10px;
margin-bottom: 10px;
border: 0;
border-collapse: collapse;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
table.align-default {
margin-left: auto;
margin-right: auto;
}
table caption span.caption-number {
font-style: italic;
}
table caption span.caption-text {
}
table.docutils td, table.docutils th {
padding: 1px 8px 1px 5px;
border-top: 0;
border-left: 0;
border-right: 0;
border-bottom: 1px solid #aaa;
}
th {
text-align: left;
padding-right: 5px;
}
table.citation {
border-left: solid 1px gray;
margin-left: 1px;
}
table.citation td {
border-bottom: none;
}
th > :first-child,
td > :first-child {
margin-top: 0px;
}
th > :last-child,
td > :last-child {
margin-bottom: 0px;
}
/* -- figures --------------------------------------------------------------- */
div.figure, figure {
margin: 0.5em;
padding: 0.5em;
}
div.figure p.caption, figcaption {
padding: 0.3em;
}
div.figure p.caption span.caption-number,
figcaption span.caption-number {
font-style: italic;
}
div.figure p.caption span.caption-text,
figcaption span.caption-text {
}
/* -- field list styles ----------------------------------------------------- */
table.field-list td, table.field-list th {
border: 0 !important;
}
.field-list ul {
margin: 0;
padding-left: 1em;
}
.field-list p {
margin: 0;
}
.field-name {
-moz-hyphens: manual;
-ms-hyphens: manual;
-webkit-hyphens: manual;
hyphens: manual;
}
/* -- hlist styles ---------------------------------------------------------- */
table.hlist {
margin: 1em 0;
}
table.hlist td {
vertical-align: top;
}
/* -- object description styles --------------------------------------------- */
.sig {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
}
.sig-name, code.descname {
background-color: transparent;
font-weight: bold;
}
.sig-name {
font-size: 1.1em;
}
code.descname {
font-size: 1.2em;
}
.sig-prename, code.descclassname {
background-color: transparent;
}
.optional {
font-size: 1.3em;
}
.sig-paren {
font-size: larger;
}
.sig-param.n {
font-style: italic;
}
/* C++ specific styling */
.sig-inline.c-texpr,
.sig-inline.cpp-texpr {
font-family: unset;
}
.sig.c .k, .sig.c .kt,
.sig.cpp .k, .sig.cpp .kt {
color: #0033B3;
}
.sig.c .m,
.sig.cpp .m {
color: #1750EB;
}
.sig.c .s, .sig.c .sc,
.sig.cpp .s, .sig.cpp .sc {
color: #067D17;
}
/* -- other body styles ----------------------------------------------------- */
ol.arabic {
list-style: decimal;
}
ol.loweralpha {
list-style: lower-alpha;
}
ol.upperalpha {
list-style: upper-alpha;
}
ol.lowerroman {
list-style: lower-roman;
}
ol.upperroman {
list-style: upper-roman;
}
:not(li) > ol > li:first-child > :first-child,
:not(li) > ul > li:first-child > :first-child {
margin-top: 0px;
}
:not(li) > ol > li:last-child > :last-child,
:not(li) > ul > li:last-child > :last-child {
margin-bottom: 0px;
}
ol.simple ol p,
ol.simple ul p,
ul.simple ol p,
ul.simple ul p {
margin-top: 0;
}
ol.simple > li:not(:first-child) > p,
ul.simple > li:not(:first-child) > p {
margin-top: 0;
}
ol.simple p,
ul.simple p {
margin-bottom: 0;
}
aside.footnote > span,
div.citation > span {
float: left;
}
aside.footnote > span:last-of-type,
div.citation > span:last-of-type {
padding-right: 0.5em;
}
aside.footnote > p {
margin-left: 2em;
}
div.citation > p {
margin-left: 4em;
}
aside.footnote > p:last-of-type,
div.citation > p:last-of-type {
margin-bottom: 0em;
}
aside.footnote > p:last-of-type:after,
div.citation > p:last-of-type:after {
content: "";
clear: both;
}
dl.field-list {
display: grid;
grid-template-columns: fit-content(30%) auto;
}
dl.field-list > dt {
font-weight: bold;
word-break: break-word;
padding-left: 0.5em;
padding-right: 5px;
}
dl.field-list > dd {
padding-left: 0.5em;
margin-top: 0em;
margin-left: 0em;
margin-bottom: 0em;
}
dl {
margin-bottom: 15px;
}
dd > :first-child {
margin-top: 0px;
}
dd ul, dd table {
margin-bottom: 10px;
}
dd {
margin-top: 3px;
margin-bottom: 10px;
margin-left: 30px;
}
.sig dd {
margin-top: 0px;
margin-bottom: 0px;
}
.sig dl {
margin-top: 0px;
margin-bottom: 0px;
}
dl > dd:last-child,
dl > dd:last-child > :last-child {
margin-bottom: 0;
}
dt:target, span.highlighted {
background-color: #fbe54e;
}
rect.highlighted {
fill: #fbe54e;
}
dl.glossary dt {
font-weight: bold;
font-size: 1.1em;
}
.versionmodified {
font-style: italic;
}
.system-message {
background-color: #fda;
padding: 5px;
border: 3px solid red;
}
.footnote:target {
background-color: #ffa;
}
.line-block {
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
.line-block .line-block {
margin-top: 0;
margin-bottom: 0;
margin-left: 1.5em;
}
.guilabel, .menuselection {
font-family: sans-serif;
}
.accelerator {
text-decoration: underline;
}
.classifier {
font-style: oblique;
}
.classifier:before {
font-style: normal;
margin: 0 0.5em;
content: ":";
display: inline-block;
}
abbr, acronym {
border-bottom: dotted 1px;
cursor: help;
}
/* -- code displays --------------------------------------------------------- */
pre {
overflow: auto;
overflow-y: hidden; /* fixes display issues on Chrome browsers */
}
pre, div[class*="highlight-"] {
clear: both;
}
span.pre {
-moz-hyphens: none;
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
white-space: nowrap;
}
div[class*="highlight-"] {
margin: 1em 0;
}
td.linenos pre {
border: 0;
background-color: transparent;
color: #aaa;
}
table.highlighttable {
display: block;
}
table.highlighttable tbody {
display: block;
}
table.highlighttable tr {
display: flex;
}
table.highlighttable td {
margin: 0;
padding: 0;
}
table.highlighttable td.linenos {
padding-right: 0.5em;
}
table.highlighttable td.code {
flex: 1;
overflow: hidden;
}
.highlight .hll {
display: block;
}
div.highlight pre,
table.highlighttable pre {
margin: 0;
}
div.code-block-caption + div {
margin-top: 0;
}
div.code-block-caption {
margin-top: 1em;
padding: 2px 5px;
font-size: small;
}
div.code-block-caption code {
background-color: transparent;
}
table.highlighttable td.linenos,
span.linenos,
div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
-webkit-user-select: text; /* Safari fallback only */
-webkit-user-select: none; /* Chrome/Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+ */
}
div.code-block-caption span.caption-number {
padding: 0.1em 0.3em;
font-style: italic;
}
div.code-block-caption span.caption-text {
}
div.literal-block-wrapper {
margin: 1em 0;
}
code.xref, a code {
background-color: transparent;
font-weight: bold;
}
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
background-color: transparent;
}
.viewcode-link {
float: right;
}
.viewcode-back {
float: right;
font-family: sans-serif;
}
div.viewcode-block:target {
margin: -1px -10px;
padding: 0 10px;
}
/* -- math display ---------------------------------------------------------- */
img.math {
vertical-align: middle;
}
div.body div.math p {
text-align: center;
}
span.eqno {
float: right;
}
span.eqno a.headerlink {
position: absolute;
z-index: 1;
}
div.math:hover a.headerlink {
visibility: visible;
}
/* -- printout stylesheet --------------------------------------------------- */
@media print {
div.document,
div.documentwrapper,
div.bodywrapper {
margin: 0 !important;
width: 100%;
}
div.sphinxsidebar,
div.related,
div.footer,
#top-link {
display: none;
}
}

View file

@ -1,150 +0,0 @@
/*
* Base JavaScript utilities for all Sphinx HTML documentation.
*/
"use strict";
const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([
"TEXTAREA",
"INPUT",
"SELECT",
"BUTTON",
]);
const _ready = (callback) => {
if (document.readyState !== "loading") {
callback();
} else {
document.addEventListener("DOMContentLoaded", callback);
}
};
/**
* Small JavaScript module for the documentation.
*/
const Documentation = {
init: () => {
Documentation.initDomainIndexTable();
Documentation.initOnKeyListeners();
},
/**
* i18n support
*/
TRANSLATIONS: {},
PLURAL_EXPR: (n) => (n === 1 ? 0 : 1),
LOCALE: "unknown",
// gettext and ngettext don't access this so that the functions
// can safely bound to a different name (_ = Documentation.gettext)
gettext: (string) => {
const translated = Documentation.TRANSLATIONS[string];
switch (typeof translated) {
case "undefined":
return string; // no translation
case "string":
return translated; // translation exists
default:
return translated[0]; // (singular, plural) translation tuple exists
}
},
ngettext: (singular, plural, n) => {
const translated = Documentation.TRANSLATIONS[singular];
if (typeof translated !== "undefined")
return translated[Documentation.PLURAL_EXPR(n)];
return n === 1 ? singular : plural;
},
addTranslations: (catalog) => {
Object.assign(Documentation.TRANSLATIONS, catalog.messages);
Documentation.PLURAL_EXPR = new Function(
"n",
`return (${catalog.plural_expr})`,
);
Documentation.LOCALE = catalog.locale;
},
/**
* helper function to focus on search bar
*/
focusSearchBar: () => {
document.querySelectorAll("input[name=q]")[0]?.focus();
},
/**
* Initialise the domain index toggle buttons
*/
initDomainIndexTable: () => {
const toggler = (el) => {
const idNumber = el.id.substr(7);
const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`);
if (el.src.substr(-9) === "minus.png") {
el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`;
toggledRows.forEach((el) => (el.style.display = "none"));
} else {
el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`;
toggledRows.forEach((el) => (el.style.display = ""));
}
};
const togglerElements = document.querySelectorAll("img.toggler");
togglerElements.forEach((el) =>
el.addEventListener("click", (event) => toggler(event.currentTarget)),
);
togglerElements.forEach((el) => (el.style.display = ""));
if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler);
},
initOnKeyListeners: () => {
// only install a listener if it is really needed
if (
!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS
&& !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS
)
return;
document.addEventListener("keydown", (event) => {
// bail for input elements
if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName))
return;
// bail with special keys
if (event.altKey || event.ctrlKey || event.metaKey) return;
if (!event.shiftKey) {
switch (event.key) {
case "ArrowLeft":
if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
const prevLink = document.querySelector('link[rel="prev"]');
if (prevLink && prevLink.href) {
window.location.href = prevLink.href;
event.preventDefault();
}
break;
case "ArrowRight":
if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
const nextLink = document.querySelector('link[rel="next"]');
if (nextLink && nextLink.href) {
window.location.href = nextLink.href;
event.preventDefault();
}
break;
}
}
// some keyboard layouts may need Shift to get /
switch (event.key) {
case "/":
if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break;
Documentation.focusSearchBar();
event.preventDefault();
}
});
},
};
// quick alias for translations
const _ = Documentation.gettext;
_ready(Documentation.init);

View file

@ -1,13 +0,0 @@
const DOCUMENTATION_OPTIONS = {
VERSION: '0.1.0',
LANGUAGE: 'fr',
COLLAPSE_INDEX: false,
BUILDER: 'html',
FILE_SUFFIX: '.html',
LINK_SUFFIX: '.html',
HAS_SOURCE: true,
SOURCELINK_SUFFIX: '',
NAVIGATION_WITH_KEYS: false,
SHOW_SEARCH_SUMMARY: true,
ENABLE_SEARCH_SHORTCUTS: true,
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 B

File diff suppressed because it is too large Load diff

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 44.4 44.4" style="enable-background:new 0 0 44.4 44.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#F5A252;stroke-width:5;stroke-miterlimit:10;}
.st1{fill:none;stroke:#579ACA;stroke-width:5;stroke-miterlimit:10;}
.st2{fill:none;stroke:#E66581;stroke-width:5;stroke-miterlimit:10;}
</style>
<title>logo</title>
<g>
<path class="st0" d="M33.9,6.4c3.6,3.9,3.4,9.9-0.5,13.5s-9.9,3.4-13.5-0.5s-3.4-9.9,0.5-13.5l0,0C24.2,2.4,30.2,2.6,33.9,6.4z"/>
<path class="st1" d="M35.1,27.3c2.6,4.6,1.1,10.4-3.5,13c-4.6,2.6-10.4,1.1-13-3.5s-1.1-10.4,3.5-13l0,0
C26.6,21.2,32.4,22.7,35.1,27.3z"/>
<path class="st2" d="M25.9,17.8c2.6,4.6,1.1,10.4-3.5,13s-10.4,1.1-13-3.5s-1.1-10.4,3.5-13l0,0C17.5,11.7,23.3,13.2,25.9,17.8z"/>
<path class="st1" d="M19.2,26.4c3.1-4.3,9.1-5.2,13.3-2.1c1.1,0.8,2,1.8,2.7,3"/>
<path class="st0" d="M19.9,19.4c-3.6-3.9-3.4-9.9,0.5-13.5s9.9-3.4,13.5,0.5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

View file

@ -1 +0,0 @@
<svg viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 128h52.512l29.539-11.077-11.077-43.487-34.051 3.693L0 128Z" fill="#0076D4"/><path fill-rule="evenodd" clip-rule="evenodd" d="M52.513 128s16.6-8.759 19.673-24.277c3.072-15.517-12.091-26.594-35.263-26.594 0-.41 20.343-28.718 20.343-28.718l49.4 1.435L95.71 107.7l-20.452 15.978L52.513 128Z" fill="#002868"/><path fill-rule="evenodd" clip-rule="evenodd" d="M0 60.718 41.025.001s1.006.01 3.282 0c16.082-.068 81.23 3.12 81.23 60.368 0 65.352-73.025 67.631-73.025 67.631s30.495-5.839 30.495-34.816c0-28.978-27.541-32.466-45.264-32.466H0Z" fill="#00A9FF"/></svg>

Before

Width:  |  Height:  |  Size: 681 B

View file

@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="38.73" height="50" viewBox="0 0 38.73 50"><defs><style>.cls-1{fill:#767677;}.cls-2{fill:#f37726;}.cls-3{fill:#9e9e9e;}.cls-4{fill:#616262;}.cls-5{font-size:17.07px;fill:#fff;font-family:Roboto-Regular, Roboto;}</style></defs><title>logo_jupyterhub</title><g id="Canvas"><path id="path7_fill" data-name="path7 fill" class="cls-1" d="M39.51,3.53a3,3,0,0,1-1.7,2.9A3,3,0,0,1,34.48,6a3,3,0,0,1-.82-3.26,3,3,0,0,1,1.05-1.41A3,3,0,0,1,37.52.86a2.88,2.88,0,0,1,1,.6,3,3,0,0,1,.7.93,3.18,3.18,0,0,1,.28,1.14Z" transform="translate(-1.87 -0.69)"/><path id="path8_fill" data-name="path8 fill" class="cls-2" d="M21.91,38.39c-8,0-15.06-2.87-18.7-7.12a19.93,19.93,0,0,0,37.39,0C37,35.52,30,38.39,21.91,38.39Z" transform="translate(-1.87 -0.69)"/><path id="path9_fill" data-name="path9 fill" class="cls-2" d="M21.91,10.78c8,0,15.05,2.87,18.69,7.12a19.93,19.93,0,0,0-37.39,0C6.85,13.64,13.86,10.78,21.91,10.78Z" transform="translate(-1.87 -0.69)"/><path id="path10_fill" data-name="path10 fill" class="cls-3" d="M10.88,46.66a3.86,3.86,0,0,1-.52,2.15,3.81,3.81,0,0,1-1.62,1.51,3.93,3.93,0,0,1-2.19.34,3.79,3.79,0,0,1-2-.94,3.73,3.73,0,0,1-1.14-1.9,3.79,3.79,0,0,1,.1-2.21,3.86,3.86,0,0,1,1.33-1.78,3.92,3.92,0,0,1,3.54-.53,3.85,3.85,0,0,1,2.14,1.93,3.74,3.74,0,0,1,.37,1.43Z" transform="translate(-1.87 -0.69)"/><path id="path11_fill" data-name="path11 fill" class="cls-4" d="M4.12,9.81A2.18,2.18,0,0,1,2.9,9.48a2.23,2.23,0,0,1-.84-1A2.26,2.26,0,0,1,1.9,7.26a2.13,2.13,0,0,1,.56-1.13,2.18,2.18,0,0,1,2.36-.56,2.13,2.13,0,0,1,1,.76,2.18,2.18,0,0,1,.42,1.2A2.22,2.22,0,0,1,4.12,9.81Z" transform="translate(-1.87 -0.69)"/></g><text class="cls-5" transform="translate(5.24 30.01)">Hub</text></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,17 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="68" height="68" viewBox="0 0 17.992 17.992">
<g transform="translate(.46 .448)">
<circle style="display:inline;opacity:1;vector-effect:none;fill:#f7dc1e;fill-opacity:1;stroke:none;stroke-width:.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers stroke fill" cx="83.727" cy="85.633" r="6.598" transform="translate(-75.191 -79.025)"/>
<rect style="display:inline;opacity:1;vector-effect:none;fill:#9e9e9e;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" width="5.653" height="1.01" x="80.831" y="92.344" ry=".413" rx=".413" transform="translate(-75.191 -79.025)"/>
<rect y="93.671" x="80.837" height="1.031" width="5.641" style="display:inline;opacity:1;vector-effect:none;fill:#767677;fill-opacity:1;stroke:none;stroke-width:1.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" ry=".421" rx=".421" transform="translate(-75.191 -79.025)"/>
<rect style="display:inline;opacity:1;vector-effect:none;fill:#616262;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers" width="3.24" height="1.02" x="82.038" y="95.019" ry=".417" rx=".417" transform="translate(-75.191 -79.025)"/>
<g style="display:inline;fill:#1a1a1a">
<g style="font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:8.46666718px;line-height:1.25;font-family:Vibur;-inkscape-font-specification:'Vibur, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0;word-spacing:0;writing-mode:lr-tb;text-anchor:start;fill:#1a1a1a;stroke-width:.26458335" aria-label="l">
<path style="font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:8.46666718px;font-family:Vibur;-inkscape-font-specification:'Vibur, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#1a1a1a;stroke-width:.26458335" d="M87.065 90.289q-.996-.041-1.475-1.216-.356-.884-.356-2.166 0-.066.008-.17.058-.872.323-1.6.43-1.173 1.215-1.173.042 0 .087.004.715.045.715.83 0 .857-.736 1.902-.467.666-.98 1.08.141.785.31 1.178.315.727.935.727.041 0 .087-.008.136-.025.38-.198.236-.162.368-.162.058 0 .112.038.12.082.12.223-.009.215-.306.442-.372.269-.807.269zm-.28-5.701q-.245.004-.43.21-.472.534-.517 2.23.248-.253.496-.596.686-.939.686-1.584 0-.033-.017-.078-.066-.182-.219-.182z" transform="translate(-76.409 -79.473)"/>
</g>
<path d="M84.998 83.663a.694.694 0 0 1-.694.695.694.694 0 0 1-.694-.695.694.694 0 0 1 .694-.694.694.694 0 0 1 .694.694z" style="opacity:1;vector-effect:none;fill:#1a1a1a;fill-opacity:1;stroke:none;stroke-width:.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers stroke fill" transform="translate(-76.409 -79.473)"/>
<g style="font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:8.46666718px;line-height:1.25;font-family:Vibur;-inkscape-font-specification:'Vibur, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0;word-spacing:0;writing-mode:lr-tb;text-anchor:start;fill:#1a1a1a;stroke-width:.26458335" aria-label="j">
<path style="font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:8.46666718px;font-family:Vibur;-inkscape-font-specification:'Vibur, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#1a1a1a;stroke-width:.26458335" d="M84.155 84.667q.269.004.277.35l-.459 3.283q1.034-.79 1.162-.893.153-.132.314-.136.058 0 .112.037.12.083.12.211-.005.199-.224.385-.607.512-1.608 1.26l-.19 1.369q-.012.095-.037.186-.19.727-.955.727-.38 0-.67-.285-.285-.285-.285-.64 0-.373.178-.612.099-.124.388-.34l.992-.744q.525-3.468.538-3.646 0-.025.004-.045.045-.467.343-.467zm-1.087 5.535.07-.504-.459.351q-.298.223-.302.422 0 .07.025.132.07.182.265.182.078 0 .14-.025.199-.087.26-.558zm.897-6.325q0-.074.037-.14.087-.162.269-.162.074 0 .144.037.158.087.158.265 0 .074-.038.145-.086.157-.264.157-.075 0-.145-.038-.161-.082-.161-.264z" transform="translate(-76.409 -79.473)"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ar\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "بواسطة"
msgid "By"
msgstr "بواسطة"
msgid "Contents"
msgstr "محتويات"
msgid "Copyright"
msgstr "حقوق النشر"
msgid "Download notebook file"
msgstr "تنزيل ملف دفتر الملاحظات"
msgid "Download source file"
msgstr "تنزيل ملف المصدر"
msgid "Download this page"
msgstr "قم بتنزيل هذه الصفحة"
msgid "Edit this page"
msgstr "قم بتحرير هذه الصفحة"
msgid "Fullscreen mode"
msgstr "وضع ملء الشاشة"
msgid "Last updated on"
msgstr "آخر تحديث في"
msgid "Launch"
msgstr "إطلاق"
msgid "Open an issue"
msgstr "افتح قضية"
msgid "Print to PDF"
msgstr "طباعة إلى PDF"
msgid "Source repository"
msgstr "مستودع المصدر"
msgid "Sphinx Book Theme"
msgstr "موضوع كتاب أبو الهول"
msgid "Theme by the"
msgstr "موضوع بواسطة"
msgid "Toggle navigation"
msgstr "تبديل التنقل"
msgid "next page"
msgstr "الصفحة التالية"
msgid "open issue"
msgstr "قضية مفتوحة"
msgid "previous page"
msgstr "الصفحة السابقة"
msgid "repository"
msgstr "مخزن"
msgid "suggest edit"
msgstr "أقترح تحرير"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: bg\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "По"
msgid "By"
msgstr "От"
msgid "Contents"
msgstr "Съдържание"
msgid "Copyright"
msgstr "Авторско право"
msgid "Download notebook file"
msgstr "Изтеглете файла на бележника"
msgid "Download source file"
msgstr "Изтеглете изходния файл"
msgid "Download this page"
msgstr "Изтеглете тази страница"
msgid "Edit this page"
msgstr "Редактирайте тази страница"
msgid "Fullscreen mode"
msgstr "Режим на цял екран"
msgid "Last updated on"
msgstr "Последна актуализация на"
msgid "Launch"
msgstr "Стартиране"
msgid "Open an issue"
msgstr "Отворете проблем"
msgid "Print to PDF"
msgstr "Печат в PDF"
msgid "Source repository"
msgstr "Хранилище на източника"
msgid "Sphinx Book Theme"
msgstr "Тема на книгата Sphinx"
msgid "Theme by the"
msgstr "Тема от"
msgid "Toggle navigation"
msgstr "Превключване на навигацията"
msgid "next page"
msgstr "Следваща страница"
msgid "open issue"
msgstr "отворен брой"
msgid "previous page"
msgstr "предишна страница"
msgid "repository"
msgstr "хранилище"
msgid "suggest edit"
msgstr "предложи редактиране"

View file

@ -1,63 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: bn\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "দ্বারা"
msgid "By"
msgstr "দ্বারা"
msgid "Copyright"
msgstr "কপিরাইট"
msgid "Download notebook file"
msgstr "নোটবুক ফাইল ডাউনলোড করুন"
msgid "Download source file"
msgstr "উত্স ফাইল ডাউনলোড করুন"
msgid "Download this page"
msgstr "এই পৃষ্ঠাটি ডাউনলোড করুন"
msgid "Edit this page"
msgstr "এই পৃষ্ঠাটি সম্পাদনা করুন"
msgid "Last updated on"
msgstr "সর্বশেষ আপডেট"
msgid "Launch"
msgstr "শুরু করা"
msgid "Open an issue"
msgstr "একটি সমস্যা খুলুন"
msgid "Print to PDF"
msgstr "পিডিএফ প্রিন্ট করুন"
msgid "Source repository"
msgstr "উত্স সংগ্রহস্থল"
msgid "Sphinx Book Theme"
msgstr "স্পিনিক্স বুক থিম"
msgid "Theme by the"
msgstr "থিম দ্বারা"
msgid "Toggle navigation"
msgstr "নেভিগেশন টগল করুন"
msgid "next page"
msgstr "পরবর্তী পৃষ্ঠা"
msgid "open issue"
msgstr "খোলা সমস্যা"
msgid "previous page"
msgstr "আগের পৃষ্ঠা"

View file

@ -1,66 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ca\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Per la"
msgid "By"
msgstr "Per"
msgid "Copyright"
msgstr "Copyright"
msgid "Download notebook file"
msgstr "Descarregar fitxer de quadern"
msgid "Download source file"
msgstr "Baixeu el fitxer font"
msgid "Download this page"
msgstr "Descarregueu aquesta pàgina"
msgid "Edit this page"
msgstr "Editeu aquesta pàgina"
msgid "Last updated on"
msgstr "Darrera actualització el"
msgid "Launch"
msgstr "Llançament"
msgid "Open an issue"
msgstr "Obriu un número"
msgid "Print to PDF"
msgstr "Imprimeix a PDF"
msgid "Source repository"
msgstr "Dipòsit de fonts"
msgid "Sphinx Book Theme"
msgstr "Tema del llibre Esfinx"
msgid "Theme by the"
msgstr "Tema del"
msgid "Toggle navigation"
msgstr "Commuta la navegació"
msgid "next page"
msgstr "pàgina següent"
msgid "open issue"
msgstr "número obert"
msgid "previous page"
msgstr "Pàgina anterior"
msgid "suggest edit"
msgstr "suggerir edició"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: cs\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Podle"
msgid "By"
msgstr "Podle"
msgid "Contents"
msgstr "Obsah"
msgid "Copyright"
msgstr "autorská práva"
msgid "Download notebook file"
msgstr "Stáhnout soubor poznámkového bloku"
msgid "Download source file"
msgstr "Stáhněte si zdrojový soubor"
msgid "Download this page"
msgstr "Stáhněte si tuto stránku"
msgid "Edit this page"
msgstr "Upravit tuto stránku"
msgid "Fullscreen mode"
msgstr "Režim celé obrazovky"
msgid "Last updated on"
msgstr "Naposledy aktualizováno"
msgid "Launch"
msgstr "Zahájení"
msgid "Open an issue"
msgstr "Otevřete problém"
msgid "Print to PDF"
msgstr "Tisk do PDF"
msgid "Source repository"
msgstr "Zdrojové úložiště"
msgid "Sphinx Book Theme"
msgstr "Téma knihy Sfinga"
msgid "Theme by the"
msgstr "Téma od"
msgid "Toggle navigation"
msgstr "Přepnout navigaci"
msgid "next page"
msgstr "další strana"
msgid "open issue"
msgstr "otevřené číslo"
msgid "previous page"
msgstr "předchozí stránka"
msgid "repository"
msgstr "úložiště"
msgid "suggest edit"
msgstr "navrhnout úpravy"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: da\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Ved"
msgid "By"
msgstr "Ved"
msgid "Contents"
msgstr "Indhold"
msgid "Copyright"
msgstr "ophavsret"
msgid "Download notebook file"
msgstr "Download notesbog-fil"
msgid "Download source file"
msgstr "Download kildefil"
msgid "Download this page"
msgstr "Download denne side"
msgid "Edit this page"
msgstr "Rediger denne side"
msgid "Fullscreen mode"
msgstr "Fuldskærmstilstand"
msgid "Last updated on"
msgstr "Sidst opdateret den"
msgid "Launch"
msgstr "Start"
msgid "Open an issue"
msgstr "Åbn et problem"
msgid "Print to PDF"
msgstr "Udskriv til PDF"
msgid "Source repository"
msgstr "Kildelager"
msgid "Sphinx Book Theme"
msgstr "Sphinx bogtema"
msgid "Theme by the"
msgstr "Tema af"
msgid "Toggle navigation"
msgstr "Skift navigation"
msgid "next page"
msgstr "Næste side"
msgid "open issue"
msgstr "åbent nummer"
msgid "previous page"
msgstr "forrige side"
msgid "repository"
msgstr "lager"
msgid "suggest edit"
msgstr "foreslå redigering"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Bis zum"
msgid "By"
msgstr "Durch"
msgid "Contents"
msgstr "Inhalt"
msgid "Copyright"
msgstr "Urheberrechte ©"
msgid "Download notebook file"
msgstr "Notebook-Datei herunterladen"
msgid "Download source file"
msgstr "Quelldatei herunterladen"
msgid "Download this page"
msgstr "Laden Sie diese Seite herunter"
msgid "Edit this page"
msgstr "Bearbeite diese Seite"
msgid "Fullscreen mode"
msgstr "Vollbildmodus"
msgid "Last updated on"
msgstr "Zuletzt aktualisiert am"
msgid "Launch"
msgstr "Starten"
msgid "Open an issue"
msgstr "Öffnen Sie ein Problem"
msgid "Print to PDF"
msgstr "In PDF drucken"
msgid "Source repository"
msgstr "Quell-Repository"
msgid "Sphinx Book Theme"
msgstr "Sphinx-Buch-Thema"
msgid "Theme by the"
msgstr "Thema von der"
msgid "Toggle navigation"
msgstr "Navigation umschalten"
msgid "next page"
msgstr "Nächste Seite"
msgid "open issue"
msgstr "offenes Thema"
msgid "previous page"
msgstr "vorherige Seite"
msgid "repository"
msgstr "Repository"
msgid "suggest edit"
msgstr "vorschlagen zu bearbeiten"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: el\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Από το"
msgid "By"
msgstr "Με"
msgid "Contents"
msgstr "Περιεχόμενα"
msgid "Copyright"
msgstr "Πνευματική ιδιοκτησία"
msgid "Download notebook file"
msgstr "Λήψη αρχείου σημειωματάριου"
msgid "Download source file"
msgstr "Λήψη αρχείου προέλευσης"
msgid "Download this page"
msgstr "Λήψη αυτής της σελίδας"
msgid "Edit this page"
msgstr "Επεξεργαστείτε αυτήν τη σελίδα"
msgid "Fullscreen mode"
msgstr "ΛΕΙΤΟΥΡΓΙΑ ΠΛΗΡΟΥΣ ΟΘΟΝΗΣ"
msgid "Last updated on"
msgstr "Τελευταία ενημέρωση στις"
msgid "Launch"
msgstr "Εκτόξευση"
msgid "Open an issue"
msgstr "Ανοίξτε ένα ζήτημα"
msgid "Print to PDF"
msgstr "Εκτύπωση σε PDF"
msgid "Source repository"
msgstr "Αποθήκη πηγής"
msgid "Sphinx Book Theme"
msgstr "Θέμα βιβλίου Sphinx"
msgid "Theme by the"
msgstr "Θέμα από το"
msgid "Toggle navigation"
msgstr "Εναλλαγή πλοήγησης"
msgid "next page"
msgstr "επόμενη σελίδα"
msgid "open issue"
msgstr "ανοιχτό ζήτημα"
msgid "previous page"
msgstr "προηγούμενη σελίδα"
msgid "repository"
msgstr "αποθήκη"
msgid "suggest edit"
msgstr "προτείνω επεξεργασία"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: eo\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Per la"
msgid "By"
msgstr "De"
msgid "Contents"
msgstr "Enhavo"
msgid "Copyright"
msgstr "Kopirajto"
msgid "Download notebook file"
msgstr "Elŝutu kajeran dosieron"
msgid "Download source file"
msgstr "Elŝutu fontodosieron"
msgid "Download this page"
msgstr "Elŝutu ĉi tiun paĝon"
msgid "Edit this page"
msgstr "Redaktu ĉi tiun paĝon"
msgid "Fullscreen mode"
msgstr "Plenekrana reĝimo"
msgid "Last updated on"
msgstr "Laste ĝisdatigita la"
msgid "Launch"
msgstr "Lanĉo"
msgid "Open an issue"
msgstr "Malfermu numeron"
msgid "Print to PDF"
msgstr "Presi al PDF"
msgid "Source repository"
msgstr "Fonto-deponejo"
msgid "Sphinx Book Theme"
msgstr "Sfinksa Libro-Temo"
msgid "Theme by the"
msgstr "Temo de la"
msgid "Toggle navigation"
msgstr "Ŝalti navigadon"
msgid "next page"
msgstr "sekva paĝo"
msgid "open issue"
msgstr "malferma numero"
msgid "previous page"
msgstr "antaŭa paĝo"
msgid "repository"
msgstr "deponejo"
msgid "suggest edit"
msgstr "sugesti redaktadon"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Por el"
msgid "By"
msgstr "Por"
msgid "Contents"
msgstr "Contenido"
msgid "Copyright"
msgstr "Derechos de autor"
msgid "Download notebook file"
msgstr "Descargar archivo de cuaderno"
msgid "Download source file"
msgstr "Descargar archivo fuente"
msgid "Download this page"
msgstr "Descarga esta pagina"
msgid "Edit this page"
msgstr "Edita esta página"
msgid "Fullscreen mode"
msgstr "Modo de pantalla completa"
msgid "Last updated on"
msgstr "Ultima actualización en"
msgid "Launch"
msgstr "Lanzamiento"
msgid "Open an issue"
msgstr "Abrir un problema"
msgid "Print to PDF"
msgstr "Imprimir en PDF"
msgid "Source repository"
msgstr "Repositorio de origen"
msgid "Sphinx Book Theme"
msgstr "Tema del libro de la esfinge"
msgid "Theme by the"
msgstr "Tema por el"
msgid "Toggle navigation"
msgstr "Navegación de palanca"
msgid "next page"
msgstr "siguiente página"
msgid "open issue"
msgstr "Tema abierto"
msgid "previous page"
msgstr "pagina anterior"
msgid "repository"
msgstr "repositorio"
msgid "suggest edit"
msgstr "sugerir editar"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: et\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Autor"
msgid "By"
msgstr "Kõrval"
msgid "Contents"
msgstr "Sisu"
msgid "Copyright"
msgstr "Autoriõigus"
msgid "Download notebook file"
msgstr "Laadige sülearvuti fail alla"
msgid "Download source file"
msgstr "Laadige alla lähtefail"
msgid "Download this page"
msgstr "Laadige see leht alla"
msgid "Edit this page"
msgstr "Muutke seda lehte"
msgid "Fullscreen mode"
msgstr "Täisekraanirežiim"
msgid "Last updated on"
msgstr "Viimati uuendatud"
msgid "Launch"
msgstr "Käivitage"
msgid "Open an issue"
msgstr "Avage probleem"
msgid "Print to PDF"
msgstr "Prindi PDF-i"
msgid "Source repository"
msgstr "Allikahoidla"
msgid "Sphinx Book Theme"
msgstr "Sfinksiraamatu teema"
msgid "Theme by the"
msgstr "Teema"
msgid "Toggle navigation"
msgstr "Lülita navigeerimine sisse"
msgid "next page"
msgstr "järgmine leht"
msgid "open issue"
msgstr "avatud küsimus"
msgid "previous page"
msgstr "eelmine leht"
msgid "repository"
msgstr "hoidla"
msgid "suggest edit"
msgstr "soovita muuta"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Mukaan"
msgid "By"
msgstr "Tekijä"
msgid "Contents"
msgstr "Sisällys"
msgid "Copyright"
msgstr "Tekijänoikeus"
msgid "Download notebook file"
msgstr "Lataa muistikirjatiedosto"
msgid "Download source file"
msgstr "Lataa lähdetiedosto"
msgid "Download this page"
msgstr "Lataa tämä sivu"
msgid "Edit this page"
msgstr "Muokkaa tätä sivua"
msgid "Fullscreen mode"
msgstr "Koko näytön tila"
msgid "Last updated on"
msgstr "Viimeksi päivitetty"
msgid "Launch"
msgstr "Tuoda markkinoille"
msgid "Open an issue"
msgstr "Avaa ongelma"
msgid "Print to PDF"
msgstr "Tulosta PDF-tiedostoon"
msgid "Source repository"
msgstr "Lähteen arkisto"
msgid "Sphinx Book Theme"
msgstr "Sphinx-kirjan teema"
msgid "Theme by the"
msgstr "Teeman tekijä"
msgid "Toggle navigation"
msgstr "Vaihda navigointia"
msgid "next page"
msgstr "seuraava sivu"
msgid "open issue"
msgstr "avoin ongelma"
msgid "previous page"
msgstr "Edellinen sivu"
msgid "repository"
msgstr "arkisto"
msgid "suggest edit"
msgstr "ehdottaa muokkausta"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Par le"
msgid "By"
msgstr "Par"
msgid "Contents"
msgstr "Contenu"
msgid "Copyright"
msgstr "droits d'auteur"
msgid "Download notebook file"
msgstr "Télécharger le fichier notebook"
msgid "Download source file"
msgstr "Télécharger le fichier source"
msgid "Download this page"
msgstr "Téléchargez cette page"
msgid "Edit this page"
msgstr "Modifier cette page"
msgid "Fullscreen mode"
msgstr "Mode plein écran"
msgid "Last updated on"
msgstr "Dernière mise à jour le"
msgid "Launch"
msgstr "lancement"
msgid "Open an issue"
msgstr "Ouvrez un problème"
msgid "Print to PDF"
msgstr "Imprimer au format PDF"
msgid "Source repository"
msgstr "Dépôt source"
msgid "Sphinx Book Theme"
msgstr "Thème du livre Sphinx"
msgid "Theme by the"
msgstr "Thème par le"
msgid "Toggle navigation"
msgstr "Basculer la navigation"
msgid "next page"
msgstr "page suivante"
msgid "open issue"
msgstr "signaler un problème"
msgid "previous page"
msgstr "page précédente"
msgid "repository"
msgstr "dépôt"
msgid "suggest edit"
msgstr "suggestion de modification"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: hr\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Od strane"
msgid "By"
msgstr "Po"
msgid "Contents"
msgstr "Sadržaj"
msgid "Copyright"
msgstr "Autorska prava"
msgid "Download notebook file"
msgstr "Preuzmi datoteku bilježnice"
msgid "Download source file"
msgstr "Preuzmi izvornu datoteku"
msgid "Download this page"
msgstr "Preuzmite ovu stranicu"
msgid "Edit this page"
msgstr "Uredite ovu stranicu"
msgid "Fullscreen mode"
msgstr "Način preko cijelog zaslona"
msgid "Last updated on"
msgstr "Posljednje ažuriranje:"
msgid "Launch"
msgstr "Pokrenite"
msgid "Open an issue"
msgstr "Otvorite izdanje"
msgid "Print to PDF"
msgstr "Ispis u PDF"
msgid "Source repository"
msgstr "Izvorno spremište"
msgid "Sphinx Book Theme"
msgstr "Tema knjige Sphinx"
msgid "Theme by the"
msgstr "Tema autora"
msgid "Toggle navigation"
msgstr "Uključi / isključi navigaciju"
msgid "next page"
msgstr "sljedeća stranica"
msgid "open issue"
msgstr "otvoreno izdanje"
msgid "previous page"
msgstr "Prethodna stranica"
msgid "repository"
msgstr "spremište"
msgid "suggest edit"
msgstr "predloži uređivanje"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: id\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Oleh"
msgid "By"
msgstr "Oleh"
msgid "Contents"
msgstr "Isi"
msgid "Copyright"
msgstr "hak cipta"
msgid "Download notebook file"
msgstr "Unduh file notebook"
msgid "Download source file"
msgstr "Unduh file sumber"
msgid "Download this page"
msgstr "Unduh halaman ini"
msgid "Edit this page"
msgstr "Edit halaman ini"
msgid "Fullscreen mode"
msgstr "Mode layar penuh"
msgid "Last updated on"
msgstr "Terakhir diperbarui saat"
msgid "Launch"
msgstr "Meluncurkan"
msgid "Open an issue"
msgstr "Buka masalah"
msgid "Print to PDF"
msgstr "Cetak ke PDF"
msgid "Source repository"
msgstr "Repositori sumber"
msgid "Sphinx Book Theme"
msgstr "Tema Buku Sphinx"
msgid "Theme by the"
msgstr "Tema oleh"
msgid "Toggle navigation"
msgstr "Alihkan navigasi"
msgid "next page"
msgstr "halaman selanjutnya"
msgid "open issue"
msgstr "masalah terbuka"
msgid "previous page"
msgstr "halaman sebelumnya"
msgid "repository"
msgstr "gudang"
msgid "suggest edit"
msgstr "menyarankan edit"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: it\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Dal"
msgid "By"
msgstr "Di"
msgid "Contents"
msgstr "Contenuti"
msgid "Copyright"
msgstr "Diritto d'autore"
msgid "Download notebook file"
msgstr "Scarica il file del taccuino"
msgid "Download source file"
msgstr "Scarica il file sorgente"
msgid "Download this page"
msgstr "Scarica questa pagina"
msgid "Edit this page"
msgstr "Modifica questa pagina"
msgid "Fullscreen mode"
msgstr "Modalità schermo intero"
msgid "Last updated on"
msgstr "Ultimo aggiornamento il"
msgid "Launch"
msgstr "Lanciare"
msgid "Open an issue"
msgstr "Apri un problema"
msgid "Print to PDF"
msgstr "Stampa in PDF"
msgid "Source repository"
msgstr "Repository di origine"
msgid "Sphinx Book Theme"
msgstr "Tema del libro della Sfinge"
msgid "Theme by the"
msgstr "Tema di"
msgid "Toggle navigation"
msgstr "Attiva / disattiva la navigazione"
msgid "next page"
msgstr "pagina successiva"
msgid "open issue"
msgstr "questione aperta"
msgid "previous page"
msgstr "pagina precedente"
msgid "repository"
msgstr "repository"
msgid "suggest edit"
msgstr "suggerisci modifica"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: iw\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "דרך"
msgid "By"
msgstr "על ידי"
msgid "Contents"
msgstr "תוכן"
msgid "Copyright"
msgstr "זכויות יוצרים"
msgid "Download notebook file"
msgstr "הורד קובץ מחברת"
msgid "Download source file"
msgstr "הורד את קובץ המקור"
msgid "Download this page"
msgstr "הורד דף זה"
msgid "Edit this page"
msgstr "ערוך דף זה"
msgid "Fullscreen mode"
msgstr "מצב מסך מלא"
msgid "Last updated on"
msgstr "עודכן לאחרונה ב"
msgid "Launch"
msgstr "לְהַשִׁיק"
msgid "Open an issue"
msgstr "פתח גיליון"
msgid "Print to PDF"
msgstr "הדפס לקובץ PDF"
msgid "Source repository"
msgstr "מאגר המקורות"
msgid "Sphinx Book Theme"
msgstr "נושא ספר ספינקס"
msgid "Theme by the"
msgstr "נושא מאת"
msgid "Toggle navigation"
msgstr "החלף ניווט"
msgid "next page"
msgstr "עמוד הבא"
msgid "open issue"
msgstr "בעיה פתוחה"
msgid "previous page"
msgstr "עמוד קודם"
msgid "repository"
msgstr "מאגר"
msgid "suggest edit"
msgstr "מציע לערוך"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ja\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "によって"
msgid "By"
msgstr "著者"
msgid "Contents"
msgstr "目次"
msgid "Copyright"
msgstr "Copyright"
msgid "Download notebook file"
msgstr "ノートブックファイルをダウンロード"
msgid "Download source file"
msgstr "ソースファイルをダウンロード"
msgid "Download this page"
msgstr "このページをダウンロード"
msgid "Edit this page"
msgstr "このページを編集"
msgid "Fullscreen mode"
msgstr "全画面モード"
msgid "Last updated on"
msgstr "最終更新日"
msgid "Launch"
msgstr "起動"
msgid "Open an issue"
msgstr "問題を報告"
msgid "Print to PDF"
msgstr "PDFに印刷"
msgid "Source repository"
msgstr "ソースリポジトリ"
msgid "Sphinx Book Theme"
msgstr "スフィンクスの本のテーマ"
msgid "Theme by the"
msgstr "のテーマ"
msgid "Toggle navigation"
msgstr "ナビゲーションを切り替え"
msgid "next page"
msgstr "次のページ"
msgid "open issue"
msgstr "未解決の問題"
msgid "previous page"
msgstr "前のページ"
msgid "repository"
msgstr "リポジトリ"
msgid "suggest edit"
msgstr "編集を提案する"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ko\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "에 의해"
msgid "By"
msgstr "으로"
msgid "Contents"
msgstr "내용"
msgid "Copyright"
msgstr "저작권"
msgid "Download notebook file"
msgstr "노트북 파일 다운로드"
msgid "Download source file"
msgstr "소스 파일 다운로드"
msgid "Download this page"
msgstr "이 페이지 다운로드"
msgid "Edit this page"
msgstr "이 페이지 편집"
msgid "Fullscreen mode"
msgstr "전체 화면으로보기"
msgid "Last updated on"
msgstr "마지막 업데이트"
msgid "Launch"
msgstr "시작하다"
msgid "Open an issue"
msgstr "이슈 열기"
msgid "Print to PDF"
msgstr "PDF로 인쇄"
msgid "Source repository"
msgstr "소스 저장소"
msgid "Sphinx Book Theme"
msgstr "스핑크스 도서 테마"
msgid "Theme by the"
msgstr "테마별"
msgid "Toggle navigation"
msgstr "탐색 전환"
msgid "next page"
msgstr "다음 페이지"
msgid "open issue"
msgstr "열린 문제"
msgid "previous page"
msgstr "이전 페이지"
msgid "repository"
msgstr "저장소"
msgid "suggest edit"
msgstr "편집 제안"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: lt\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Prie"
msgid "By"
msgstr "Iki"
msgid "Contents"
msgstr "Turinys"
msgid "Copyright"
msgstr "Autorių teisės"
msgid "Download notebook file"
msgstr "Atsisiųsti nešiojamojo kompiuterio failą"
msgid "Download source file"
msgstr "Atsisiųsti šaltinio failą"
msgid "Download this page"
msgstr "Atsisiųskite šį puslapį"
msgid "Edit this page"
msgstr "Redaguoti šį puslapį"
msgid "Fullscreen mode"
msgstr "Pilno ekrano režimas"
msgid "Last updated on"
msgstr "Paskutinį kartą atnaujinta"
msgid "Launch"
msgstr "Paleiskite"
msgid "Open an issue"
msgstr "Atidarykite problemą"
msgid "Print to PDF"
msgstr "Spausdinti į PDF"
msgid "Source repository"
msgstr "Šaltinio saugykla"
msgid "Sphinx Book Theme"
msgstr "Sfinkso knygos tema"
msgid "Theme by the"
msgstr "Tema"
msgid "Toggle navigation"
msgstr "Perjungti naršymą"
msgid "next page"
msgstr "Kitas puslapis"
msgid "open issue"
msgstr "atviras klausimas"
msgid "previous page"
msgstr "Ankstesnis puslapis"
msgid "repository"
msgstr "saugykla"
msgid "suggest edit"
msgstr "pasiūlyti redaguoti"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: lv\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Ar"
msgid "By"
msgstr "Autors"
msgid "Contents"
msgstr "Saturs"
msgid "Copyright"
msgstr "Autortiesības"
msgid "Download notebook file"
msgstr "Lejupielādēt piezīmju grāmatiņu"
msgid "Download source file"
msgstr "Lejupielādēt avota failu"
msgid "Download this page"
msgstr "Lejupielādējiet šo lapu"
msgid "Edit this page"
msgstr "Rediģēt šo lapu"
msgid "Fullscreen mode"
msgstr "Pilnekrāna režīms"
msgid "Last updated on"
msgstr "Pēdējoreiz atjaunināts"
msgid "Launch"
msgstr "Uzsākt"
msgid "Open an issue"
msgstr "Atveriet problēmu"
msgid "Print to PDF"
msgstr "Drukāt PDF formātā"
msgid "Source repository"
msgstr "Avota krātuve"
msgid "Sphinx Book Theme"
msgstr "Sfinksa grāmatas tēma"
msgid "Theme by the"
msgstr "Autora tēma"
msgid "Toggle navigation"
msgstr "Pārslēgt navigāciju"
msgid "next page"
msgstr "nākamā lapaspuse"
msgid "open issue"
msgstr "atklāts jautājums"
msgid "previous page"
msgstr "iepriekšējā lapa"
msgid "repository"
msgstr "krātuve"
msgid "suggest edit"
msgstr "ieteikt rediģēt"

View file

@ -1,66 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ml\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "എഴുതിയത്"
msgid "By"
msgstr "എഴുതിയത്"
msgid "Copyright"
msgstr "പകർപ്പവകാശം"
msgid "Download notebook file"
msgstr "നോട്ട്ബുക്ക് ഫയൽ ഡൺലോഡ് ചെയ്യുക"
msgid "Download source file"
msgstr "ഉറവിട ഫയൽ ഡൗൺലോഡുചെയ്യുക"
msgid "Download this page"
msgstr "ഈ പേജ് ഡൗൺലോഡുചെയ്യുക"
msgid "Edit this page"
msgstr "ഈ പേജ് എഡിറ്റുചെയ്യുക"
msgid "Last updated on"
msgstr "അവസാനം അപ്‌ഡേറ്റുചെയ്‌തത്"
msgid "Launch"
msgstr "സമാരംഭിക്കുക"
msgid "Open an issue"
msgstr "ഒരു പ്രശ്നം തുറക്കുക"
msgid "Print to PDF"
msgstr "PDF- ലേക്ക് പ്രിന്റുചെയ്യുക"
msgid "Source repository"
msgstr "ഉറവിട ശേഖരം"
msgid "Sphinx Book Theme"
msgstr "സ്ഫിങ്ക്സ് പുസ്തക തീം"
msgid "Theme by the"
msgstr "പ്രമേയം"
msgid "Toggle navigation"
msgstr "നാവിഗേഷൻ ടോഗിൾ ചെയ്യുക"
msgid "next page"
msgstr "അടുത്ത പേജ്"
msgid "open issue"
msgstr "തുറന്ന പ്രശ്നം"
msgid "previous page"
msgstr "മുൻപത്തെ താൾ"
msgid "suggest edit"
msgstr "എഡിറ്റുചെയ്യാൻ നിർദ്ദേശിക്കുക"

View file

@ -1,66 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: mr\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "द्वारा"
msgid "By"
msgstr "द्वारा"
msgid "Copyright"
msgstr "कॉपीराइट"
msgid "Download notebook file"
msgstr "नोटबुक फाईल डाउनलोड करा"
msgid "Download source file"
msgstr "स्त्रोत फाइल डाउनलोड करा"
msgid "Download this page"
msgstr "हे पृष्ठ डाउनलोड करा"
msgid "Edit this page"
msgstr "हे पृष्ठ संपादित करा"
msgid "Last updated on"
msgstr "अखेरचे अद्यतनित"
msgid "Launch"
msgstr "लाँच करा"
msgid "Open an issue"
msgstr "एक मुद्दा उघडा"
msgid "Print to PDF"
msgstr "पीडीएफवर मुद्रित करा"
msgid "Source repository"
msgstr "स्त्रोत भांडार"
msgid "Sphinx Book Theme"
msgstr "स्फिंक्स बुक थीम"
msgid "Theme by the"
msgstr "द्वारा थीम"
msgid "Toggle navigation"
msgstr "नेव्हिगेशन टॉगल करा"
msgid "next page"
msgstr "पुढील पृष्ठ"
msgid "open issue"
msgstr "खुला मुद्दा"
msgid "previous page"
msgstr "मागील पान"
msgid "suggest edit"
msgstr "संपादन सुचवा"

View file

@ -1,66 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ms\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Oleh"
msgid "By"
msgstr "Oleh"
msgid "Copyright"
msgstr "hak cipta"
msgid "Download notebook file"
msgstr "Muat turun fail buku nota"
msgid "Download source file"
msgstr "Muat turun fail sumber"
msgid "Download this page"
msgstr "Muat turun halaman ini"
msgid "Edit this page"
msgstr "Edit halaman ini"
msgid "Last updated on"
msgstr "Terakhir dikemas kini pada"
msgid "Launch"
msgstr "Lancarkan"
msgid "Open an issue"
msgstr "Buka masalah"
msgid "Print to PDF"
msgstr "Cetak ke PDF"
msgid "Source repository"
msgstr "Repositori sumber"
msgid "Sphinx Book Theme"
msgstr "Tema Buku Sphinx"
msgid "Theme by the"
msgstr "Tema oleh"
msgid "Toggle navigation"
msgstr "Togol navigasi"
msgid "next page"
msgstr "muka surat seterusnya"
msgid "open issue"
msgstr "isu terbuka"
msgid "previous page"
msgstr "halaman sebelumnya"
msgid "suggest edit"
msgstr "cadangkan edit"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: nl\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Door de"
msgid "By"
msgstr "Door"
msgid "Contents"
msgstr "Inhoud"
msgid "Copyright"
msgstr "auteursrechten"
msgid "Download notebook file"
msgstr "Download notebookbestand"
msgid "Download source file"
msgstr "Download het bronbestand"
msgid "Download this page"
msgstr "Download deze pagina"
msgid "Edit this page"
msgstr "bewerk deze pagina"
msgid "Fullscreen mode"
msgstr "Volledig scherm"
msgid "Last updated on"
msgstr "Laatst geupdate op"
msgid "Launch"
msgstr "Lancering"
msgid "Open an issue"
msgstr "Open een probleem"
msgid "Print to PDF"
msgstr "Afdrukken naar pdf"
msgid "Source repository"
msgstr "Bronopslagplaats"
msgid "Sphinx Book Theme"
msgstr "Sphinx-boekthema"
msgid "Theme by the"
msgstr "Thema door de"
msgid "Toggle navigation"
msgstr "Schakel navigatie"
msgid "next page"
msgstr "volgende bladzijde"
msgid "open issue"
msgstr "open probleem"
msgid "previous page"
msgstr "vorige pagina"
msgid "repository"
msgstr "repository"
msgid "suggest edit"
msgstr "suggereren bewerken"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: no\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Ved"
msgid "By"
msgstr "Av"
msgid "Contents"
msgstr "Innhold"
msgid "Copyright"
msgstr "opphavsrett"
msgid "Download notebook file"
msgstr "Last ned notatbokfilen"
msgid "Download source file"
msgstr "Last ned kildefilen"
msgid "Download this page"
msgstr "Last ned denne siden"
msgid "Edit this page"
msgstr "Rediger denne siden"
msgid "Fullscreen mode"
msgstr "Fullskjerm-modus"
msgid "Last updated on"
msgstr "Sist oppdatert den"
msgid "Launch"
msgstr "Start"
msgid "Open an issue"
msgstr "Åpne et problem"
msgid "Print to PDF"
msgstr "Skriv ut til PDF"
msgid "Source repository"
msgstr "Kildedepot"
msgid "Sphinx Book Theme"
msgstr "Sphinx boktema"
msgid "Theme by the"
msgstr "Tema av"
msgid "Toggle navigation"
msgstr "Bytt navigasjon"
msgid "next page"
msgstr "neste side"
msgid "open issue"
msgstr "åpent nummer"
msgid "previous page"
msgstr "forrige side"
msgid "repository"
msgstr "oppbevaringssted"
msgid "suggest edit"
msgstr "foreslå redigering"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pl\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Przez"
msgid "By"
msgstr "Przez"
msgid "Contents"
msgstr "Zawartość"
msgid "Copyright"
msgstr "prawa autorskie"
msgid "Download notebook file"
msgstr "Pobierz plik notatnika"
msgid "Download source file"
msgstr "Pobierz plik źródłowy"
msgid "Download this page"
msgstr "Pobierz tę stronę"
msgid "Edit this page"
msgstr "Edytuj tę strone"
msgid "Fullscreen mode"
msgstr "Pełny ekran"
msgid "Last updated on"
msgstr "Ostatnia aktualizacja"
msgid "Launch"
msgstr "Uruchomić"
msgid "Open an issue"
msgstr "Otwórz problem"
msgid "Print to PDF"
msgstr "Drukuj do PDF"
msgid "Source repository"
msgstr "Repozytorium źródłowe"
msgid "Sphinx Book Theme"
msgstr "Motyw książki Sphinx"
msgid "Theme by the"
msgstr "Motyw autorstwa"
msgid "Toggle navigation"
msgstr "Przełącz nawigację"
msgid "next page"
msgstr "Następna strona"
msgid "open issue"
msgstr "otwarty problem"
msgid "previous page"
msgstr "Poprzednia strona"
msgid "repository"
msgstr "magazyn"
msgid "suggest edit"
msgstr "zaproponuj edycję"

View file

@ -1,75 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Sphinx-Book-Theme\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "By the"
msgstr "Pelo"
msgid "By"
msgstr "De"
msgid "Contents"
msgstr "Conteúdo"
msgid "Copyright"
msgstr "direito autoral"
msgid "Download notebook file"
msgstr "Baixar arquivo de notebook"
msgid "Download source file"
msgstr "Baixar arquivo fonte"
msgid "Download this page"
msgstr "Baixe esta página"
msgid "Edit this page"
msgstr "Edite essa página"
msgid "Fullscreen mode"
msgstr "Modo tela cheia"
msgid "Last updated on"
msgstr "Última atualização em"
msgid "Launch"
msgstr "Lançamento"
msgid "Open an issue"
msgstr "Abra um problema"
msgid "Print to PDF"
msgstr "Imprimir em PDF"
msgid "Source repository"
msgstr "Repositório fonte"
msgid "Sphinx Book Theme"
msgstr "Tema do livro Sphinx"
msgid "Theme by the"
msgstr "Tema por"
msgid "Toggle navigation"
msgstr "Alternar de navegação"
msgid "next page"
msgstr "próxima página"
msgid "open issue"
msgstr "questão aberta"
msgid "previous page"
msgstr "página anterior"
msgid "repository"
msgstr "repositório"
msgid "suggest edit"
msgstr "sugerir edição"

Some files were not shown because too many files have changed in this diff Show more