go-network-configurator/cloudinit.go
2026-07-01 15:17:11 -05:00

1255 lines
34 KiB
Go

package netconfig
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"path"
"sort"
"strings"
"time"
"dario.cat/mergo"
"github.com/vishvananda/netlink"
"gopkg.in/yaml.v3"
)
const (
cloudInitDefault = "/etc/cloud/cloud.cfg"
cloudInitDir = "/etc/cloud/cloud.cfg.d"
cloudBaseInit = "/curtin/network.json"
)
// Name server cvonfigurations.
type ciNameservers struct {
Search []string `yaml:"search,omitempty,flow" json:"search,omitempty"`
Addresses []string `yaml:"addresses,omitempty,flow" json:"addresses,omitempty"`
}
// Common route configs.
type ciRoute struct {
To string `yaml:"to,omitempty" json:"to,omitempty"`
Via string `yaml:"via,omitempty" json:"via,omitempty"`
Metric *int `yaml:"metric,omitempty" json:"metric,omitempty"`
}
// Match options.
type ciMatch struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
MACAddress string `yaml:"macaddress,omitempty" json:"macaddress,omitempty"`
Driver string `yaml:"driver,omitempty" json:"driver,omitempty"`
}
// Common configurations on physical interfaces.
type ciPhysical struct {
Match ciMatch `yaml:"match,omitempty" json:"match,omitempty"`
SetName string `yaml:"set-name,omitempty" json:"set-name,omitempty"`
Wakeonlan bool `yaml:"wakeonlan,omitempty" json:"wakeonlan,omitempty"`
}
// Common cloudinit interface configurations.
type ciInterface struct {
Addresses []string `yaml:"addresses,omitempty" json:"addresses,omitempty"`
DHCP4 *bool `yaml:"dhcp4,omitempty" json:"dhcp4,omitempty"`
DHCP6 *bool `yaml:"dhcp6,omitempty" json:"dhcp6,omitempty"`
DHCP4Overrides map[string]any `yaml:"dhcp4-overrides,omitempty" json:"dhcp4-overrides,omitempty"`
DHCP6Overrides map[string]any `yaml:"dhcp6-overrides,omitempty" json:"dhcp6-overrides,omitempty"`
Gateway4 string `yaml:"gateway4,omitempty" json:"gateway4,omitempty"`
Gateway6 string `yaml:"gateway6,omitempty" json:"gateway6,omitempty"`
Nameservers ciNameservers `yaml:"nameservers,omitempty" json:"nameservers,omitempty"`
MTU int `yaml:"mtu,omitempty" json:"mtu,omitempty"`
Renderer string `yaml:"renderer,omitempty" json:"renderer,omitempty"`
Routes []ciRoute `yaml:"routes,omitempty" json:"routes,omitempty"`
Optional bool `yaml:"optional,omitempty" json:"optional,omitempty"`
}
// Physical ethernet interface.
type ciEthernet struct {
ciPhysical `yaml:",inline" json:",inline"`
ciInterface `yaml:",inline" json:",inline"`
}
// Bridge interface.
type ciBridgeParameters struct {
AgeingTime *int `yaml:"ageing-time,omitempty" json:"ageing-time,omitempty"`
ForwardDelay intString `yaml:"forward-delay,omitempty" json:"forward-delay,omitempty"`
HelloTime *int `yaml:"hello-time,omitempty" json:"hello-time,omitempty"`
MaxAge *int `yaml:"max-age,omitempty" json:"max-age,omitempty"`
PathCost *int `yaml:"path-cost,omitempty" json:"path-cost,omitempty"`
Priority *int `yaml:"priority,omitempty" json:"priority,omitempty"`
STP *bool `yaml:"stp,omitempty" json:"stp,omitempty"`
}
type ciBridge struct {
Interfaces []string `yaml:"interfaces,omitempty,flow" json:"interfaces,omitempty"`
ciInterface `yaml:",inline" json:",inline"`
Parameters ciBridgeParameters `yaml:"parameters,omitempty" json:"parameters,omitempty"`
}
// Bond interfaces.
type ciBondParameters struct {
Mode intString `yaml:"mode,omitempty" json:"mode,omitempty"`
LACPRate intString `yaml:"lacp-rate,omitempty" json:"lacp-rate,omitempty"`
MIIMonitorInterval intString `yaml:"mii-monitor-interval,omitempty" json:"mii-monitor-interval,omitempty"`
MinLinks *int `yaml:"min-links,omitempty" json:"min-links,omitempty"`
TransmitHashPolicy string `yaml:"transmit-hash-policy,omitempty" json:"transmit-hash-policy,omitempty"`
ADSelect intString `yaml:"ad-select,omitempty" json:"ad-select,omitempty"`
AllSlavesActive *bool `yaml:"all-slaves-active,omitempty" json:"all-slaves-active,omitempty"`
ARPInterval *int `yaml:"arp-interval,omitempty" json:"arp-interval,omitempty"`
ARPIPTargets []string `yaml:"arp-ip-targets,omitempty" json:"arp-ip-targets,omitempty"`
ARPValidate intString `yaml:"arp-validate,omitempty" json:"arp-validate,omitempty"`
ARPAllTargets intString `yaml:"arp-all-targets,omitempty" json:"arp-all-targets,omitempty"`
UpDelay intString `yaml:"up-delay,omitempty" json:"up-delay,omitempty"`
DownDelay intString `yaml:"down-delay,omitempty" json:"down-delay,omitempty"`
FailOverMACPolicy intString `yaml:"fail-over-mac-policy,omitempty" json:"fail-over-mac-policy,omitempty"`
GratuitousARP *int `yaml:"gratuitous-arp,omitempty" json:"gratuitous-arp,omitempty"`
PacketsPerSlave *int `yaml:"packets-per-slave,omitempty" json:"packets-per-slave,omitempty"`
PrimaryReselectPolicy intString `yaml:"primary-reselect-policy,omitempty" json:"primary-reselect-policy,omitempty"`
LearnPacketInterval *int `yaml:"learn-packet-interval,omitempty" json:"learn-packet-interval,omitempty"`
}
type ciBond struct {
Interfaces []string `yaml:"interfaces,omitempty,flow" json:"interfaces,omitempty"`
ciInterface `yaml:",inline" json:",inline"`
Parameters ciBondParameters `yaml:"parameters,omitempty" json:"parameters,omitempty"`
}
// VLAN interface.
type ciVLAN struct {
Id *int `yaml:"id,omitempty" json:"id,omitempty"`
Link string `yaml:"link,omitempty" json:"link,omitempty"`
ciInterface `yaml:",inline" json:",inline"`
}
// Cloud-init network version 1 config.
type ciSubnetRoute struct {
Destination string `yaml:"destination,omitempty" json:"destination,omitempty"`
Netmask string `yaml:"netmask,omitempty" json:"netmask,omitempty"`
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
Metric int `yaml:"metric,omitempty" json:"metric,omitempty"`
}
type ciSubnet struct {
Type string `yaml:"type" json:"type"`
Control intString `yaml:"control,omitempty" json:"control,omitempty"`
Address string `yaml:"address,omitempty" json:"address,omitempty"`
Netmask string `yaml:"netmask,omitempty" json:"netmask,omitempty"`
Broadcast string `yaml:"broadcast,omitempty" json:"broadcast,omitempty"`
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
DNSNameservers []string `yaml:"dns_nameservers,omitempty" json:"dns_nameservers,omitempty"`
DNSSearch []string `yaml:"dns_search,omitempty" json:"dns_search,omitempty"`
Routes []ciSubnetRoute `yaml:"routes,omitempty" json:"routes,omitempty"`
}
type ciConfig struct {
// Common options.
Type string `yaml:"type" json:"type"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
MACAddress string `yaml:"mac_address,omitempty" json:"mac_address,omitempty"`
MTU int `yaml:"mtu,omitempty" json:"mtu,omitempty"`
AcceptRA *bool `yaml:"accept-ra,omitempty" json:"accept-ra,omitempty"`
Subnets []ciSubnet `yaml:"subnets,omitempty" json:"subnets,omitempty"`
// Bridge and bond options.
BondInterfaces []string `yaml:"bond_interfaces,omitempty,flow" json:"bond_interfaces,omitempty"`
BridgeInterfaces []string `yaml:"bridge_interfaces,omitempty,flow" json:"bridge_interfaces,omitempty"`
Params map[string]string `yaml:"params,omitempty" json:"params,omitempty"`
// VLAN options.
VLANLink string `yaml:"vlan_link,omitempty" json:"vlan_link,omitempty"`
VLANID int `yaml:"vlan_id,omitempty" json:"vlan_id,omitempty"`
// Nameserver options.
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
Addresses []string `yaml:"address,omitempty" json:"address,omitempty"`
Search []string `yaml:"search,omitempty" json:"search,omitempty"`
// Route options.
Destination string `yaml:"destination,omitempty" json:"destination,omitempty"`
Netmask string `yaml:"netmask,omitempty" json:"netmask,omitempty"`
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
Metric int `yaml:"metric,omitempty" json:"metric,omitempty"`
}
// Common network structure for both v1 and v2.
type ciNetwork struct {
Version int `yaml:"version" json:"version"`
Renderer string `yaml:"renderer,omitempty" json:"renderer,omitempty"`
Ethernets map[string]ciEthernet `yaml:"ethernets,omitempty" json:"ethernets,omitempty"`
Bridges map[string]ciBridge `yaml:"bridges,omitempty" json:"bridges,omitempty"`
Bonds map[string]ciBond `yaml:"bonds,omitempty" json:"bonds,omitempty"`
VLANs map[string]ciVLAN `yaml:"vlans,omitempty" json:"vlans,omitempty"`
Configs []ciConfig `yaml:"config,omitempty" json:"config,omitempty"`
}
// The default netplan configuration structure and interface to configuring netplan.
type ciFile struct {
Path string
OnlyNetwork bool
NetworkEncap bool
}
type cloudInit struct {
Network ciNetwork `yaml:"network" json:"network"`
configFiles []ciFile `yaml:"-" json:"-"`
backupRetention int `yaml:"-" json:"-"`
}
// Find cloud-init and cloudbase-init network configurations.
func newCloudInit(backupRetention int) (ci *cloudInit, err error) {
ci, err = newCloudInitWith(cloudInitDefault, cloudBaseInit, cloudInitDir)
if ci != nil {
ci.backupRetention = backupRetention
}
return
}
func newCloudInitWith(cloudinitFile, cloudbaseFile, cloudinitDir string) (ci *cloudInit, err error) {
// Read data.
var mergedConfig map[string]any
var configFiles []ciFile
var files []string
// Check which base file exists.
if _, serr := os.Stat(cloudinitFile); serr == nil {
files = append(files, cloudinitFile)
} else if _, serr := os.Stat(cloudbaseFile); serr == nil {
files = append(files, cloudbaseFile)
}
// No files, end.
if len(files) == 0 {
return nil, fmt.Errorf("unable to find cloudinit config")
}
// Try to read the directory files.
unsortedEntries, _ := os.ReadDir(cloudinitDir)
entries := sortableDirEntries(unsortedEntries)
sort.Sort(entries)
// Add found files to list.
for _, entry := range entries {
// If not an cfg file, skip.
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".cfg") {
continue
}
files = append(files, path.Join(cloudinitDir, entry.Name()))
}
// Parse config files.
for _, configFile := range files {
var data []byte
data, err = os.ReadFile(configFile)
if err != nil {
return
}
// Setup cloud-init file info.
var file ciFile
file.Path = configFile
// Parse the yaml to an map.
var config map[string]any
if strings.HasSuffix(configFile, ".json") {
err = json.Unmarshal(data, &config)
configS, ok := config["config"]
if ok {
version, ok := config["version"]
if ok {
network := make(map[string]any)
network["config"] = configS
network["version"] = version
config = make(map[string]any)
config["network"] = network
file.NetworkEncap = true
}
}
} else {
err = yaml.Unmarshal(data, &config)
}
if err != nil {
return
}
// Determine if this config only contains network configs.
foundNetwork := false
foundNonNetwork := false
for key, value := range config {
if key == "network" {
// This is a network config, unless the network config
// only contains the config disabled option.
foundNetwork = true
configS, ok := value.(map[string]any)
if ok {
// We found disabled if the config key is a string
// with disabled.
foundDisable := false
configV, ok := configS["config"].(string)
if ok && configV == "disabled" {
foundDisable = true
}
// If there are other configs that are not
// the config key, there are other configs to
// parse.
for keyV := range configS {
if keyV != "config" {
foundDisable = false
}
}
// If we only found a disabled config, no network
// config exists here.
if foundDisable {
foundNetwork = false
}
}
} else {
foundNonNetwork = true
}
}
// If there are no network configs in this file, skip it.
if !foundNetwork {
continue
}
// If there are network configs, determine if its only network
// and add to list of config files.
file.OnlyNetwork = !foundNonNetwork
configFiles = append(configFiles, file)
// If this is the first config file read, don't merge.
if mergedConfig == nil {
mergedConfig = config
continue
}
// Merge the configurations.
err = mergo.Merge(&mergedConfig, config)
if err != nil {
return
}
}
// Convert merged configs back into yaml so we can re-parse into a struct.
mergedData, err := yaml.Marshal(mergedConfig)
if err != nil {
return
}
// Parse the cloud-init config.
ci = new(cloudInit)
err = yaml.Unmarshal(mergedData, ci)
if err != nil {
return nil, err
}
// If there are no ethernet configurations or configs in cloud-init, we consider the cloud-init configurations to not exist.
if len(ci.Network.Ethernets) == 0 && len(ci.Network.Configs) == 0 {
return nil, fmt.Errorf("no ethernet configurations in cloud-init")
}
// Successfully parsed, we should return.
ci.configFiles = configFiles
return
}
// Convert cloud-init interface to a netconfig Interface.
func (*cloudInit) ConvertInterface(name string, MAC string, config ciInterface, links []netlink.Link) *Interface {
// Find the MAC address, starting with the list of interfaces
// on the machine.
var mac net.HardwareAddr
var foundLink netlink.Link
for _, link := range links {
if link.Attrs().Name == name {
mac = link.Attrs().HardwareAddr
foundLink = link
}
}
// If the MAC was not found, try parsing the config.
if mac == nil && MAC != "" {
mac, _ = net.ParseMAC(MAC)
}
// Setup interface with the name and MAC.
i := new(Interface)
i.Name = name
i.MAC = mac
i.Link = foundLink
// Parse addresses.
for _, addr := range config.Addresses {
ip, ipnet, err := net.ParseCIDR(addr)
if err != nil {
continue
}
ipnet.IP = ip
i.Addresses = append(i.Addresses, ipnet)
}
// Add gateways.
if config.Gateway4 != "" {
i.Gateway4 = net.ParseIP(config.Gateway4)
}
if config.Gateway6 != "" {
i.Gateway6 = net.ParseIP(config.Gateway6)
}
// Parse nameservers.
for _, addr := range config.Nameservers.Addresses {
if ip := net.ParseIP(addr); ip != nil {
i.DNS = append(i.DNS, ip)
}
}
i.SearchDomains = config.Nameservers.Search
// Parse routes.
for _, route := range config.Routes {
dstIP, dst, err := net.ParseCIDR(route.To)
if err != nil {
continue
}
dst.IP = dstIP
ip := net.ParseIP(route.Via)
r := new(Route)
r.Destination = dst
r.Gateway = ip
if route.Metric != nil {
r.Metric = *route.Metric
} else {
r.Metric = 256
}
i.Routes = append(i.Routes, r)
}
return i
}
// Get interfaces configured.
func (ci *cloudInit) GetInterfaces() (interfaces []*Interface, err error) {
// Connect to netlink.
var h *netlink.Handle
h, err = netlink.NewHandle()
if err != nil {
return
}
defer h.Close()
// Get list of interfaces to match with MAC address.
var links []netlink.Link
links, err = h.LinkList()
if err != nil {
return
}
// Add configs.
for _, config := range ci.Network.Configs {
// Skip types we cannot parse.
switch config.Type {
case "physical", "bond", "bridge", "vlan":
default:
continue
}
// Find the MAC address, starting with the list of interfaces
// on the machine.
var mac net.HardwareAddr
var foundLink netlink.Link
for _, link := range links {
if link.Attrs().Name == config.Name {
mac = link.Attrs().HardwareAddr
foundLink = link
}
}
// If we did not find the MAC address, but one is defined. Parse it.
if mac == nil && config.MACAddress != "" {
mac, _ = net.ParseMAC(config.MACAddress)
}
// Setup interface with the name and MAC.
i := new(Interface)
i.Name = config.Name
i.MAC = mac
i.Link = foundLink
// Parse subnet configs.
for _, subnet := range config.Subnets {
// Add DNS servers and search domains.
for _, dns := range subnet.DNSNameservers {
if ip := net.ParseIP(dns); ip != nil {
i.DNS = append(i.DNS, ip)
}
}
i.SearchDomains = append(i.SearchDomains, subnet.DNSSearch...)
// Add routes.
for _, route := range subnet.Routes {
r := new(Route)
// Try and parse the destination.
var ipnet *net.IPNet
var ip net.IP
if strings.Contains(route.Destination, "/") {
ip, ipnet, err = net.ParseCIDR(route.Destination)
if err != nil {
continue
}
ipnet.IP = ip
} else {
ipnet = &net.IPNet{
IP: net.ParseIP(route.Destination),
}
if ipnet.IP.To4() == nil {
ipnet.Mask = net.CIDRMask(128, 128)
} else {
ipnet.Mask = net.CIDRMask(32, 32)
}
}
// If the IP wasn't parsed, skip this route.
if ipnet.IP == nil {
continue
}
// If the netmask exists, parse and add to the destination.
if route.Netmask != "" {
ipMask := net.ParseIP(route.Netmask).To4()
if ipMask == nil {
continue
}
ipnet.Mask = net.IPMask(ipMask)
}
r.Destination = ipnet
// Parse the gateway, requiring it.
gateway := net.ParseIP(route.Gateway)
if gateway == nil {
continue
}
r.Gateway = gateway
// Add the route.
r.Metric = route.Metric
i.Routes = append(i.Routes, r)
}
// Only add subnet as address if static type.
if subnet.Type != "static" {
continue
}
// Parse the address field.
var ipnet *net.IPNet
var ip net.IP
if strings.Contains(subnet.Address, "/") {
ip, ipnet, err = net.ParseCIDR(subnet.Address)
if err != nil {
continue
}
ipnet.IP = ip
} else {
ipnet = &net.IPNet{
IP: net.ParseIP(subnet.Address),
}
if ipnet.IP.To4() == nil {
ipnet.Mask = net.CIDRMask(128, 128)
} else {
ipnet.Mask = net.CIDRMask(32, 32)
}
}
// If IP not parsed, skip.
if ipnet.IP == nil {
continue
}
// If subnet assigned, parse and add to address.
if subnet.Netmask != "" {
ipMask := net.ParseIP(subnet.Netmask).To4()
if ipMask == nil {
continue
}
ipnet.Mask = net.IPMask(ipMask)
}
// Add address.
i.Addresses = append(i.Addresses, ipnet)
// Parse the gateway.
gateway := net.ParseIP(subnet.Gateway)
if gateway != nil {
if gateway.To4() == nil {
i.Gateway6 = gateway
} else {
i.Gateway4 = gateway
}
}
}
// Add interface to list.
interfaces = append(interfaces, i)
}
// Add ethernet interfaces to the list.
var keys []string
for key := range ci.Network.Ethernets {
keys = append(keys, key)
}
sort.Strings(keys)
for _, name := range keys {
config := ci.Network.Ethernets[name]
// If the name is being changed in the config, use that
// as the name.
if config.SetName != "" {
name = config.SetName
}
// Get interface.
i := ci.ConvertInterface(name, config.Match.MACAddress, config.ciInterface, links)
// Add the interface.
interfaces = append(interfaces, i)
}
// Add Bridge interfaces to the list.
keys = nil
for key := range ci.Network.Bridges {
keys = append(keys, key)
}
sort.Strings(keys)
for _, name := range keys {
config := ci.Network.Bridges[name]
// Get interface.
i := ci.ConvertInterface(name, "", config.ciInterface, links)
// Add the interface.
interfaces = append(interfaces, i)
}
// Add Bond interfaces to the list.
keys = nil
for key := range ci.Network.Bonds {
keys = append(keys, key)
}
sort.Strings(keys)
for _, name := range keys {
config := ci.Network.Bonds[name]
// Get interface.
i := ci.ConvertInterface(name, "", config.ciInterface, links)
// Add the interface.
interfaces = append(interfaces, i)
}
// Add VLAN interfaces to the list.
keys = nil
for key := range ci.Network.VLANs {
keys = append(keys, key)
}
sort.Strings(keys)
for _, name := range keys {
config := ci.Network.VLANs[name]
// Get interface.
i := ci.ConvertInterface(name, "", config.ciInterface, links)
// Add the interface.
interfaces = append(interfaces, i)
}
return
}
// Save configurations.
func (ci *cloudInit) Save() error {
// First we are to determine which configuration we will store the network
// configurations to. We will call this the main config, and will consider
// the first network only config file as the main config.
mainConfig := ""
for _, file := range ci.configFiles {
if file.OnlyNetwork {
mainConfig = file.Path
break
}
}
// If there is no network only configs, choose the first config file.
if mainConfig == "" {
mainConfig = ci.configFiles[0].Path
}
// Update all files with new configurations.
tmpSuffix := fmt.Sprintf(".tmp.%d", time.Now().UnixNano())
bakSuffix := fmt.Sprintf(".bak.%d", time.Now().UnixNano())
for _, file := range ci.configFiles {
configPath := file.Path
configDir, configName := path.Split(configPath)
tmpName := configName + tmpSuffix
tmpPath := path.Join(configDir, tmpName)
bakName := configName + bakSuffix
bakPath := path.Join(configDir, bakName)
// Try to encode/save the config, start by encoding to yaml,
// then decode to a map.
data, err := yaml.Marshal(ci)
if err != nil {
return err
}
var config map[string]any
err = yaml.Unmarshal(data, &config)
if err != nil {
return err
}
// Decode the current config.
data, err = os.ReadFile(configPath)
if err != nil {
return err
}
var origConfig map[string]any
if strings.HasSuffix(configPath, ".json") {
err = json.Unmarshal(data, &origConfig)
} else {
err = yaml.Unmarshal(data, &origConfig)
}
if err != nil {
return err
}
// Update the original config with the new config.
// On files which were not determined as the main config,
// we will delete the network config.
if configPath == mainConfig {
if file.NetworkEncap {
network := config["network"].(map[string]any)
origConfig["config"] = network["config"]
} else {
origConfig["network"] = config["network"]
}
} else {
if file.NetworkEncap {
delete(origConfig, "config")
} else {
delete(origConfig, "network")
}
}
// Save to temporary file.
if strings.HasSuffix(configPath, ".json") {
data, err = json.MarshalIndent(origConfig, "", " ")
data = append(data, '\n')
} else {
data, err = yaml.Marshal(origConfig)
}
if err != nil {
return err
}
err = os.WriteFile(tmpPath, data, 0644)
if err != nil {
return err
}
// Backup original file.
err = fileMove(configPath, bakPath)
if err != nil {
return err
}
// Move new config in place.
err = fileMove(tmpPath, configPath)
if err != nil {
return err
}
// Prune old backups of this config file beyond the retention count.
pruneBackups(configDir, suffixBackupMatcher(configName), ci.backupRetention)
}
return nil
}
// Set addresses to interface.
func (ci *cloudInit) SetIfaceAddresses(_ context.Context, iface string, addrs []*net.IPNet, gateway4, gateway6 net.IP) (err error) {
// Apply changes to v1 configs.
for c, config := range ci.Network.Configs {
// Skip types we cannot parse.
switch config.Type {
case "physical", "bond", "bridge", "vlan":
default:
continue
}
// Skip interfaces that are not the one we're updating.
if config.Name != iface {
continue
}
// Parse all routes and DNS config so we can re-add as needed.
var routes []Route
var dns []string
var domains []string
for _, subnet := range config.Subnets {
for _, route := range subnet.Routes {
var r Route
// Try and parse the destination.
var ipnet *net.IPNet
var ip net.IP
if strings.Contains(route.Destination, "/") {
ip, ipnet, err = net.ParseCIDR(route.Destination)
if err != nil {
continue
}
ipnet.IP = ip
} else {
ipnet = &net.IPNet{
IP: net.ParseIP(route.Destination),
}
if ipnet.IP.To4() == nil {
ipnet.Mask = net.CIDRMask(128, 128)
} else {
ipnet.Mask = net.CIDRMask(32, 32)
}
}
// If the IP wasn't parsed, skip this route.
if ipnet.IP == nil {
continue
}
// If the netmask exists, parse and add to the destination.
if route.Netmask != "" {
ipMask := net.ParseIP(route.Netmask).To4()
if ipMask == nil {
continue
}
ipnet.Mask = net.IPMask(ipMask)
}
r.Destination = ipnet
// Parse the gateway, requiring it.
gateway := net.ParseIP(route.Gateway)
if gateway == nil {
continue
}
r.Gateway = gateway
// Add the route.
r.Metric = route.Metric
routes = append(routes, r)
}
for _, d := range subnet.DNSNameservers {
found := false
for _, d2 := range dns {
if d2 == d {
found = true
break
}
}
if !found {
dns = append(dns, d)
}
}
for _, d := range subnet.DNSSearch {
found := false
for _, d2 := range domains {
if d2 == d {
found = true
break
}
}
if !found {
domains = append(domains, d)
}
}
}
// Clear existing subnets.
config.Subnets = nil
// Add subnets.
for _, addr := range addrs {
var subnet ciSubnet
subnet.Type = "static"
subnet.Address = addr.String()
if addr.Contains(gateway4) {
subnet.Gateway = gateway4.String()
} else if addr.Contains(gateway6) {
subnet.Gateway = gateway6.String()
}
// Add back DNS config.
subnet.DNSNameservers = dns
subnet.DNSSearch = domains
// Add all pertaining routes.
for {
foundRoute := false
for r, route := range routes {
if addr.Contains(route.Gateway) {
foundRoute = true
subnet.Routes = append(subnet.Routes, ciSubnetRoute{
Destination: route.Destination.String(),
Gateway: route.Gateway.String(),
Metric: route.Metric,
})
routes = append(routes[:r], routes[r+1:]...)
break
}
}
if !foundRoute {
break
}
}
// Add subnet.
config.Subnets = append(config.Subnets, subnet)
}
// Add stray routes to the first subnet.
if len(routes) != 0 && len(config.Subnets) != 0 {
for _, route := range routes {
config.Subnets[0].Routes = append(config.Subnets[0].Routes, ciSubnetRoute{
Destination: route.Destination.String(),
Gateway: route.Gateway.String(),
Metric: route.Metric,
})
}
}
// Update the config.
ci.Network.Configs[c] = config
}
// Check ethernet interfaces.
for name, config := range ci.Network.Ethernets {
// If the name is being changed in the config, use that
// as the name.
ifName := name
if config.SetName != "" {
ifName = config.SetName
}
// If this is not the interface we're updating addresses on.
if ifName != iface {
continue
}
// Re-configure with specified addresses.
config.Addresses = nil
for _, addr := range addrs {
config.Addresses = append(config.Addresses, addr.String())
}
if gateway4 == nil {
config.Gateway4 = ""
} else {
config.Gateway4 = gateway4.String()
}
if gateway6 == nil {
config.Gateway6 = ""
} else {
config.Gateway6 = gateway6.String()
}
ci.Network.Ethernets[name] = config
}
// Check bridges.
{
config, ok := ci.Network.Bridges[iface]
if ok {
// Re-configure with specified addresses.
config.Addresses = nil
for _, addr := range addrs {
config.Addresses = append(config.Addresses, addr.String())
}
if gateway4 == nil {
config.Gateway4 = ""
} else {
config.Gateway4 = gateway4.String()
}
if gateway6 == nil {
config.Gateway6 = ""
} else {
config.Gateway6 = gateway6.String()
}
ci.Network.Bridges[iface] = config
}
}
// Check bonds.
{
config, ok := ci.Network.Bonds[iface]
if ok {
// Re-configure with specified addresses.
config.Addresses = nil
for _, addr := range addrs {
config.Addresses = append(config.Addresses, addr.String())
}
if gateway4 == nil {
config.Gateway4 = ""
} else {
config.Gateway4 = gateway4.String()
}
if gateway6 == nil {
config.Gateway6 = ""
} else {
config.Gateway6 = gateway6.String()
}
ci.Network.Bonds[iface] = config
}
}
// Check VLAN interfaces.
{
config, ok := ci.Network.VLANs[iface]
if ok {
// Re-configure with specified addresses.
config.Addresses = nil
for _, addr := range addrs {
config.Addresses = append(config.Addresses, addr.String())
}
if gateway4 == nil {
config.Gateway4 = ""
} else {
config.Gateway4 = gateway4.String()
}
if gateway6 == nil {
config.Gateway6 = ""
} else {
config.Gateway6 = gateway6.String()
}
ci.Network.VLANs[iface] = config
}
}
// Try to save changes.
err = ci.Save()
return
}
// Set static routes to interface.
func (ci *cloudInit) SetIfaceRoutes(_ context.Context, iface string, routes []*Route) (err error) {
// Apply changes to v1 configs.
for c, config := range ci.Network.Configs {
// Skip types we cannot parse.
switch config.Type {
case "physical", "bond", "bridge", "vlan":
default:
continue
}
// Skip interfaces that are not the one we're updating.
if config.Name != iface {
continue
}
if len(config.Subnets) == 0 {
return fmt.Errorf("no subnets on interface")
}
// Remove existing routes, and parse destination.
subnetMap := make(map[int]*net.IPNet)
for s, subnet := range config.Subnets {
config.Subnets[s].Routes = nil
// Only add subnet as address if static type.
if subnet.Type != "static" {
continue
}
// Parse the address field.
var ipnet *net.IPNet
var ip net.IP
if strings.Contains(subnet.Address, "/") {
ip, ipnet, err = net.ParseCIDR(subnet.Address)
if err != nil {
continue
}
ipnet.IP = ip
} else {
ipnet = &net.IPNet{
IP: net.ParseIP(subnet.Address),
}
if ipnet.IP.To4() == nil {
ipnet.Mask = net.CIDRMask(128, 128)
} else {
ipnet.Mask = net.CIDRMask(32, 32)
}
}
// If IP not parsed, skip.
if ipnet.IP == nil {
continue
}
// If subnet assigned, parse and add to address.
if subnet.Netmask != "" {
ipMask := net.ParseIP(subnet.Netmask).To4()
if ipMask == nil {
continue
}
ipnet.Mask = net.IPMask(ipMask)
}
// Add to map.
subnetMap[s] = ipnet
}
// Add each route accordingly.
for _, route := range routes {
// Find subnet to assign route, defaulting to 0.
idx := 0
for s, ipnet := range subnetMap {
if ipnet.Contains(route.Gateway) {
idx = s
break
}
}
// Add route.
var r ciSubnetRoute
r.Destination = route.Destination.String()
r.Gateway = route.Gateway.String()
r.Metric = route.Metric
config.Subnets[idx].Routes = append(config.Subnets[idx].Routes, r)
}
// Update configs.
ci.Network.Configs[c] = config
}
// Check ethernet interfaces.
for name, config := range ci.Network.Ethernets {
// If the name is being changed in the config, use that
// as the name.
ifName := name
if config.SetName != "" {
ifName = config.SetName
}
// If this is not the interface we're updating routes on.
if ifName != iface {
continue
}
// Re-configure with specified routes.
config.Routes = nil
for _, route := range routes {
config.Routes = append(config.Routes, ciRoute{
To: route.Destination.String(),
Via: route.Gateway.String(),
Metric: &route.Metric,
})
}
ci.Network.Ethernets[name] = config
}
// Check bridges.
{
config, ok := ci.Network.Bridges[iface]
if ok {
// Re-configure with specified routes.
config.Routes = nil
for _, route := range routes {
config.Routes = append(config.Routes, ciRoute{
To: route.Destination.String(),
Via: route.Gateway.String(),
Metric: &route.Metric,
})
}
ci.Network.Bridges[iface] = config
}
}
// Check bonds.
{
config, ok := ci.Network.Bonds[iface]
if ok {
// Re-configure with specified routes.
config.Routes = nil
for _, route := range routes {
config.Routes = append(config.Routes, ciRoute{
To: route.Destination.String(),
Via: route.Gateway.String(),
Metric: &route.Metric,
})
}
ci.Network.Bonds[iface] = config
}
}
// Check VLAN interfaces.
{
config, ok := ci.Network.VLANs[iface]
if ok {
// Re-configure with specified routes.
config.Routes = nil
for _, route := range routes {
config.Routes = append(config.Routes, ciRoute{
To: route.Destination.String(),
Via: route.Gateway.String(),
Metric: &route.Metric,
})
}
ci.Network.VLANs[iface] = config
}
}
// Try to save changes.
err = ci.Save()
return
}
// Set DNS servers and search domains on interface.
func (ci *cloudInit) SetIfaceDNS(_ context.Context, iface string, servers []net.IP, searchDomains []string) (err error) {
dnsStrings := ipStrings(servers)
nameservers := ciNameservers{
Search: searchDomains,
Addresses: dnsStrings,
}
// Apply changes to v1 configs, where DNS lives on each subnet.
for c, config := range ci.Network.Configs {
switch config.Type {
case "physical", "bond", "bridge", "vlan":
default:
continue
}
if config.Name != iface {
continue
}
for s := range config.Subnets {
config.Subnets[s].DNSNameservers = dnsStrings
config.Subnets[s].DNSSearch = searchDomains
}
ci.Network.Configs[c] = config
}
// Apply changes to v2 ethernet interfaces.
for name, config := range ci.Network.Ethernets {
ifName := name
if config.SetName != "" {
ifName = config.SetName
}
if ifName != iface {
continue
}
config.Nameservers = nameservers
ci.Network.Ethernets[name] = config
}
// Apply changes to v2 bridges, bonds, and VLANs.
if config, ok := ci.Network.Bridges[iface]; ok {
config.Nameservers = nameservers
ci.Network.Bridges[iface] = config
}
if config, ok := ci.Network.Bonds[iface]; ok {
config.Nameservers = nameservers
ci.Network.Bonds[iface] = config
}
if config, ok := ci.Network.VLANs[iface]; ok {
config.Nameservers = nameservers
ci.Network.VLANs[iface] = config
}
// Try to save changes.
err = ci.Save()
return
}