google_guest_agent/network/manager/wicked_linux.go (237 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 is responsible for detecting the current network manager service, and // writing and rolling back appropriate configurations for each network manager service. package manager import ( "context" "fmt" "os" "os/exec" "path/filepath" "regexp" "slices" "strings" "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg" "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/run" "github.com/GoogleCloudPlatform/guest-agent/utils" "github.com/GoogleCloudPlatform/guest-logging-go/logger" ) type wicked struct { // configDir is the directory to which to write configuration files. configDir string // wickedCommand contains the fully qualified path of wicked cli. wickedCommand string } const ( // defaultWickedConfigDir is the default location for wicked configuration files. defaultWickedConfigDir = "/etc/sysconfig/network" // defaultWickedCommand is the expected path to the wicked CLI. defaultWickedCommand = "/usr/sbin/wicked" ) // Name returns the name of this network manager service. func (n *wicked) Name() string { return "wicked" } // Configure gives the opportunity for the Service implementation to adjust its configuration // based on the Guest Agent configuration. func (n *wicked) Configure(ctx context.Context, config *cfg.Sections) { wickedCommand, err := exec.LookPath("wicked") if err != nil { logger.Infof("failed to find wicked path, falling back to default: %+v", err) wickedCommand = defaultWickedCommand } n.wickedCommand = wickedCommand } // IsManaging checks whether wicked is managing the provided interface. func (n *wicked) IsManaging(ctx context.Context, iface string) (bool, error) { // Check the current main network service. Primarily applicable to SUSE images. res := run.WithOutput(ctx, "systemctl", "status", "network.service") if strings.Contains(res.StdOut, "wicked.service") { return true, nil } // Check if the wicked service is running. res = run.WithOutput(ctx, "systemctl", "is-active", "wicked.service") if res.ExitCode != 0 { return false, nil } // Check the status of configured interfaces. res = run.WithOutput(ctx, n.wickedCommand, "ifstatus", iface, "--brief") if res.ExitCode != 0 { return false, fmt.Errorf("failed to check status of wicked configuration: %s", res.StdErr) } fields := strings.Fields(res.StdOut) if fields[1] == "up" || fields[1] == "setup-in-progress" { return true, nil } return false, nil } // SetupEthernetInterface writes the necessary configuration files for each interface and enables them. func (n *wicked) SetupEthernetInterface(ctx context.Context, cfg *cfg.Sections, nics *Interfaces) error { ifaces, err := interfaceNames(nics.EthernetInterfaces) if err != nil { return fmt.Errorf("failed to get network interfaces: %v", err) } changed, err := n.writeEthernetConfigs(ifaces) if err != nil { return fmt.Errorf("error writing wicked configurations: %v", err) } // https://manpages.opensuse.org/Tumbleweed/wicked/wicked.8.en.html#ifreload_-_checks_whether_a_configuration_has_changed,_and_applies_accordingly. // Only apply configuration changes for interfaces for which configurations // were written or changed. if len(changed) > 0 { args := append([]string{"ifreload"}, changed...) if err = run.Quiet(ctx, n.wickedCommand, args...); err != nil { return fmt.Errorf("error enabling interfaces: %v", err) } } return nil } // SetupVlanInterface writes the apppropriate vLAN interfaces configuration for the network manager service // for all configured interfaces. func (n *wicked) SetupVlanInterface(ctx context.Context, cfg *cfg.Sections, nics *Interfaces) error { var keepMe []string // Make sure the dhclient route priority is in a different range of ethernet nics. priority := 20200 for _, curr := range nics.VlanInterfaces { iface := fmt.Sprintf("gcp.%s.%d", curr.ParentInterfaceID, curr.Vlan) configLines := []string{ googleComment, "BOOTPROTO=dhcp", // NOTE: 'dhcp' is the dhcp4+dhcp6 option. "VLAN=yes", "ETHTOOL_OPTIONS=reorder_hdr off", fmt.Sprintf("DEVICE=%s", iface), fmt.Sprintf("MTU=%d", curr.MTU), fmt.Sprintf("LLADDR=%s", curr.Mac), fmt.Sprintf("ETHERDEVICE=%s", curr.ParentInterfaceID), fmt.Sprintf("VLAN_ID=%d", curr.Vlan), fmt.Sprintf("DHCLIENT_ROUTE_PRIORITY=%d", priority), } ifcfg, err := os.Create(n.ifcfgFilePath(iface)) if err != nil { return fmt.Errorf("failed to create vlan's ifcfg file: %+v", err) } content := strings.Join(configLines, "\n") writeLen, err := ifcfg.WriteString(content) if err != nil { return fmt.Errorf("error writing vlan's icfg file for %s: %v", iface, err) } contentLen := len(content) if writeLen != contentLen { return fmt.Errorf("error writing vlan's ifcfg file, wrote %d bytes, config content size is %d bytes", writeLen, contentLen) } if err = run.Quiet(ctx, n.wickedCommand, "ifup", iface); err != nil { return fmt.Errorf("error enabling vlan's interfaces: %v", err) } priority += 100 keepMe = append(keepMe, iface) } if err := n.cleanupVlanInterfaces(ctx, keepMe); err != nil { return fmt.Errorf("failed to cleanup vlan interfaces: %+v", err) } return nil } // cleanupVlanInterfaces removes vlan interfaces no longer configured, i.e. they were previously // added by the user but removed later. func (n *wicked) cleanupVlanInterfaces(ctx context.Context, keepMe []string) error { files, err := os.ReadDir(n.configDir) if err != nil { return fmt.Errorf("failed to read content from %s: %+v", n.configDir, err) } configExp := `(?P<prefix>ifcfg)-(?P<interface>gcp\..*\..*)` configRegex := regexp.MustCompile(configExp) for _, file := range files { var ( iface string found bool ) if file.IsDir() { continue } groups := utils.RegexGroupsMap(configRegex, file.Name()) // If we don't have a matching interface skip it. if iface, found = groups["interface"]; !found { continue } if slices.Contains(keepMe, iface) { continue } if err := n.removeInterface(ctx, iface); err != nil { return fmt.Errorf("failed to remove vlan interface: %+v", err) } } return nil } // writeEthernetConfigs writes config files for the given ifaces in the given configuration // directory. func (n *wicked) writeEthernetConfigs(ifaces []string) ([]string, error) { var priority = 10100 var changed []string // Write the config for all the non-primary network interfaces. for i, iface := range ifaces { if !shouldManageInterface(i == 0) { logger.Debugf("ManagePrimaryNIC is disabled, skipping wicked writeEthernetConfig for %s", iface) continue } if isInvalid(iface) { continue } logger.Debugf("write enabling ifcfg-%s config", iface) ifcfg := n.ifcfgFilePath(iface) // Avoid writing the configuration file if the configuration already exists. if utils.FileExists(ifcfg, utils.TypeFile) { logger.Infof("Wicked config %q already exists, will skip writing", ifcfg) continue } contents := []string{ googleComment, "STARTMODE=hotplug", // NOTE: 'dhcp' is the dhcp4+dhcp6 option. "BOOTPROTO=dhcp", fmt.Sprintf("DHCLIENT_ROUTE_PRIORITY=%d", priority), } contentBytes := []byte(strings.Join(contents, "\n")) // Write the file. if err := os.WriteFile(ifcfg, contentBytes, 0644); err != nil { return nil, fmt.Errorf("error writing config file for %s: %v", iface, err) } priority += 100 changed = append(changed, iface) } return changed, nil } // ifcfgFilePath gets the file path for the configuration file for the given interface. func (n *wicked) ifcfgFilePath(iface string) string { return filepath.Join(n.configDir, fmt.Sprintf("ifcfg-%s", iface)) } func (n *wicked) removeInterface(ctx context.Context, iface string) error { configFilePath := n.ifcfgFilePath(iface) // Check if the file exists. info, err := os.Stat(configFilePath) if err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("failed to stat wicked ifcfg file: %+v", err) } commentLen := len(googleComment) // We definetly don't manage this file, skip it. if info.Size() < int64(commentLen) { return nil } configFile, err := os.Open(configFilePath) if err != nil { return fmt.Errorf("failed to open wicked ifcfg file: %+v", err) } defer configFile.Close() buffer := make([]byte, commentLen) readSize, err := configFile.Read(buffer) if err != nil { return fmt.Errorf("failed to read google comment from wicked ifcfg file: %+v", err) } if readSize != commentLen { return fmt.Errorf("failed to read comment section, read %d bytes, expected to read %d", readSize, commentLen) } // This file is clearly not managed by us. if string(buffer) != googleComment { return nil } // Delete the ifcfg file. if err = os.Remove(configFilePath); err != nil { return fmt.Errorf("error deleting config file for %s: %v", iface, err) } // Reload for this interface. if err = run.Quiet(ctx, n.wickedCommand, "ifreload", iface); err != nil { return fmt.Errorf("error reloading config for %s: %v", iface, err) } return nil } // Rollback deletes all the ifcfg files written by Setup, then reloads wicked.service. func (n *wicked) Rollback(ctx context.Context, nics *Interfaces) error { if err := n.RollbackNics(ctx, nics); err != nil { return fmt.Errorf("failed to rollback wicked ethernet interfaces: %w", err) } for _, curr := range nics.VlanInterfaces { iface := fmt.Sprintf("%s.%d", curr.ParentInterfaceID, curr.Vlan) if err := n.removeInterface(ctx, iface); err != nil { return fmt.Errorf("failed to rollback wicked vlan ethernet interface: %+v", err) } } return nil } // Rollback deletes all the ifcfg files written by Setup for regular nics only, // then reloads wicked.service. func (n *wicked) RollbackNics(ctx context.Context, nics *Interfaces) error { ifaces, err := interfaceNames(nics.EthernetInterfaces) if err != nil { return fmt.Errorf("failed to get network interfaces: %v", err) } // Since configuration files are only written for non-primary, only check // for non-primary configuration files. for _, iface := range ifaces[1:] { if err := n.removeInterface(ctx, iface); err != nil { logger.Errorf("failed to rollback wicked ethernet interface: %+v", err) } } return nil }