docs: build de 01c67bd0f9
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 ».
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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/<vm>/"| 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.
|
||||
|
|
@ -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-<subnet><br/>interface_ip"]
|
||||
BR --- VX["vxlan<vni>"] --- 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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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["<os>-tmp.qcow2<br/><i>overlay, jetable</i>"] --> BVM["VM de construction"]
|
||||
BVM --> ROOT["<os>-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 ?
|
||||
|
|
@ -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
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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`.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
```{include} ../../release_notes/0.1.0.md
|
||||
```
|
||||
|
|
@ -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_
|
||||
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
Before Width: | Height: | Size: 286 B |
|
|
@ -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 |
|
Before Width: | Height: | Size: 7.4 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 "أقترح تحرير"
|
||||
|
|
@ -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 "предложи редактиране"
|
||||
|
|
@ -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 "আগের পৃষ্ঠা"
|
||||
|
|
@ -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ó"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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 "προτείνω επεξεργασία"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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 "מציע לערוך"
|
||||
|
|
@ -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 "編集を提案する"
|
||||
|
|
@ -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 "편집 제안"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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 "എഡിറ്റുചെയ്യാൻ നിർദ്ദേശിക്കുക"
|
||||
|
|
@ -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 "संपादन सुचवा"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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ę"
|
||||
|
|
@ -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"
|
||||