f-50: lab: rôles des nœuds, adresses du lien, loopback et installation de FRR #50
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
32940c57b4
commit
b348652995
21 changed files with 962 additions and 58 deletions
BIN
internal/lab/render/frrouting.gpg
Normal file
BIN
internal/lab/render/frrouting.gpg
Normal file
Binary file not shown.
|
|
@ -30,6 +30,7 @@ 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 {
|
||||
|
|
@ -45,6 +46,11 @@ 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)
|
||||
|
|
@ -52,7 +58,7 @@ func Render(p *topology.Plan, o Options) ([]Node, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := userData(p, n, o.AuthorizedKeys)
|
||||
user, err := userData(p, n, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
188
internal/lab/render/roles_test.go
Normal file
188
internal/lab/render/roles_test.go
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"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 }
|
||||
hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] }
|
||||
`
|
||||
|
||||
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)
|
||||
}
|
||||
wantCmds := []string{"systemctl daemon-reload", "systemctl enable --now lab-node.service", "/usr/local/sbin/lab-frr"}
|
||||
if got := runcmd(t, rr1); !reflect.DeepEqual(got, wantCmds) {
|
||||
t.Errorf("runcmd = %q, want %q", got, wantCmds)
|
||||
}
|
||||
}
|
||||
|
||||
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_FRRInstallRunsLast(t *testing.T) {
|
||||
nodes := renderRoles(t)
|
||||
for name, want := range map[string][]string{
|
||||
"sw1": {"systemctl daemon-reload", "systemctl enable --now lab-switch.service", "/usr/local/sbin/lab-frr"},
|
||||
"hv1": {"/usr/local/sbin/lab-frr"},
|
||||
} {
|
||||
if got := runcmd(t, nodeNamed(t, nodes, name)); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s runcmd = %q, want %q", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoles_NodeWithoutRoleFieldsGetsNothingExtra(t *testing.T) {
|
||||
cfg := user(t, nodeNamed(t, renderRoles(t), "hv2"))
|
||||
if len(cfg.WriteFiles) != 0 || len(cfg.Runcmd) != 0 {
|
||||
t.Errorf("hv2 write_files %d, runcmd %q", len(cfg.WriteFiles), cfg.Runcmd)
|
||||
}
|
||||
}
|
||||
|
||||
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,6 +1,8 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
|
@ -13,8 +15,21 @@ 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"
|
||||
)
|
||||
|
||||
//go:embed frrouting.gpg
|
||||
var frrKey []byte
|
||||
|
||||
type metaDoc struct {
|
||||
InstanceID string `yaml:"instance-id"`
|
||||
LocalHostname string `yaml:"local-hostname"`
|
||||
|
|
@ -23,6 +38,7 @@ type metaDoc struct {
|
|||
type writeFile struct {
|
||||
Path string `yaml:"path"`
|
||||
Permissions string `yaml:"permissions"`
|
||||
Encoding string `yaml:"encoding,omitempty"`
|
||||
Content string `yaml:"content"`
|
||||
}
|
||||
|
||||
|
|
@ -68,24 +84,42 @@ 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, keys []string) ([]byte, error) {
|
||||
func userData(p *topology.Plan, n topology.NodePlan, o Options) ([]byte, error) {
|
||||
cfg := cloudConfig{
|
||||
Hostname: n.Name,
|
||||
SSHPwauth: false,
|
||||
DisableRoot: true,
|
||||
SSHAuthorizedKeys: keys,
|
||||
SSHAuthorizedKeys: o.AuthorizedKeys,
|
||||
}
|
||||
if n.Role == topology.RoleSwitch {
|
||||
switch {
|
||||
case n.Role == topology.RoleSwitch:
|
||||
cfg.Packages = []string{"nftables"}
|
||||
cfg.WriteFiles = []writeFile{
|
||||
{Path: SwitchScript, Permissions: "0755", Content: switchScript(p, n.Name)},
|
||||
{Path: SwitchScript, Permissions: "0755", Content: switchScript(p, n)},
|
||||
{Path: SwitchNFT, Permissions: "0644", Content: switchNFT(p, n.Name)},
|
||||
{Path: SwitchUnit, Permissions: "0644", Content: switchUnit()},
|
||||
{Path: SwitchUnit, Permissions: "0644", Content: unit("Lab switch: bridges, gateways and NAT", SwitchScript)},
|
||||
}
|
||||
cfg.Runcmd = [][]string{
|
||||
{"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-node.service"},
|
||||
}
|
||||
}
|
||||
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)},
|
||||
)
|
||||
cfg.Runcmd = append(cfg.Runcmd, []string{FRRScript})
|
||||
}
|
||||
body, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
|
|
@ -94,6 +128,35 @@ func userData(p *topology.Plan, n topology.NodePlan, keys []string) ([]byte, err
|
|||
return append([]byte("#cloud-config\n"), body...), nil
|
||||
}
|
||||
|
||||
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("n=0\nuntil apt-get update -qq --error-on=any; do\n n=$((n + 1))\n [ \"$n\" -lt 30 ] || exit 1\n sleep 10\ndone\n")
|
||||
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{
|
||||
|
|
@ -121,6 +184,9 @@ 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}
|
||||
|
|
@ -139,7 +205,8 @@ func gatewayOf(p *topology.Plan, segment string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func switchScript(p *topology.Plan, name string) string {
|
||||
func switchScript(p *topology.Plan, n topology.NodePlan) string {
|
||||
name := n.Name
|
||||
var b strings.Builder
|
||||
b.WriteString("#!/bin/sh\nset -eu\nsysctl -qw net.ipv4.ip_forward=1\n")
|
||||
for _, s := range switchSegments(p, name) {
|
||||
|
|
@ -153,8 +220,12 @@ func switchScript(p *topology.Plan, name string) 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()
|
||||
}
|
||||
|
|
@ -175,9 +246,9 @@ table ip lab_nat {
|
|||
`, strings.Join(networks, ", "), AdminInterface)
|
||||
}
|
||||
|
||||
func switchUnit() string {
|
||||
func unit(description, script string) string {
|
||||
return fmt.Sprintf(`[Unit]
|
||||
Description=Lab switch: bridges, gateways and NAT
|
||||
Description=%s
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
|
|
@ -188,5 +259,5 @@ ExecStart=%s
|
|||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, SwitchScript)
|
||||
`, description, script)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue