Compare commits
No commits in common. "f9aadc00fc56240967e7988bfedfb976c6cce7e3" and "32940c57b4a28e2794ce4ae253087f39ceae2e29" have entirely different histories.
f9aadc00fc
...
32940c57b4
33 changed files with 202 additions and 1749 deletions
|
|
@ -130,12 +130,7 @@ func renderCmd(args []string, stdout, stderr io.Writer) int {
|
|||
fmt.Fprintf(stderr, "lab: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
frr, err := provision.ReadFRR(p)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "lab: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
nodes, err := render.Render(p, render.Options{RunDir: dir, AuthorizedKeys: authorized, FRR: frr})
|
||||
nodes, err := render.Render(p, render.Options{RunDir: dir, AuthorizedKeys: authorized})
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "lab: %v\n", err)
|
||||
return 1
|
||||
|
|
|
|||
|
|
@ -33,9 +33,8 @@ func TestRun_PlanOfTheShippedExampleTopology(t *testing.T) {
|
|||
}
|
||||
for _, want := range []string{
|
||||
"lab evpn-2hv: nodes 4, segments 1, cables 3",
|
||||
"gateway 192.168.14.1",
|
||||
"hv2 underlay 192.168.14.12/24 02:4c:00:03:00:00 20004 <-> sw1 p2",
|
||||
"rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf",
|
||||
"gateway 10.250.0.1",
|
||||
"hv2 underlay 10.250.0.4/24 02:4c:00:03:00:00 20004 <-> sw1 p2",
|
||||
} {
|
||||
if !strings.Contains(stdout, want) {
|
||||
t.Errorf("output does not contain %q:\n%s", want, stdout)
|
||||
|
|
@ -300,55 +299,3 @@ func TestRun_UpRefusesToReplaceARunningLab(t *testing.T) {
|
|||
t.Error("the topology of a running lab was replaced")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_RenderShipsTheExampleFRRConfigs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
key := filepath.Join(dir, "lab.pub")
|
||||
if err := os.WriteFile(key, []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFG/JMmjfko96WkJV8DiL6rip/H/q/R++y8s27Z+Cj6O two-lab-automation\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := filepath.Join(dir, "run")
|
||||
code, _, stderr := runLab("render", "-key", key, filepath.Join("..", "..", "conf", "lab", "evpn-2hv.yml"), out)
|
||||
if code != 0 {
|
||||
t.Fatalf("code %d, stderr %s", code, stderr)
|
||||
}
|
||||
for node, want := range map[string]string{
|
||||
"sw1": "router bgp 65100",
|
||||
"rr1": "bgp listen range 192.168.14.0/24 peer-group fabric",
|
||||
"hv1": "bgp router-id 192.168.14.11",
|
||||
"hv2": "bgp router-id 192.168.14.12",
|
||||
} {
|
||||
data, err := os.ReadFile(filepath.Join(out, node, "user-data"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(data), want) {
|
||||
t.Errorf("%s user-data does not carry %q", node, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_RenderReportsAMissingFRRConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
key := filepath.Join(dir, "lab.pub")
|
||||
if err := os.WriteFile(key, []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFG/JMmjfko96WkJV8DiL6rip/H/q/R++y8s27Z+Cj6O x\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topo := filepath.Join(dir, "lab.yml")
|
||||
doc := `name: x
|
||||
images:
|
||||
deb: { url: https://example.invalid/deb.qcow2, sums: https://example.invalid/SHA512SUMS }
|
||||
segments:
|
||||
underlay: { switch: sw1, cidr: 10.1.0.0/24 }
|
||||
nodes:
|
||||
sw1: { role: switch, image: deb, cpus: 1, memory: 512, frr: frr/absent.conf }
|
||||
hv1: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [underlay], release: 0.2.0rc002 }
|
||||
`
|
||||
if err := os.WriteFile(topo, []byte(doc), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
code, _, stderr := runLab("render", "-key", key, topo, filepath.Join(dir, "run"))
|
||||
if code != 1 || !strings.Contains(stderr, "node sw1: ") || !strings.Contains(stderr, filepath.Join(dir, "frr", "absent.conf")) {
|
||||
t.Errorf("code %d, stderr %q", code, stderr)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,20 +8,11 @@ images:
|
|||
segments:
|
||||
underlay:
|
||||
switch: sw1
|
||||
cidr: 192.168.14.0/24
|
||||
cidr: 10.250.0.0/24
|
||||
mtu: 9000
|
||||
|
||||
nodes:
|
||||
sw1:
|
||||
{ role: switch, image: debian12, cpus: 2, memory: 1024,
|
||||
secondary: { underlay: [169.254.0.1/28] }, frr: frr/sw1.conf }
|
||||
rr1:
|
||||
{ role: rr, image: debian12, cpus: 1, memory: 1024, segments: [underlay],
|
||||
addresses: { underlay: 192.168.14.2 }, secondary: { underlay: [169.254.0.3/28] },
|
||||
loopback: 10.255.255.1/32, frr: frr/rr1.conf }
|
||||
hv1:
|
||||
{ role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay],
|
||||
addresses: { underlay: 192.168.14.11 }, frr: frr/hv1.conf, release: 0.2.0rc002 }
|
||||
hv2:
|
||||
{ role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay],
|
||||
addresses: { underlay: 192.168.14.12 }, frr: frr/hv2.conf, release: 0.2.0rc002 }
|
||||
sw1: { role: switch, image: debian12, cpus: 2, memory: 1024 }
|
||||
rr1: { role: rr, image: debian12, cpus: 1, memory: 1024, segments: [underlay] }
|
||||
hv1: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay] }
|
||||
hv2: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay] }
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
frr defaults traditional
|
||||
hostname hv1
|
||||
log syslog informational
|
||||
!
|
||||
router bgp 64600
|
||||
bgp router-id 192.168.14.11
|
||||
no bgp default ipv4-unicast
|
||||
neighbor fabric peer-group
|
||||
neighbor fabric remote-as 64600
|
||||
neighbor fabric capability extended-nexthop
|
||||
neighbor 10.255.255.1 peer-group fabric
|
||||
!
|
||||
address-family l2vpn evpn
|
||||
neighbor fabric activate
|
||||
advertise-all-vni
|
||||
exit-address-family
|
||||
!
|
||||
exit
|
||||
!
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
frr defaults traditional
|
||||
hostname hv2
|
||||
log syslog informational
|
||||
!
|
||||
router bgp 64600
|
||||
bgp router-id 192.168.14.12
|
||||
no bgp default ipv4-unicast
|
||||
neighbor fabric peer-group
|
||||
neighbor fabric remote-as 64600
|
||||
neighbor fabric capability extended-nexthop
|
||||
neighbor 10.255.255.1 peer-group fabric
|
||||
!
|
||||
address-family l2vpn evpn
|
||||
neighbor fabric activate
|
||||
advertise-all-vni
|
||||
exit-address-family
|
||||
!
|
||||
exit
|
||||
!
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
frr defaults traditional
|
||||
hostname rr1
|
||||
log syslog informational
|
||||
!
|
||||
ip prefix-list RR-LOOPBACK-OUT seq 10 permit 10.255.255.1/32
|
||||
!
|
||||
route-map NO-IN deny 999
|
||||
description deny
|
||||
exit
|
||||
!
|
||||
router bgp 65000
|
||||
no bgp default ipv4-unicast
|
||||
bgp router-id 10.255.255.1
|
||||
bgp cluster-id 10.255.255.1
|
||||
neighbor CLUSTER peer-group
|
||||
neighbor CLUSTER remote-as 65100
|
||||
neighbor CLUSTER bfd
|
||||
neighbor 169.254.0.1 peer-group CLUSTER
|
||||
neighbor 169.254.0.1 description router-1
|
||||
neighbor 169.254.0.2 peer-group CLUSTER
|
||||
neighbor 169.254.0.2 description router-2
|
||||
neighbor fabric peer-group
|
||||
neighbor fabric remote-as 64600
|
||||
neighbor fabric local-as 64600 no-prepend replace-as
|
||||
neighbor fabric capability extended-nexthop
|
||||
neighbor fabric update-source 10.255.255.1
|
||||
bgp listen range 192.168.14.0/24 peer-group fabric
|
||||
bgp listen limit 200
|
||||
!
|
||||
address-family ipv4 unicast
|
||||
network 10.255.255.1/32
|
||||
neighbor CLUSTER activate
|
||||
neighbor CLUSTER prefix-list RR-LOOPBACK-OUT out
|
||||
neighbor CLUSTER route-map NO-IN in
|
||||
exit-address-family
|
||||
!
|
||||
address-family l2vpn evpn
|
||||
neighbor fabric activate
|
||||
neighbor fabric route-reflector-client
|
||||
exit-address-family
|
||||
!
|
||||
exit
|
||||
!
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
frr defaults traditional
|
||||
hostname sw1
|
||||
log syslog informational
|
||||
!
|
||||
ip prefix-list RR-LOOPBACK-IN seq 10 permit 10.255.255.1/32
|
||||
!
|
||||
route-map RR-IN permit 10
|
||||
match ip address prefix-list RR-LOOPBACK-IN
|
||||
exit
|
||||
!
|
||||
route-map NO-OUT deny 999
|
||||
exit
|
||||
!
|
||||
router bgp 65100
|
||||
no bgp default ipv4-unicast
|
||||
bgp router-id 169.254.0.1
|
||||
neighbor 169.254.0.3 remote-as 65000
|
||||
neighbor 169.254.0.3 description rr1
|
||||
neighbor 169.254.0.3 bfd
|
||||
!
|
||||
address-family ipv4 unicast
|
||||
neighbor 169.254.0.3 activate
|
||||
neighbor 169.254.0.3 route-map RR-IN in
|
||||
neighbor 169.254.0.3 route-map NO-OUT out
|
||||
exit-address-family
|
||||
!
|
||||
exit
|
||||
!
|
||||
|
|
@ -4,23 +4,21 @@ Architecture du cluster
|
|||
Topologie
|
||||
---------
|
||||
|
||||
.. figure:: /schemas/architecture-cluster.svg
|
||||
:alt: Architecture du cluster : routeurs, route reflector, hyperviseurs, plans de données et de contrôle
|
||||
.. figure:: /schemas/topologie-cluster.svg
|
||||
:alt: Topologie du cluster : routeurs, route reflector et hyperviseurs
|
||||
:align: center
|
||||
:width: 100%
|
||||
:class: only-light
|
||||
|
||||
Architecture cible. Trait bleu : plan de données (VXLAN). Tirets violets : plan de contrôle
|
||||
(sessions BGP). Pointillé gris : interfaces créées par l'agent.
|
||||
Topologie cible. Trait plein : plan de données. Trait pointillé : plan de contrôle.
|
||||
|
||||
.. figure:: /schemas/architecture-cluster-dark.svg
|
||||
:alt: Architecture du cluster : routeurs, route reflector, hyperviseurs, plans de données et 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
|
||||
|
||||
Architecture cible. Trait bleu : plan de données (VXLAN). Tirets violets : plan de contrôle
|
||||
(sessions BGP). Pointillé gris : interfaces créées par l'agent.
|
||||
Topologie cible. Trait plein : plan de données. Trait pointillé : plan de contrôle.
|
||||
|
||||
Deux plans distincts, à ne pas confondre au moment du diagnostic :
|
||||
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ champ ``password`` de l'API de l'agent ne sert donc **pas** à ouvrir une sessio
|
|||
datasource_list: [ NoCloud ]
|
||||
datasource:
|
||||
NoCloud:
|
||||
seedfrom: 'http://169.254.169.254:80/'
|
||||
seedfrom: 'http://169.254.169.254:80'
|
||||
timeout: 5
|
||||
max_wait: 10
|
||||
ENDFILE
|
||||
|
|
@ -315,20 +315,12 @@ Points de vigilance
|
|||
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.
|
||||
|
||||
.. warning::
|
||||
.. note::
|
||||
|
||||
**La barre oblique finale de** ``seedfrom`` **est indispensable avant cloud-init 23.1.**
|
||||
cloud-init construit l'URL des documents en concaténant ``seedfrom`` avec ``meta-data``,
|
||||
``user-data`` et ``vendor-data`` (``util.read_seeded``). Jusqu'à la 22.4 — celle de Debian 12,
|
||||
22.4.2 —, sans barre oblique finale il demande ``http://169.254.169.254:80meta-data`` : la
|
||||
source échoue, la VM démarre en ``DataSourceNone``, sans nom d'hôte ni user-data. À partir de
|
||||
23.1, un drapeau actif par défaut (``NOCLOUD_SEED_URL_APPEND_FORWARD_SLASH``) ajoute la barre
|
||||
oblique manquante, ce qui explique qu'une image récente fonctionne sans elle.
|
||||
|
||||
Vérifié le 2026-10-04 dans le lab (#50), sur une VM Debian 12 lancée par l'agent : sans barre
|
||||
oblique, ``Datasource DataSourceNone`` ; avec, ``DataSourceNoCloudNet
|
||||
[seed=…http://169.254.169.254…]`` et le nom d'hôte de la VM. Avec la barre oblique, la
|
||||
configuration fonctionne quelle que soit la version de cloud-init.
|
||||
**À 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::
|
||||
|
||||
|
|
|
|||
|
|
@ -8,106 +8,15 @@ 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.
|
||||
|
||||
Adressage
|
||||
---------
|
||||
|
||||
Le route reflector est **sur le même segment L2 que les hyperviseurs**, avec trois adresses :
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 30 25 45
|
||||
|
||||
* - Adresse
|
||||
- Interface
|
||||
- Rôle
|
||||
* - une adresse de ``192.168.14.0/24``
|
||||
- interface principale
|
||||
- celle de n'importe quelle machine du segment ; les réponses aux hyperviseurs en partent
|
||||
* - ``169.254.0.3/28``
|
||||
- interface principale, **adresse secondaire**
|
||||
- le lien avec les routeurs de cluster (``169.254.0.1`` et ``.2``), sur le même L2
|
||||
* - ``10.255.255.1/32``
|
||||
- ``lo1``, interface ``dummy``
|
||||
- ``router-id``, ``cluster-id`` et source des sessions EVPN ; seule route annoncée aux routeurs
|
||||
|
||||
Les hyperviseurs ne connaissent que la loopback : ils ouvrent leur session vers ``10.255.255.1``
|
||||
par leur passerelle (``192.168.14.1``), les routeurs l'ayant apprise du route reflector en eBGP.
|
||||
Aucune adresse d'hyperviseur n'est déclarée côté route reflector : il accepte toute session venant
|
||||
du segment (``bgp listen range``).
|
||||
|
||||
L'adresse du lien est une **adresse secondaire** de l'interface principale, pas une interface à
|
||||
part : même L2, même MAC sur le fil, une interface de moins qu'avec un ``ipvlan``. Elle doit
|
||||
survivre aux renouvellements DHCP de l'adresse principale : la déclarer dans la configuration
|
||||
réseau du système, pas la poser à la main.
|
||||
|
||||
.. note::
|
||||
|
||||
**Non vérifié sur l'image de production.** Avec NetworkManager, la forme attendue est :
|
||||
**À rédiger.** À documenter :
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
nmcli connection modify <connexion> +ipv4.addresses 169.254.0.3/28
|
||||
nmcli connection add type dummy ifname lo1 con-name lo1 \
|
||||
ipv4.method manual ipv4.addresses 10.255.255.1/32 ipv6.method disabled
|
||||
|
||||
Le lab, en Debian, pose ces adresses par cloud-init et un script rejoué au démarrage ; ces
|
||||
commandes restent à valider sur l'image golden.
|
||||
|
||||
Configuration de FRR
|
||||
--------------------
|
||||
|
||||
``bgpd`` et ``bfdd`` activés dans ``/etc/frr/daemons``. La configuration ci-dessous est **celle du
|
||||
lab** (``conf/lab/frr/rr1.conf``) : ASN, loopback et plages sont ceux de la production, seul le nom
|
||||
d'hôte diffère. Le lab qualifie donc exactement ce fichier.
|
||||
|
||||
.. literalinclude:: ../../conf/lab/frr/rr1.conf
|
||||
:language: text
|
||||
|
||||
Ce qu'elle établit :
|
||||
|
||||
* **Routeurs de cluster** (groupe ``CLUSTER``) : eBGP vers l'AS 65100, avec BFD, en IPv4
|
||||
unicast. Le route reflector n'annonce que sa loopback (``RR-LOOPBACK-OUT``) et **n'accepte
|
||||
rien** (``NO-IN``) : il ne reçoit aucune route des routeurs.
|
||||
* **Hyperviseurs** (groupe ``fabric``) : voisins dynamiques, toute session venant de
|
||||
``192.168.14.0/24`` étant acceptée, jusqu'à 200. ``local-as 64600 no-prepend replace-as`` fait
|
||||
que, du point de vue des hyperviseurs, la session est en iBGP dans l'AS 64600 — celui de leur
|
||||
configuration — alors que le route reflector est en AS 65000 face aux routeurs.
|
||||
* **EVPN** : seule famille activée vers les hyperviseurs, qui sont ses clients
|
||||
(``route-reflector-client``) : il réfléchit les routes EVPN de chacun vers tous les autres.
|
||||
|
||||
Vérifié dans le lab
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Le 2026-10-04, FRR 10.7.1, route reflector en Debian 12 sur le segment des hyperviseurs, avec
|
||||
l'adresse du lien en secondaire et la loopback sur ``lo1`` :
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 55 45
|
||||
|
||||
* - Vérification
|
||||
- Résultat
|
||||
* - session avec le routeur, IPv4 unicast
|
||||
- Established ; le routeur reçoit **un seul** préfixe, la loopback, et l'installe via
|
||||
``169.254.0.3``
|
||||
* - BFD avec le routeur
|
||||
- up des deux côtés
|
||||
* - sessions des deux hyperviseurs vers ``10.255.255.1``
|
||||
- Established, voisins dynamiques, iBGP AS 64600, famille L2VPN EVPN négociée
|
||||
* - routes EVPN échangées
|
||||
- aucune : rien à annoncer tant que two n'a pas créé de VXLAN — voir
|
||||
:doc:`premier-hyperviseur`
|
||||
|
||||
Le routeur du lab n'est qu'une configuration minimale écrite pour l'essai, pas celle des routeurs
|
||||
de cluster (:doc:`routeurs`).
|
||||
|
||||
.. note::
|
||||
|
||||
**À rédiger.** Reste à documenter :
|
||||
|
||||
* la création de la VM : ressources, subnet et mode utilisés (``bridge``, pour pouvoir la
|
||||
lancer sur n'importe quel hyperviseur), 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` ;
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -116,9 +116,8 @@ Commandes
|
|||
que si l'entrée standard en est un
|
||||
prepare installe sur le serveur ce dont lab a besoin (qemu, genisoimage), vérifie
|
||||
/dev/kvm et la virtualisation imbriquée ; lancé aussi par up
|
||||
push <topologie> compile cmd/lab pour linux/amd64 et dépose sur le serveur ~/lab et le
|
||||
répertoire de la topologie dans ~/topology/ (avec les fichiers qu'elle
|
||||
référence) ; ensuite : ssh './lab up topology/<topologie>'
|
||||
push <topologie> compile cmd/lab pour linux/amd64 et dépose sur le serveur ~/lab et
|
||||
~/<topologie> ; ensuite : ssh './lab up <topologie>'
|
||||
down supprime tous les serveurs de lab du projet et attend leur disparition
|
||||
session [cmd] up, puis la commande distante (ou un shell), puis down quoi qu'il arrive
|
||||
|
||||
|
|
@ -179,15 +178,13 @@ Une campagne sur le lab enchaîne ces commandes depuis le Mac ; ``lab`` s'exécu
|
|||
|
||||
scripts/lab-host.sh up
|
||||
scripts/lab-host.sh push conf/lab/evpn-2hv.yml
|
||||
scripts/lab-host.sh ssh './lab up topology/evpn-2hv.yml'
|
||||
scripts/lab-host.sh ssh './lab up evpn-2hv.yml'
|
||||
scripts/lab-host.sh ssh './lab ssh hv1' # shell interactif sur hv1
|
||||
scripts/lab-host.sh ssh './lab ssh hv1 ip -br a' # commande, code de retour propagé
|
||||
scripts/lab-host.sh down
|
||||
|
||||
``push`` transfère par la connexion SSH du script — mêmes options, même clé, même
|
||||
``known_hosts`` que ``ssh`` : le binaire par ``cat``, le répertoire de la topologie par ``tar``
|
||||
(sans les métadonnées macOS), chacun renommé une fois complet. Tout le répertoire part, pour que
|
||||
les fichiers que la topologie référence (``frr/*.conf``) arrivent avec elle.
|
||||
``push`` transfère par la connexion SSH du script (``cat`` côté serveur, fichier renommé une fois
|
||||
complet) : mêmes options, même clé, même ``known_hosts`` que ``ssh``.
|
||||
|
||||
Topologie
|
||||
---------
|
||||
|
|
@ -215,26 +212,9 @@ Ce que le fichier déclare :
|
|||
``nodes``
|
||||
``role`` (``switch``, ``rr`` ou ``hypervisor``), ``image``, ``cpus``, ``memory`` en Mio
|
||||
(256 au moins), ``segments`` auxquels le nœud est relié, et ``addresses`` pour fixer
|
||||
l'adresse d'un nœud sur un segment (``addresses: {underlay: 192.168.14.50}``). Un switch ne
|
||||
l'adresse d'un nœud sur un segment (``addresses: {underlay: 10.250.0.50}``). Un switch ne
|
||||
déclare ni ``segments`` ni ``addresses`` : il porte ceux dont il est le ``switch``.
|
||||
|
||||
Champs de rôle, facultatifs :
|
||||
|
||||
* ``secondary`` — des adresses supplémentaires par segment, avec leur longueur de préfixe
|
||||
(``secondary: {underlay: [169.254.0.3/28]}``), posées sur la même interface que l'adresse
|
||||
principale : même L2, même MAC. Elles doivent être **hors** du CIDR du segment, pour ne
|
||||
jamais croiser l'attribution automatique. Sur un switch, elles vont sur le bridge du
|
||||
segment ;
|
||||
* ``loopback`` — une adresse sur une interface ``dummy`` nommée ``lo1``
|
||||
(``loopback: 10.255.255.1/32``) ;
|
||||
* ``frr`` — le chemin d'un ``frr.conf``, relatif au fichier de topologie : FRR est installé
|
||||
au démarrage et la configuration déposée **telle quelle** (voir `Rôles`_).
|
||||
* ``release`` — **obligatoire pour un hyperviseur**, refusé ailleurs : le tag de la release de
|
||||
two que ``deploy.sh`` installe (``release: 0.2.0rc002``). Sans lui, ``deploy.sh`` prendrait la
|
||||
dernière release, et le lab ne serait plus reproductible.
|
||||
|
||||
``mgmt0`` et ``lo1`` sont réservés : aucun segment ne peut porter ces noms.
|
||||
|
||||
Ce que l'outil en déduit, de façon déterministe — même fichier, même plan :
|
||||
|
||||
.. list-table::
|
||||
|
|
@ -277,18 +257,11 @@ Limites : 1000 nœuds, 256 segments, et autant de câbles que la plage UDP le pe
|
|||
hv1 hypervisor debian12 4 16384 MiB 127.0.0.1:2202
|
||||
hv2 hypervisor debian12 4 16384 MiB 127.0.0.1:2203
|
||||
|
||||
roles
|
||||
name loopback secondary frr release
|
||||
sw1 - underlay 169.254.0.1/28 sw1.conf -
|
||||
rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf -
|
||||
hv1 - - hv1.conf 0.2.0rc002
|
||||
hv2 - - hv2.conf 0.2.0rc002
|
||||
|
||||
segment underlay: 192.168.14.0/24, mtu 9000, switch sw1, bridge br-underlay, gateway 192.168.14.1
|
||||
node interface address mac udp switch port mac udp
|
||||
rr1 underlay 192.168.14.2/24 02:4c:00:01:00:00 20000 <-> sw1 p0 02:4c:00:01:00:01 20001
|
||||
hv1 underlay 192.168.14.11/24 02:4c:00:02:00:00 20002 <-> sw1 p1 02:4c:00:02:00:01 20003
|
||||
hv2 underlay 192.168.14.12/24 02:4c:00:03:00:00 20004 <-> sw1 p2 02:4c:00:03:00:01 20005
|
||||
segment underlay: 10.250.0.0/24, mtu 9000, switch sw1, bridge br-underlay, gateway 10.250.0.1
|
||||
node interface address mac udp switch port mac udp
|
||||
rr1 underlay 10.250.0.2/24 02:4c:00:01:00:00 20000 <-> sw1 p0 02:4c:00:01:00:01 20001
|
||||
hv1 underlay 10.250.0.3/24 02:4c:00:02:00:00 20002 <-> sw1 p1 02:4c:00:02:00:01 20003
|
||||
hv2 underlay 10.250.0.4/24 02:4c:00:03:00:00 20004 <-> sw1 p2 02:4c:00:03:00:01 20005
|
||||
|
||||
Un fichier invalide est refusé avec **toutes** ses erreurs à la fois, et un code de sortie 1. Les
|
||||
champs inconnus et les clés en double sont refusés aussi :
|
||||
|
|
@ -301,122 +274,8 @@ champs inconnus et les clés en double sont refusés aussi :
|
|||
segment underlay: cidr 10.250.0.0/31 prefix length out of range [/8, /30]
|
||||
node sw1: switch carries no segment
|
||||
|
||||
Les ASN, la loopback du route reflector, le lien ``169.254.0.0/28`` et le subnet des hyperviseurs
|
||||
de l'exemple sont **ceux de la production** (décision du 2026-10-04, #50) : les fichiers de
|
||||
``conf/lab/`` restent ainsi au plus près de ce qui tourne réellement. Toutes les adresses y sont
|
||||
**fixées** par ``addresses`` — le route reflector en ``.2``, les hyperviseurs à partir de ``.11`` —
|
||||
pour que le modèle se lise sans le plan et ne dépende pas de l'ordre de déclaration : le
|
||||
``frr.conf`` d'un hyperviseur, écrit à la main, porte son adresse en ``router-id``. Seul le switch
|
||||
n'en déclare pas : il porte toujours la passerelle, la première adresse du segment.
|
||||
|
||||
Rôles
|
||||
~~~~~
|
||||
|
||||
Les configurations FRR du lab vivent dans ``conf/lab/frr/``, une par nœud, **écrites à la main** :
|
||||
ce sont les mêmes fichiers que la documentation de déploiement inclut, pour que le lab qualifie
|
||||
exactement ce qu'elle prescrit. Celle du route reflector :
|
||||
|
||||
.. literalinclude:: ../../conf/lab/frr/rr1.conf
|
||||
:language: text
|
||||
|
||||
Au premier démarrage, cloud-init installe FRR (``frr-stable`` de ``deb.frrouting.org``, sans les
|
||||
paquets recommandés), active ``bgpd`` — et ``bfdd`` sur le switch et le route reflector —, puis
|
||||
dépose le ``frr.conf`` du nœud et redémarre FRR. La mise à jour des index de paquets est réessayée
|
||||
pendant cinq minutes : un nœud peut démarrer avant que le switch, par lequel il sort, n'ait posé
|
||||
son NAT.
|
||||
|
||||
La **clé du dépôt FRR** n'est pas téléchargée au démarrage : elle est enregistrée dans ``lab``
|
||||
(``internal/lab/render/frrouting.gpg``) et déposée par cloud-init. Elle a été récupérée le
|
||||
2026-10-04 sur ``deb.frrouting.org`` ; les empreintes de ses clés primaires sont publiées sous la
|
||||
même valeur sur ``keys.openpgp.org`` et ``keyserver.ubuntu.com`` :
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
3D99 68AC 9AE7 BE11 6928 8DDB 1FD5 8398 95F5 7FDA David Lamparter
|
||||
4A56 C773 8BB3 F815 95A8 05D2 A832 7699 08F1 3ED1 FRRouting Debian Repository
|
||||
A90F C36D 9429 4097 98E9 C2D8 74DE ED43 AB19 4DBF Jafar Al-Gharaibeh
|
||||
|
||||
Une clé renouvelée par FRR fera échouer l'installation (signature inconnue) : remplacer le fichier
|
||||
après avoir vérifié les nouvelles empreintes.
|
||||
|
||||
**Hyperviseurs.** Ils se déploient comme en production, par ``deploy.sh`` — celui **du dépôt**,
|
||||
embarqué dans ``lab`` avec ``bootstrap_kvm.sh`` (paquet ``scripts``) et déposé dans
|
||||
``/opt/two/scripts/``, où ``deploy.sh`` cherche d'abord ``bootstrap_kvm.sh`` :
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
deploy.sh --noup_script -i -u <segment> -t <release>
|
||||
|
||||
``--noup_script`` empêche l'auto-mise à jour de remplacer le script par celui de ``main`` : le lab
|
||||
teste les scripts de sa branche. L'uplink ``-u`` est l'interface qui porte la route par défaut —
|
||||
celle du premier segment de l'hyperviseur dans l'ordre de déclaration des segments — parce que
|
||||
``deploy.sh`` y lit l'adresse et la passerelle qu'il déplace sur ``br-000000``. ``deploy.sh``
|
||||
télécharge la release sur ``git.g3e.fr`` sans réessayer : le lancement attend d'abord que le serveur
|
||||
réponde, à travers le switch. FRR est installé **après** : il démarre sur le réseau final.
|
||||
|
||||
**Un seul script de provisionnement par nœud.** cloud-init exécute ``runcmd`` comme un script
|
||||
``sh`` sans ``set -e`` : seule la dernière commande compte, et un ``deploy.sh`` en échec suivi d'un
|
||||
FRR installé avec succès passerait pour un démarrage réussi. Chaque nœud reçoit donc
|
||||
``/usr/local/sbin/lab-provision``, en ``set -eu``, qui enchaîne ses étapes ; ``runcmd`` n'appelle
|
||||
que lui, et la première étape en échec met cloud-init en erreur.
|
||||
|
||||
**Ce que vérifie** ``lab up``, une fois cloud-init terminé sans erreur : ``agent.service`` actif
|
||||
sur chaque hyperviseur, ``frr`` actif sur chaque nœud qui en a un. Ce contrôle couvre ce que
|
||||
cloud-init ne voit pas — si la migration réseau échoue, ``deploy.sh`` arme un redémarrage de
|
||||
secours, et la VM redémarrée ne rejoue pas ``runcmd``.
|
||||
|
||||
.. warning::
|
||||
|
||||
**Un hyperviseur du lab ne survit pas à un redémarrage.** En production, la racine est en
|
||||
tmpfs et ``deploy.sh --bootstrap`` est rejoué à chaque démarrage ; dans le lab, ``-i`` n'est
|
||||
exécuté qu'au premier, et la migration réseau, qui n'est pas persistée, est perdue. Recréer le
|
||||
lab : ``lab down`` puis ``lab up``.
|
||||
|
||||
Vérifié le 2026-10-04 sur le serveur de lab, topologie ``evpn-2hv``, release ``0.2.0rc002`` :
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ scripts/lab-host.sh ssh './lab up -timeout 25m topology/evpn-2hv.yml'
|
||||
sw1: started
|
||||
rr1: started
|
||||
hv1: started
|
||||
hv2: started
|
||||
sw1: ready
|
||||
rr1: ready
|
||||
hv1: ready
|
||||
hv2: ready
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:widths: 55 45
|
||||
|
||||
* - Vérification
|
||||
- Résultat
|
||||
* - ``lab up`` complet : FRR, ``deploy.sh`` et vérification des services de chaque rôle
|
||||
- 6 min 15
|
||||
* - session switch ↔ route reflector, IPv4 unicast, BFD
|
||||
- Established, BFD up ; le switch reçoit la seule loopback du route reflector
|
||||
* - sessions EVPN des deux hyperviseurs vers la loopback du route reflector
|
||||
- Established, voisins dynamiques, stables
|
||||
* - réseau d'un hyperviseur après ``deploy.sh``
|
||||
- adresse sur ``br-000000``, MTU 9000, API de l'agent qui répond
|
||||
* - VPC, subnet ``vxlan`` et VM Debian ``genericcloud`` créés par l'API de hv1
|
||||
- ``login:`` en 20 s, en KVM imbriqué
|
||||
* - DHCP et routes (option 121) servis par two à la VM
|
||||
- conformes, route ``/32`` vers ``169.254.169.254`` comprise
|
||||
* - métadonnées, image configurée selon :doc:`/deploiement/image-qcow2`
|
||||
- ``DataSourceNoCloudNet``, nom d'hôte appliqué — avec la barre oblique finale de
|
||||
``seedfrom`` (voir cette page)
|
||||
* - VM ↔ VM entre les deux hyperviseurs, même subnet ``vxlan``
|
||||
- **échec** : les VXLAN de two n'ont pas d'adresse VTEP locale, rien n'est annoncé en EVPN —
|
||||
`#51 <https://git.g3e.fr/syonad/two/issues/51>`_ ; avec l'adresse posée, ping et MTU 1500
|
||||
passent
|
||||
|
||||
.. note::
|
||||
|
||||
La configuration du switch (``conf/lab/frr/sw1.conf``) **n'est pas celle des routeurs** : écrite
|
||||
pour l'essai du 2026-10-04, elle se contente d'établir la session avec le route reflector et de
|
||||
n'accepter que sa loopback. Elle sera remplacée par la configuration réelle des routeurs.
|
||||
Les plages d'adresses de l'exemple sont des valeurs de travail : le plan d'adressage du lab reste à
|
||||
définir (#50).
|
||||
|
||||
Rendu des VM
|
||||
------------
|
||||
|
|
@ -737,10 +596,6 @@ Sécurité
|
|||
* **Clés d'hôte des VM non vérifiées** par ``lab ssh`` (``known_hosts`` jetable) : elles changent
|
||||
à chaque ``up``. Acceptable uniquement parce que la connexion reste sur la boucle locale d'un
|
||||
serveur auquel on s'est authentifié.
|
||||
* **Code exécuté sans épinglage par** ``deploy.sh`` : la bibliothèque ``shflags`` est récupérée
|
||||
par ``curl`` sur la branche ``main`` d'un autre dépôt (``H6N/tools``) et exécutée par ``eval``,
|
||||
sans vérification d'intégrité — dans le lab comme en production. Les artefacts de la release
|
||||
sont, eux, vérifiés contre ``SHA256SUMS``.
|
||||
* **Image** : ``SHA512SUMS`` vient de la même origine que l'image, en HTTPS. La vérification
|
||||
protège contre la corruption, pas contre une origine compromise ; la signature GPG de Debian
|
||||
(``SHA512SUMS.sign``) n'est pas encore vérifiée.
|
||||
|
|
|
|||
|
|
@ -1,155 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 736" width="1000" height="736" role="img" aria-label="Architecture du cluster : routeurs, route reflector, hyperviseurs, plans de données et de contrôle">
|
||||
<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; }
|
||||
.ctrlt { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 11px; fill: #c297ff; }
|
||||
.datat { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 11px; font-weight: 600; fill: #4493f8; }
|
||||
.box { fill: #22272e; stroke: #6e7781; stroke-width: 1.5; }
|
||||
.group { fill: #1c2128; stroke: #444c56; stroke-width: 1.5; }
|
||||
.pill { fill: #22272e; stroke: #9198a1; stroke-width: 1.5; }
|
||||
.bus { stroke: #9198a1; stroke-width: 4; fill: none; stroke-linecap: round; }
|
||||
.link { stroke: #9198a1; stroke-width: 1.8; fill: none; }
|
||||
.ctrl { stroke: #c297ff; stroke-width: 1.6; fill: none; stroke-dasharray: 5 4; }
|
||||
.data { stroke: #4493f8; stroke-width: 3; fill: none; }
|
||||
.made { stroke: #6e7781; stroke-width: 1.5; fill: none; stroke-dasharray: 2 3; }
|
||||
.head-ctrl { fill: #c297ff; }
|
||||
.head-data { fill: #4493f8; }
|
||||
.head-made { fill: #6e7781; }
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="a-ctrl" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="head-ctrl" d="M0 0 L10 5 L0 10 z"/></marker>
|
||||
<marker id="a-data" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path class="head-data" d="M0 0 L10 5 L0 10 z"/></marker>
|
||||
<marker id="a-made" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="head-made" d="M0 0 L10 5 L0 10 z"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect class="group" x="40" y="20" width="400" height="144" rx="8"/>
|
||||
<text class="grp" x="60" y="44">ROUTEURS DE CLUSTER · AS 65100</text>
|
||||
<rect class="box" x="60" y="66" width="170" height="60" rx="6"/>
|
||||
<text class="lbl" x="145" y="92" text-anchor="middle">routeur 1</text>
|
||||
<text class="sub" x="145" y="111" text-anchor="middle">169.254.0.1/28</text>
|
||||
<rect class="box" x="250" y="66" width="170" height="60" rx="6"/>
|
||||
<text class="lbl" x="335" y="92" text-anchor="middle">routeur 2</text>
|
||||
<text class="sub" x="335" y="111" text-anchor="middle">169.254.0.2/28</text>
|
||||
<text class="sub" x="240" y="150" text-anchor="middle">passerelle 192.168.14.1/24</text>
|
||||
|
||||
<rect class="group" x="560" y="20" width="400" height="144" rx="8"/>
|
||||
<text class="grp" x="580" y="44">VM ROUTE REFLECTOR · AS 65000</text>
|
||||
<text class="lbl" x="580" y="70">lo1 · 10.255.255.1/32</text>
|
||||
<text class="sub" x="580" y="87">router-id, cluster-id, source des sessions EVPN</text>
|
||||
<text class="lbl" x="580" y="111">192.168.14.2/24 · 169.254.0.3/28</text>
|
||||
<text class="sub" x="580" y="128">principale, et secondaire pour le lien avec les routeurs</text>
|
||||
<text class="sub" x="580" y="150">hébergée par le cluster lui-même</text>
|
||||
|
||||
<path class="ctrl" d="M440 92 L560 92"/>
|
||||
<text class="ctrlt" x="500" y="84" text-anchor="middle">eBGP · BFD (×2)</text>
|
||||
<text class="ctrlt" x="500" y="108" text-anchor="middle">annonce lo1 seule</text>
|
||||
<text class="ctrlt" x="500" y="122" text-anchor="middle">n'accepte rien</text>
|
||||
|
||||
<path class="link" d="M145 126 L145 196"/>
|
||||
<path class="link" d="M335 126 L335 196"/>
|
||||
<path class="link" d="M760 164 L760 196"/>
|
||||
|
||||
<path class="bus" d="M40 196 L960 196"/>
|
||||
<rect class="pill" x="376" y="177" width="216" height="38" rx="19"/>
|
||||
<text class="lbl" x="484" y="194" text-anchor="middle">segment underlay · L2</text>
|
||||
<text class="sub" x="484" y="209" text-anchor="middle">192.168.14.0/24 · 169.254.0.0/28</text>
|
||||
|
||||
<rect class="group" x="40" y="262" width="430" height="306" rx="8"/>
|
||||
<text class="grp" x="60" y="284">HYPERVISEUR 1</text>
|
||||
|
||||
<rect class="box" x="274" y="298" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="364" y="317" text-anchor="middle">br-000000</text>
|
||||
<text class="sub" x="364" y="334" text-anchor="middle">uplink · 192.168.14.11</text>
|
||||
<rect class="box" x="274" y="366" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="364" y="385" text-anchor="middle">vxlan-<vni></text>
|
||||
<text class="sub" x="364" y="402" text-anchor="middle">dans br-<subnet>, sans learning</text>
|
||||
<rect class="box" x="274" y="434" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="364" y="453" text-anchor="middle">netns du VPC</text>
|
||||
<text class="sub" x="364" y="470" text-anchor="middle">passerelle, DHCP</text>
|
||||
<rect class="box" x="274" y="502" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="364" y="521" text-anchor="middle">VM</text>
|
||||
<text class="sub" x="364" y="538" text-anchor="middle">QEMU / KVM</text>
|
||||
<path class="link" d="M364 344 L364 366"/>
|
||||
<text class="sub" x="372" y="359">VTEP</text>
|
||||
<path class="link" d="M364 412 L364 434"/>
|
||||
<text class="sub" x="372" y="427">veth</text>
|
||||
<path class="link" d="M364 480 L364 502"/>
|
||||
<text class="sub" x="372" y="495">tap</text>
|
||||
|
||||
<rect class="box" x="56" y="298" width="180" height="70" rx="6"/>
|
||||
<text class="lbl" x="146" y="321" text-anchor="middle">FRR</text>
|
||||
<text class="sub" x="146" y="340" text-anchor="middle">bgpd · AS 64600</text>
|
||||
<text class="sub" x="146" y="356" text-anchor="middle">advertise-all-vni</text>
|
||||
<rect class="box" x="56" y="434" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="146" y="453" text-anchor="middle">agent</text>
|
||||
<text class="sub" x="146" y="470" text-anchor="middle">API REST · Badger</text>
|
||||
|
||||
<path class="ctrl" d="M236 352 L255 352 L255 389 L272 389" marker-end="url(#a-ctrl)"/>
|
||||
<text class="ctrlt" x="255" y="345" text-anchor="middle">FDB</text>
|
||||
<path class="made" d="M236 457 L272 457" marker-end="url(#a-made)"/>
|
||||
<text class="sub" x="255" y="450" text-anchor="middle">crée</text>
|
||||
|
||||
<rect class="group" x="530" y="262" width="430" height="306" rx="8"/>
|
||||
<text class="grp" x="656" y="284">HYPERVISEUR 2</text>
|
||||
|
||||
<rect class="box" x="546" y="298" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="636" y="317" text-anchor="middle">br-000000</text>
|
||||
<text class="sub" x="636" y="334" text-anchor="middle">uplink · 192.168.14.12</text>
|
||||
<rect class="box" x="546" y="366" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="636" y="385" text-anchor="middle">vxlan-<vni></text>
|
||||
<text class="sub" x="636" y="402" text-anchor="middle">dans br-<subnet>, sans learning</text>
|
||||
<rect class="box" x="546" y="434" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="636" y="453" text-anchor="middle">netns du VPC</text>
|
||||
<text class="sub" x="636" y="470" text-anchor="middle">passerelle, DHCP</text>
|
||||
<rect class="box" x="546" y="502" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="636" y="521" text-anchor="middle">VM</text>
|
||||
<text class="sub" x="636" y="538" text-anchor="middle">QEMU / KVM</text>
|
||||
<path class="link" d="M636 344 L636 366"/>
|
||||
<text class="sub" x="628" y="359" text-anchor="end">VTEP</text>
|
||||
<path class="link" d="M636 412 L636 434"/>
|
||||
<text class="sub" x="628" y="427" text-anchor="end">veth</text>
|
||||
<path class="link" d="M636 480 L636 502"/>
|
||||
<text class="sub" x="628" y="495" text-anchor="end">tap</text>
|
||||
|
||||
<rect class="box" x="764" y="298" width="180" height="70" rx="6"/>
|
||||
<text class="lbl" x="854" y="321" text-anchor="middle">FRR</text>
|
||||
<text class="sub" x="854" y="340" text-anchor="middle">bgpd · AS 64600</text>
|
||||
<text class="sub" x="854" y="356" text-anchor="middle">advertise-all-vni</text>
|
||||
<rect class="box" x="764" y="434" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="854" y="453" text-anchor="middle">agent</text>
|
||||
<text class="sub" x="854" y="470" text-anchor="middle">API REST · Badger</text>
|
||||
|
||||
<path class="ctrl" d="M764 352 L745 352 L745 389 L728 389" marker-end="url(#a-ctrl)"/>
|
||||
<text class="ctrlt" x="745" y="345" text-anchor="middle">FDB</text>
|
||||
<path class="made" d="M764 457 L728 457" marker-end="url(#a-made)"/>
|
||||
<text class="sub" x="745" y="450" text-anchor="middle">crée</text>
|
||||
|
||||
<path class="link" d="M364 298 L364 196"/>
|
||||
<path class="link" d="M636 298 L636 196"/>
|
||||
|
||||
<path class="ctrl" d="M210 298 L210 246 L357 246"/>
|
||||
<path class="ctrl" d="M371 246 L612 246 L612 164"/>
|
||||
<path class="ctrl" d="M854 298 L854 164"/>
|
||||
<text class="ctrlt" x="384" y="240">iBGP l2vpn evpn · AS 64600</text>
|
||||
<text class="ctrlt" x="862" y="240">iBGP</text>
|
||||
|
||||
<path class="data" d="M458 389 L542 389" marker-start="url(#a-data)" marker-end="url(#a-data)"/>
|
||||
<text class="datat" x="500" y="378" text-anchor="middle">VXLAN</text>
|
||||
<text class="datat" x="500" y="406" text-anchor="middle">UDP 4789</text>
|
||||
|
||||
<path class="link" d="M40 604 L76 604"/>
|
||||
<text class="sub" x="84" y="608">lien L2 / interface</text>
|
||||
<path class="data" d="M220 604 L256 604"/>
|
||||
<text class="sub" x="264" y="608">plan de données : tunnels VXLAN</text>
|
||||
<path class="ctrl" d="M470 604 L506 604"/>
|
||||
<text class="sub" x="514" y="608">plan de contrôle : sessions BGP</text>
|
||||
<path class="made" d="M716 604 L752 604"/>
|
||||
<text class="sub" x="760" y="608">créé par l'agent</text>
|
||||
|
||||
<text class="sub" x="40" y="640">Les hyperviseurs joignent 10.255.255.1 par leur passerelle 192.168.14.1 : les routeurs l'ont apprise du route reflector en eBGP, sans rien lui renvoyer.</text>
|
||||
<text class="sub" x="40" y="658">Le route reflector accepte toute session venant de 192.168.14.0/24 (bgp listen range) et réfléchit les routes EVPN entre hyperviseurs.</text>
|
||||
<text class="sub" x="40" y="676">Chaque FRR en peuple la FDB des VXLAN locaux — intégration de FRR avec les interfaces créées par two : à qualifier (#50, E5).</text>
|
||||
<text class="sub" x="40" y="694">MTU du segment : 1550 au moins (VXLAN figé à 1500 par l'agent, + 50 octets d'encapsulation) ; 9000 dans le lab.</text>
|
||||
<text class="sub" x="40" y="712">ASN, loopback et plages : valeurs de production (#50). Adresses en 192.168.14.0/24 : celles du lab. Placement de la VM route reflector : à rédiger.</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 10 KiB |
|
|
@ -1,155 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 736" width="1000" height="736" role="img" aria-label="Architecture du cluster : routeurs, route reflector, hyperviseurs, plans de données et de contrôle">
|
||||
<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; }
|
||||
.ctrlt { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 11px; fill: #8250df; }
|
||||
.datat { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 11px; font-weight: 600; fill: #0969da; }
|
||||
.box { fill: #ffffff; stroke: #8c959f; stroke-width: 1.5; }
|
||||
.group { fill: #f6f8fa; stroke: #d0d7de; stroke-width: 1.5; }
|
||||
.pill { fill: #ffffff; stroke: #57606a; stroke-width: 1.5; }
|
||||
.bus { stroke: #57606a; stroke-width: 4; fill: none; stroke-linecap: round; }
|
||||
.link { stroke: #57606a; stroke-width: 1.8; fill: none; }
|
||||
.ctrl { stroke: #8250df; stroke-width: 1.6; fill: none; stroke-dasharray: 5 4; }
|
||||
.data { stroke: #0969da; stroke-width: 3; fill: none; }
|
||||
.made { stroke: #8c959f; stroke-width: 1.5; fill: none; stroke-dasharray: 2 3; }
|
||||
.head-ctrl { fill: #8250df; }
|
||||
.head-data { fill: #0969da; }
|
||||
.head-made { fill: #8c959f; }
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="a-ctrl" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="head-ctrl" d="M0 0 L10 5 L0 10 z"/></marker>
|
||||
<marker id="a-data" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path class="head-data" d="M0 0 L10 5 L0 10 z"/></marker>
|
||||
<marker id="a-made" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="head-made" d="M0 0 L10 5 L0 10 z"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect class="group" x="40" y="20" width="400" height="144" rx="8"/>
|
||||
<text class="grp" x="60" y="44">ROUTEURS DE CLUSTER · AS 65100</text>
|
||||
<rect class="box" x="60" y="66" width="170" height="60" rx="6"/>
|
||||
<text class="lbl" x="145" y="92" text-anchor="middle">routeur 1</text>
|
||||
<text class="sub" x="145" y="111" text-anchor="middle">169.254.0.1/28</text>
|
||||
<rect class="box" x="250" y="66" width="170" height="60" rx="6"/>
|
||||
<text class="lbl" x="335" y="92" text-anchor="middle">routeur 2</text>
|
||||
<text class="sub" x="335" y="111" text-anchor="middle">169.254.0.2/28</text>
|
||||
<text class="sub" x="240" y="150" text-anchor="middle">passerelle 192.168.14.1/24</text>
|
||||
|
||||
<rect class="group" x="560" y="20" width="400" height="144" rx="8"/>
|
||||
<text class="grp" x="580" y="44">VM ROUTE REFLECTOR · AS 65000</text>
|
||||
<text class="lbl" x="580" y="70">lo1 · 10.255.255.1/32</text>
|
||||
<text class="sub" x="580" y="87">router-id, cluster-id, source des sessions EVPN</text>
|
||||
<text class="lbl" x="580" y="111">192.168.14.2/24 · 169.254.0.3/28</text>
|
||||
<text class="sub" x="580" y="128">principale, et secondaire pour le lien avec les routeurs</text>
|
||||
<text class="sub" x="580" y="150">hébergée par le cluster lui-même</text>
|
||||
|
||||
<path class="ctrl" d="M440 92 L560 92"/>
|
||||
<text class="ctrlt" x="500" y="84" text-anchor="middle">eBGP · BFD (×2)</text>
|
||||
<text class="ctrlt" x="500" y="108" text-anchor="middle">annonce lo1 seule</text>
|
||||
<text class="ctrlt" x="500" y="122" text-anchor="middle">n'accepte rien</text>
|
||||
|
||||
<path class="link" d="M145 126 L145 196"/>
|
||||
<path class="link" d="M335 126 L335 196"/>
|
||||
<path class="link" d="M760 164 L760 196"/>
|
||||
|
||||
<path class="bus" d="M40 196 L960 196"/>
|
||||
<rect class="pill" x="376" y="177" width="216" height="38" rx="19"/>
|
||||
<text class="lbl" x="484" y="194" text-anchor="middle">segment underlay · L2</text>
|
||||
<text class="sub" x="484" y="209" text-anchor="middle">192.168.14.0/24 · 169.254.0.0/28</text>
|
||||
|
||||
<rect class="group" x="40" y="262" width="430" height="306" rx="8"/>
|
||||
<text class="grp" x="60" y="284">HYPERVISEUR 1</text>
|
||||
|
||||
<rect class="box" x="274" y="298" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="364" y="317" text-anchor="middle">br-000000</text>
|
||||
<text class="sub" x="364" y="334" text-anchor="middle">uplink · 192.168.14.11</text>
|
||||
<rect class="box" x="274" y="366" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="364" y="385" text-anchor="middle">vxlan-<vni></text>
|
||||
<text class="sub" x="364" y="402" text-anchor="middle">dans br-<subnet>, sans learning</text>
|
||||
<rect class="box" x="274" y="434" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="364" y="453" text-anchor="middle">netns du VPC</text>
|
||||
<text class="sub" x="364" y="470" text-anchor="middle">passerelle, DHCP</text>
|
||||
<rect class="box" x="274" y="502" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="364" y="521" text-anchor="middle">VM</text>
|
||||
<text class="sub" x="364" y="538" text-anchor="middle">QEMU / KVM</text>
|
||||
<path class="link" d="M364 344 L364 366"/>
|
||||
<text class="sub" x="372" y="359">VTEP</text>
|
||||
<path class="link" d="M364 412 L364 434"/>
|
||||
<text class="sub" x="372" y="427">veth</text>
|
||||
<path class="link" d="M364 480 L364 502"/>
|
||||
<text class="sub" x="372" y="495">tap</text>
|
||||
|
||||
<rect class="box" x="56" y="298" width="180" height="70" rx="6"/>
|
||||
<text class="lbl" x="146" y="321" text-anchor="middle">FRR</text>
|
||||
<text class="sub" x="146" y="340" text-anchor="middle">bgpd · AS 64600</text>
|
||||
<text class="sub" x="146" y="356" text-anchor="middle">advertise-all-vni</text>
|
||||
<rect class="box" x="56" y="434" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="146" y="453" text-anchor="middle">agent</text>
|
||||
<text class="sub" x="146" y="470" text-anchor="middle">API REST · Badger</text>
|
||||
|
||||
<path class="ctrl" d="M236 352 L255 352 L255 389 L272 389" marker-end="url(#a-ctrl)"/>
|
||||
<text class="ctrlt" x="255" y="345" text-anchor="middle">FDB</text>
|
||||
<path class="made" d="M236 457 L272 457" marker-end="url(#a-made)"/>
|
||||
<text class="sub" x="255" y="450" text-anchor="middle">crée</text>
|
||||
|
||||
<rect class="group" x="530" y="262" width="430" height="306" rx="8"/>
|
||||
<text class="grp" x="656" y="284">HYPERVISEUR 2</text>
|
||||
|
||||
<rect class="box" x="546" y="298" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="636" y="317" text-anchor="middle">br-000000</text>
|
||||
<text class="sub" x="636" y="334" text-anchor="middle">uplink · 192.168.14.12</text>
|
||||
<rect class="box" x="546" y="366" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="636" y="385" text-anchor="middle">vxlan-<vni></text>
|
||||
<text class="sub" x="636" y="402" text-anchor="middle">dans br-<subnet>, sans learning</text>
|
||||
<rect class="box" x="546" y="434" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="636" y="453" text-anchor="middle">netns du VPC</text>
|
||||
<text class="sub" x="636" y="470" text-anchor="middle">passerelle, DHCP</text>
|
||||
<rect class="box" x="546" y="502" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="636" y="521" text-anchor="middle">VM</text>
|
||||
<text class="sub" x="636" y="538" text-anchor="middle">QEMU / KVM</text>
|
||||
<path class="link" d="M636 344 L636 366"/>
|
||||
<text class="sub" x="628" y="359" text-anchor="end">VTEP</text>
|
||||
<path class="link" d="M636 412 L636 434"/>
|
||||
<text class="sub" x="628" y="427" text-anchor="end">veth</text>
|
||||
<path class="link" d="M636 480 L636 502"/>
|
||||
<text class="sub" x="628" y="495" text-anchor="end">tap</text>
|
||||
|
||||
<rect class="box" x="764" y="298" width="180" height="70" rx="6"/>
|
||||
<text class="lbl" x="854" y="321" text-anchor="middle">FRR</text>
|
||||
<text class="sub" x="854" y="340" text-anchor="middle">bgpd · AS 64600</text>
|
||||
<text class="sub" x="854" y="356" text-anchor="middle">advertise-all-vni</text>
|
||||
<rect class="box" x="764" y="434" width="180" height="46" rx="6"/>
|
||||
<text class="lbl" x="854" y="453" text-anchor="middle">agent</text>
|
||||
<text class="sub" x="854" y="470" text-anchor="middle">API REST · Badger</text>
|
||||
|
||||
<path class="ctrl" d="M764 352 L745 352 L745 389 L728 389" marker-end="url(#a-ctrl)"/>
|
||||
<text class="ctrlt" x="745" y="345" text-anchor="middle">FDB</text>
|
||||
<path class="made" d="M764 457 L728 457" marker-end="url(#a-made)"/>
|
||||
<text class="sub" x="745" y="450" text-anchor="middle">crée</text>
|
||||
|
||||
<path class="link" d="M364 298 L364 196"/>
|
||||
<path class="link" d="M636 298 L636 196"/>
|
||||
|
||||
<path class="ctrl" d="M210 298 L210 246 L357 246"/>
|
||||
<path class="ctrl" d="M371 246 L612 246 L612 164"/>
|
||||
<path class="ctrl" d="M854 298 L854 164"/>
|
||||
<text class="ctrlt" x="384" y="240">iBGP l2vpn evpn · AS 64600</text>
|
||||
<text class="ctrlt" x="862" y="240">iBGP</text>
|
||||
|
||||
<path class="data" d="M458 389 L542 389" marker-start="url(#a-data)" marker-end="url(#a-data)"/>
|
||||
<text class="datat" x="500" y="378" text-anchor="middle">VXLAN</text>
|
||||
<text class="datat" x="500" y="406" text-anchor="middle">UDP 4789</text>
|
||||
|
||||
<path class="link" d="M40 604 L76 604"/>
|
||||
<text class="sub" x="84" y="608">lien L2 / interface</text>
|
||||
<path class="data" d="M220 604 L256 604"/>
|
||||
<text class="sub" x="264" y="608">plan de données : tunnels VXLAN</text>
|
||||
<path class="ctrl" d="M470 604 L506 604"/>
|
||||
<text class="sub" x="514" y="608">plan de contrôle : sessions BGP</text>
|
||||
<path class="made" d="M716 604 L752 604"/>
|
||||
<text class="sub" x="760" y="608">créé par l'agent</text>
|
||||
|
||||
<text class="sub" x="40" y="640">Les hyperviseurs joignent 10.255.255.1 par leur passerelle 192.168.14.1 : les routeurs l'ont apprise du route reflector en eBGP, sans rien lui renvoyer.</text>
|
||||
<text class="sub" x="40" y="658">Le route reflector accepte toute session venant de 192.168.14.0/24 (bgp listen range) et réfléchit les routes EVPN entre hyperviseurs.</text>
|
||||
<text class="sub" x="40" y="676">Chaque FRR en peuple la FDB des VXLAN locaux — intégration de FRR avec les interfaces créées par two : à qualifier (#50, E5).</text>
|
||||
<text class="sub" x="40" y="694">MTU du segment : 1550 au moins (VXLAN figé à 1500 par l'agent, + 50 octets d'encapsulation) ; 9000 dans le lab.</text>
|
||||
<text class="sub" x="40" y="712">ASN, loopback et plages : valeurs de production (#50). Adresses en 192.168.14.0/24 : celles du lab. Placement de la VM route reflector : à rédiger.</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 10 KiB |
52
docs/schemas/topologie-cluster-dark.svg
Normal file
52
docs/schemas/topologie-cluster-dark.svg
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 500" width="900" height="500" role="img" aria-label="Topologie du cluster">
|
||||
<style>
|
||||
.lbl { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 14px; fill: #e6edf3; }
|
||||
.sub { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 11px; fill: #9198a1; }
|
||||
.grp { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 12px; font-weight: 600; fill: #9198a1; letter-spacing: .04em; }
|
||||
.box { fill: #22272e; stroke: #6e7781; stroke-width: 1.5; }
|
||||
.group { fill: #1c2128; stroke: #444c56; stroke-width: 1.5; stroke-dasharray: 0; }
|
||||
.link { stroke: #9198a1; stroke-width: 1.8; fill: none; }
|
||||
.ctrl { stroke: #c297ff; stroke-width: 1.6; fill: none; stroke-dasharray: 5 4; }
|
||||
</style>
|
||||
|
||||
<rect class="group" x="250" y="24" width="400" height="96" rx="8"/>
|
||||
<text class="grp" x="270" y="46">ROUTEURS DE CLUSTER</text>
|
||||
<rect class="box" x="275" y="58" width="160" height="42" rx="6"/>
|
||||
<text class="lbl" x="355" y="84" text-anchor="middle">routeur A</text>
|
||||
<rect class="box" x="465" y="58" width="160" height="42" rx="6"/>
|
||||
<text class="lbl" x="545" y="84" text-anchor="middle">routeur B</text>
|
||||
|
||||
<rect class="box" x="350" y="168" width="200" height="56" rx="6"/>
|
||||
<text class="lbl" x="450" y="191" text-anchor="middle">VM route reflector</text>
|
||||
<text class="sub" x="450" y="210" text-anchor="middle">plan de contrôle</text>
|
||||
|
||||
<rect class="group" x="40" y="278" width="370" height="196" rx="8"/>
|
||||
<text class="grp" x="60" y="300">HYPERVISEUR 1</text>
|
||||
<rect class="box" x="145" y="312" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="225" y="337" text-anchor="middle">br-000000</text>
|
||||
<rect class="box" x="145" y="372" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="225" y="397" text-anchor="middle">FRR</text>
|
||||
<rect class="box" x="145" y="424" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="225" y="449" text-anchor="middle">agent</text>
|
||||
<path class="link" d="M225 352 L225 372"/>
|
||||
<path class="link" d="M225 412 L225 424"/>
|
||||
|
||||
<rect class="group" x="490" y="278" width="370" height="196" rx="8"/>
|
||||
<text class="grp" x="510" y="300">HYPERVISEUR 2</text>
|
||||
<rect class="box" x="595" y="312" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="675" y="337" text-anchor="middle">br-000000</text>
|
||||
<rect class="box" x="595" y="372" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="675" y="397" text-anchor="middle">FRR</text>
|
||||
<rect class="box" x="595" y="424" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="675" y="449" text-anchor="middle">agent</text>
|
||||
<path class="link" d="M675 352 L675 372"/>
|
||||
<path class="link" d="M675 412 L675 424"/>
|
||||
|
||||
<path class="link" d="M225 312 L225 148 L330 148 L330 120"/>
|
||||
<path class="link" d="M675 312 L675 148 L570 148 L570 120"/>
|
||||
<text class="sub" x="232" y="266">uplink</text>
|
||||
<text class="sub" x="682" y="266">uplink</text>
|
||||
|
||||
<path class="ctrl" d="M350 196 L322 196 L322 392 L305 392"/>
|
||||
<path class="ctrl" d="M550 196 L578 196 L578 392 L595 392"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3 KiB |
52
docs/schemas/topologie-cluster.svg
Normal file
52
docs/schemas/topologie-cluster.svg
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 500" width="900" height="500" role="img" aria-label="Topologie du cluster">
|
||||
<style>
|
||||
.lbl { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 14px; fill: #1f2328; }
|
||||
.sub { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 11px; fill: #57606a; }
|
||||
.grp { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 12px; font-weight: 600; fill: #57606a; letter-spacing: .04em; }
|
||||
.box { fill: #ffffff; stroke: #8c959f; stroke-width: 1.5; }
|
||||
.group { fill: #f6f8fa; stroke: #d0d7de; stroke-width: 1.5; stroke-dasharray: 0; }
|
||||
.link { stroke: #57606a; stroke-width: 1.8; fill: none; }
|
||||
.ctrl { stroke: #8250df; stroke-width: 1.6; fill: none; stroke-dasharray: 5 4; }
|
||||
</style>
|
||||
|
||||
<rect class="group" x="250" y="24" width="400" height="96" rx="8"/>
|
||||
<text class="grp" x="270" y="46">ROUTEURS DE CLUSTER</text>
|
||||
<rect class="box" x="275" y="58" width="160" height="42" rx="6"/>
|
||||
<text class="lbl" x="355" y="84" text-anchor="middle">routeur A</text>
|
||||
<rect class="box" x="465" y="58" width="160" height="42" rx="6"/>
|
||||
<text class="lbl" x="545" y="84" text-anchor="middle">routeur B</text>
|
||||
|
||||
<rect class="box" x="350" y="168" width="200" height="56" rx="6"/>
|
||||
<text class="lbl" x="450" y="191" text-anchor="middle">VM route reflector</text>
|
||||
<text class="sub" x="450" y="210" text-anchor="middle">plan de contrôle</text>
|
||||
|
||||
<rect class="group" x="40" y="278" width="370" height="196" rx="8"/>
|
||||
<text class="grp" x="60" y="300">HYPERVISEUR 1</text>
|
||||
<rect class="box" x="145" y="312" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="225" y="337" text-anchor="middle">br-000000</text>
|
||||
<rect class="box" x="145" y="372" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="225" y="397" text-anchor="middle">FRR</text>
|
||||
<rect class="box" x="145" y="424" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="225" y="449" text-anchor="middle">agent</text>
|
||||
<path class="link" d="M225 352 L225 372"/>
|
||||
<path class="link" d="M225 412 L225 424"/>
|
||||
|
||||
<rect class="group" x="490" y="278" width="370" height="196" rx="8"/>
|
||||
<text class="grp" x="510" y="300">HYPERVISEUR 2</text>
|
||||
<rect class="box" x="595" y="312" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="675" y="337" text-anchor="middle">br-000000</text>
|
||||
<rect class="box" x="595" y="372" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="675" y="397" text-anchor="middle">FRR</text>
|
||||
<rect class="box" x="595" y="424" width="160" height="40" rx="6"/>
|
||||
<text class="lbl" x="675" y="449" text-anchor="middle">agent</text>
|
||||
<path class="link" d="M675 352 L675 372"/>
|
||||
<path class="link" d="M675 412 L675 424"/>
|
||||
|
||||
<path class="link" d="M225 312 L225 148 L330 148 L330 120"/>
|
||||
<path class="link" d="M675 312 L675 148 L570 148 L570 120"/>
|
||||
<text class="sub" x="232" y="266">uplink</text>
|
||||
<text class="sub" x="682" y="266">uplink</text>
|
||||
|
||||
<path class="ctrl" d="M350 196 L322 196 L322 392 L305 392"/>
|
||||
<path class="ctrl" d="M550 196 L578 196 L578 392 L595 392"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3 KiB |
|
|
@ -125,10 +125,6 @@ func (l Lab) Up(ctx context.Context, fetcher provision.Fetcher, timeout time.Dur
|
|||
errs = append(errs, fmt.Errorf("node %s: %w", n.Name, err))
|
||||
continue
|
||||
}
|
||||
if err := l.checkServices(ctx, n); err != nil {
|
||||
errs = append(errs, fmt.Errorf("node %s: %w", n.Name, err))
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(l.Out, "%s: ready\n", n.Name)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
|
|
@ -167,26 +163,6 @@ func (l Lab) waitReady(ctx context.Context, n topology.NodePlan) error {
|
|||
}
|
||||
}
|
||||
|
||||
func services(n topology.NodePlan) []string {
|
||||
var units []string
|
||||
if n.Role == topology.RoleHypervisor {
|
||||
units = append(units, "agent.service")
|
||||
}
|
||||
if n.FRR != "" {
|
||||
units = append(units, "frr.service")
|
||||
}
|
||||
return units
|
||||
}
|
||||
|
||||
func (l Lab) checkServices(ctx context.Context, n topology.NodePlan) error {
|
||||
for _, unit := range services(n) {
|
||||
if err := l.Runner.Run(ctx, "ssh", l.sshArgs(n, true, []string{"systemctl", "is-active", "--quiet", unit})...); err != nil {
|
||||
return fmt.Errorf("%s is not active: %w", unit, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func exitCode(err error) int {
|
||||
var coded interface{ ExitCode() int }
|
||||
if errors.As(err, &coded) {
|
||||
|
|
|
|||
|
|
@ -34,10 +34,9 @@ func (e exitErr) ExitCode() int { return int(e) }
|
|||
type fakeRunner struct {
|
||||
mu sync.Mutex
|
||||
calls [][]string
|
||||
fail string
|
||||
ssh map[string][]error
|
||||
always map[string]error
|
||||
inactive string
|
||||
fail string
|
||||
ssh map[string][]error
|
||||
always map[string]error
|
||||
}
|
||||
|
||||
func (f *fakeRunner) Run(_ context.Context, name string, args ...string) error {
|
||||
|
|
@ -56,9 +55,6 @@ func (f *fakeRunner) Run(_ context.Context, name string, args ...string) error {
|
|||
return os.WriteFile(private+".pub", []byte(labKey+"\n"), 0o644)
|
||||
case "ssh":
|
||||
port := args[indexOf(args, "-p")+1]
|
||||
if args[len(args)-1] == f.inactive {
|
||||
return exitErr(3)
|
||||
}
|
||||
answers := f.ssh[port]
|
||||
if len(answers) == 0 {
|
||||
return f.always[port]
|
||||
|
|
@ -128,7 +124,7 @@ nodes:
|
|||
}
|
||||
|
||||
const switchLast = ` rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay] }
|
||||
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 }
|
||||
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] }
|
||||
sw1: { role: switch, image: deb, cpus: 2, memory: 1024 }
|
||||
`
|
||||
|
||||
|
|
@ -413,8 +409,8 @@ func TestUp_StartsSwitchesFirstAndWaitsForEveryNode(t *testing.T) {
|
|||
}
|
||||
|
||||
ssh := f.runner.commands("ssh")
|
||||
if len(ssh) != 4 {
|
||||
t.Fatalf("%d ssh calls, want 4", len(ssh))
|
||||
if len(ssh) != 3 {
|
||||
t.Fatalf("%d ssh calls, want 3", len(ssh))
|
||||
}
|
||||
want := []string{"ssh",
|
||||
"-i", filepath.Join(f.lab.RunDir, "lab_ed25519"),
|
||||
|
|
@ -444,8 +440,8 @@ func TestUp_RetriesWhileSSHIsUnreachable(t *testing.T) {
|
|||
if err := f.lab.Up(context.Background(), f.fetcher, time.Second); err != nil {
|
||||
t.Fatalf("Up: %v", err)
|
||||
}
|
||||
if n := len(f.runner.commands("ssh")); n != 6 {
|
||||
t.Errorf("%d ssh calls, want 6", n)
|
||||
if n := len(f.runner.commands("ssh")); n != 5 {
|
||||
t.Errorf("%d ssh calls, want 5", n)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -545,44 +541,3 @@ func TestStop_RefusesPidsThatTargetAGroup(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUp_ChecksTheServicesOfEachRole(t *testing.T) {
|
||||
f := newFixture(t, switchLast)
|
||||
conf := filepath.Join(t.TempDir(), "sw1.conf")
|
||||
if err := os.WriteFile(conf, []byte("hostname sw1\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.lab.Plan.Nodes[2].FRR = conf
|
||||
|
||||
if err := f.lab.Up(context.Background(), f.fetcher, time.Second); err != nil {
|
||||
t.Fatalf("Up: %v", err)
|
||||
}
|
||||
|
||||
var checks []string
|
||||
for _, c := range f.runner.commands("ssh") {
|
||||
if c[len(c)-4] == "systemctl" {
|
||||
checks = append(checks, c[indexOf(c, "-p")+1]+" "+strings.Join(c[len(c)-4:], " "))
|
||||
}
|
||||
}
|
||||
want := []string{
|
||||
"2201 systemctl is-active --quiet agent.service",
|
||||
"2202 systemctl is-active --quiet frr.service",
|
||||
}
|
||||
if !reflect.DeepEqual(checks, want) {
|
||||
t.Errorf("checks = %q, want %q", checks, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUp_ReportsAnInactiveServiceAndWaitsForTheOthers(t *testing.T) {
|
||||
f := newFixture(t, switchLast)
|
||||
f.runner.inactive = "agent.service"
|
||||
|
||||
err := f.lab.Up(context.Background(), f.fetcher, time.Second)
|
||||
|
||||
if err == nil || err.Error() != "node hv1: agent.service is not active: exit status 3" {
|
||||
t.Errorf("error = %v", err)
|
||||
}
|
||||
if !strings.Contains(f.out.String(), "rr1: ready\n") || !strings.Contains(f.out.String(), "sw1: ready\n") || strings.Contains(f.out.String(), "hv1: ready") {
|
||||
t.Errorf("output = %q", f.out.String())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,11 +72,7 @@ func Prepare(ctx context.Context, p *topology.Plan, o Options) ([]render.Node, e
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frr, err := ReadFRR(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes, err := render.Render(p, render.Options{RunDir: o.RunDir, AuthorizedKeys: []string{key}, FRR: frr})
|
||||
nodes, err := render.Render(p, render.Options{RunDir: o.RunDir, AuthorizedKeys: []string{key}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -88,21 +84,6 @@ func Prepare(ctx context.Context, p *topology.Plan, o Options) ([]render.Node, e
|
|||
return nodes, nil
|
||||
}
|
||||
|
||||
func ReadFRR(p *topology.Plan) (map[string]string, error) {
|
||||
configs := map[string]string{}
|
||||
for _, n := range p.Nodes {
|
||||
if n.FRR == "" {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(n.FRR)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("node %s: %w", n.Name, err)
|
||||
}
|
||||
configs[n.Name] = string(data)
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func EnsureKey(ctx context.Context, r Runner, dir string) (string, error) {
|
||||
private := filepath.Join(dir, KeyFile)
|
||||
if _, err := os.Stat(private); errors.Is(err, os.ErrNotExist) {
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ segments:
|
|||
nodes:
|
||||
sw1: { role: switch, image: debian12, cpus: 2, memory: 1024 }
|
||||
rr1: { role: rr, image: debian12, cpus: 1, memory: 1024, segments: [underlay] }
|
||||
hv1: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 }
|
||||
hv2: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 }
|
||||
hv1: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay] }
|
||||
hv2: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay] }
|
||||
`
|
||||
topo, err := topology.Parse([]byte(doc))
|
||||
if err != nil {
|
||||
|
|
@ -315,73 +315,3 @@ func TestEnsureKey_WithTheRealSSHKeygen(t *testing.T) {
|
|||
t.Errorf("private key mode = %v, %v", info.Mode().Perm(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFRR_ReadsOnlyTheNodesThatDeclareOne(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
conf := filepath.Join(dir, "rr1.conf")
|
||||
if err := os.WriteFile(conf, []byte("hostname rr1\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := newMirror(t)
|
||||
p := labPlan(t, m)
|
||||
p.Nodes[1].FRR = conf
|
||||
|
||||
got, err := ReadFRR(p)
|
||||
|
||||
if err != nil || !reflect.DeepEqual(got, map[string]string{"rr1": "hostname rr1\n"}) {
|
||||
t.Errorf("ReadFRR = %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFRR_NamesTheNodeOfAMissingFile(t *testing.T) {
|
||||
m := newMirror(t)
|
||||
p := labPlan(t, m)
|
||||
p.Nodes[2].FRR = filepath.Join(t.TempDir(), "absent.conf")
|
||||
|
||||
if _, err := ReadFRR(p); err == nil || !strings.Contains(err.Error(), "node hv1: ") || !strings.Contains(err.Error(), "absent.conf") {
|
||||
t.Errorf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepare_PutsTheFRRConfigIntoTheSeed(t *testing.T) {
|
||||
m := newMirror(t)
|
||||
publish(m, []byte("qcow2 image"))
|
||||
root := t.TempDir()
|
||||
conf := filepath.Join(root, "hv1.conf")
|
||||
if err := os.WriteFile(conf, []byte("hostname hv1\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := labPlan(t, m)
|
||||
p.Nodes[2].FRR = conf
|
||||
|
||||
nodes, err := Prepare(context.Background(), p, Options{
|
||||
RunDir: filepath.Join(root, "run"),
|
||||
Fetcher: Fetcher{Client: m.server.Client(), CacheDir: filepath.Join(root, "cache")},
|
||||
Runner: &fakeRunner{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Prepare: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(nodes[2].Dir, "user-data"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg struct {
|
||||
Files []struct {
|
||||
Path string `yaml:"path"`
|
||||
Content string `yaml:"content"`
|
||||
} `yaml:"write_files"`
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range cfg.Files {
|
||||
if f.Path == "/etc/lab/frr.conf" {
|
||||
if f.Content != "hostname hv1\n" {
|
||||
t.Errorf("frr.conf = %q", f.Content)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("hv1 user-data has no /etc/lab/frr.conf:\n%s", data)
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -30,7 +30,6 @@ var Nameservers = []string{"1.1.1.1", "8.8.8.8"}
|
|||
type Options struct {
|
||||
RunDir string
|
||||
AuthorizedKeys []string
|
||||
FRR map[string]string
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
|
|
@ -46,11 +45,6 @@ func Render(p *topology.Plan, o Options) ([]Node, error) {
|
|||
if err := o.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, n := range p.Nodes {
|
||||
if _, ok := o.FRR[n.Name]; n.FRR != "" && !ok {
|
||||
return nil, fmt.Errorf("node %s: frr configuration %s was not read", n.Name, n.FRR)
|
||||
}
|
||||
}
|
||||
var nodes []Node
|
||||
for index, n := range p.Nodes {
|
||||
dir := filepath.Join(o.RunDir, n.Name)
|
||||
|
|
@ -58,7 +52,7 @@ func Render(p *topology.Plan, o Options) ([]Node, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := userData(p, n, o)
|
||||
user, err := userData(p, n, o.AuthorizedKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ segments:
|
|||
nodes:
|
||||
sw1: { role: switch, image: deb, cpus: 2, memory: 1024 }
|
||||
rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay] }
|
||||
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 }
|
||||
hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 }
|
||||
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] }
|
||||
hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] }
|
||||
`
|
||||
|
||||
const twoSegments = `name: two-seg
|
||||
|
|
@ -36,7 +36,7 @@ segments:
|
|||
blue: { switch: sw, cidr: 10.2.0.0/24, mtu: 1500 }
|
||||
nodes:
|
||||
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
|
||||
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [blue, red], release: 0.2.0rc002 }
|
||||
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [blue, red] }
|
||||
`
|
||||
|
||||
func plan(t *testing.T, doc string) *topology.Plan {
|
||||
|
|
@ -317,7 +317,7 @@ func TestNetworkConfig_DefaultRouteOnlyOnFirstSegment(t *testing.T) {
|
|||
|
||||
func TestUserData_Hypervisor(t *testing.T) {
|
||||
cfg := user(t, nodeNamed(t, renderAll(t, twoHypervisors), "hv1"))
|
||||
if cfg.Hostname != "hv1" || !cfg.DisableRoot || len(cfg.Packages) != 0 || !reflect.DeepEqual(cfg.Runcmd, [][]string{{"/usr/local/sbin/lab-provision"}}) {
|
||||
if cfg.Hostname != "hv1" || !cfg.DisableRoot || len(cfg.WriteFiles) != 0 || len(cfg.Runcmd) != 0 || len(cfg.Packages) != 0 {
|
||||
t.Errorf("hv1 user-data = %+v", cfg)
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.SSHAuthorizedKeys, []string{labKey}) {
|
||||
|
|
@ -376,12 +376,9 @@ nft -f /etc/lab-switch.nft
|
|||
t.Errorf("unit misses %q", want)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.Runcmd, [][]string{{"/usr/local/sbin/lab-provision"}}) {
|
||||
if !reflect.DeepEqual(cfg.Runcmd, [][]string{{"systemctl", "daemon-reload"}, {"systemctl", "enable", "--now", "lab-switch.service"}}) {
|
||||
t.Errorf("runcmd = %v", cfg.Runcmd)
|
||||
}
|
||||
if got := fileAt(t, cfg, "/usr/local/sbin/lab-provision").Content; got != "#!/bin/sh\nset -eu\nsystemctl daemon-reload\nsystemctl enable --now lab-switch.service\n" {
|
||||
t.Errorf("lab-provision:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserData_SwitchWithTwoSegments(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,264 +0,0 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const withRoles = `name: evpn-2hv
|
||||
images:
|
||||
deb:
|
||||
url: https://example.invalid/deb.qcow2
|
||||
sums: https://example.invalid/SHA512SUMS
|
||||
segments:
|
||||
underlay: { switch: sw1, cidr: 192.168.14.0/24 }
|
||||
nodes:
|
||||
sw1: { role: switch, image: deb, cpus: 2, memory: 1024, secondary: { underlay: [169.254.0.1/28] }, frr: sw1.conf }
|
||||
rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay], secondary: { underlay: [169.254.0.3/28] }, loopback: 10.255.255.1/32, frr: rr1.conf }
|
||||
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], frr: hv1.conf, release: 0.2.0rc002 }
|
||||
hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 }
|
||||
`
|
||||
|
||||
var frrConfigs = map[string]string{
|
||||
"sw1": "hostname sw1\nrouter bgp 65100\n",
|
||||
"rr1": "hostname rr1\nrouter bgp 65000\n",
|
||||
"hv1": "hostname hv1\nrouter bgp 64600\n",
|
||||
}
|
||||
|
||||
func renderRoles(t *testing.T) map[string]Node {
|
||||
t.Helper()
|
||||
nodes, err := Render(plan(t, withRoles), Options{RunDir: "/srv/lab", AuthorizedKeys: []string{labKey}, FRR: frrConfigs})
|
||||
if err != nil {
|
||||
t.Fatalf("Render: %v", err)
|
||||
}
|
||||
out := map[string]Node{}
|
||||
for _, n := range nodes {
|
||||
out[n.Name] = n
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func runcmd(t *testing.T, n Node) []string {
|
||||
t.Helper()
|
||||
var out []string
|
||||
for _, c := range user(t, n).Runcmd {
|
||||
out = append(out, strings.Join(c, " "))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const frrScriptHead = `#!/bin/sh
|
||||
set -eu
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
. /etc/os-release
|
||||
echo "deb [signed-by=/usr/share/keyrings/frrouting.gpg] https://deb.frrouting.org/frr ${VERSION_CODENAME} frr-stable" > /etc/apt/sources.list.d/frr.list
|
||||
n=0
|
||||
until apt-get update -qq --error-on=any; do
|
||||
n=$((n + 1))
|
||||
[ "$n" -lt 30 ] || exit 1
|
||||
sleep 10
|
||||
done
|
||||
apt-get install -y -qq --no-install-recommends frr frr-pythontools
|
||||
sed -i 's/^bgpd=no/bgpd=yes/' /etc/frr/daemons
|
||||
`
|
||||
|
||||
func TestRoles_SecondaryAddressFollowsThePrimaryOnTheNode(t *testing.T) {
|
||||
doc := network(t, nodeNamed(t, renderRoles(t), "rr1"))
|
||||
got := iface(t, doc, "underlay").Addresses
|
||||
if !reflect.DeepEqual(got, []string{"192.168.14.2/24", "169.254.0.3/28"}) {
|
||||
t.Errorf("rr1 underlay addresses = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_SwitchCarriesItsSecondaryOnTheBridge(t *testing.T) {
|
||||
script := fileAt(t, user(t, nodeNamed(t, renderRoles(t), "sw1")), "/usr/local/sbin/lab-switch").Content
|
||||
want := "ip addr replace 192.168.14.1/24 dev br-underlay\nip addr replace 169.254.0.1/28 dev br-underlay\nip link set dev br-underlay up\n"
|
||||
if !strings.Contains(script, want) {
|
||||
t.Errorf("lab-switch:\n%s\ndoes not contain:\n%s", script, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_LoopbackOnADummyInterfaceReplayedAtBoot(t *testing.T) {
|
||||
rr1 := nodeNamed(t, renderRoles(t), "rr1")
|
||||
cfg := user(t, rr1)
|
||||
|
||||
script := fileAt(t, cfg, "/usr/local/sbin/lab-node")
|
||||
want := "#!/bin/sh\nset -eu\nip link add lo1 type dummy 2>/dev/null || true\nip addr replace 10.255.255.1/32 dev lo1\nip link set dev lo1 up\n"
|
||||
if script.Content != want || script.Permissions != "0755" {
|
||||
t.Errorf("lab-node (%s):\n%s", script.Permissions, script.Content)
|
||||
}
|
||||
unit := fileAt(t, cfg, "/etc/systemd/system/lab-node.service").Content
|
||||
if !strings.Contains(unit, "ExecStart=/usr/local/sbin/lab-node\n") || !strings.Contains(unit, "WantedBy=multi-user.target\n") {
|
||||
t.Errorf("lab-node.service:\n%s", unit)
|
||||
}
|
||||
want = "#!/bin/sh\nset -eu\nsystemctl daemon-reload\nsystemctl enable --now lab-node.service\n/usr/local/sbin/lab-frr\n"
|
||||
if got := fileAt(t, cfg, "/usr/local/sbin/lab-provision").Content; got != want {
|
||||
t.Errorf("lab-provision:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_FRRConfigIsWrittenVerbatimAndPrivately(t *testing.T) {
|
||||
for _, name := range []string{"sw1", "rr1", "hv1"} {
|
||||
f := fileAt(t, user(t, nodeNamed(t, renderRoles(t), name)), "/etc/lab/frr.conf")
|
||||
if f.Content != frrConfigs[name] || f.Permissions != "0640" {
|
||||
t.Errorf("%s frr.conf (%s) = %q", name, f.Permissions, f.Content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_FRRDaemonsDependOnTheRole(t *testing.T) {
|
||||
tail := "install -o frr -g frr -m 0640 /etc/lab/frr.conf /etc/frr/frr.conf\nsystemctl restart frr\n"
|
||||
bfd := "sed -i 's/^bfdd=no/bfdd=yes/' /etc/frr/daemons\n"
|
||||
nodes := renderRoles(t)
|
||||
for name, want := range map[string]string{
|
||||
"sw1": frrScriptHead + bfd + tail,
|
||||
"rr1": frrScriptHead + bfd + tail,
|
||||
"hv1": frrScriptHead + tail,
|
||||
} {
|
||||
f := fileAt(t, user(t, nodeNamed(t, nodes, name)), "/usr/local/sbin/lab-frr")
|
||||
if f.Content != want || f.Permissions != "0755" {
|
||||
t.Errorf("%s lab-frr (%s):\n%s\nwant:\n%s", name, f.Permissions, f.Content, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_FRRKeyIsThePinnedRepositoryKey(t *testing.T) {
|
||||
f := fileAt(t, user(t, nodeNamed(t, renderRoles(t), "hv1")), "/usr/share/keyrings/frrouting.gpg")
|
||||
if f.Encoding != "b64" || f.Permissions != "0644" {
|
||||
t.Errorf("key file: encoding %q, permissions %q", f.Encoding, f.Permissions)
|
||||
}
|
||||
key, err := base64.StdEncoding.DecodeString(f.Content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum := sha256.Sum256(key)
|
||||
if got := hex.EncodeToString(sum[:]); got != "bf10935b9296e2ce7c5d9855fa29ef30c35810b0fc4b1f53005494a04a33554d" {
|
||||
t.Errorf("key sha256 = %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_ProvisioningIsOneScriptThatStopsAtTheFirstFailure(t *testing.T) {
|
||||
nodes := renderRoles(t)
|
||||
for name, steps := range map[string]string{
|
||||
"sw1": "systemctl daemon-reload\nsystemctl enable --now lab-switch.service\n/usr/local/sbin/lab-frr\n",
|
||||
"hv1": "/usr/local/sbin/lab-deploy\n/usr/local/sbin/lab-frr\n",
|
||||
"hv2": "/usr/local/sbin/lab-deploy\n",
|
||||
} {
|
||||
cfg := user(t, nodeNamed(t, nodes, name))
|
||||
if !reflect.DeepEqual(cfg.Runcmd, [][]string{{"/usr/local/sbin/lab-provision"}}) {
|
||||
t.Errorf("%s runcmd = %q", name, cfg.Runcmd)
|
||||
}
|
||||
f := fileAt(t, cfg, "/usr/local/sbin/lab-provision")
|
||||
if f.Content != "#!/bin/sh\nset -eu\n"+steps || f.Permissions != "0755" {
|
||||
t.Errorf("%s lab-provision (%s):\n%s", name, f.Permissions, f.Content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_HypervisorDeploysTheReleaseThroughTheRepositoryScripts(t *testing.T) {
|
||||
cfg := user(t, nodeNamed(t, renderRoles(t), "hv1"))
|
||||
|
||||
f := fileAt(t, cfg, "/usr/local/sbin/lab-deploy")
|
||||
want := `#!/bin/sh
|
||||
set -eu
|
||||
n=0
|
||||
until curl -fsS -o /dev/null https://git.g3e.fr/; do
|
||||
n=$((n + 1))
|
||||
[ "$n" -lt 30 ] || exit 1
|
||||
sleep 10
|
||||
done
|
||||
cd /opt/two/scripts
|
||||
bash ./deploy.sh --noup_script -i -u underlay -t 0.2.0rc002
|
||||
`
|
||||
if f.Content != want || f.Permissions != "0755" {
|
||||
t.Errorf("lab-deploy (%s):\n%s\nwant:\n%s", f.Permissions, f.Content, want)
|
||||
}
|
||||
|
||||
for path, source := range map[string]string{
|
||||
"/opt/two/scripts/deploy.sh": "../../../scripts/deploy.sh",
|
||||
"/opt/two/scripts/bootstrap_kvm.sh": "../../../scripts/bootstrap_kvm.sh",
|
||||
} {
|
||||
f := fileAt(t, cfg, path)
|
||||
got, err := base64.StdEncoding.DecodeString(f.Content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repo, err := os.ReadFile(source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Encoding != "b64" || f.Permissions != "0755" || !bytes.Equal(got, repo) {
|
||||
t.Errorf("%s: encoding %q, permissions %q, identical to %s: %v", path, f.Encoding, f.Permissions, source, bytes.Equal(got, repo))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_UplinkIsTheInterfaceCarryingTheDefaultRoute(t *testing.T) {
|
||||
hv := nodeNamed(t, renderAll(t, twoSegments), "hv")
|
||||
var withDefault []string
|
||||
for name, e := range network(t, hv).Ethernets {
|
||||
for _, r := range e.Routes {
|
||||
if r.To == "0.0.0.0/0" {
|
||||
withDefault = append(withDefault, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(withDefault, []string{"red"}) {
|
||||
t.Fatalf("default route on %v, want [red]", withDefault)
|
||||
}
|
||||
if got := fileAt(t, user(t, hv), "/usr/local/sbin/lab-deploy").Content; !strings.HasSuffix(got, "bash ./deploy.sh --noup_script -i -u red -t 0.2.0rc002\n") {
|
||||
t.Errorf("lab-deploy:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_OnlyHypervisorsDeployTwo(t *testing.T) {
|
||||
nodes := renderRoles(t)
|
||||
for _, name := range []string{"sw1", "rr1"} {
|
||||
for _, f := range user(t, nodeNamed(t, nodes, name)).WriteFiles {
|
||||
if strings.HasPrefix(f.Path, "/opt/two/") || f.Path == "/usr/local/sbin/lab-deploy" {
|
||||
t.Errorf("%s receives %s", name, f.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_NodeWithoutRoleFieldsGetsNoFRR(t *testing.T) {
|
||||
for _, f := range user(t, nodeNamed(t, renderRoles(t), "hv2")).WriteFiles {
|
||||
if strings.Contains(f.Path, "frr") {
|
||||
t.Errorf("hv2 receives %s", f.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_RefusesAnUnreadFRRConfig(t *testing.T) {
|
||||
configs := map[string]string{"sw1": "x", "rr1": "y"}
|
||||
_, err := Render(plan(t, withRoles), Options{RunDir: "/srv/lab", AuthorizedKeys: []string{labKey}, FRR: configs})
|
||||
if err == nil || err.Error() != "node hv1: frr configuration hv1.conf was not read" {
|
||||
t.Errorf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_SwitchLoopbackIsCreatedByTheSwitchScript(t *testing.T) {
|
||||
doc := strings.Replace(withRoles, "secondary: { underlay: [169.254.0.1/28] }, frr: sw1.conf", "secondary: { underlay: [169.254.0.1/28] }, loopback: 10.255.254.1/32, frr: sw1.conf", 1)
|
||||
nodes, err := Render(plan(t, doc), Options{RunDir: "/srv/lab", AuthorizedKeys: []string{labKey}, FRR: frrConfigs})
|
||||
if err != nil {
|
||||
t.Fatalf("Render: %v", err)
|
||||
}
|
||||
var sw1 Node
|
||||
for _, n := range nodes {
|
||||
if n.Name == "sw1" {
|
||||
sw1 = n
|
||||
}
|
||||
}
|
||||
script := fileAt(t, user(t, sw1), "/usr/local/sbin/lab-switch").Content
|
||||
want := "ip link set dev br-underlay up\nip link add lo1 type dummy 2>/dev/null || true\nip addr replace 10.255.254.1/32 dev lo1\nip link set dev lo1 up\nnft -f /etc/lab-switch.nft\n"
|
||||
if !strings.HasSuffix(script, want) {
|
||||
t.Errorf("lab-switch:\n%s\ndoes not end with:\n%s", script, want)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,20 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"git.g3e.fr/syonad/two/internal/lab/topology"
|
||||
"git.g3e.fr/syonad/two/scripts"
|
||||
)
|
||||
|
||||
const (
|
||||
SwitchScript = "/usr/local/sbin/lab-switch"
|
||||
SwitchNFT = "/etc/lab-switch.nft"
|
||||
SwitchUnit = "/etc/systemd/system/lab-switch.service"
|
||||
|
||||
NodeScript = "/usr/local/sbin/lab-node"
|
||||
NodeUnit = "/etc/systemd/system/lab-node.service"
|
||||
|
||||
FRRKey = "/usr/share/keyrings/frrouting.gpg"
|
||||
FRRConfig = "/etc/lab/frr.conf"
|
||||
FRRScript = "/usr/local/sbin/lab-frr"
|
||||
FRRSuite = "frr-stable"
|
||||
FRRRepo = "https://deb.frrouting.org/frr"
|
||||
FRRPackages = "frr frr-pythontools"
|
||||
|
||||
ProvisionScript = "/usr/local/sbin/lab-provision"
|
||||
DeployScript = "/usr/local/sbin/lab-deploy"
|
||||
TwoScriptsDir = "/opt/two/scripts"
|
||||
TwoGitServer = "https://git.g3e.fr/"
|
||||
)
|
||||
|
||||
//go:embed frrouting.gpg
|
||||
var frrKey []byte
|
||||
|
||||
type metaDoc struct {
|
||||
InstanceID string `yaml:"instance-id"`
|
||||
LocalHostname string `yaml:"local-hostname"`
|
||||
|
|
@ -44,7 +23,6 @@ type metaDoc struct {
|
|||
type writeFile struct {
|
||||
Path string `yaml:"path"`
|
||||
Permissions string `yaml:"permissions"`
|
||||
Encoding string `yaml:"encoding,omitempty"`
|
||||
Content string `yaml:"content"`
|
||||
}
|
||||
|
||||
|
|
@ -90,51 +68,24 @@ func metaData(p *topology.Plan, n topology.NodePlan) ([]byte, error) {
|
|||
return yaml.Marshal(metaDoc{InstanceID: p.Name + "-" + n.Name, LocalHostname: n.Name})
|
||||
}
|
||||
|
||||
func userData(p *topology.Plan, n topology.NodePlan, o Options) ([]byte, error) {
|
||||
func userData(p *topology.Plan, n topology.NodePlan, keys []string) ([]byte, error) {
|
||||
cfg := cloudConfig{
|
||||
Hostname: n.Name,
|
||||
SSHPwauth: false,
|
||||
DisableRoot: true,
|
||||
SSHAuthorizedKeys: o.AuthorizedKeys,
|
||||
SSHAuthorizedKeys: keys,
|
||||
}
|
||||
var steps []string
|
||||
switch {
|
||||
case n.Role == topology.RoleSwitch:
|
||||
if n.Role == topology.RoleSwitch {
|
||||
cfg.Packages = []string{"nftables"}
|
||||
cfg.WriteFiles = []writeFile{
|
||||
{Path: SwitchScript, Permissions: "0755", Content: switchScript(p, n)},
|
||||
{Path: SwitchScript, Permissions: "0755", Content: switchScript(p, n.Name)},
|
||||
{Path: SwitchNFT, Permissions: "0644", Content: switchNFT(p, n.Name)},
|
||||
{Path: SwitchUnit, Permissions: "0644", Content: unit("Lab switch: bridges, gateways and NAT", SwitchScript)},
|
||||
{Path: SwitchUnit, Permissions: "0644", Content: switchUnit()},
|
||||
}
|
||||
steps = append(steps, "systemctl daemon-reload", "systemctl enable --now lab-switch.service")
|
||||
case n.Loopback.IsValid():
|
||||
cfg.WriteFiles = []writeFile{
|
||||
{Path: NodeScript, Permissions: "0755", Content: "#!/bin/sh\nset -eu\n" + loopbackLines(n)},
|
||||
{Path: NodeUnit, Permissions: "0644", Content: unit("Lab node: loopback", NodeScript)},
|
||||
cfg.Runcmd = [][]string{
|
||||
{"systemctl", "daemon-reload"},
|
||||
{"systemctl", "enable", "--now", "lab-switch.service"},
|
||||
}
|
||||
steps = append(steps, "systemctl daemon-reload", "systemctl enable --now lab-node.service")
|
||||
}
|
||||
if n.Role == topology.RoleHypervisor {
|
||||
cfg.WriteFiles = append(cfg.WriteFiles,
|
||||
writeFile{Path: TwoScriptsDir + "/deploy.sh", Permissions: "0755", Encoding: "b64", Content: base64.StdEncoding.EncodeToString(scripts.Deploy)},
|
||||
writeFile{Path: TwoScriptsDir + "/bootstrap_kvm.sh", Permissions: "0755", Encoding: "b64", Content: base64.StdEncoding.EncodeToString(scripts.BootstrapKVM)},
|
||||
writeFile{Path: DeployScript, Permissions: "0755", Content: deployScript(p, n)},
|
||||
)
|
||||
steps = append(steps, DeployScript)
|
||||
}
|
||||
if n.FRR != "" {
|
||||
cfg.WriteFiles = append(cfg.WriteFiles,
|
||||
writeFile{Path: FRRKey, Permissions: "0644", Encoding: "b64", Content: base64.StdEncoding.EncodeToString(frrKey)},
|
||||
writeFile{Path: FRRConfig, Permissions: "0640", Content: o.FRR[n.Name]},
|
||||
writeFile{Path: FRRScript, Permissions: "0755", Content: frrScript(n)},
|
||||
)
|
||||
steps = append(steps, FRRScript)
|
||||
}
|
||||
if len(steps) > 0 {
|
||||
cfg.WriteFiles = append(cfg.WriteFiles,
|
||||
writeFile{Path: ProvisionScript, Permissions: "0755", Content: "#!/bin/sh\nset -eu\n" + strings.Join(steps, "\n") + "\n"},
|
||||
)
|
||||
cfg.Runcmd = [][]string{{ProvisionScript}}
|
||||
}
|
||||
body, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
|
|
@ -143,47 +94,6 @@ func userData(p *topology.Plan, n topology.NodePlan, o Options) ([]byte, error)
|
|||
return append([]byte("#cloud-config\n"), body...), nil
|
||||
}
|
||||
|
||||
func retry(command string) string {
|
||||
return "n=0\nuntil " + command + "; do\n n=$((n + 1))\n [ \"$n\" -lt 30 ] || exit 1\n sleep 10\ndone\n"
|
||||
}
|
||||
|
||||
func deployScript(p *topology.Plan, n topology.NodePlan) string {
|
||||
uplink := nodeCables(p, n.Name)[0].NodeInterface
|
||||
return "#!/bin/sh\nset -eu\n" +
|
||||
retry("curl -fsS -o /dev/null "+TwoGitServer) +
|
||||
"cd " + TwoScriptsDir + "\n" +
|
||||
fmt.Sprintf("bash ./deploy.sh --noup_script -i -u %s -t %s\n", uplink, n.Release)
|
||||
}
|
||||
|
||||
func frrDaemons(n topology.NodePlan) []string {
|
||||
if n.Role == topology.RoleHypervisor {
|
||||
return []string{"bgpd"}
|
||||
}
|
||||
return []string{"bgpd", "bfdd"}
|
||||
}
|
||||
|
||||
func frrScript(n topology.NodePlan) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("#!/bin/sh\nset -eu\nexport DEBIAN_FRONTEND=noninteractive\n. /etc/os-release\n")
|
||||
fmt.Fprintf(&b, "echo \"deb [signed-by=%s] %s ${VERSION_CODENAME} %s\" > /etc/apt/sources.list.d/frr.list\n", FRRKey, FRRRepo, FRRSuite)
|
||||
b.WriteString(retry("apt-get update -qq --error-on=any"))
|
||||
fmt.Fprintf(&b, "apt-get install -y -qq --no-install-recommends %s\n", FRRPackages)
|
||||
for _, d := range frrDaemons(n) {
|
||||
fmt.Fprintf(&b, "sed -i 's/^%s=no/%s=yes/' /etc/frr/daemons\n", d, d)
|
||||
}
|
||||
fmt.Fprintf(&b, "install -o frr -g frr -m 0640 %s /etc/frr/frr.conf\n", FRRConfig)
|
||||
b.WriteString("systemctl restart frr\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func loopbackLines(n topology.NodePlan) string {
|
||||
if !n.Loopback.IsValid() {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("ip link add %s type dummy 2>/dev/null || true\nip addr replace %s dev %s\nip link set dev %s up\n",
|
||||
topology.LoopbackInterface, n.Loopback, topology.LoopbackInterface, topology.LoopbackInterface)
|
||||
}
|
||||
|
||||
func networkConfig(p *topology.Plan, n topology.NodePlan, index int) ([]byte, error) {
|
||||
doc := networkDoc{Version: 2, Ethernets: map[string]ethernet{}}
|
||||
admin := ethernet{
|
||||
|
|
@ -211,9 +121,6 @@ func networkConfig(p *topology.Plan, n topology.NodePlan, index int) ([]byte, er
|
|||
MTU: c.MTU,
|
||||
Addresses: []string{c.NodeAddress.String()},
|
||||
}
|
||||
for _, prefix := range n.Secondary[c.Segment] {
|
||||
e.Addresses = append(e.Addresses, prefix.String())
|
||||
}
|
||||
if i == 0 {
|
||||
e.Routes = []route{{To: "0.0.0.0/0", Via: gatewayOf(p, c.Segment)}}
|
||||
e.Nameservers = &nameservers{Addresses: Nameservers}
|
||||
|
|
@ -232,8 +139,7 @@ func gatewayOf(p *topology.Plan, segment string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func switchScript(p *topology.Plan, n topology.NodePlan) string {
|
||||
name := n.Name
|
||||
func switchScript(p *topology.Plan, name string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("#!/bin/sh\nset -eu\nsysctl -qw net.ipv4.ip_forward=1\n")
|
||||
for _, s := range switchSegments(p, name) {
|
||||
|
|
@ -247,12 +153,8 @@ func switchScript(p *topology.Plan, n topology.NodePlan) string {
|
|||
}
|
||||
fmt.Fprintf(&b, "ip link set dev %s mtu %d\n", s.Bridge, s.MTU)
|
||||
fmt.Fprintf(&b, "ip addr replace %s/%d dev %s\n", s.Gateway, s.Network.Bits(), s.Bridge)
|
||||
for _, prefix := range n.Secondary[s.Name] {
|
||||
fmt.Fprintf(&b, "ip addr replace %s dev %s\n", prefix, s.Bridge)
|
||||
}
|
||||
fmt.Fprintf(&b, "ip link set dev %s up\n", s.Bridge)
|
||||
}
|
||||
b.WriteString(loopbackLines(n))
|
||||
fmt.Fprintf(&b, "nft -f %s\n", SwitchNFT)
|
||||
return b.String()
|
||||
}
|
||||
|
|
@ -273,9 +175,9 @@ table ip lab_nat {
|
|||
`, strings.Join(networks, ", "), AdminInterface)
|
||||
}
|
||||
|
||||
func unit(description, script string) string {
|
||||
func switchUnit() string {
|
||||
return fmt.Sprintf(`[Unit]
|
||||
Description=%s
|
||||
Description=Lab switch: bridges, gateways and NAT
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
|
|
@ -286,5 +188,5 @@ ExecStart=%s
|
|||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, description, script)
|
||||
`, SwitchScript)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ package topology
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
|
|
@ -20,20 +17,6 @@ func (p *Plan) Write(w io.Writer) error {
|
|||
fmt.Fprintf(tw, " %s\t%s\t%s\t%d\t%d MiB\t127.0.0.1:%d\n", n.Name, n.Role, n.Image, n.CPUs, n.Memory, n.SSHPort)
|
||||
}
|
||||
|
||||
var extras []NodePlan
|
||||
for _, n := range p.Nodes {
|
||||
if len(n.Secondary) > 0 || n.Loopback.IsValid() || n.FRR != "" || n.Release != "" {
|
||||
extras = append(extras, n)
|
||||
}
|
||||
}
|
||||
if len(extras) > 0 {
|
||||
fmt.Fprintf(tw, "\nroles\n")
|
||||
fmt.Fprintf(tw, " name\tloopback\tsecondary\tfrr\trelease\n")
|
||||
for _, n := range extras {
|
||||
fmt.Fprintf(tw, " %s\t%s\t%s\t%s\t%s\n", n.Name, orDash(loopback(n)), orDash(secondary(n)), orDash(filepath.Base(n.FRR)), orDash(n.Release))
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range p.Segments {
|
||||
fmt.Fprintf(tw, "\nsegment %s: %s, mtu %d, switch %s, bridge %s, gateway %s\n",
|
||||
s.Name, s.Network, s.MTU, s.Switch, s.Bridge, s.Gateway)
|
||||
|
|
@ -49,32 +32,3 @@ func (p *Plan) Write(w io.Writer) error {
|
|||
}
|
||||
return tw.Flush()
|
||||
}
|
||||
|
||||
func loopback(n NodePlan) string {
|
||||
if !n.Loopback.IsValid() {
|
||||
return ""
|
||||
}
|
||||
return LoopbackInterface + " " + n.Loopback.String()
|
||||
}
|
||||
|
||||
func secondary(n NodePlan) string {
|
||||
segments := make([]string, 0, len(n.Secondary))
|
||||
for s := range n.Secondary {
|
||||
segments = append(segments, s)
|
||||
}
|
||||
sort.Strings(segments)
|
||||
var parts []string
|
||||
for _, s := range segments {
|
||||
for _, prefix := range n.Secondary[s] {
|
||||
parts = append(parts, s+" "+prefix.String())
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func orDash(s string) string {
|
||||
if s == "" || s == "." {
|
||||
return "-"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,16 +34,12 @@ type SegmentPlan struct {
|
|||
}
|
||||
|
||||
type NodePlan struct {
|
||||
Name string
|
||||
Role string
|
||||
Image string
|
||||
CPUs int
|
||||
Memory int
|
||||
SSHPort int
|
||||
Secondary map[string][]netip.Prefix
|
||||
Loopback netip.Prefix
|
||||
FRR string
|
||||
Release string
|
||||
Name string
|
||||
Role string
|
||||
Image string
|
||||
CPUs int
|
||||
Memory int
|
||||
SSHPort int
|
||||
}
|
||||
|
||||
type Cable struct {
|
||||
|
|
@ -69,28 +65,14 @@ func Compute(t *Topology) (*Plan, error) {
|
|||
|
||||
p := &Plan{Name: t.Name, Images: append([]Image(nil), t.Images...)}
|
||||
for i, n := range t.Nodes {
|
||||
node := NodePlan{
|
||||
p.Nodes = append(p.Nodes, NodePlan{
|
||||
Name: n.Name,
|
||||
Role: n.Role,
|
||||
Image: n.Image,
|
||||
CPUs: n.CPUs,
|
||||
Memory: n.Memory,
|
||||
SSHPort: SSHBasePort + i,
|
||||
FRR: n.FRR,
|
||||
Release: n.Release,
|
||||
}
|
||||
for segment, raws := range n.Secondary {
|
||||
for _, raw := range raws {
|
||||
if node.Secondary == nil {
|
||||
node.Secondary = map[string][]netip.Prefix{}
|
||||
}
|
||||
node.Secondary[segment] = append(node.Secondary[segment], netip.MustParsePrefix(raw))
|
||||
}
|
||||
}
|
||||
if n.Loopback != "" {
|
||||
node.Loopback = netip.MustParsePrefix(n.Loopback)
|
||||
}
|
||||
p.Nodes = append(p.Nodes, node)
|
||||
})
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ segments:
|
|||
nodes:
|
||||
sw1: { role: switch, image: deb, cpus: 2, memory: 1024 }
|
||||
rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay] }
|
||||
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 }
|
||||
hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 }
|
||||
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] }
|
||||
hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] }
|
||||
`
|
||||
|
||||
func TestCompute_TwoHypervisorsPlan(t *testing.T) {
|
||||
|
|
@ -165,7 +165,7 @@ segments:
|
|||
blue: { switch: sw, cidr: 10.2.0.0/24, mtu: 1500 }
|
||||
nodes:
|
||||
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
|
||||
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [blue, red], release: 0.2.0rc002 }
|
||||
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [blue, red] }
|
||||
rr: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red] }
|
||||
`)
|
||||
got := make([]string, 0, len(p.Cables))
|
||||
|
|
@ -193,7 +193,7 @@ nodes:
|
|||
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
|
||||
n1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [a, b] }
|
||||
n2: { role: rr, image: deb, cpus: 1, memory: 512, segments: [a, b] }
|
||||
n3: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [b, a], release: 0.2.0rc002 }
|
||||
n3: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [b, a] }
|
||||
`)
|
||||
macs := map[string]bool{}
|
||||
ports := map[int]bool{}
|
||||
|
|
@ -314,11 +314,6 @@ nodes
|
|||
hv1 hypervisor deb 4 16384 MiB 127.0.0.1:2202
|
||||
hv2 hypervisor deb 4 16384 MiB 127.0.0.1:2203
|
||||
|
||||
roles
|
||||
name loopback secondary frr release
|
||||
hv1 - - - 0.2.0rc002
|
||||
hv2 - - - 0.2.0rc002
|
||||
|
||||
segment underlay: 10.250.0.0/24, mtu 9000, switch sw1, bridge br-underlay, gateway 10.250.0.1
|
||||
node interface address mac udp switch port mac udp
|
||||
rr1 underlay 10.250.0.2/24 02:4c:00:01:00:00 20000 <-> sw1 p0 02:4c:00:01:00:01 20001
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
package topology
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const withRoles = header + `
|
||||
segments:
|
||||
underlay: { switch: sw1, cidr: 192.168.14.0/24 }
|
||||
nodes:
|
||||
sw1: { role: switch, image: deb, cpus: 2, memory: 1024, secondary: { underlay: [169.254.0.1/28] }, frr: frr/sw1.conf }
|
||||
rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay], secondary: { underlay: [169.254.0.3/28] }, loopback: 10.255.255.1/32, frr: frr/rr1.conf }
|
||||
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], frr: /etc/lab/hv1.conf, release: 0.2.0rc002 }
|
||||
`
|
||||
|
||||
func TestCompute_CarriesTheRoleFields(t *testing.T) {
|
||||
p := compute(t, withRoles)
|
||||
|
||||
rr1 := nodeOf(t, p, "rr1")
|
||||
if !reflect.DeepEqual(rr1.Secondary, map[string][]netip.Prefix{"underlay": {netip.MustParsePrefix("169.254.0.3/28")}}) {
|
||||
t.Errorf("rr1 secondary = %v", rr1.Secondary)
|
||||
}
|
||||
if rr1.Loopback != netip.MustParsePrefix("10.255.255.1/32") {
|
||||
t.Errorf("rr1 loopback = %v", rr1.Loopback)
|
||||
}
|
||||
if rr1.FRR != "frr/rr1.conf" {
|
||||
t.Errorf("rr1 frr = %q", rr1.FRR)
|
||||
}
|
||||
hv1 := nodeOf(t, p, "hv1")
|
||||
if hv1.Secondary != nil || hv1.Loopback.IsValid() {
|
||||
t.Errorf("hv1 = %+v, want no secondary and no loopback", hv1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_ResolvesFRRPathsAgainstTheTopologyFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "lab.yml")
|
||||
if err := os.WriteFile(path, []byte(withRoles), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
topo, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
|
||||
want := map[string]string{
|
||||
"sw1": filepath.Join(dir, "frr", "sw1.conf"),
|
||||
"rr1": filepath.Join(dir, "frr", "rr1.conf"),
|
||||
"hv1": "/etc/lab/hv1.conf",
|
||||
}
|
||||
for _, n := range topo.Nodes {
|
||||
if n.FRR != want[n.Name] {
|
||||
t.Errorf("%s frr = %q, want %q", n.Name, n.FRR, want[n.Name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_RoleFieldRejections(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
node string
|
||||
want string
|
||||
}{
|
||||
"secondary on a segment not attached": {
|
||||
`rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], secondary: { blue: [169.254.0.3/28] } }`,
|
||||
"node rr1: secondary address given for segment blue it is not attached to",
|
||||
},
|
||||
"secondary without prefix length": {
|
||||
`rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], secondary: { red: [169.254.0.3] } }`,
|
||||
`node rr1: secondary address "169.254.0.3" on red`,
|
||||
},
|
||||
"secondary in IPv6": {
|
||||
`rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], secondary: { red: ["fd00::3/64"] } }`,
|
||||
"node rr1: secondary address fd00::3/64 on red is not IPv4",
|
||||
},
|
||||
"secondary inside the segment": {
|
||||
`rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], secondary: { red: [10.1.0.9/24] } }`,
|
||||
"node rr1: secondary address 10.1.0.9/24 is inside segment red (10.1.0.0/24), use addresses instead",
|
||||
},
|
||||
"loopback without prefix length": {
|
||||
`rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], loopback: 10.255.255.1 }`,
|
||||
`node rr1: loopback "10.255.255.1"`,
|
||||
},
|
||||
"loopback in IPv6": {
|
||||
`rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], loopback: "fd00::1/128" }`,
|
||||
"node rr1: loopback fd00::1/128 is not IPv4",
|
||||
},
|
||||
}
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
doc := header + `
|
||||
segments:
|
||||
red: { switch: sw, cidr: 10.1.0.0/24 }
|
||||
blue: { switch: sw, cidr: 10.2.0.0/24 }
|
||||
nodes:
|
||||
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
|
||||
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red, blue], release: 0.2.0rc002 }
|
||||
` + c.node + `
|
||||
`
|
||||
requireContains(t, validationError(t, doc), c.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_SwitchSecondaryOnlyOnItsOwnSegments(t *testing.T) {
|
||||
doc := header + `
|
||||
segments:
|
||||
red: { switch: sw, cidr: 10.1.0.0/24 }
|
||||
blue: { switch: other, cidr: 10.2.0.0/24 }
|
||||
nodes:
|
||||
sw: { role: switch, image: deb, cpus: 1, memory: 512, secondary: { red: [169.254.0.1/28], blue: [169.254.1.1/28] } }
|
||||
other: { role: switch, image: deb, cpus: 1, memory: 512 }
|
||||
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red, blue], release: 0.2.0rc002 }
|
||||
`
|
||||
msg := validationError(t, doc)
|
||||
requireContains(t, msg, "node sw: secondary address given for segment blue it is not attached to")
|
||||
if bytes.Contains([]byte(msg), []byte("segment red")) {
|
||||
t.Errorf("the switch's own segment was refused:\n%s", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_LoopbackInterfaceNameIsReserved(t *testing.T) {
|
||||
doc := header + `
|
||||
segments:
|
||||
lo1: { switch: sw, cidr: 10.1.0.0/24 }
|
||||
nodes:
|
||||
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
|
||||
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [lo1], release: 0.2.0rc002 }
|
||||
`
|
||||
requireContains(t, validationError(t, doc), "segment lo1: name is reserved for the loopback interface")
|
||||
}
|
||||
|
||||
func TestWrite_ShowsTheRoles(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
if err := compute(t, withRoles).Write(&buf); err != nil {
|
||||
t.Fatalf("Write: %v", err)
|
||||
}
|
||||
want := `
|
||||
roles
|
||||
name loopback secondary frr release
|
||||
sw1 - underlay 169.254.0.1/28 sw1.conf -
|
||||
rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf -
|
||||
hv1 - - hv1.conf 0.2.0rc002
|
||||
`
|
||||
if !bytes.Contains(buf.Bytes(), []byte(want)) {
|
||||
t.Errorf("plan:\n%s\ndoes not contain:\n%s", buf.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite_NoRolesSectionWithoutRoleFields(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
doc := header + `
|
||||
segments:
|
||||
underlay: { switch: sw1, cidr: 10.250.0.0/24 }
|
||||
nodes:
|
||||
sw1: { role: switch, image: deb, cpus: 1, memory: 512 }
|
||||
rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [underlay] }
|
||||
`
|
||||
if err := compute(t, doc).Write(&buf); err != nil {
|
||||
t.Fatalf("Write: %v", err)
|
||||
}
|
||||
if bytes.Contains(buf.Bytes(), []byte("roles")) {
|
||||
t.Errorf("plan shows a roles section:\n%s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_ReleaseRejections(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
node string
|
||||
want string
|
||||
}{
|
||||
"hypervisor without release": {
|
||||
`hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red] }`,
|
||||
"node hv: a hypervisor needs the release of two to deploy (release: <tag>)",
|
||||
},
|
||||
"release with shell characters": {
|
||||
`hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red], release: "0.2.0; reboot" }`,
|
||||
`node hv: release "0.2.0; reboot" must match`,
|
||||
},
|
||||
"release on a route reflector": {
|
||||
`hv: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], release: 0.2.0rc002 }`,
|
||||
"node hv: release is only for hypervisors",
|
||||
},
|
||||
"release on a switch": {
|
||||
`hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red], release: 0.2.0rc002 }
|
||||
sw2: { role: switch, image: deb, cpus: 1, memory: 512, release: 0.2.0rc002 }`,
|
||||
"node sw2: release is only for hypervisors",
|
||||
},
|
||||
}
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
doc := header + `
|
||||
segments:
|
||||
red: { switch: sw, cidr: 10.1.0.0/24 }
|
||||
nodes:
|
||||
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
|
||||
` + c.node + `
|
||||
`
|
||||
requireContains(t, validationError(t, doc), c.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompute_CarriesTheRelease(t *testing.T) {
|
||||
if got := nodeOf(t, compute(t, withRoles), "hv1").Release; got != "0.2.0rc002" {
|
||||
t.Errorf("hv1 release = %q", got)
|
||||
}
|
||||
if got := nodeOf(t, compute(t, withRoles), "rr1").Release; got != "" {
|
||||
t.Errorf("rr1 release = %q", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
|
@ -46,10 +45,6 @@ type Node struct {
|
|||
Memory int
|
||||
Segments []string
|
||||
Addresses map[string]string
|
||||
Secondary map[string][]string
|
||||
Loopback string
|
||||
FRR string
|
||||
Release string
|
||||
}
|
||||
|
||||
type fileImage struct {
|
||||
|
|
@ -64,16 +59,12 @@ type fileSegment struct {
|
|||
}
|
||||
|
||||
type fileNode struct {
|
||||
Role string `yaml:"role"`
|
||||
Image string `yaml:"image"`
|
||||
CPUs int `yaml:"cpus"`
|
||||
Memory int `yaml:"memory"`
|
||||
Segments []string `yaml:"segments"`
|
||||
Addresses map[string]string `yaml:"addresses"`
|
||||
Secondary map[string][]string `yaml:"secondary"`
|
||||
Loopback string `yaml:"loopback"`
|
||||
FRR string `yaml:"frr"`
|
||||
Release string `yaml:"release"`
|
||||
Role string `yaml:"role"`
|
||||
Image string `yaml:"image"`
|
||||
CPUs int `yaml:"cpus"`
|
||||
Memory int `yaml:"memory"`
|
||||
Segments []string `yaml:"segments"`
|
||||
Addresses map[string]string `yaml:"addresses"`
|
||||
}
|
||||
|
||||
type file struct {
|
||||
|
|
@ -92,11 +83,6 @@ func Load(path string) (*Topology, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
for i, n := range t.Nodes {
|
||||
if n.FRR != "" && !filepath.IsAbs(n.FRR) {
|
||||
t.Nodes[i].FRR = filepath.Join(filepath.Dir(path), n.FRR)
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
|
|
@ -140,10 +126,6 @@ func Parse(data []byte) (*Topology, error) {
|
|||
Memory: n.Memory,
|
||||
Segments: n.Segments,
|
||||
Addresses: n.Addresses,
|
||||
Secondary: n.Secondary,
|
||||
Loopback: n.Loopback,
|
||||
FRR: n.FRR,
|
||||
Release: n.Release,
|
||||
})
|
||||
}
|
||||
return t, nil
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@ const (
|
|||
MinPrefix = 8
|
||||
|
||||
ReservedInterface = "mgmt0"
|
||||
LoopbackInterface = "lo1"
|
||||
)
|
||||
|
||||
var (
|
||||
namePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{0,14}$`)
|
||||
segmentPattern = regexp.MustCompile(`^[a-z][a-z0-9]{0,11}$`)
|
||||
releasePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
|
||||
)
|
||||
|
||||
func (t *Topology) Validate() error {
|
||||
|
|
@ -70,9 +68,6 @@ func (t *Topology) Validate() error {
|
|||
if s.Name == ReservedInterface {
|
||||
add("segment %s: name is reserved for the administration interface", s.Name)
|
||||
}
|
||||
if s.Name == LoopbackInterface {
|
||||
add("segment %s: name is reserved for the loopback interface", s.Name)
|
||||
}
|
||||
sw, ok := nodes[s.Switch]
|
||||
switch {
|
||||
case s.Switch == "":
|
||||
|
|
@ -121,7 +116,6 @@ func (t *Topology) Validate() error {
|
|||
if n.Memory < MinMemory {
|
||||
add("node %s: memory must be at least %d MiB", n.Name, MinMemory)
|
||||
}
|
||||
validateExtras(n, segments, add)
|
||||
if n.Role == RoleSwitch {
|
||||
if len(n.Segments) > 0 || len(n.Addresses) > 0 {
|
||||
add("node %s: a switch carries its segments through segments.<name>.switch, not through segments or addresses", n.Name)
|
||||
|
|
@ -171,61 +165,6 @@ func (t *Topology) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateExtras(n Node, segments map[string]Segment, add func(string, ...any)) {
|
||||
carried := map[string]bool{}
|
||||
if n.Role == RoleSwitch {
|
||||
for name, s := range segments {
|
||||
if s.Switch == n.Name {
|
||||
carried[name] = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, name := range n.Segments {
|
||||
carried[name] = true
|
||||
}
|
||||
}
|
||||
names := make([]string, 0, len(n.Secondary))
|
||||
for name := range n.Secondary {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
if !carried[name] {
|
||||
add("node %s: secondary address given for segment %s it is not attached to", n.Name, name)
|
||||
continue
|
||||
}
|
||||
network, _ := netip.ParsePrefix(segments[name].CIDR)
|
||||
for _, raw := range n.Secondary[name] {
|
||||
prefix, err := netip.ParsePrefix(raw)
|
||||
switch {
|
||||
case err != nil:
|
||||
add("node %s: secondary address %q on %s: %v", n.Name, raw, name, err)
|
||||
case !prefix.Addr().Is4():
|
||||
add("node %s: secondary address %s on %s is not IPv4", n.Name, raw, name)
|
||||
case network.IsValid() && network.Contains(prefix.Addr()):
|
||||
add("node %s: secondary address %s is inside segment %s (%s), use addresses instead", n.Name, raw, name, network)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case n.Role == RoleHypervisor && n.Release == "":
|
||||
add("node %s: a hypervisor needs the release of two to deploy (release: <tag>)", n.Name)
|
||||
case n.Role == RoleHypervisor && !releasePattern.MatchString(n.Release):
|
||||
add("node %s: release %q must match %s", n.Name, n.Release, releasePattern)
|
||||
case n.Role != RoleHypervisor && n.Release != "":
|
||||
add("node %s: release is only for hypervisors", n.Name)
|
||||
}
|
||||
if n.Loopback != "" {
|
||||
prefix, err := netip.ParsePrefix(n.Loopback)
|
||||
switch {
|
||||
case err != nil:
|
||||
add("node %s: loopback %q: %v", n.Name, n.Loopback, err)
|
||||
case !prefix.Addr().Is4():
|
||||
add("node %s: loopback %s is not IPv4", n.Name, n.Loopback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]string) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
package scripts
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed deploy.sh
|
||||
var Deploy []byte
|
||||
|
||||
//go:embed bootstrap_kvm.sh
|
||||
var BootstrapKVM []byte
|
||||
|
|
@ -28,16 +28,14 @@ usage: ${0##*/} <commande> [arguments]
|
|||
|
||||
plan résout l'offre horaire, l'OS et les clés SSH, affiche la requête de création
|
||||
et le prix ; ne crée rien
|
||||
up crée le serveur de lab, attend la fin de son installation et son SSH,
|
||||
puis le prépare (voir prepare)
|
||||
up crée le serveur de lab, attend la fin de son installation et son SSH
|
||||
status liste les serveurs de lab du projet
|
||||
ssh [commande] se connecte au serveur de lab ; avec une commande, un terminal n'est demandé
|
||||
que si l'entrée standard en est un
|
||||
prepare installe sur le serveur ce dont lab a besoin (qemu, genisoimage), vérifie
|
||||
/dev/kvm et la virtualisation imbriquée ; lancé aussi par up
|
||||
push <topologie> compile cmd/lab pour linux/amd64 et dépose sur le serveur ~/lab et le
|
||||
répertoire de la topologie dans ~/topology/ (avec les fichiers qu'elle
|
||||
référence) ; ensuite : ssh './lab up topology/<topologie>'
|
||||
push <topologie> compile cmd/lab pour linux/amd64 et dépose sur le serveur ~/lab et
|
||||
~/<topologie> ; ensuite : ssh './lab up <topologie>'
|
||||
down supprime tous les serveurs de lab du projet et attend leur disparition
|
||||
session [cmd] up, puis la commande distante (ou un shell), puis down quoi qu'il arrive
|
||||
|
||||
|
|
@ -334,12 +332,6 @@ push_file () {
|
|||
ssh_run -- "cat > '${TARGET}.part' && chmod ${MODE} '${TARGET}.part' && mv '${TARGET}.part' '${TARGET}'" < "${SOURCE}"
|
||||
}
|
||||
|
||||
push_dir () {
|
||||
local SOURCE="${1}"
|
||||
COPYFILE_DISABLE=1 tar --no-xattrs -C "${SOURCE}" -cf - . \
|
||||
| ssh_run -- "rm -rf topology.part && mkdir topology.part && tar -C topology.part --no-same-owner -xf - && rm -rf topology && mv topology.part topology"
|
||||
}
|
||||
|
||||
cmd_push () {
|
||||
local TOPOLOGY="${1:-}"
|
||||
local NAME="${TOPOLOGY##*/}"
|
||||
|
|
@ -352,8 +344,8 @@ cmd_push () {
|
|||
(cd "${REPO_DIR}" && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "${BINARY}" ./cmd/lab) \
|
||||
|| die "compilation de lab échouée"
|
||||
push_file "${BINARY}" lab 755 || die "envoi de lab échoué"
|
||||
push_dir "$(dirname "${TOPOLOGY}")" || die "envoi du répertoire de ${NAME} échoué"
|
||||
info "déposés sur le serveur : ~/lab, ~/topology/ — ensuite : ${0##*/} ssh './lab up topology/${NAME}'"
|
||||
push_file "${TOPOLOGY}" "${NAME}" 644 || die "envoi de ${NAME} échoué"
|
||||
info "déposés sur le serveur : ~/lab, ~/${NAME} — ensuite : ${0##*/} ssh './lab up ${NAME}'"
|
||||
}
|
||||
|
||||
delete_server () {
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ case "$*" in
|
|||
*"bash -s"*)
|
||||
cat > "${FAKE_DIR}/prepare.sh"
|
||||
exit "${FAKE_PREPARE_RC:-0}" ;;
|
||||
*"cat > "*|*"-xf -"*)
|
||||
*"cat > "*)
|
||||
N=$(ls "${FAKE_DIR}" | grep -c '^pushed\.')
|
||||
cat > "${FAKE_DIR}/pushed.$(( N + 1 ))"
|
||||
exit "${FAKE_PUSH_RC:-0}" ;;
|
||||
|
|
@ -659,27 +659,18 @@ test_prepare_without_known_server_is_refused () {
|
|||
}
|
||||
|
||||
test_push_builds_for_linux_and_sends_binary_and_topology () {
|
||||
setup "push : compile lab pour linux/amd64, envoie le binaire et le répertoire de la topologie"
|
||||
setup "push : compile lab pour linux/amd64, envoie le binaire et la topologie"
|
||||
known_server
|
||||
mkdir -p "${WORK}/conf/frr"
|
||||
printf 'name: evpn-2hv\n' > "${WORK}/conf/evpn-2hv.yml"
|
||||
printf 'hostname rr1\n' > "${WORK}/conf/frr/rr1.conf"
|
||||
xattr -w com.apple.test lab "${WORK}/conf/evpn-2hv.yml" 2>/dev/null || true
|
||||
run_lab push "${WORK}/conf/evpn-2hv.yml" || fail "code de sortie $?"
|
||||
local REPO LISTING
|
||||
printf 'name: evpn-2hv\n' > "${WORK}/evpn-2hv.yml"
|
||||
run_lab push "${WORK}/evpn-2hv.yml" || fail "code de sortie $?"
|
||||
local REPO
|
||||
REPO="$(cd "$(dirname "${SCRIPT}")/.." && pwd)"
|
||||
[[ $(cat "${WORK}/go.log") == "${REPO}|build -o ${WORK}/.cache/two-lab/lab ./cmd/lab|CGO_ENABLED=0 GOOS=linux GOARCH=amd64" ]] \
|
||||
|| fail "compilation : $(cat "${WORK}/go.log")"
|
||||
grep -q "debian@203.0.113.7 cat > 'lab.part' && chmod 755 'lab.part' && mv 'lab.part' 'lab'" "${WORK}/ssh.log" || fail "envoi de lab absent"
|
||||
grep -q "debian@203.0.113.7 rm -rf topology.part && mkdir topology.part && tar -C topology.part --no-same-owner -xf - && rm -rf topology && mv topology.part topology" "${WORK}/ssh.log" \
|
||||
|| fail "envoi du répertoire absent"
|
||||
grep -q "debian@203.0.113.7 cat > 'evpn-2hv.yml.part' && chmod 644 'evpn-2hv.yml.part' && mv 'evpn-2hv.yml.part' 'evpn-2hv.yml'" "${WORK}/ssh.log" || fail "envoi de la topologie absent"
|
||||
[[ $(cat "${WORK}/pushed.1") == "binaire-lab" ]] || fail "contenu de lab : $(cat "${WORK}/pushed.1")"
|
||||
LISTING=$(tar -tf "${WORK}/pushed.2" | sed 's|^\./||' | grep -v '/$' | grep -v '^$' | sort | tr '\n' ' ')
|
||||
[[ "${LISTING}" == "evpn-2hv.yml frr/rr1.conf " ]] || fail "contenu de l'archive : ${LISTING}"
|
||||
grep -aq 'LIBARCHIVE.xattr' "${WORK}/pushed.2" && fail "attributs étendus macOS dans l'archive"
|
||||
mkdir "${WORK}/x" && tar -C "${WORK}/x" -xf "${WORK}/pushed.2"
|
||||
[[ $(cat "${WORK}/x/frr/rr1.conf") == "hostname rr1" ]] || fail "frr/rr1.conf altéré"
|
||||
grep -q "ssh './lab up topology/evpn-2hv.yml'" "${WORK}/out.log" || fail "consigne finale absente"
|
||||
[[ $(cat "${WORK}/pushed.2") == "name: evpn-2hv" ]] || fail "contenu de la topologie : $(cat "${WORK}/pushed.2")"
|
||||
teardown
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue