| // 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 | |
| // | |
| // https://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" | |
| "net" | |
| "os" | |
| "path/filepath" | |
| "runtime" | |
| "strings" | |
| "time" | |
| "github.com/GoogleCloudPlatform/galog" | |
| "github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg" | |
| "github.com/GoogleCloudPlatform/google-guest-agent/internal/ps" | |
| "github.com/GoogleCloudPlatform/google-guest-agent/internal/resource" | |
| "github.com/GoogleCloudPlatform/google-guest-agent/internal/retry" | |
| "github.com/GoogleCloudPlatform/google-guest-agent/internal/run" | |
| "github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file" | |
| acmpb "github.com/GoogleCloudPlatform/google-guest-agent/internal/acp/proto/google_guest_agent/acp" | |
| ) | |
| const ( | |
| // udsProtocol is UDS protocol name to use for gRPC communication. | |
| udsProtocol = "unix" | |
| // tcpProtocol is TCP protocol name to use for gRPC communication. | |
| tcpProtocol = "tcp" | |
| ) | |
| // launchStep implements the plugin launch. | |
| type launchStep struct { | |
| // entryPath is the path to binary to launch plugin. | |
| entryPath string | |
| // maxMemoryUsage is the allowed maximum memory usage, in bytes, for the | |
| // plugin. | |
| maxMemoryUsage int64 | |
| // maxCPUUsage is the maximum allowed CPU usage in percentage for the plugin. | |
| maxCPUUsage int32 | |
| // startAttempts is the number of times to attempt to launch the plugin. | |
| startAttempts int | |
| // protocol (TCP/UDS) is the protocol used for gRPC communication. | |
| protocol string | |
| // extraArgs are additional arguments (opaque to agent) set by plugin writers | |
| // to pass down on process launch. | |
| extraArgs []string | |
| } | |
| // Name returns the name of the step. | |
| func (l *launchStep) Name() string { return "LaunchPluginStep" } | |
| // Status returns the plugin state for current step. | |
| func (l *launchStep) Status() acmpb.CurrentPluginStates_StatusValue { | |
| return acmpb.CurrentPluginStates_STARTING | |
| } | |
| // Status returns the plugin state for current step. | |
| func (l *launchStep) ErrorStatus() acmpb.CurrentPluginStates_StatusValue { | |
| return acmpb.CurrentPluginStates_CRASHED | |
| } | |
| // Run unpacks to the target directory and deletes the archive file. | |
| func (l *launchStep) Run(ctx context.Context, p *Plugin) error { | |
| policy := retry.Policy{MaxAttempts: l.startAttempts, BackoffFactor: 1, Jitter: time.Second} | |
| addr, err := address(ctx, l.protocol, p.FullName(), policy) | |
| if err != nil { | |
| return fmt.Errorf("failed to get address: %w", err) | |
| } | |
| p.Address = addr | |
| p.Manifest.MaxMemoryUsage = l.maxMemoryUsage | |
| p.Manifest.MaxCPUUsage = l.maxCPUUsage | |
| p.Manifest.StartAttempts = l.startAttempts | |
| p.EntryPath = l.entryPath | |
| p.Protocol = l.protocol | |
| p.Manifest.LaunchArguments = l.extraArgs | |
| stateDir := p.stateDir() | |
| // Create state directory for the plugin. | |
| if err := os.MkdirAll(stateDir, 0755); err != nil { | |
| return fmt.Errorf("create state directory %q for plugin %q: %w", stateDir, p.FullName(), err) | |
| } | |
| // Update plugin path symlink to point to latest. Core plugin install path is | |
| // static and does not change across revisions. For instance on Linux it is | |
| // always /usr/lib/google/guest_agent/core_plugin. Skip symlink setup for core | |
| // plugin. | |
| if !p.IsLocal() { | |
| if err := file.UpdateSymlink(p.staticInstallPath(), p.InstallPath); err != nil { | |
| return fmt.Errorf("UpdateSymlink(%q, %q) failed for plugin %q: %w", p.staticInstallPath(), p.InstallPath, p.FullName(), err) | |
| } | |
| } | |
| // Launch the plugin. | |
| if err := launchPlugin(ctx, p, l.protocol, policy); err != nil { | |
| return err | |
| } | |
| pluginPid := p.pid() | |
| // Wait for the socket to be verified if the protocol is UDS. This simply | |
| // checks to see if the socket file exists up until the default connect | |
| // timeout is reached. If the timeout is reached, we try to relaunch the | |
| // plugin using TCP. | |
| if p.Protocol == udsProtocol { | |
| socketPolicy := retry.Policy{MaxAttempts: defaultConnectTimeoutTries, BackoffFactor: 1, Jitter: time.Second * 1} | |
| if err := retry.Run(ctx, socketPolicy, func() error { | |
| if file.Exists(p.Address, file.TypeFile) { | |
| return nil | |
| } | |
| return fmt.Errorf("socket %q does not exist for plugin %q", p.Address, p.FullName()) | |
| }); err != nil { | |
| // If the socket does not exist after some time, we assume that UDS may | |
| // not be working for whatever reason. We therefore try to relaunch the | |
| // plugin using TCP. | |
| galog.Warnf("Failed to verify socket %q for plugin %q, trying TCP: %v", p.Address, p.FullName(), err) | |
| // Kill the existing plugin process before trying to relaunch using TCP. | |
| if err := ps.KillProcess(pluginPid, ps.KillModeWait); err != nil { | |
| galog.Warnf("Failed to kill plugin process %q: %v", p.FullName(), err) | |
| } | |
| // Get a new address using TCP. | |
| addr, err := address(ctx, tcpProtocol, p.FullName(), policy) | |
| if err != nil { | |
| return fmt.Errorf("failed to get TCP address: %w", err) | |
| } | |
| p.Address = addr | |
| p.Protocol = tcpProtocol | |
| // Relaunch the plugin using TCP. | |
| if err := launchPlugin(ctx, p, tcpProtocol, policy); err != nil { | |
| return err | |
| } | |
| pluginPid = p.pid() | |
| } | |
| } | |
| // Prepare for resource constraint. | |
| constraintFunc := func() error { | |
| constraint := resource.Constraint{ | |
| PID: pluginPid, | |
| Name: p.FullName(), | |
| MaxMemoryUsage: p.Manifest.MaxMemoryUsage, | |
| MaxCPUUsage: p.Manifest.MaxCPUUsage, | |
| } | |
| if err := resource.Apply(constraint); err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| // Apply resource constraint. | |
| if err := retry.Run(ctx, policy, constraintFunc); err != nil { | |
| sendEvent(ctx, p, acmpb.PluginEventMessage_PLUGIN_START_FAILED, fmt.Sprintf("Failed to apply resource constraint: [%v]", err)) | |
| p.setState(acmpb.CurrentPluginStates_CRASHED) | |
| return err | |
| } | |
| if err := p.Connect(ctx); err != nil { | |
| p.setState(acmpb.CurrentPluginStates_CRASHED) | |
| return fmt.Errorf("failed to connect plugin %s: %w", p.FullName(), err) | |
| } | |
| _, status := p.Start(ctx) | |
| if status.Err() != nil { | |
| sendEvent(ctx, p, acmpb.PluginEventMessage_PLUGIN_START_FAILED, fmt.Sprintf("Failed to start plugin: [%+v]. Plugin logs: %s", status, readPluginLogs(p.logfile()))) | |
| p.setState(acmpb.CurrentPluginStates_CRASHED) | |
| return fmt.Errorf("failed to start plugin %s with error: %w", p.FullName(), status.Err()) | |
| } | |
| sendEvent(ctx, p, acmpb.PluginEventMessage_PLUGIN_STARTED, "Successfully started the plugin.") | |
| p.setState(acmpb.CurrentPluginStates_RUNNING) | |
| galog.Infof("Successfully started plugin %q", p.FullName()) | |
| if err := p.Store(); err != nil { | |
| return fmt.Errorf("store plugin %s info failed: %w", p.FullName(), err) | |
| } | |
| return nil | |
| } | |
| // launchPlugin launches the plugin and applies resource constraints. | |
| func launchPlugin(ctx context.Context, p *Plugin, protocol string, policy retry.Policy) error { | |
| args := p.Manifest.LaunchArguments | |
| args = append(args, fmt.Sprintf("--protocol=%s", protocol), fmt.Sprintf("--address=%s", p.Address), fmt.Sprintf("--errorlogfile=%s", p.logfile())) | |
| pluginExecMode := run.ExecModeDetach | |
| if p.IsLocal() { | |
| pluginExecMode = run.ExecModeAsync | |
| } | |
| launchFunc := func() error { | |
| opts := run.Options{ | |
| Name: p.EntryPath, | |
| Args: args, | |
| ExecMode: pluginExecMode, | |
| } | |
| res, err := run.WithContext(ctx, opts) | |
| if err != nil { | |
| return fmt.Errorf("failed to launch plugin from %q: %w", p.EntryPath, err) | |
| } | |
| p.setPid(res.Pid) | |
| return nil | |
| } | |
| // Launch the plugin. | |
| if err := retry.Run(ctx, policy, launchFunc); err != nil { | |
| sendEvent(ctx, p, acmpb.PluginEventMessage_PLUGIN_START_FAILED, fmt.Sprintf("Failed to launch plugin: [%v]. Plugin logs: %s", err, readPluginLogs(p.logfile()))) | |
| p.setState(acmpb.CurrentPluginStates_CRASHED) | |
| return err | |
| } | |
| pluginPid := p.pid() | |
| galog.Debugf("Launched a plugin process from %q with pid %d", p.EntryPath, pluginPid) | |
| return nil | |
| } | |
| // connectionsPath returns the path to the socket connections directory. | |
| func connectionsPath() string { | |
| return filepath.Clean(cfg.Retrieve().Plugin.SocketConnectionsDir) | |
| } | |
| // connectionSetup creates the socket connections directory and removes the | |
| // previous socket address file if it exists. Do this setup before plugin launch | |
| // so every plugin doesn't need to handle this separately. | |
| func connectionSetup(address string) error { | |
| dir := filepath.Dir(address) | |
| if err := os.MkdirAll(dir, 0755); err != nil { | |
| return fmt.Errorf("create %q connections directory: %w", dir, err) | |
| } | |
| // Skip cleanup for unit test addresses, unit tests handles itself. | |
| if strings.Contains(address, os.TempDir()) && strings.Contains(address, "Test") { | |
| return nil | |
| } | |
| if file.Exists(address, file.TypeFile) { | |
| galog.Debugf("Cleaning up previous socket address file %q", address) | |
| if err := os.RemoveAll(address); err != nil { | |
| // In case of unix sockets they must be unlinked (listener.Close()) before | |
| // they're reused again. If file already exist bind can fail. | |
| return fmt.Errorf("failed to remove socket address file %q: %v", address, err) | |
| } | |
| } | |
| return nil | |
| } | |
| // address returns the socket address or port to use for gRPC communication. | |
| func address(ctx context.Context, protocol, id string, policy retry.Policy) (string, error) { | |
| if protocol == udsProtocol { | |
| addr := filepath.Join(connectionsPath(), id+".sock") | |
| if err := connectionSetup(addr); err != nil { | |
| return "", fmt.Errorf("failed to setup socket connections directory: %w", err) | |
| } | |
| return addr, nil | |
| } | |
| // If using TCP find a free open port ready to use. | |
| f := func() (string, error) { | |
| listener, err := net.Listen("tcp", ":0") | |
| if err != nil { | |
| return "", fmt.Errorf("failed to get new TCP listener: %w", err) | |
| } | |
| addr := listener.Addr().String() | |
| if err := listener.Close(); err != nil { | |
| return "", fmt.Errorf("failed to close new TCP listener: %w", err) | |
| } | |
| return addr, nil | |
| } | |
| return retry.RunWithResponse(ctx, policy, f) | |
| } | |
| // defaultIsUDSSupported returns true if UDS is supported on Windows. Instead of | |
| // going by version to figure out if UDS is supported, try listening on test | |
| // address using UDS, if it gets listener successfully consider UDS is supported. | |
| func defaultIsUDSSupported() bool { | |
| if runtime.GOOS == "linux" { | |
| return true | |
| } | |
| connDir := connectionsPath() | |
| if err := os.MkdirAll(connDir, 0755); err != nil { | |
| galog.Debugf("Failed to test if UDS is supported, could not create connections directory: %v", err) | |
| return false | |
| } | |
| sockAddr := filepath.Join(connDir, "test-connection") | |
| defer func() { | |
| if err := os.RemoveAll(sockAddr); err != nil { | |
| galog.Debugf("Failed to remove socket file %q: %v", sockAddr, err) | |
| } | |
| }() | |
| listener, err := net.Listen(udsProtocol, sockAddr) | |
| if err != nil { | |
| galog.Infof("Cannot listen on %q, UDS not supported/disabled, fallback to TCP: %v", sockAddr, err) | |
| return false | |
| } | |
| // Ignore these errors - we just need to know if UDS is supported and this | |
| // address is not reused. | |
| if err := listener.Close(); err != nil { | |
| galog.Debugf("Failed to close UDS listener: %v", err) | |
| } | |
| return true | |
| } |