999 lines
27 KiB
Go
999 lines
27 KiB
Go
package netconfig
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"dario.cat/mergo"
|
|
"github.com/vishvananda/netlink"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Name server cvonfigurations.
|
|
type npNameservers struct {
|
|
Search []string `yaml:"search,omitempty,flow"`
|
|
Addresses []string `yaml:"addresses,omitempty,flow"`
|
|
}
|
|
|
|
// Route configurations.
|
|
type npRoute struct {
|
|
From string `yaml:"from,omitempty"`
|
|
OnLink *bool `yaml:"on-link,omitempty"`
|
|
Scope string `yaml:"scope,omitempty"`
|
|
Table *int `yaml:"table,omitempty"`
|
|
To string `yaml:"to,omitempty"`
|
|
Type string `yaml:"type,omitempty"`
|
|
Via string `yaml:"via,omitempty"`
|
|
Metric *int `yaml:"metric,omitempty"`
|
|
}
|
|
type npRoutePolicy struct {
|
|
From string `yaml:"from,omitempty"`
|
|
Mark *int `yaml:"mark,omitempty"`
|
|
Priority *int `yaml:"priority,omitempty"`
|
|
Table *int `yaml:"table,omitempty"`
|
|
To string `yaml:"to,omitempty"`
|
|
TypeOfService *int `yaml:"type-of-service,omitempty"`
|
|
}
|
|
|
|
// Match options.
|
|
type npMatch struct {
|
|
Name string `yaml:"name,omitempty"`
|
|
MACAddress string `yaml:"macaddress,omitempty"`
|
|
Driver string `yaml:"driver,omitempty"`
|
|
}
|
|
|
|
// Common configurations on physical interfaces.
|
|
type npPhysical struct {
|
|
Match npMatch `yaml:"match,omitempty"`
|
|
SetName string `yaml:"set-name,omitempty"`
|
|
Wakeonlan bool `yaml:"wakeonlan,omitempty"`
|
|
}
|
|
|
|
// Common netplan interface configurations.
|
|
type npInterface struct {
|
|
AcceptRA *bool `yaml:"accept-ra,omitempty"`
|
|
Addresses []string `yaml:"addresses,omitempty"`
|
|
Critical bool `yaml:"critical,omitempty"`
|
|
DHCP4 *bool `yaml:"dhcp4,omitempty"`
|
|
DHCP6 *bool `yaml:"dhcp6,omitempty"`
|
|
DHCP4Overrides map[string]any `yaml:"dhcp4-overrides,omitempty"`
|
|
DHCP6Overrides map[string]any `yaml:"dhcp6-overrides,omitempty"`
|
|
DHCPIdentifier string `yaml:"dhcp-identifier,omitempty"`
|
|
Gateway4 string `yaml:"gateway4,omitempty"`
|
|
Gateway6 string `yaml:"gateway6,omitempty"`
|
|
Nameservers npNameservers `yaml:"nameservers,omitempty"`
|
|
MACAddress string `yaml:"macaddress,omitempty"`
|
|
MTU int `yaml:"mtu,omitempty"`
|
|
Renderer string `yaml:"renderer,omitempty"`
|
|
Routes []npRoute `yaml:"routes,omitempty"`
|
|
RoutingPolicy []npRoutePolicy `yaml:"routing-policy,omitempty"`
|
|
Optional bool `yaml:"optional,omitempty"`
|
|
|
|
// Configure the link-local addresses to bring up. Valid options are
|
|
// "ipv4" and "ipv6". According to the netplan reference, netplan will
|
|
// only bring up ipv6 addresses if *no* link-local attribute is
|
|
// specified. On the other hand, if an empty link-local attribute is
|
|
// specified, this instructs netplan not to bring any ipv4/ipv6 address
|
|
// up.
|
|
LinkLocal *[]string `yaml:"link-local,omitempty"`
|
|
|
|
// According to the netplan examples, this section typically includes
|
|
// some OVS-specific configuration bits. However, MAAS may just
|
|
// include an empty block to indicate the presence of an OVS-managed
|
|
// bridge (LP1942328). As a workaround, we make this an optional map
|
|
// so we can tell whether it is present (but empty) vs not being
|
|
// present.
|
|
//
|
|
// See: https://github.com/canonical/netplan/blob/main/examples/openvswitch.yaml
|
|
OVSParameters *map[string]interface{} `yaml:"openvswitch,omitempty"`
|
|
}
|
|
|
|
// Physical ethernet configurations.
|
|
type npEthernet struct {
|
|
npPhysical `yaml:",inline"`
|
|
npInterface `yaml:",inline"`
|
|
}
|
|
|
|
// Wifi configurations.
|
|
type npAccessPoint struct {
|
|
Password string `yaml:"password,omitempty"`
|
|
Mode string `yaml:"mode,omitempty"`
|
|
Channel int `yaml:"channel,omitempty"`
|
|
}
|
|
type npWifi struct {
|
|
npPhysical `yaml:",inline"`
|
|
AccessPoints map[string]npAccessPoint `yaml:"access-points,omitempty"`
|
|
npInterface `yaml:",inline"`
|
|
}
|
|
|
|
// Bridge interface configurations.
|
|
type npBridgeParameters struct {
|
|
AgeingTime *int `yaml:"ageing-time,omitempty"`
|
|
ForwardDelay intString `yaml:"forward-delay,omitempty"`
|
|
HelloTime *int `yaml:"hello-time,omitempty"`
|
|
MaxAge *int `yaml:"max-age,omitempty"`
|
|
PathCost map[string]int `yaml:"path-cost,omitempty"`
|
|
PortPriority map[string]int `yaml:"port-priority,omitempty"`
|
|
Priority *int `yaml:"priority,omitempty"`
|
|
STP *bool `yaml:"stp,omitempty"`
|
|
}
|
|
type npBridge struct {
|
|
Interfaces []string `yaml:"interfaces,omitempty,flow"`
|
|
npInterface `yaml:",inline"`
|
|
Parameters npBridgeParameters `yaml:"parameters,omitempty"`
|
|
}
|
|
|
|
// Bond interface configurations.
|
|
type npBondParameters struct {
|
|
Mode intString `yaml:"mode,omitempty"`
|
|
LACPRate intString `yaml:"lacp-rate,omitempty"`
|
|
MIIMonitorInterval intString `yaml:"mii-monitor-interval,omitempty"`
|
|
MinLinks *int `yaml:"min-links,omitempty"`
|
|
TransmitHashPolicy string `yaml:"transmit-hash-policy,omitempty"`
|
|
ADSelect intString `yaml:"ad-select,omitempty"`
|
|
AllSlavesActive *bool `yaml:"all-slaves-active,omitempty"`
|
|
ARPInterval *int `yaml:"arp-interval,omitempty"`
|
|
ARPIPTargets []string `yaml:"arp-ip-targets,omitempty"`
|
|
ARPValidate intString `yaml:"arp-validate,omitempty"`
|
|
ARPAllTargets intString `yaml:"arp-all-targets,omitempty"`
|
|
UpDelay intString `yaml:"up-delay,omitempty"`
|
|
DownDelay intString `yaml:"down-delay,omitempty"`
|
|
FailOverMACPolicy intString `yaml:"fail-over-mac-policy,omitempty"`
|
|
// Netplan misspelled this and retained it along with the corrected spelling.
|
|
GratuitousARPLegacy *int `yaml:"gratuitious-arp,omitempty"` // nolint: misspell
|
|
GratuitousARP *int `yaml:"gratuitous-arp,omitempty"`
|
|
PacketsPerSlave *int `yaml:"packets-per-slave,omitempty"`
|
|
PrimaryReselectPolicy intString `yaml:"primary-reselect-policy,omitempty"`
|
|
ResendIGMP *int `yaml:"resend-igmp,omitempty"`
|
|
// bonding.txt says that this can be a value from 1-0x7fffffff, should we be forcing it to be a hex value?
|
|
LearnPacketInterval *int `yaml:"learn-packet-interval,omitempty"`
|
|
Primary string `yaml:"primary,omitempty"`
|
|
}
|
|
type npBond struct {
|
|
Interfaces []string `yaml:"interfaces,omitempty,flow"`
|
|
npInterface `yaml:",inline"`
|
|
Parameters npBondParameters `yaml:"parameters,omitempty"`
|
|
}
|
|
|
|
// VLAN interface configurations.
|
|
type npVLAN struct {
|
|
Id *int `yaml:"id,omitempty"`
|
|
Link string `yaml:"link,omitempty"`
|
|
npInterface `yaml:",inline"`
|
|
}
|
|
|
|
// Common netplan network configs.
|
|
type npNetwork struct {
|
|
Version intString `yaml:"version"`
|
|
Renderer string `yaml:"renderer,omitempty"`
|
|
Ethernets map[string]npEthernet `yaml:"ethernets,omitempty"`
|
|
Wifis map[string]npWifi `yaml:"wifis,omitempty"`
|
|
Bridges map[string]npBridge `yaml:"bridges,omitempty"`
|
|
Bonds map[string]npBond `yaml:"bonds,omitempty"`
|
|
VLANs map[string]npVLAN `yaml:"vlans,omitempty"`
|
|
Routes []npRoute `yaml:"routes,omitempty"`
|
|
}
|
|
|
|
// The default netplan configuration structure and interface to configuring netplan.
|
|
type npConfig struct {
|
|
Network npNetwork `yaml:"network"`
|
|
file string `yaml:"-"`
|
|
empty bool `yaml:"-"`
|
|
// dirty marks that a Set* call actually modified this file's in-memory
|
|
// config during the current operation. Save only rewrites/backs up dirty
|
|
// files, so files nobody touched (including legitimately empty netplan
|
|
// fragments that were never merged away) are left untouched on disk.
|
|
dirty bool `yaml:"-"`
|
|
}
|
|
|
|
type netplan struct {
|
|
configs []*npConfig `yaml:"-"`
|
|
configDir string `yaml:"-"`
|
|
backupRetention int
|
|
}
|
|
|
|
// Verify netplan exists, and try parsing its configurations.
|
|
func newNetplan(backupRetention int) (np *netplan, err error) {
|
|
// Calling netplan info will verify netplan is on the machine.
|
|
_, err = runCommand(context.Background(), "netplan", "info")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Try to parse the netplan configurations. /etc/netplan is the durable,
|
|
// admin-facing source of truth and is tried first so persisted changes
|
|
// actually survive a reboot; /run/netplan is a volatile tmpfs directory
|
|
// that netplan also honors (e.g. "netplan try") and is only used as a
|
|
// fallback, otherwise a host with any file in /run/netplan would have its
|
|
// real /etc/netplan configuration silently ignored.
|
|
np, err = readNetplanConfigDirectory("/etc/netplan")
|
|
if err != nil {
|
|
np, err = readNetplanConfigDirectory("/run/netplan")
|
|
if err != nil {
|
|
np, err = readNetplanConfigDirectory("/lib/netplan")
|
|
}
|
|
}
|
|
if np != nil {
|
|
np.backupRetention = backupRetention
|
|
}
|
|
return
|
|
}
|
|
|
|
// Read an netplan configuration directory for netplan configuration files.
|
|
func readNetplanConfigDirectory(dirPath string) (np *netplan, err error) {
|
|
// Try to read the directory files.
|
|
unsortedEntries, err := os.ReadDir(dirPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Sort by name.
|
|
entries := sortableDirEntries(unsortedEntries)
|
|
sort.Sort(entries)
|
|
|
|
// Temporary struct to store unparsed configs.
|
|
type npConfigFile struct {
|
|
data map[string]any
|
|
file string
|
|
empty bool
|
|
// dirty marks that this file's in-memory data no longer matches what
|
|
// is on disk because a duplicate key was merged into (or out of) it
|
|
// during parsing, so it must be rewritten on the next Save even if no
|
|
// Set* call ends up touching an interface still contained in it.
|
|
dirty bool
|
|
}
|
|
var configs []*npConfigFile
|
|
configTypes := []string{"ethernets", "wifis", "bridges", "bonds", "vlans", "routes"}
|
|
|
|
// Read each yaml file and merge into a single config.
|
|
for _, entry := range entries {
|
|
// If not an yaml file, skip.
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
|
|
continue
|
|
}
|
|
config := &npConfigFile{
|
|
file: entry.Name(),
|
|
empty: false,
|
|
}
|
|
|
|
// Read the bytes from the file.
|
|
configPath := path.Join(dirPath, entry.Name())
|
|
var data []byte
|
|
data, err = os.ReadFile(configPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Parse the yaml to an map.
|
|
err = yaml.Unmarshal(data, &config.data)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Check if prior configs have same network configs and merge.
|
|
configCount := 0
|
|
network, ok := config.data["network"].(map[string]any)
|
|
if !ok {
|
|
goto addConfig
|
|
}
|
|
|
|
// Merge types with prior configs.
|
|
for _, t := range configTypes {
|
|
// First get the config type.
|
|
c, ok := network[t].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Loop through all prior parsed configs,
|
|
// and find ones that has same type.
|
|
for _, thisConfig := range configs {
|
|
// If empty, there is no config so skip.
|
|
if thisConfig.empty {
|
|
continue
|
|
}
|
|
|
|
// Try and get the config type from this config.
|
|
thisC, ok := thisConfig.data["network"].(map[string]any)[t].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Find entries in this config which match the current config.
|
|
for key, _ := range thisC {
|
|
// Find and if not found, skip.
|
|
k, ok := c[key].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
thisK := thisC[key].(map[string]any)
|
|
|
|
// This config contains a key from the current.
|
|
// We need to merge.
|
|
err = mergo.Merge(&thisK, k)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Update the previously parsed data with merged data.
|
|
thisConfig.data["network"].(map[string]any)[t].(map[string]any)[key] = thisK
|
|
thisConfig.dirty = true
|
|
|
|
// Delete the config from the current config.
|
|
delete(config.data["network"].(map[string]any)[t].(map[string]any), key)
|
|
delete(network[t].(map[string]any), key)
|
|
config.dirty = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Count number of configs to determine if file is empty.
|
|
for _, t := range configTypes {
|
|
// First get the config type.
|
|
c, ok := network[t].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
configCount += len(c)
|
|
}
|
|
addConfig:
|
|
routes, ok := config.data["routes"].([]any)
|
|
if ok {
|
|
configCount += len(routes)
|
|
}
|
|
config.empty = configCount == 0
|
|
|
|
// Add config to slice of configs.
|
|
configs = append(configs, config)
|
|
}
|
|
|
|
// No config when no configs.
|
|
if len(configs) == 0 {
|
|
return nil, fmt.Errorf("no netplan configurations found")
|
|
}
|
|
err = nil
|
|
|
|
// Build the netplan data.
|
|
np = new(netplan)
|
|
np.configDir = dirPath
|
|
for _, config := range configs {
|
|
// Convert config data back into yaml so we can re-parse into a struct.
|
|
data, err := yaml.Marshal(config.data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse the netplan config.
|
|
cfg := new(npConfig)
|
|
err = yaml.Unmarshal(data, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Successfully parsed, append to the configs slice.
|
|
cfg.file = config.file
|
|
cfg.dirty = config.dirty
|
|
cfg.empty = config.empty
|
|
np.configs = append(np.configs, cfg)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Convert netplan interface to a netconfig Interface.
|
|
func (*netplan) ConvertInterface(name string, config npInterface, 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 && config.MACAddress != "" {
|
|
mac, _ = net.ParseMAC(config.MACAddress)
|
|
}
|
|
|
|
// 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 (np *netplan) 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 each config.
|
|
for _, c := range np.configs {
|
|
if c.empty {
|
|
continue
|
|
}
|
|
|
|
// Add ethernet interfaces to the list.
|
|
var keys []string
|
|
for key := range c.Network.Ethernets {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, name := range keys {
|
|
config := c.Network.Ethernets[name]
|
|
// If the name is being changed in the config, use that
|
|
// as the name.
|
|
if config.SetName != "" {
|
|
name = config.SetName
|
|
}
|
|
|
|
// Update the config MAC to the match MAC.
|
|
if config.MACAddress == "" {
|
|
config.MACAddress = config.Match.MACAddress
|
|
}
|
|
|
|
// Get interface.
|
|
i := np.ConvertInterface(name, config.npInterface, links)
|
|
|
|
// Add the interface.
|
|
interfaces = append(interfaces, i)
|
|
}
|
|
|
|
// Add Wifi interfaces to the list.
|
|
keys = nil
|
|
for key := range c.Network.Wifis {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, name := range keys {
|
|
config := c.Network.Wifis[name]
|
|
// If the name is being changed in the config, use that
|
|
// as the name.
|
|
if config.SetName != "" {
|
|
name = config.SetName
|
|
}
|
|
|
|
// Update the config MAC to the match MAC.
|
|
if config.MACAddress == "" {
|
|
config.MACAddress = config.Match.MACAddress
|
|
}
|
|
|
|
// Get interface.
|
|
i := np.ConvertInterface(name, config.npInterface, links)
|
|
|
|
// Add the interface.
|
|
interfaces = append(interfaces, i)
|
|
}
|
|
|
|
// Add Bridge interfaces to the list.
|
|
keys = nil
|
|
for key := range c.Network.Bridges {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, name := range keys {
|
|
config := c.Network.Bridges[name]
|
|
// Get interface.
|
|
i := np.ConvertInterface(name, config.npInterface, links)
|
|
|
|
// Add the interface.
|
|
interfaces = append(interfaces, i)
|
|
}
|
|
|
|
// Add Bond interfaces to the list.
|
|
keys = nil
|
|
for key := range c.Network.Bonds {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, name := range keys {
|
|
config := c.Network.Bonds[name]
|
|
// Get interface.
|
|
i := np.ConvertInterface(name, config.npInterface, links)
|
|
|
|
// Add the interface.
|
|
interfaces = append(interfaces, i)
|
|
}
|
|
|
|
// Add VLAN interfaces to the list.
|
|
keys = nil
|
|
for key := range c.Network.VLANs {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, name := range keys {
|
|
config := c.Network.VLANs[name]
|
|
// Get interface.
|
|
i := np.ConvertInterface(name, config.npInterface, links)
|
|
|
|
// Add the interface.
|
|
interfaces = append(interfaces, i)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Save configurations.
|
|
func (np *netplan) Save() error {
|
|
// Update each configuration file that was actually touched by this
|
|
// operation. Configs nobody modified are left alone on disk, so an
|
|
// unrelated legitimately-empty netplan fragment is never backed up and
|
|
// removed as a side effect of changing a different interface.
|
|
for _, config := range np.configs {
|
|
if !config.dirty {
|
|
continue
|
|
}
|
|
|
|
// Get config paths.
|
|
configPath := path.Join(np.configDir, config.file)
|
|
tmpName := fmt.Sprintf("%s.tmp.%d", config.file, time.Now().UnixNano())
|
|
tmpPath := path.Join(np.configDir, tmpName)
|
|
bakupName := fmt.Sprintf("%s.bak.%d", config.file, time.Now().UnixNano())
|
|
bakupPath := path.Join(np.configDir, bakupName)
|
|
|
|
// Only save if not empty.
|
|
if !config.empty {
|
|
// Try to encode/save the config.
|
|
data, err := yaml.Marshal(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(tmpPath, data, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Backup old config.
|
|
err := fileMove(configPath, bakupPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Move new config in place.
|
|
if !config.empty {
|
|
// 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(np.configDir, suffixBackupMatcher(config.file), np.backupRetention)
|
|
|
|
// The file on disk now matches this config's in-memory state, so it
|
|
// does not need rewriting again until something else marks it dirty.
|
|
// Configs that turned out empty are left dirty so the cleanup pass
|
|
// below still notices they were just removed from disk.
|
|
if !config.empty {
|
|
config.dirty = false
|
|
}
|
|
}
|
|
|
|
// Now that touched configurations were updated, drop the ones we just
|
|
// emptied out and removed from disk above. Configs that were already
|
|
// empty but untouched this call were left on disk, so they stay tracked.
|
|
// Rebuild the slice rather than deleting in place while ranging over it,
|
|
// which would shift the backing array under the loop and skip or drop the
|
|
// wrong entries when more than one config was emptied.
|
|
kept := np.configs[:0]
|
|
for _, config := range np.configs {
|
|
if config.dirty && config.empty {
|
|
continue
|
|
}
|
|
kept = append(kept, config)
|
|
}
|
|
np.configs = kept
|
|
|
|
return nil
|
|
}
|
|
|
|
// Set addresses to interface.
|
|
func (np *netplan) SetIfaceAddresses(_ context.Context, iface string, addrs []*net.IPNet, gateway4, gateway6 net.IP) (err error) {
|
|
// Update each config.
|
|
for _, c := range np.configs {
|
|
if c.empty {
|
|
continue
|
|
}
|
|
|
|
// Check ethernet interfaces.
|
|
for name, config := range c.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
|
|
}
|
|
c.dirty = true
|
|
|
|
// 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()
|
|
}
|
|
c.Network.Ethernets[name] = config
|
|
}
|
|
|
|
// Check Wifi interfaces.
|
|
for name, config := range c.Network.Wifis {
|
|
// 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 the interface we're updating addresses on.
|
|
if ifName != iface {
|
|
continue
|
|
}
|
|
c.dirty = true
|
|
|
|
// 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()
|
|
}
|
|
c.Network.Wifis[name] = config
|
|
}
|
|
|
|
// Check bridges.
|
|
{
|
|
config, ok := c.Network.Bridges[iface]
|
|
if ok {
|
|
c.dirty = true
|
|
// 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()
|
|
}
|
|
c.Network.Bridges[iface] = config
|
|
}
|
|
}
|
|
|
|
// Check bonds.
|
|
{
|
|
config, ok := c.Network.Bonds[iface]
|
|
if ok {
|
|
c.dirty = true
|
|
// 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()
|
|
}
|
|
c.Network.Bonds[iface] = config
|
|
}
|
|
}
|
|
|
|
// Check VLAN interfaces.
|
|
{
|
|
config, ok := c.Network.VLANs[iface]
|
|
if ok {
|
|
c.dirty = true
|
|
// 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()
|
|
}
|
|
c.Network.VLANs[iface] = config
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to save changes.
|
|
err = np.Save()
|
|
return
|
|
}
|
|
|
|
// Set static routes to interface.
|
|
func (np *netplan) SetIfaceRoutes(_ context.Context, iface string, routes []*Route) (err error) {
|
|
// Update each config.
|
|
for _, c := range np.configs {
|
|
if c.empty {
|
|
continue
|
|
}
|
|
|
|
// Check ethernet interfaces.
|
|
for name, config := range c.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
|
|
}
|
|
c.dirty = true
|
|
|
|
// Re-configure with specified routes.
|
|
config.Routes = nil
|
|
for _, route := range routes {
|
|
config.Routes = append(config.Routes, npRoute{
|
|
To: route.Destination.String(),
|
|
Via: route.Gateway.String(),
|
|
Metric: &route.Metric,
|
|
})
|
|
}
|
|
c.Network.Ethernets[name] = config
|
|
}
|
|
|
|
// Check Wifi interfaces.
|
|
for name, config := range c.Network.Wifis {
|
|
// 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 the interface we're updating routes on.
|
|
if ifName != iface {
|
|
continue
|
|
}
|
|
c.dirty = true
|
|
|
|
// Re-configure with specified routes.
|
|
config.Routes = nil
|
|
for _, route := range routes {
|
|
config.Routes = append(config.Routes, npRoute{
|
|
To: route.Destination.String(),
|
|
Via: route.Gateway.String(),
|
|
Metric: &route.Metric,
|
|
})
|
|
}
|
|
c.Network.Wifis[name] = config
|
|
}
|
|
|
|
// Check bridges.
|
|
{
|
|
config, ok := c.Network.Bridges[iface]
|
|
if ok {
|
|
c.dirty = true
|
|
// Re-configure with specified routes.
|
|
config.Routes = nil
|
|
for _, route := range routes {
|
|
config.Routes = append(config.Routes, npRoute{
|
|
To: route.Destination.String(),
|
|
Via: route.Gateway.String(),
|
|
Metric: &route.Metric,
|
|
})
|
|
}
|
|
c.Network.Bridges[iface] = config
|
|
}
|
|
}
|
|
|
|
// Check bonds.
|
|
{
|
|
config, ok := c.Network.Bonds[iface]
|
|
if ok {
|
|
c.dirty = true
|
|
// Re-configure with specified routes.
|
|
config.Routes = nil
|
|
for _, route := range routes {
|
|
config.Routes = append(config.Routes, npRoute{
|
|
To: route.Destination.String(),
|
|
Via: route.Gateway.String(),
|
|
Metric: &route.Metric,
|
|
})
|
|
}
|
|
c.Network.Bonds[iface] = config
|
|
}
|
|
}
|
|
|
|
// Check VLAN interfaces.
|
|
{
|
|
config, ok := c.Network.VLANs[iface]
|
|
if ok {
|
|
c.dirty = true
|
|
// Re-configure with specified routes.
|
|
config.Routes = nil
|
|
for _, route := range routes {
|
|
config.Routes = append(config.Routes, npRoute{
|
|
To: route.Destination.String(),
|
|
Via: route.Gateway.String(),
|
|
Metric: &route.Metric,
|
|
})
|
|
}
|
|
c.Network.VLANs[iface] = config
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to save changes.
|
|
err = np.Save()
|
|
return
|
|
}
|
|
|
|
// Set DNS servers and search domains on interface.
|
|
func (np *netplan) SetIfaceDNS(_ context.Context, iface string, servers []net.IP, searchDomains []string) (err error) {
|
|
nameservers := npNameservers{
|
|
Search: searchDomains,
|
|
Addresses: ipStrings(servers),
|
|
}
|
|
|
|
// Update each config.
|
|
for _, c := range np.configs {
|
|
if c.empty {
|
|
continue
|
|
}
|
|
|
|
// Check ethernet interfaces.
|
|
for name, config := range c.Network.Ethernets {
|
|
ifName := name
|
|
if config.SetName != "" {
|
|
ifName = config.SetName
|
|
}
|
|
if ifName != iface {
|
|
continue
|
|
}
|
|
c.dirty = true
|
|
config.Nameservers = nameservers
|
|
c.Network.Ethernets[name] = config
|
|
}
|
|
|
|
// Check Wifi interfaces.
|
|
for name, config := range c.Network.Wifis {
|
|
ifName := name
|
|
if config.SetName != "" {
|
|
ifName = config.SetName
|
|
}
|
|
if ifName != iface {
|
|
continue
|
|
}
|
|
c.dirty = true
|
|
config.Nameservers = nameservers
|
|
c.Network.Wifis[name] = config
|
|
}
|
|
|
|
// Check bridges.
|
|
if config, ok := c.Network.Bridges[iface]; ok {
|
|
c.dirty = true
|
|
config.Nameservers = nameservers
|
|
c.Network.Bridges[iface] = config
|
|
}
|
|
|
|
// Check bonds.
|
|
if config, ok := c.Network.Bonds[iface]; ok {
|
|
c.dirty = true
|
|
config.Nameservers = nameservers
|
|
c.Network.Bonds[iface] = config
|
|
}
|
|
|
|
// Check VLAN interfaces.
|
|
if config, ok := c.Network.VLANs[iface]; ok {
|
|
c.dirty = true
|
|
config.Nameservers = nameservers
|
|
c.Network.VLANs[iface] = config
|
|
}
|
|
}
|
|
|
|
// Try to save changes.
|
|
err = np.Save()
|
|
return
|
|
}
|