Implement a lightweight cloud-init system for first-boot configuration: - Go parser for YAML config (hostname, network, KubeSolo settings) - Static/DHCP network modes with DNS override - KubeSolo extra flags and API server SAN configuration - Portainer Edge Agent and air-gapped deployment support - New init stage 45-cloud-init.sh runs before network/hostname stages - Stages 50/60 skip gracefully when cloud-init has already applied - Build script compiles static Linux/amd64 binary (~2.7 MB) - 17 unit tests covering parsing, validation, and example files - Full documentation at docs/cloud-init.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
4.9 KiB
Go
175 lines
4.9 KiB
Go
package cloudinit
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// ApplyNetwork configures the network interface based on cloud-init config.
|
|
// For static mode, it sets the IP, gateway, and DNS directly.
|
|
// For DHCP mode, it runs udhcpc on the target interface.
|
|
func ApplyNetwork(cfg *Config) error {
|
|
iface := cfg.Network.Interface
|
|
if iface == "" {
|
|
var err error
|
|
iface, err = detectPrimaryInterface()
|
|
if err != nil {
|
|
return fmt.Errorf("detecting primary interface: %w", err)
|
|
}
|
|
}
|
|
|
|
slog.Info("configuring network", "interface", iface, "mode", cfg.Network.Mode)
|
|
|
|
// Bring up the interface
|
|
if err := run("ip", "link", "set", iface, "up"); err != nil {
|
|
return fmt.Errorf("bringing up %s: %w", iface, err)
|
|
}
|
|
|
|
switch cfg.Network.Mode {
|
|
case "static":
|
|
return applyStatic(iface, cfg)
|
|
case "dhcp", "":
|
|
return applyDHCP(iface, cfg)
|
|
default:
|
|
return fmt.Errorf("unknown network mode: %s", cfg.Network.Mode)
|
|
}
|
|
}
|
|
|
|
func applyStatic(iface string, cfg *Config) error {
|
|
// Set IP address
|
|
if err := run("ip", "addr", "add", cfg.Network.Address, "dev", iface); err != nil {
|
|
return fmt.Errorf("setting address %s on %s: %w", cfg.Network.Address, iface, err)
|
|
}
|
|
|
|
// Set default gateway
|
|
if err := run("ip", "route", "add", "default", "via", cfg.Network.Gateway, "dev", iface); err != nil {
|
|
return fmt.Errorf("setting gateway %s: %w", cfg.Network.Gateway, err)
|
|
}
|
|
|
|
// Write DNS configuration
|
|
if len(cfg.Network.DNS) > 0 {
|
|
if err := writeDNS(cfg.Network.DNS); err != nil {
|
|
return fmt.Errorf("writing DNS config: %w", err)
|
|
}
|
|
}
|
|
|
|
slog.Info("static network configured",
|
|
"interface", iface,
|
|
"address", cfg.Network.Address,
|
|
"gateway", cfg.Network.Gateway,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func applyDHCP(iface string, cfg *Config) error {
|
|
// Try udhcpc (BusyBox), then dhcpcd
|
|
if path, err := exec.LookPath("udhcpc"); err == nil {
|
|
args := []string{"-i", iface, "-s", "/usr/share/udhcpc/default.script",
|
|
"-t", "10", "-T", "3", "-A", "5", "-b", "-q"}
|
|
if err := run(path, args...); err != nil {
|
|
return fmt.Errorf("udhcpc on %s: %w", iface, err)
|
|
}
|
|
} else if path, err := exec.LookPath("dhcpcd"); err == nil {
|
|
if err := run(path, iface); err != nil {
|
|
return fmt.Errorf("dhcpcd on %s: %w", iface, err)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("no DHCP client available (need udhcpc or dhcpcd)")
|
|
}
|
|
|
|
// Override DNS if specified in config
|
|
if len(cfg.Network.DNS) > 0 {
|
|
if err := writeDNS(cfg.Network.DNS); err != nil {
|
|
return fmt.Errorf("writing DNS config: %w", err)
|
|
}
|
|
}
|
|
|
|
slog.Info("DHCP network configured", "interface", iface)
|
|
return nil
|
|
}
|
|
|
|
// SaveNetworkConfig writes a shell script that restores the current network
|
|
// config on subsequent boots (before cloud-init runs).
|
|
func SaveNetworkConfig(cfg *Config, destDir string) error {
|
|
iface := cfg.Network.Interface
|
|
if iface == "" {
|
|
var err error
|
|
iface, err = detectPrimaryInterface()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(destDir, 0o755); err != nil {
|
|
return fmt.Errorf("creating network config dir: %w", err)
|
|
}
|
|
|
|
dest := filepath.Join(destDir, "interfaces.sh")
|
|
var sb strings.Builder
|
|
sb.WriteString("#!/bin/sh\n")
|
|
sb.WriteString("# Auto-generated by KubeSolo OS cloud-init\n")
|
|
sb.WriteString(fmt.Sprintf("ip link set %s up\n", iface))
|
|
|
|
switch cfg.Network.Mode {
|
|
case "static":
|
|
sb.WriteString(fmt.Sprintf("ip addr add %s dev %s\n", cfg.Network.Address, iface))
|
|
sb.WriteString(fmt.Sprintf("ip route add default via %s dev %s\n", cfg.Network.Gateway, iface))
|
|
if len(cfg.Network.DNS) > 0 {
|
|
sb.WriteString(": > /etc/resolv.conf\n")
|
|
for _, ns := range cfg.Network.DNS {
|
|
sb.WriteString(fmt.Sprintf("echo 'nameserver %s' >> /etc/resolv.conf\n", ns))
|
|
}
|
|
}
|
|
case "dhcp", "":
|
|
sb.WriteString("udhcpc -i " + iface + " -s /usr/share/udhcpc/default.script -t 10 -T 3 -A 5 -b -q 2>/dev/null\n")
|
|
}
|
|
|
|
if err := os.WriteFile(dest, []byte(sb.String()), 0o755); err != nil {
|
|
return fmt.Errorf("writing network config: %w", err)
|
|
}
|
|
|
|
slog.Info("network config saved", "path", dest)
|
|
return nil
|
|
}
|
|
|
|
func writeDNS(servers []string) error {
|
|
var sb strings.Builder
|
|
for _, ns := range servers {
|
|
sb.WriteString("nameserver " + ns + "\n")
|
|
}
|
|
return os.WriteFile("/etc/resolv.conf", []byte(sb.String()), 0o644)
|
|
}
|
|
|
|
func detectPrimaryInterface() (string, error) {
|
|
entries, err := os.ReadDir("/sys/class/net")
|
|
if err != nil {
|
|
return "", fmt.Errorf("reading /sys/class/net: %w", err)
|
|
}
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
switch {
|
|
case name == "lo",
|
|
strings.HasPrefix(name, "docker"),
|
|
strings.HasPrefix(name, "veth"),
|
|
strings.HasPrefix(name, "br"),
|
|
strings.HasPrefix(name, "cni"),
|
|
strings.HasPrefix(name, "flannel"),
|
|
strings.HasPrefix(name, "cali"):
|
|
continue
|
|
}
|
|
return name, nil
|
|
}
|
|
return "", fmt.Errorf("no suitable network interface found")
|
|
}
|
|
|
|
func run(name string, args ...string) error {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|