google_guest_agent/network/manager/netplan_linux.go (531 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package manager
import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg"
"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/osinfo"
"github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/run"
"github.com/GoogleCloudPlatform/guest-agent/metadata"
"github.com/GoogleCloudPlatform/guest-agent/utils"
"github.com/GoogleCloudPlatform/guest-logging-go/logger"
)
const (
// netplanEthernetSuffix is the ethernet drop-in's file suffix.
netplanEthernetSuffix = "ethernet"
// netplanVlanSuffix is the vlan drop-in's file suffix.
netplanVlanSuffix = "vlan"
// netplanConfigVersion defines the version we are using for netplan's drop-in
// files.
netplanConfigVersion = 2
)
// netplan is the netplan's Service interface implementation. From the guest agent's
// network manager perspective the current form only supports the
// systemd-networkd + netplan combination. Since netplan also supports NetworkManager
// as a backend we'll eventually support such a combination in the future.
type netplan struct {
// netplanConfigDir determines where the agent writes netplan configuration files.
netplanConfigDir string
// networkdDropinDir determines where the agent writes the networkd configuration files.
networkdDropinDir string
// priority dictates the priority with which guest-agent should write
// the configuration files.
priority int
// interfacePrefix is prefix used to override default netplan config. This prefix is
// used with netplan interface config keys in /run/netplan/20-google-guest-agent-ethernet.yaml
// and systemd drop-in directory name like /etc/systemd/network/10-netplan-a-ens4.network.d/
interfacePrefix string
}
// netplanDropin maps the netplan dropin configuration yaml entries/data
// structure.
type netplanDropin struct {
Network netplanNetwork `yaml:"network"`
}
// netplanNetwork is the netplan's drop-in network section.
type netplanNetwork struct {
// Version is the netplan's drop-in format version.
Version int `yaml:"version"`
// Ethernets are the ethernet configuration entries map.
Ethernets map[string]netplanEthernet `yaml:"ethernets,omitempty"`
// Vlans are the vlan interface's configuration entries map.
Vlans map[string]netplanVlan `yaml:"vlans,omitempty"`
}
// netplanEthernet describes the actual ethernet configuration. Refer
// https://netplan.readthedocs.io/en/stable/netplan-yaml/#properties-for-device-type-ethernets
// for more details.
type netplanEthernet struct {
// Match is the interface's matching rule.
Match netplanMatch `yaml:"match"`
// MTU defines the interface's MTU configuration.
MTU *int
// DHCPv4 determines if DHCPv4 support must be enabled to such an interface.
DHCPv4 *bool `yaml:"dhcp4,omitempty"`
DHCP4Overrides *netplanDHCPOverrides `yaml:"dhcp4-overrides,omitempty"`
// DHCPv6 determines if DHCPv6 support must be enabled to such an interface.
DHCPv6 *bool `yaml:"dhcp6,omitempty"`
DHCP6Overrides *netplanDHCPOverrides `yaml:"dhcp6-overrides,omitempty"`
}
// netplanDHCPOverrides sets the netplan dhcp-overrides configuration.
type netplanDHCPOverrides struct {
// When true, the domain name received from the DHCP server will be used as DNS
// search domain over this link.
UseDomains *bool `yaml:"use-domains,omitempty"`
}
// netplanMatch contains the keys uses to match an interface.
type netplanMatch struct {
// Name is the key used to match an interface by its name.
Name string `yaml:"name"`
}
// netplanVlan describes the netplan's vlan interface configuration.
// Refer https://netplan.readthedocs.io/en/stable/netplan-yaml/#properties-for-device-type-vlans
// for more details.
type netplanVlan struct {
// ID is the the VLAN ID.
ID int `yaml:"id,omitempty"`
// Link is the vlan's parent interface.
Link string `yaml:"link,omitempty"`
// DHCPv4 determines if DHCPv4 support must be enabled to such an interface.
DHCPv4 *bool `yaml:"dhcp4,omitempty"`
// DHCPv6 determines if DHCPv6 support must be enabled to such an interface.
DHCPv6 *bool `yaml:"dhcp6,omitempty"`
// OverrideMacAddress sets the device’s MAC address. By default it will use same as
// parent NIC.
OverrideMacAddress string `yaml:"macaddress,omitempty"`
// MTU sets the MTU for interface. The default is 1500.
MTU int `yaml:"mtu,omitempty"`
// DHCP4Overrides sets DHCP4 overrides for the vlan.
DHCP4Overrides *netplanDHCPOverrides `yaml:"dhcp4-overrides,omitempty"`
// DHCP6Overrides sets DHCP6 overrides for the vlan.
DHCP6Overrides *netplanDHCPOverrides `yaml:"dhcp6-overrides,omitempty"`
}
// networkdNetplanDropin maps systemd-networkd's overriding drop-in if networkd
// is present.
type networkdNetplanDropin struct {
// Match is the systemd-networkd ini file's [Match] section.
Match systemdMatchConfig
// Network is the systemd-networkd ini file's [Network] section.
Network systemdNetworkConfig `ini:"Network"`
// DHCPv4 is the systemd-networkd ini file's [DHCPv4] section.
DHCPv4 *systemdDHCPConfig `ini:",omitempty"`
}
// Name returns the name of the network manager service.
func (n *netplan) Name() string {
return "netplan"
}
// Configure gives the opportunity for the Service implementation to adjust its configuration
// based on the Guest Agent configuration.
func (n *netplan) Configure(ctx context.Context, config *cfg.Sections) {
os := osinfo.Get()
// Debian 12 has a pretty generic matching netplan configuration for gce,
// regex in /etc/netplan/90-default.yaml matches all en* and eth* nics.
// Until we have that changed we are adjusting the configuration so we can
// override the defaults.
if os.OS == "debian" && os.Version.Major == 12 {
n.interfacePrefix = "a"
logger.Infof("Setting up Debian 12, overriding interface prefix with: %q", n.interfacePrefix)
}
}
// IsManaging checks whether netplan is present in the system.
func (n *netplan) IsManaging(ctx context.Context, iface string) (bool, error) {
if isUbuntu1804() {
logger.Infof("Running on Ubuntu 18.04, skipping use of netplan, falling back to dhclient")
return false, nil
}
// Check if the netplan CLI exists.
return cliExists("netplan")
}
// SetupEthernetInterface sets the network interfaces for netplan by writing drop-in files to the specified
// configuration directory.
func (n *netplan) SetupEthernetInterface(ctx context.Context, config *cfg.Sections, nics *Interfaces) error {
// Create a network configuration file with default configurations for each network interface.
googleInterfaces, googleIpv6Interfaces := interfaceListsIpv4Ipv6(nics.EthernetInterfaces)
mtuMap, err := interfacesMTUMap(nics.EthernetInterfaces)
if err != nil {
return fmt.Errorf("error listing interface's MTU configuration: %w", err)
}
// Write the config files.
reload1, err := n.writeNetplanEthernetDropin(mtuMap, googleInterfaces, googleIpv6Interfaces)
if err != nil {
return fmt.Errorf("error writing network configs: %v", err)
}
// If we are running netplan+systemd-networkd we try to write networkd's drop-in for configs
// not mapped/supported by netplan.
reload2, err := n.writeNetworkdDropin(googleInterfaces, googleIpv6Interfaces)
if err != nil {
return fmt.Errorf("error writing systemd-networkd's drop-in: %v", err)
}
// Avoid unnecessary reloads, if we've really updated some config then only do a reload.
if reload1 || reload2 {
if err := n.reloadConfigs(ctx); err != nil {
return fmt.Errorf("error applying ethernet interface configs: %w", err)
}
}
return nil
}
// reloadConfigs triggers config reload to make sure ethernet/vlan configs are written
// on disk are applied by netplan.
func (n *netplan) reloadConfigs(ctx context.Context) error {
logger.Infof("Reloading netplan configs...")
// Avoid restarting netplan.
if err := run.Quiet(ctx, "netplan", "generate"); err != nil {
return fmt.Errorf("error generating netplan based config: %w", err)
}
// Avoid restarting systemd-networkd.
if err := run.Quiet(ctx, "networkctl", "reload"); err != nil {
return fmt.Errorf("error reloading systemd-networkd network configs: %v", err)
}
return nil
}
// writeNetworkdDropin writes the overloading network-manager's drop-in file for the configurations
// not supported by netplan.
func (n *netplan) writeNetworkdDropin(interfaces, ipv6Interfaces []string) (bool, error) {
var requiresReload bool
stat, err := os.Stat(n.networkdDropinDir)
if err != nil {
return false, fmt.Errorf("failed to stat systemd-networkd's drop-in root dir: %w", err)
}
if !stat.IsDir() {
return false, fmt.Errorf("systemd-networkd drop-in dir(%s) is not a dir", n.networkdDropinDir)
}
for i, iface := range interfaces {
if !shouldManageInterface(i == 0) {
logger.Debugf("ManagePrimaryNIC is disabled, skipping writeNetworkdDropin for %s", iface)
continue
}
if isInvalid(iface) {
continue
}
logger.Debugf("writing systemd-networkd drop-in config for %s", iface)
var dhcp = "ipv4"
if slices.Contains(ipv6Interfaces, iface) {
dhcp = "yes"
}
// Create and setup ini file.
data := networkdNetplanDropin{
Match: systemdMatchConfig{
Name: iface,
},
Network: systemdNetworkConfig{
DNSDefaultRoute: true,
DHCP: dhcp,
},
}
// We are only interested on DHCP offered routes on the primary nic,
// ignore it for the secondary ones.
if i != 0 {
data.Network.DNSDefaultRoute = false
data.DHCPv4 = &systemdDHCPConfig{
RoutesToDNS: false,
RoutesToNTP: false,
}
}
wrote, err := data.write(n, iface)
if err != nil {
return false, fmt.Errorf("failed to write systemd drop-in config: %w", err)
}
if wrote {
requiresReload = true
}
}
return requiresReload, nil
}
// networkdDropinFile returns an interface's netplan drop-in file path.
func (n *netplan) networkdDropinFile(iface string) string {
// We are hardcoding the netplan priority to 10 since we are deriving the netplan
// networkd configuration name based on the interface name only - aligning with
// the commonly used value for netplan.
if n.interfacePrefix != "" {
return filepath.Join(n.networkdDropinDir, fmt.Sprintf("10-netplan-%s-%s.network.d", n.interfacePrefix, iface), "override.conf")
}
return filepath.Join(n.networkdDropinDir, fmt.Sprintf("10-netplan-%s.network.d", iface), "override.conf")
}
// isSame unmarshals netplan networkd dropin config from cfgFile and compares it with
// own instance. If it fails to read it returns false to allow caller to try
// overwriting and fix any issues if file already exists.
func (nd networkdNetplanDropin) isSame(cfgFile string) bool {
existingCfgs := networkdNetplanDropin{}
if err := readIniFile(cfgFile, &existingCfgs); err != nil {
logger.Debugf("Failed to read %q while comparing netplan networkd dropins with error: %v", cfgFile, err)
return false
}
return reflect.DeepEqual(nd, existingCfgs)
}
// write writes systemd's drop-in config file.
func (nd networkdNetplanDropin) write(n *netplan, iface string) (bool, error) {
dropinFile := n.networkdDropinFile(iface)
logger.Infof("writing systemd drop in to: %s", dropinFile)
dropinDir := filepath.Dir(dropinFile)
if err := os.MkdirAll(dropinDir, 0755); err != nil {
return false, fmt.Errorf("failed to create networkd dropin dir: %w", err)
}
if nd.isSame(dropinFile) {
logger.Infof("Exact same config already exists at location %q, skipping overwriting to avoid network reload", dropinFile)
return false, nil
}
if err := writeIniFile(dropinFile, &nd); err != nil {
return false, fmt.Errorf("error saving netword drop-in file for %s: %v", iface, err)
}
return true, nil
}
// shouldUseDomains returns true if interface index is 0.
func shouldUseDomains(idx int) *bool {
res := idx == 0
return &res
}
// writeNetplanEthernetDropin selects the ethernet configuration, transforms it
// into a netplan dropin format and writes it down to the netplan's drop-in directory.
func (n *netplan) writeNetplanEthernetDropin(mtuMap map[string]int, interfaces, ipv6Interfaces []string) (bool, error) {
dropin := netplanDropin{
Network: netplanNetwork{
Version: netplanConfigVersion,
Ethernets: make(map[string]netplanEthernet),
},
}
for i, iface := range interfaces {
if !shouldManageInterface(i == 0) {
logger.Debugf("ManagePrimaryNIC is disabled, skipping writeNetplanEthernetDropin for %s", iface)
continue
}
if isInvalid(iface) {
continue
}
logger.Debugf("Adding %s(%d) to drop-in configuration.", iface, i)
trueVal := true
ne := netplanEthernet{
Match: netplanMatch{Name: iface},
DHCPv4: &trueVal,
DHCP4Overrides: &netplanDHCPOverrides{
UseDomains: shouldUseDomains(i),
},
}
if mtu, found := mtuMap[iface]; found {
ne.MTU = &mtu
}
if slices.Contains(ipv6Interfaces, iface) {
ne.DHCPv6 = &trueVal
ne.DHCP6Overrides = &netplanDHCPOverrides{
UseDomains: shouldUseDomains(i),
}
}
key := n.ID(iface)
dropin.Network.Ethernets[key] = ne
}
// This can happen if its a single NIC VM and primary NIC is not managed
// by Guest Agent. No need to write a file with just version in [dropin].
if len(dropin.Network.Ethernets) == 0 {
logger.Infof("No NICs to configure, skipping writeNetplanEthernetDropin")
return false, nil
}
wrote, err := n.write(dropin, netplanEthernetSuffix)
if err != nil {
return false, fmt.Errorf("failed to write netplan ethernet drop-in config: %+v", err)
}
return wrote, nil
}
// ID returns the Netplan ID used for referencing parent NIC in VLAN NIC
// configuration and the key in ethernet based NIC configuration.
func (n *netplan) ID(iface string) string {
key := iface
if n.interfacePrefix != "" {
key = fmt.Sprintf("%s-%s", n.interfacePrefix, iface)
}
return key
}
// isSame unmarshals netplan dropin config from cfgFile and compares it with
// own instance. If it fails to read it returns false to allow caller to try
// overwriting and fix any issues if file already exists.
func (nd netplanDropin) isSame(cfgFile string) bool {
existingDropin := netplanDropin{}
if err := readYamlFile(cfgFile, &existingDropin); err != nil {
logger.Debugf("Failed to read %q while comparing netplan dropins with error: %v", cfgFile, err)
return false
}
return reflect.DeepEqual(nd, existingDropin)
}
// write writes the netplan dropin file.
func (n *netplan) write(nd netplanDropin, suffix string) (bool, error) {
dropinFile := n.dropinFile(suffix)
dropinDir := filepath.Dir(dropinFile)
err := os.MkdirAll(dropinDir, 0755)
if err != nil {
return false, fmt.Errorf("failed to create networkd dropin dir: %w", err)
}
if nd.isSame(dropinFile) {
logger.Infof("Exact same config already exists at location %q, skipping overwriting to avoid network reload", dropinFile)
return false, nil
}
if err := writeYamlFile(dropinFile, &nd); err != nil {
return false, fmt.Errorf("error saving netplan drop-in file %s: %w", dropinFile, err)
}
return true, nil
}
// dropinFile returns the netplan drop-in file.
// Priority is lexicographically sorted in ascending order by file name. So a configuration
// starting with '1-' takes priority over a configuration file starting with '10-'. Setting
// a priority of 1 allows the guest-agent to override any existing default configurations
// while also allowing users the freedom of using priorities of '0...' to override the
// agent's own configurations.
func (n *netplan) dropinFile(suffix string) string {
return filepath.Join(n.netplanConfigDir, fmt.Sprintf("%d-google-guest-agent-%s.yaml", n.priority, suffix))
}
func (n *netplan) vlanInterfaceName(parentInterface string, vlanID int) string {
return fmt.Sprintf("gcp.%s.%d", parentInterface, vlanID)
}
// SetupVlanInterface writes the apppropriate vLAN interfaces netplan configuration.
func (n *netplan) SetupVlanInterface(ctx context.Context, config *cfg.Sections, nics *Interfaces) error {
var reload1, reload2, reload3 bool
var err error
toRemove, err := n.findVlanDiff(nics)
if err != nil {
return fmt.Errorf("unable to detect vlan nics to delete: %w", err)
}
if toRemove != nil {
reload1, err = n.rollbackVlanNics(ctx, toRemove)
if err != nil {
return fmt.Errorf("unable to remove vlan interfaces (%+v): %w", toRemove, err)
}
}
reload2, err = n.writeNetplanVLANDropin(nics)
if err != nil {
return fmt.Errorf("unable to write netplan VLAN dropin: %w", err)
}
reload3, err = n.writeNetworkdVLANDropin(nics)
if err != nil {
return fmt.Errorf("unable to write netplan networkd VLAN dropin: %w", err)
}
if reload1 || reload2 || reload3 {
if err = n.reloadConfigs(ctx); err != nil {
return fmt.Errorf("error applying vlan interface configs: %w", err)
}
}
return nil
}
// interfaceFromLink gets the interface name from link name in netplan config.
// Link name in some cases might not be same as interface name as we prefix with "a"
// for precedence.
func (n *netplan) interfaceFromLink(link string) string {
iface := link
if n.interfacePrefix != "" {
iface = strings.TrimPrefix(iface, fmt.Sprintf("%s-", n.interfacePrefix))
}
return iface
}
// findVlanDiff compares expectedNics with one configured with netplan config on disk
// and returns only the vlan interfaces to delete.
func (n *netplan) findVlanDiff(expectedNics *Interfaces) (*Interfaces, error) {
keepInterfaces := make(map[string]string)
toRemove := Interfaces{VlanInterfaces: make(map[string]VlanInterface)}
existingVlanCfgs := netplanDropin{}
netplanVlanDropinFile := n.dropinFile(netplanVlanSuffix)
// There's no config file per interface, single netplan config file lists all the interfaces.
if !utils.FileExists(netplanVlanDropinFile, utils.TypeFile) {
logger.Infof("File %q does not exist, nothing to rollback", netplanVlanDropinFile)
return nil, nil
}
if err := readYamlFile(netplanVlanDropinFile, &existingVlanCfgs); err != nil {
return nil, fmt.Errorf("unable to read %q trying rollback configs: %w", netplanVlanDropinFile, err)
}
if len(existingVlanCfgs.Network.Vlans) == 0 {
logger.Debugf("No existing VLAN configs found at %q, skipping rollback", netplanVlanDropinFile)
return nil, nil
}
for _, iface := range expectedNics.VlanInterfaces {
// Set netplan vlan drop-in file for removal.
ifaceName := n.vlanInterfaceName(iface.ParentInterfaceID, iface.Vlan)
key := n.ID(ifaceName)
keepInterfaces[key] = ifaceName
}
for vlanKey, vlan := range existingVlanCfgs.Network.Vlans {
_, ok := keepInterfaces[vlanKey]
if !ok {
parentID := n.interfaceFromLink(vlan.Link)
vlanID := fmt.Sprintf("%s-%d", parentID, vlan.ID)
toRemove.VlanInterfaces[vlanID] = VlanInterface{
ParentInterfaceID: parentID,
VlanInterface: metadata.VlanInterface{
Vlan: vlan.ID,
},
}
}
}
return &toRemove, nil
}
// rollbackVlanNics removes the [nics] and its config (netplan and networkd dropin both) on disk.
func (n *netplan) rollbackVlanNics(ctx context.Context, nics *Interfaces) (bool, error) {
var deleteNics []string
var deleteDirs []string
if len(nics.VlanInterfaces) == 0 {
logger.Debugf("No VLAN interfaces in args, skipping rollback")
return false, nil
}
existingVlanCfgs := netplanDropin{}
netplanVlanDropinFile := n.dropinFile(netplanVlanSuffix)
// There's no config file per interface, single netplan config file lists all the interfaces.
if utils.FileExists(netplanVlanDropinFile, utils.TypeFile) {
if err := readYamlFile(netplanVlanDropinFile, &existingVlanCfgs); err != nil {
return false, fmt.Errorf("unable to read %q trying rollback configs: %w", netplanVlanDropinFile, err)
}
}
for _, iface := range nics.VlanInterfaces {
ifaceName := n.vlanInterfaceName(iface.ParentInterfaceID, iface.VlanInterface.Vlan)
key := n.ID(ifaceName)
deleteNics = append(deleteNics, key)
delete(existingVlanCfgs.Network.Vlans, key)
dropin := filepath.Join(n.networkdDropinDir, fmt.Sprintf("10-netplan-%s.network.d", key))
if utils.FileExists(dropin, utils.TypeDir) {
// Networkd dropin is a directory for each interface with override.conf file.
// Remove should delete the complete directory instead of that one file.
deleteDirs = append(deleteDirs, dropin)
}
}
logger.Infof("Deleting VLAN NICs: %v", deleteNics)
// Simply removing configs on disk and reloading netplan/networkctl doesn't remove
// existing vlan nics, it requires instance reboot or systemd-networkd restart. Instead,
// make sure its removed by [networkctl delete <interfaces>] command.
args := []string{"delete"}
args = append(args, deleteNics...)
if err := run.Quiet(ctx, "networkctl", args...); err != nil {
return false, fmt.Errorf("networkctl %v failed with error: %w", args, err)
}
// If no more VLANs exist simply remove the file.
if len(existingVlanCfgs.Network.Vlans) == 0 {
logger.Infof("Removing %s dropin file for vlan rollback", netplanVlanDropinFile)
if err := os.Remove(netplanVlanDropinFile); err != nil {
return false, fmt.Errorf("unable to remove netplan vlan dropin (%s): %w", netplanVlanDropinFile, err)
}
} else {
logger.Infof("Updating %s dropin file for vlan rollback", netplanVlanDropinFile)
// Otherwise, overwrite configs to reflect expected interfaces.
if _, err := n.write(existingVlanCfgs, netplanVlanSuffix); err != nil {
return false, fmt.Errorf("unable to update vlan config at (%s): %w", netplanVlanDropinFile, err)
}
}
logger.Infof("Removing directories %v as part of vlan rollback", deleteDirs)
for _, dir := range deleteDirs {
if err := os.RemoveAll(dir); err != nil {
return false, fmt.Errorf("unable to remove directory %q: %w", dir, err)
}
}
return true, nil
}
func (n *netplan) writeNetplanVLANDropin(nics *Interfaces) (bool, error) {
dropin := netplanDropin{
Network: netplanNetwork{
Version: netplanConfigVersion,
Vlans: make(map[string]netplanVlan),
},
}
for _, curr := range nics.VlanInterfaces {
iface := n.vlanInterfaceName(curr.ParentInterfaceID, curr.Vlan)
logger.Debugf("Adding %s(%d) to drop-in configuration.", iface, curr.Vlan)
trueVal := true
falseVal := false
nv := netplanVlan{
ID: curr.Vlan,
Link: n.ID(curr.ParentInterfaceID),
DHCPv4: &trueVal,
OverrideMacAddress: curr.Mac,
MTU: curr.MTU,
DHCP4Overrides: &netplanDHCPOverrides{UseDomains: &falseVal},
DHCP6Overrides: &netplanDHCPOverrides{UseDomains: &falseVal},
}
if len(curr.IPv6) > 0 {
nv.DHCPv6 = &trueVal
}
key := n.ID(iface)
dropin.Network.Vlans[key] = nv
}
if len(nics.VlanInterfaces) == 0 {
return false, nil
}
wrote, err := n.write(dropin, netplanVlanSuffix)
if err != nil {
return false, fmt.Errorf("failed to write netplan vlan drop-in config: %+v", err)
}
return wrote, nil
}
func (n *netplan) writeNetworkdVLANDropin(nics *Interfaces) (bool, error) {
var reload bool
stat, err := os.Stat(n.networkdDropinDir)
if err != nil {
return false, fmt.Errorf("failed to stat systemd-networkd's drop-in root dir: %w", err)
}
if !stat.IsDir() {
return false, fmt.Errorf("systemd-networkd drop-in dir(%s) is not a dir", n.networkdDropinDir)
}
for _, iface := range nics.VlanInterfaces {
logger.Debugf("writing systemd-networkd drop-in config for VLAN ID: %d", iface.Vlan)
var dhcp = "ipv4"
if iface.DHCPv6Refresh != "" {
dhcp = "yes"
}
ifaceName := n.vlanInterfaceName(iface.ParentInterfaceID, iface.Vlan)
matchID := n.ID(ifaceName)
// Create and setup ini file.
data := networkdNetplanDropin{
Match: systemdMatchConfig{
Name: matchID,
},
Network: systemdNetworkConfig{
DNSDefaultRoute: false,
DHCP: dhcp,
},
DHCPv4: &systemdDHCPConfig{
RoutesToDNS: false,
RoutesToNTP: false,
},
}
wrote, err := data.write(n, ifaceName)
if err != nil {
return false, fmt.Errorf("failed to write systemd drop-in config for VLAN ID(%s): %w", ifaceName, err)
}
if wrote {
reload = true
}
}
return reload, nil
}
// rollbackConfigs is the low level implementation for Rollback and RollbackNics interface.
// If removeVlan is true both regular nics and vlan nics are rolled back.
func (n *netplan) rollbackConfigs(ctx context.Context, nics *Interfaces, removeVlan bool) error {
var reload bool
interfaces, err := interfaceNames(nics.EthernetInterfaces)
if err != nil {
return fmt.Errorf("failed to get list of interface names: %v", err)
}
netplanEthernetDropinFile := n.dropinFile(netplanEthernetSuffix)
existingEthernetCfgs := netplanDropin{}
if utils.FileExists(netplanEthernetDropinFile, utils.TypeFile) {
if err := readYamlFile(netplanEthernetDropinFile, &existingEthernetCfgs); err != nil {
return fmt.Errorf("unable to read %q trying rollback configs: %w", netplanEthernetDropinFile, err)
}
}
var deleteMe []string
for _, iface := range interfaces {
// Set networkd drop-in override file for removal.
networkdDropinFile := n.networkdDropinFile(iface)
deleteMe = append(deleteMe, networkdDropinFile)
// Set netplan ethernet drop-in file for removal.
if _, ok := existingEthernetCfgs.Network.Ethernets[iface]; ok {
deleteMe = append(deleteMe, netplanEthernetDropinFile)
}
}
if removeVlan {
if done, err := n.rollbackVlanNics(ctx, nics); err != nil {
logger.Debugf("Failed to remove vlan interfaces: %v", err)
} else {
if done {
reload = true
}
}
}
for _, configFile := range deleteMe {
logger.Debugf("Removing config file: %q", configFile)
if err := os.Remove(configFile); err != nil {
if !os.IsNotExist(err) {
logger.Debugf("Failed to remove drop-in file(%s): %s", configFile, err)
} else {
logger.Debugf("No such drop-in file(%s), ignoring.", configFile)
}
continue
}
reload = true
}
if !reload {
return nil
}
if err := n.reloadConfigs(ctx); err != nil {
return fmt.Errorf("error reloading configs: %v", err)
}
return nil
}
// Rollback deletes the ethernet and VLAN interfaces netplan drop-in files.
func (n *netplan) Rollback(ctx context.Context, nics *Interfaces) error {
logger.Infof("rolling back changes for %s", n.Name())
return n.rollbackConfigs(ctx, nics, true)
}
// Rollback deletes the ethernet interfaces netplan drop-in files - only
// regular nics are handled.
func (n *netplan) RollbackNics(ctx context.Context, nics *Interfaces) error {
logger.Infof("rolling back regular ethernet changes for %s", n.Name())
return n.rollbackConfigs(ctx, nics, false)
}