google_guest_agent/network/manager/common.go (173 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" "errors" "fmt" "net" "os" "os/exec" "strings" "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-logging-go/logger" "github.com/go-ini/ini" "gopkg.in/yaml.v3" ) var ( badMAC = make(map[string]net.Interface) // execLookPath points to the function to check if a path exists. execLookPath = exec.LookPath ) func cliExists(name string) (bool, error) { _, err := execLookPath(name) if err == nil { return true, nil } if errors.Is(err, exec.ErrNotFound) { return false, nil } return false, fmt.Errorf("error looking up path for %q: %v", name, err) } // logInterfaceState logs all network interface state present on the machine. func logInterfaceState(ctx context.Context) { logger.Infof("Getting current interface state and routes") ifaces, err := net.Interfaces() if err != nil { logger.Warningf("Unable to get all interface: %v, will skip logging state", err) } for _, iface := range ifaces { addrs, err := iface.Addrs() if err != nil { logger.Warningf("Unable to get interface (%s) addresses: %v", iface.Name, err) } logger.Infof("Interface(%s), State: %+v, Addresses: %+v", iface.Name, iface, addrs) } res := run.WithOutput(ctx, "ip", "route") if res.ExitCode != 0 { logger.Warningf("Unable to get ip routes: %s", res.StdErr) return } logger.Infof("Currently present IP routes:\n %s", res.StdOut) } // interfaceNames extracts the names of the network interfaces from the provided list // of network interfaces. func interfaceNames(nics []metadata.NetworkInterfaces) ([]string, error) { var ifaces []string for _, ni := range nics { iface, err := GetInterfaceByMAC(ni.Mac) ifaceName := iface.Name if err != nil { if _, found := badMAC[ni.Mac]; !found { logger.Errorf("Error getting interface %s: %v", ni.Mac, err) badMAC[ni.Mac] = iface } // Mark the iface as invalid, and include its Mac. This is important // to avoid shifting indices if a MAC is invalid, which will cause // problems with network and VLAN setup. ifaceName = fmt.Sprintf("invalid-%s", ni.Mac) } ifaces = append(ifaces, ifaceName) } return ifaces, nil } // isInvalid checks if the provided interface is invalid. This is used to skip // writing configurations for interfaces that have been disabled or otherwise // made invalid. The `invalid` tag is added to ifaces whose MACs are invalid. // This logic is handled in the `interfaceListsIpv4Ipv6` function below. // // Marking an interface as invalid allows us to keep consistency with the lists // of interfaces returned by `interfaceNames` and `interfaceListsIpv4Ipv6`. In // cases where a NIC is disabled, skipping them would result in shifting of // indices, which will cause problems with both network setup (especially in cases // where the primary NIC is disabled for some reason) and VLAN setup, which depends // on properly pairing with respective NIC indices. // // For example, if the primary NIC was disabled, then the current network // setup implementation will start treating the first secondary NIC as the // primary NIC. In VLAN's case, a VLAN NIC may be improperly paired with the // wrong parent NIC. func isInvalid(iface string) bool { invalid := strings.Contains(iface, "invalid") if invalid { logger.Debugf("Invalid interface %s, skipping", iface) } return invalid } // interfaceListsIpv4Ipv6 gets a list of interface names. The first list is a list of all // interfaces, and the second list consists of only interfaces that support IPv6. func interfaceListsIpv4Ipv6(nics []metadata.NetworkInterfaces) ([]string, []string) { var googleInterfaces []string var googleIpv6Interfaces []string for _, ni := range nics { iface, err := GetInterfaceByMAC(ni.Mac) ifaceName := iface.Name if err != nil { if _, found := badMAC[ni.Mac]; !found { logger.Errorf("error getting interface: %s", err) badMAC[ni.Mac] = iface } // Mark the iface as invalid, and include its Mac. This is important // to avoid shifting indices if a MAC is invalid, which will cause // problems with network and VLAN setup. ifaceName = fmt.Sprintf("invalid-%s", ni.Mac) } if ni.DHCPv6Refresh != "" { googleIpv6Interfaces = append(googleIpv6Interfaces, ifaceName) } googleInterfaces = append(googleInterfaces, ifaceName) } return googleInterfaces, googleIpv6Interfaces } // interfacesMTUMap returns a map indexes by the interface's name with the MTU value // provided by the metadata descriptor. func interfacesMTUMap(nics []metadata.NetworkInterfaces) (map[string]int, error) { res := make(map[string]int) for _, ni := range nics { iface, err := GetInterfaceByMAC(ni.Mac) if err != nil { if _, found := badMAC[ni.Mac]; !found { logger.Errorf("error getting interface: %s", err) badMAC[ni.Mac] = iface } continue } res[iface.Name] = ni.MTU } return res, nil } // GetInterfaceByMAC gets the interface given the mac string. func GetInterfaceByMAC(mac string) (net.Interface, error) { hwaddr, err := net.ParseMAC(mac) if err != nil { return net.Interface{}, err } interfaces, err := net.Interfaces() if err != nil { return net.Interface{}, fmt.Errorf("failed to get interfaces: %v", err) } for _, iface := range interfaces { if iface.HardwareAddr.String() == hwaddr.String() { return iface, nil } } return net.Interface{}, fmt.Errorf("no interface found with MAC %s", mac) } // readIniFile reads and parses the content of filePath and loads it into ptr. func readIniFile(filePath string, ptr any) error { opts := ini.LoadOptions{ Loose: true, Insensitive: true, AllowShadows: true, } config, err := ini.LoadSources(opts, filePath) if err != nil { return fmt.Errorf("failed to load ini file file: %+v", err) } // Parse the config ini. if err = config.MapTo(ptr); err != nil { return fmt.Errorf("error parsing ini: %v", err) } return nil } // writeIniFile writes ptr data into filePath file marshalled in a ini file format. func writeIniFile(filePath string, ptr any) error { config := ini.Empty() if err := ini.ReflectFrom(config, ptr); err != nil { return fmt.Errorf("error creating .netdev config ini: %v", err) } if err := config.SaveTo(filePath); err != nil { return fmt.Errorf("error saving config: %v", err) } return nil } // writeYamlFile writes ptr data into filePath file marshalled as a yaml file format. func writeYamlFile(filePath string, ptr any) error { data, err := yaml.Marshal(ptr) if err != nil { return fmt.Errorf("error marshalling yaml file: %w", err) } if err := os.WriteFile(filePath, data, 0600); err != nil { return fmt.Errorf("error writing yaml file: %w", err) } return nil } // readYamlFile reads and parses the content of filePath and loads it into ptr. func readYamlFile(filepath string, ptr any) error { bytes, err := os.ReadFile(filepath) if err != nil { return fmt.Errorf("unable to read %q: %w", filepath, err) } return yaml.Unmarshal(bytes, ptr) } // isUbuntu1804 checks if agent is running on Ubuntu 18.04. This is a helper // method to support some exceptions we have for 18.04. func isUbuntu1804() bool { info := osinfo.Get() if info.OS == "ubuntu" && info.VersionID == "18.04" { return true } return false }