google_guest_agent/network/manager/manager.go (232 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" "net" "os" "reflect" "time" "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg" "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/osinfo" "github.com/GoogleCloudPlatform/guest-agent/metadata" "github.com/GoogleCloudPlatform/guest-agent/utils" "github.com/GoogleCloudPlatform/guest-logging-go/logger" ) // Service is an interface for setting up network configurations // using different network managing services, such as systemd-networkd and wicked. type Service interface { // Configure gives the opportunity for the Service implementation to adjust its configuration // based on the Guest Agent configuration. Configure(ctx context.Context, config *cfg.Sections) // IsManaging checks whether this network manager service is managing the provided interface. IsManaging(ctx context.Context, iface string) (bool, error) // Name is the name of the network manager service. Name() string // SetupEthernetInterface writes the appropriate configurations for the network manager service for all // non-primary network interfaces. SetupEthernetInterface(ctx context.Context, config *cfg.Sections, nics *Interfaces) error // SetupVlanInterface writes the apppropriate vLAN interfaces configuration for the network manager service // for all configured interfaces. SetupVlanInterface(ctx context.Context, config *cfg.Sections, nics *Interfaces) error // Rollback rolls back the changes created in Setup. Rollback(ctx context.Context, nics *Interfaces) error // RollbackNics rolls back only changes to regular nics (vlan nics are not handled). RollbackNics(ctx context.Context, nics *Interfaces) error } // serviceStatus is an internal wrapper of a service implementation and its status. type serviceStatus struct { // manager is the network manager implementation. manager Service // active indicates this service is active/present in the system. active bool } // VlanInterface are [metadata.VlanInterface] offered by MDS with derived Parent Interface // name added to it for convenience. type VlanInterface struct { metadata.VlanInterface // ParentInterfaceID is the interface name on the host. All network managers should refer // this interface name instead of one present in [metadata.VlanInterface] which is just an // index to interface in [EthernetInterfaces] ParentInterfaceID string } // Interfaces wraps both ethernet and vlan interfaces. type Interfaces struct { // EthernetInterfaces are the regular ethernet interfaces descriptors offered by metadata. EthernetInterfaces []metadata.NetworkInterfaces // VlanInterfaces are the vLAN interfaces descriptors offered by metadata. VlanInterfaces map[string]VlanInterface } // guestAgentSection is the section added to guest-agent-written ini files to indicate // that the ini file is managed by the agent. type guestAgentSection struct { // ManagedByGuestAgent indicates whether this ini file is managed by the agent. ManagedByGuestAgent bool } const ( googleComment = "# Added by Google Compute Engine Guest Agent." debian12NetplanFile = "/etc/netplan/90-default.yaml" debian12NetplanConfig = `network: version: 2 ethernets: all-en: match: name: en* dhcp4: true dhcp4-overrides: use-domains: true dhcp6: true dhcp6-overrides: use-domains: true all-eth: match: name: eth* dhcp4: true dhcp4-overrides: use-domains: true dhcp6: true dhcp6-overrides: use-domains: true ` ) var ( // knownNetworkManagers contains the list of known network managers. This is // used to determine the network manager service that is managing the primary // network interface. knownNetworkManagers []Service // osinfoGet points to the function to use for getting osInfo. // Primarily used for testing. osinfoGet = osinfo.Get // seenMetadata keeps a copy of MDS descriptor that was already seen and applied // in terms of VLAN/Ethernet NIC configuration by the manager. seenMetadata *metadata.Descriptor ) // detectNetworkManager detects the network manager managing the primary network interface. // This network manager will be used to set up primary and secondary network interfaces. func detectNetworkManager(ctx context.Context, iface string) (*serviceStatus, error) { logger.Infof("Detecting network manager...") for _, curr := range knownNetworkManagers { active, err := curr.IsManaging(ctx, iface) if err != nil { return nil, err } if active { return &serviceStatus{manager: curr, active: active}, nil } } return nil, fmt.Errorf("no network manager impl found for %s", iface) } // reformatVlanNics reads VLAN NIC information from metadata descriptor and formats // it into [Interfaces.VlanInterfaces] that every network manager understands. func reformatVlanNics(mds *metadata.Descriptor, nics *Interfaces, ethernetInterfaces []string) error { for parentID, vlans := range mds.Instance.VlanNetworkInterfaces { if parentID >= len(ethernetInterfaces) { return fmt.Errorf("invalid parent index(%d), known interfaces count: %d", parentID, len(ethernetInterfaces)) } for vlanID, vlan := range vlans { mapID := fmt.Sprintf("%d-%d", parentID, vlanID) nics.VlanInterfaces[mapID] = VlanInterface{VlanInterface: vlan, ParentInterfaceID: ethernetInterfaces[parentID]} } } return nil } // SetupInterfaces sets up all secondary network interfaces on the system, and primary network // interface if enabled in the configuration using the native network manager service detected // to be managing the primary network interface. func SetupInterfaces(ctx context.Context, config *cfg.Sections, mds *metadata.Descriptor) error { if seenMetadata != nil { diff := reflect.DeepEqual(mds.Instance.NetworkInterfaces, seenMetadata.Instance.NetworkInterfaces) && reflect.DeepEqual(mds.Instance.VlanNetworkInterfaces, seenMetadata.Instance.VlanNetworkInterfaces) if diff { logger.Debugf("MDS returned Ethernet NICs [%+v] and VLAN NICs [%+v] are already seen and applied, skipping", seenMetadata.Instance.NetworkInterfaces, seenMetadata.Instance.VlanNetworkInterfaces) return nil } } // User may have disabled network interface setup entirely. if !config.NetworkInterfaces.Setup { logger.Infof("Network interface setup disabled, skipping...") return nil } nics := &Interfaces{ EthernetInterfaces: mds.Instance.NetworkInterfaces, VlanInterfaces: map[string]VlanInterface{}, } interfaces, err := interfaceNames(nics.EthernetInterfaces) if err != nil { return fmt.Errorf("error getting interface names: %v", err) } primaryInterface := interfaces[0] // Get the network manager. activeService, err := detectNetworkManager(ctx, primaryInterface) if err != nil { return fmt.Errorf("error detecting network manager service: %v", err) } if err := rollbackLeftoverConfigs(ctx, config, mds); err != nil { logger.Errorf("Failed to rollback left over configs: %v", err) } // Attempt to rollback any left over configuration of non active network managers. for _, svc := range knownNetworkManagers { if svc == activeService.manager { continue } logger.Infof("Rolling back %s", svc.Name()) if err = svc.Rollback(ctx, nics); err != nil { logger.Warningf("Unable to roll back config for %s: %v", svc.Name(), err) } } // Attempt to configure all present/active network managers. activeService.manager.Configure(ctx, config) logger.Infof("Setting up %s", activeService.manager.Name()) if err = activeService.manager.SetupEthernetInterface(ctx, config, nics); err != nil { return fmt.Errorf("manager(%s): error setting up ethernet interfaces: %v", activeService.manager.Name(), err) } if config.NetworkInterfaces.VlanSetupEnabled { logger.Infof("VLAN setup is enabled via config file, setting up interfaces") if err := reformatVlanNics(mds, nics, interfaces); err != nil { return fmt.Errorf("unable to read vlans, invalid format: %w", err) } if err = activeService.manager.SetupVlanInterface(ctx, config, nics); err != nil { return fmt.Errorf("manager(%s): error setting up vlan interfaces: %v", activeService.manager.Name(), err) } } logger.Infof("Finished setting up %s", activeService.manager.Name()) go func() { // Setup might not have finished when we log and collect this information. Adding this // temporary sleep for debugging purposes to make sure we have up-to-date information. time.Sleep(2 * time.Second) logInterfaceState(ctx) }() seenMetadata = mds return nil } // Remove only primary nics left over configs. func rollbackLeftoverConfigs(ctx context.Context, config *cfg.Sections, mds *metadata.Descriptor) error { // If we are running debian 12 and failed to restore default netplan config // we should not rollback dangling/left over configs. if err := restoreDebian12NetplanConfig(config); err != nil { return fmt.Errorf("Failed to restore debian-12 netplan configuration: %v", err) } // If we are managing primary nics we don't want to rollback "dangling/left over" configs // since we are actually managing them. if config.NetworkInterfaces.ManagePrimaryNIC { return nil } primaryInterface := mds.Instance.NetworkInterfaces[0] nic := &Interfaces{ EthernetInterfaces: []metadata.NetworkInterfaces{primaryInterface}, } for _, svc := range knownNetworkManagers { if err := svc.RollbackNics(ctx, nic); err != nil { logger.Warningf("Failed to rollback primary nic (left over) config for %s: %v", svc.Name(), err) } } return nil } // restoreDebian12NetplanConfig recreates the default netplan configuration // for debian-12 in case user hasn't disabled it and the running system is // indeed a debian-12 system. func restoreDebian12NetplanConfig(config *cfg.Sections) error { if !config.NetworkInterfaces.RestoreDebian12NetplanConfig { logger.Debugf("User provided configuration requested to skip debian-12 netplan configuration") return nil } osDesc := osinfo.Get() if osDesc.OS != "debian" || osDesc.Version.Major != 12 { logger.Debugf("Not running a debian-12 system, skipping netplan configuration restore") return nil } if _, err := os.Stat(debian12NetplanFile); err != nil { if !os.IsNotExist(err) { return err } if err := utils.WriteFile([]byte(debian12NetplanConfig), debian12NetplanFile, 0644); err != nil { return fmt.Errorf("Failed to recreate default netplan config: %w", err) } logger.Debugf("Recreated default netplan config...") } return nil } // FallbackToDefault will attempt to rescue broken networking by rolling back // all guest-agent modifications to the network configuration. func FallbackToDefault(ctx context.Context) error { nics, err := buildInterfacesFromAllPhysicalNICs() if err != nil { return fmt.Errorf("could not build list of NICs for fallback: %v", err) } // Rollback every NIC with every known network manager. for _, svc := range knownNetworkManagers { logger.Infof("Rolling back %s", svc.Name()) if err := svc.Rollback(ctx, nics); err != nil { logger.Warningf("Failed to roll back config for %s: %v", svc.Name(), err) } } return nil } // Build a *Interfaces from all physical interfaces rather than the MDS. func buildInterfacesFromAllPhysicalNICs() (*Interfaces, error) { nics := &Interfaces{ EthernetInterfaces: nil, VlanInterfaces: map[string]VlanInterface{}, } interfaces, err := net.Interfaces() if err != nil { return nil, fmt.Errorf("failed to get interfaces: %v", err) } for _, iface := range interfaces { mac := iface.HardwareAddr.String() if mac == "" { continue } nics.EthernetInterfaces = append(nics.EthernetInterfaces, metadata.NetworkInterfaces{ Mac: mac, }) } return nics, nil } // shouldManageInterface returns whether the guest agent should manage an interface // provided whether the interface of interest is the primary interface or not. func shouldManageInterface(isPrimary bool) bool { if isPrimary { return cfg.Get().NetworkInterfaces.ManagePrimaryNIC } return true }