go-network-configurator/netplan.go
James Coleman fae43247de Switch unit tests to testify
Bring an administratively down interface up in AddAddress before writing
its addresses and routes. The kernel installs an address's connected
route only while its link is up, so adding a gateway to a down interface
was rejected as unreachable. A link raised this way is part of the
pre-change state AddAddress restores, so it is returned to down on any
path that does not complete.

Also fix the rollback masking its own error: deleting the address it
added takes the default route with it, since that route resolved its
nexthop through the address's connected subnet. The following RouteDel
then answered ESRCH and was reported in place of "aborted operation due
to loss of internet".
2026-07-10 08:52:52 -05:00

1101 lines
30 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"`
}
// dhcpState reports whether each family's DHCP client is enabled. An absent
// key means netplan's default, which is off.
func (c *npInterface) dhcpState() (dhcp4, dhcp6 bool) {
return c.DHCP4 != nil && *c.DHCP4, c.DHCP6 != nil && *c.DHCP6
}
// setDHCP records the requested DHCP client state for both families. Disabling
// a family drops its dhcp4-overrides/dhcp6-overrides, which netplan only reads
// while that client runs. A family being disabled that was already absent is
// left absent rather than written out as false, so reading and rewriting a
// config that never mentioned DHCP does not churn it.
func (c *npInterface) setDHCP(dhcp4, dhcp6 bool) {
if dhcp4 || c.DHCP4 != nil {
c.DHCP4 = boolPtr(dhcp4)
}
if !dhcp4 {
c.DHCP4Overrides = nil
}
if dhcp6 || c.DHCP6 != nil {
c.DHCP6 = boolPtr(dhcp6)
}
if !dhcp6 {
c.DHCP6Overrides = nil
}
}
// 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. The `netplan info`
// probe is bound to ctx so a wedged netplan cannot stall construction.
func newNetplan(ctx context.Context, backupRetention int) (np *netplan, err error) {
// Calling netplan info will verify netplan is on the machine and working,
// not merely installed.
_, err = runCommand(ctx, "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
i.DHCP4, i.DHCP6 = config.dhcpState()
// 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 the DHCP client state on an interface.
func (np *netplan) SetIfaceDHCP(_ context.Context, iface string, dhcp4, dhcp6 bool) (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 ifName != iface {
continue
}
c.dirty = true
config.setDHCP(dhcp4, dhcp6)
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.setDHCP(dhcp4, dhcp6)
c.Network.Wifis[name] = config
}
// Check bridges.
if config, ok := c.Network.Bridges[iface]; ok {
c.dirty = true
config.setDHCP(dhcp4, dhcp6)
c.Network.Bridges[iface] = config
}
// Check bonds.
if config, ok := c.Network.Bonds[iface]; ok {
c.dirty = true
config.setDHCP(dhcp4, dhcp6)
c.Network.Bonds[iface] = config
}
// Check VLAN interfaces.
if config, ok := c.Network.VLANs[iface]; ok {
c.dirty = true
config.setDHCP(dhcp4, dhcp6)
c.Network.VLANs[iface] = config
}
}
// Try to save changes.
err = np.Save()
return
}
// renewDHCP makes netplan realize the configuration that was just written, so a
// newly enabled DHCP client starts and acquires a lease. `netplan apply` is the
// only supported way in; it reconciles every interface, not just this one, and
// so is a no-op for the interfaces whose configuration did not change.
func (np *netplan) renewDHCP(ctx context.Context, _ string) error {
_, err := runCommand(ctx, "netplan", "apply")
return err
}
// 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
}