Files
monok8s/clitools/pkg/node/network.go

218 lines
5.6 KiB
Go

package node
import (
"context"
"fmt"
"net"
"os"
"strings"
"k8s.io/klog/v2"
system "undecided.project/monok8s/pkg/system"
)
type NetworkConfig struct {
Hostname string
MgmtIface string
MgmtAddress string
MgmtGateway string
DNSNameservers []string
DNSSearchDomains []string
}
func ConfigureMgmtInterface(cfg NetworkConfig) Step {
return func(ctx context.Context, nctx *NodeContext) error {
if strings.TrimSpace(cfg.MgmtIface) == "" {
return fmt.Errorf("mgmt interface is required")
}
if strings.TrimSpace(cfg.MgmtAddress) == "" {
return fmt.Errorf("mgmt address is required")
}
ip, ipNet, err := net.ParseCIDR(strings.TrimSpace(cfg.MgmtAddress))
if err != nil {
return fmt.Errorf("invalid mgmt address %q: %w", cfg.MgmtAddress, err)
}
ip4 := ip.To4()
if ip4 == nil {
return fmt.Errorf("mgmt address must be IPv4: %q", cfg.MgmtAddress)
}
wantIP := ip4.String()
wantCIDR := fmt.Sprintf("%s/%d", wantIP, maskSize(ipNet.Mask))
if gw := strings.TrimSpace(cfg.MgmtGateway); gw != "" {
gwIP := net.ParseIP(gw)
if gwIP == nil || gwIP.To4() == nil {
return fmt.Errorf("invalid mgmt gateway %q", gw)
}
}
if _, err := nctx.SystemRunner.Run(ctx, "ip", "link", "show", "dev", cfg.MgmtIface); err != nil {
return fmt.Errorf("interface not found: %s: %w", cfg.MgmtIface, err)
}
if _, err := nctx.SystemRunner.Run(ctx, "ip", "link", "set", "dev", cfg.MgmtIface, "up"); err != nil {
return fmt.Errorf("failed to bring up interface %s: %w", cfg.MgmtIface, err)
}
hasAddr, err := interfaceHasIPv4(ctx, nctx, cfg.MgmtIface, wantIP)
if err != nil {
return fmt.Errorf("failed checking existing address on %s: %w", cfg.MgmtIface, err)
}
if hasAddr {
klog.Infof("address already present on %s: %s", cfg.MgmtIface, wantCIDR)
} else {
if _, err := nctx.SystemRunner.Run(ctx, "ip", "addr", "add", wantCIDR, "dev", cfg.MgmtIface); err != nil {
return fmt.Errorf("failed assigning %s to %s: %w", wantCIDR, cfg.MgmtIface, err)
}
}
if gw := strings.TrimSpace(cfg.MgmtGateway); gw != "" {
if _, err := nctx.SystemRunner.Run(ctx, "ip", "route", "replace", "default", "via", gw, "dev", cfg.MgmtIface); err != nil {
return fmt.Errorf("failed setting default route via %s dev %s: %w", gw, cfg.MgmtIface, err)
}
}
return nil
}
}
func maskSize(m net.IPMask) int {
ones, _ := m.Size()
return ones
}
func EnsureIPForward(ctx context.Context, n *NodeContext) error {
return system.EnsureSysctl(ctx, n.SystemRunner, "net.ipv4.ip_forward", "1")
}
func ConfigureHostname(cfg NetworkConfig) Step {
return func(context.Context, *NodeContext) error {
want := strings.TrimSpace(cfg.Hostname)
if want == "" {
return fmt.Errorf("hostname is required")
}
current, err := os.Hostname()
if err != nil {
current = ""
}
if current == want {
return nil
}
if err := system.SetHostname(want); err != nil {
return fmt.Errorf("set hostname to %q: %w", want, err)
}
if err := os.WriteFile("/etc/hostname", []byte(want+"\n"), 0o644); err != nil {
return fmt.Errorf("write /etc/hostname: %w", err)
}
current, err = os.Hostname()
if err != nil {
current = ""
}
if current != want {
return fmt.Errorf("Unable to set hostname: %q", want)
}
return nil
}
}
func ConfigureDNS(cfg NetworkConfig) Step {
return func(context.Context, *NodeContext) error {
if len(cfg.DNSNameservers) == 0 {
return nil
}
var nameservers []string
for _, ns := range cfg.DNSNameservers {
ns = strings.TrimSpace(ns)
if ns == "" {
continue
}
if ip := net.ParseIP(ns); ip == nil {
return fmt.Errorf("invalid DNS nameserver %q", ns)
}
nameservers = append(nameservers, ns)
}
if len(nameservers) == 0 {
return fmt.Errorf("DNSNameservers is set but no valid nameservers were parsed")
}
var searchDomains []string
for _, d := range cfg.DNSSearchDomains {
d = strings.TrimSpace(d)
if d == "" {
continue
}
searchDomains = append(searchDomains, d)
}
var b strings.Builder
if len(searchDomains) > 0 {
b.WriteString("search ")
b.WriteString(strings.Join(searchDomains, " "))
b.WriteByte('\n')
}
for _, ns := range nameservers {
b.WriteString("nameserver ")
b.WriteString(ns)
b.WriteByte('\n')
}
b.WriteString("options timeout:2 attempts:3\n")
const (
resolvDir = "/etc"
tmpPath = "/etc/resolv.conf.monok8s.tmp"
resolvPath = "/etc/resolv.conf"
)
if err := os.MkdirAll(resolvDir, 0o755); err != nil {
klog.Warningf("failed to create %s for DNS config: %v; leaving %s unchanged", resolvDir, err, resolvPath)
return nil
}
if err := os.WriteFile(tmpPath, []byte(b.String()), 0o644); err != nil {
klog.Warningf("failed to write temporary DNS config %s: %v; leaving %s unchanged", tmpPath, err, resolvPath)
return nil
}
if err := os.Rename(tmpPath, resolvPath); err != nil {
_ = os.Remove(tmpPath)
klog.Warningf("failed to install DNS config at %s: %v; leaving existing DNS config unchanged", resolvPath, err)
return nil
}
return nil
}
}
func interfaceHasIPv4(ctx context.Context, nctx *NodeContext, iface, wantIP string) (bool, error) {
res, err := nctx.SystemRunner.Run(ctx, "ip", "-o", "-4", "addr", "show", "dev", iface)
if err != nil {
return false, err
}
for _, line := range strings.Split(res.Stdout, "\n") {
fields := strings.Fields(strings.TrimSpace(line))
for i := 0; i < len(fields)-1; i++ {
if fields[i] != "inet" {
continue
}
ip, _, err := net.ParseCIDR(fields[i+1])
if err == nil && ip.String() == wantIP {
return true, nil
}
}
}
return false, nil
}