290 lines
9.3 KiB
Go
290 lines
9.3 KiB
Go
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"`
|
|
}
|
|
|
|
type writeFile struct {
|
|
Path string `yaml:"path"`
|
|
Permissions string `yaml:"permissions"`
|
|
Encoding string `yaml:"encoding,omitempty"`
|
|
Content string `yaml:"content"`
|
|
}
|
|
|
|
type cloudConfig struct {
|
|
Hostname string `yaml:"hostname"`
|
|
SSHPwauth bool `yaml:"ssh_pwauth"`
|
|
DisableRoot bool `yaml:"disable_root"`
|
|
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys"`
|
|
Packages []string `yaml:"packages,omitempty"`
|
|
WriteFiles []writeFile `yaml:"write_files,omitempty"`
|
|
Runcmd [][]string `yaml:"runcmd,omitempty"`
|
|
}
|
|
|
|
type match struct {
|
|
MACAddress string `yaml:"macaddress"`
|
|
}
|
|
|
|
type route struct {
|
|
To string `yaml:"to"`
|
|
Via string `yaml:"via"`
|
|
}
|
|
|
|
type nameservers struct {
|
|
Addresses []string `yaml:"addresses"`
|
|
}
|
|
|
|
type ethernet struct {
|
|
Match match `yaml:"match"`
|
|
SetName string `yaml:"set-name"`
|
|
DHCP4 bool `yaml:"dhcp4"`
|
|
MTU int `yaml:"mtu,omitempty"`
|
|
Addresses []string `yaml:"addresses,omitempty"`
|
|
Routes []route `yaml:"routes,omitempty"`
|
|
Nameservers *nameservers `yaml:"nameservers,omitempty"`
|
|
}
|
|
|
|
type networkDoc struct {
|
|
Version int `yaml:"version"`
|
|
Ethernets map[string]ethernet `yaml:"ethernets"`
|
|
}
|
|
|
|
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) {
|
|
cfg := cloudConfig{
|
|
Hostname: n.Name,
|
|
SSHPwauth: false,
|
|
DisableRoot: true,
|
|
SSHAuthorizedKeys: o.AuthorizedKeys,
|
|
}
|
|
var steps []string
|
|
switch {
|
|
case n.Role == topology.RoleSwitch:
|
|
cfg.Packages = []string{"nftables"}
|
|
cfg.WriteFiles = []writeFile{
|
|
{Path: SwitchScript, Permissions: "0755", Content: switchScript(p, n)},
|
|
{Path: SwitchNFT, Permissions: "0644", Content: switchNFT(p, n.Name)},
|
|
{Path: SwitchUnit, Permissions: "0644", Content: unit("Lab switch: bridges, gateways and NAT", SwitchScript)},
|
|
}
|
|
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)},
|
|
}
|
|
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 {
|
|
return nil, err
|
|
}
|
|
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{
|
|
Match: match{MACAddress: adminMAC(index).String()},
|
|
SetName: AdminInterface,
|
|
Addresses: []string{AdminAddress},
|
|
}
|
|
if n.Role == topology.RoleSwitch {
|
|
admin.Routes = []route{{To: "0.0.0.0/0", Via: AdminGateway}}
|
|
admin.Nameservers = &nameservers{Addresses: []string{AdminDNS}}
|
|
for _, c := range switchCables(p, n.Name) {
|
|
doc.Ethernets[c.SwitchInterface] = ethernet{
|
|
Match: match{MACAddress: c.SwitchMAC.String()},
|
|
SetName: c.SwitchInterface,
|
|
MTU: c.MTU,
|
|
}
|
|
}
|
|
}
|
|
doc.Ethernets[AdminInterface] = admin
|
|
|
|
for i, c := range nodeCables(p, n.Name) {
|
|
e := ethernet{
|
|
Match: match{MACAddress: c.NodeMAC.String()},
|
|
SetName: c.NodeInterface,
|
|
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}
|
|
}
|
|
doc.Ethernets[c.NodeInterface] = e
|
|
}
|
|
return yaml.Marshal(doc)
|
|
}
|
|
|
|
func gatewayOf(p *topology.Plan, segment string) string {
|
|
for _, s := range p.Segments {
|
|
if s.Name == segment {
|
|
return s.Gateway.String()
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
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) {
|
|
fmt.Fprintf(&b, "ip link add %s type bridge stp_state 0 2>/dev/null || true\n", s.Bridge)
|
|
for _, c := range switchCables(p, name) {
|
|
if c.Segment != s.Name {
|
|
continue
|
|
}
|
|
fmt.Fprintf(&b, "ip link set dev %s master %s\n", c.SwitchInterface, s.Bridge)
|
|
fmt.Fprintf(&b, "ip link set dev %s up\n", c.SwitchInterface)
|
|
}
|
|
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()
|
|
}
|
|
|
|
func switchNFT(p *topology.Plan, name string) string {
|
|
var networks []string
|
|
for _, s := range switchSegments(p, name) {
|
|
networks = append(networks, s.Network.String())
|
|
}
|
|
return fmt.Sprintf(`add table ip lab_nat
|
|
delete table ip lab_nat
|
|
table ip lab_nat {
|
|
chain postrouting {
|
|
type nat hook postrouting priority srcnat; policy accept;
|
|
ip saddr { %s } oifname "%s" masquerade
|
|
}
|
|
}
|
|
`, strings.Join(networks, ", "), AdminInterface)
|
|
}
|
|
|
|
func unit(description, script string) string {
|
|
return fmt.Sprintf(`[Unit]
|
|
Description=%s
|
|
Wants=network-online.target
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
ExecStart=%s
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`, description, script)
|
|
}
|