| // Copyright 2018 Google Inc. All Rights Reserved. |
| // |
| // 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. |
| |
| // osconfig_agent interacts with the osconfig api. |
| package main |
| |
| import ( |
| "context" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "math/rand" |
| "net/http" |
| "os" |
| "os/signal" |
| "path/filepath" |
| "runtime" |
| "syscall" |
| "time" |
| |
| "github.com/GoogleCloudPlatform/guest-logging-go/logger" |
| "github.com/GoogleCloudPlatform/osconfig/agentconfig" |
| "github.com/GoogleCloudPlatform/osconfig/agentendpoint" |
| "github.com/GoogleCloudPlatform/osconfig/clog" |
| "github.com/GoogleCloudPlatform/osconfig/policies" |
| "github.com/GoogleCloudPlatform/osconfig/tasker" |
| "github.com/GoogleCloudPlatform/osconfig/util" |
| "github.com/tarm/serial" |
| |
| _ "net/http/pprof" |
| |
| _ "google.golang.org/genproto/googleapis/rpc/errdetails" |
| ) |
| |
| var ( |
| version string |
| profile = flag.Bool("profile", false, "serve profiling data at localhost:6060/debug/pprof") |
| ) |
| |
| func init() { |
| if version == "" { |
| version = "manual-" + time.Now().Format(time.RFC3339) |
| } |
| // We do this here so the -X value doesn't need the full path. |
| agentconfig.SetVersion(version) |
| |
| os.MkdirAll(filepath.Dir(agentconfig.RestartFile()), 0755) |
| } |
| |
| type serialPort struct { |
| port string |
| } |
| |
| func (s *serialPort) Write(b []byte) (int, error) { |
| c := &serial.Config{Name: s.port, Baud: 115200} |
| p, err := serial.OpenPort(c) |
| if err != nil { |
| return 0, err |
| } |
| defer p.Close() |
| |
| return p.Write(b) |
| } |
| |
| var deferredFuncs []func() |
| |
| // RegisterAgent is a blocking call, the RPC itself has retry logic baked in |
| // with jitter and backoff up to a total of 10 minutes. |
| // If client creation or register agent (after retries) fail we then wait for |
| // 5 minutes and try again. |
| func registerAgent(ctx context.Context) { |
| for { |
| if client, err := agentendpoint.NewClient(ctx); err != nil { |
| logger.Errorf("%v", err.Error()) |
| } else if err := client.RegisterAgent(ctx); err != nil { |
| logger.Errorf("%v", err.Error()) |
| client.Close() |
| } else { |
| // RegisterAgent completed successfully. |
| client.Close() |
| return |
| } |
| time.Sleep(5 * time.Minute) |
| } |
| } |
| |
| func run(ctx context.Context) { |
| // Setup logging. |
| opts := logger.LogOpts{LoggerName: "OSConfigAgent", UserAgent: agentconfig.UserAgent(), DisableLocalLogging: agentconfig.DisableLocalLogging()} |
| if agentconfig.Stdout() { |
| opts.Writers = []io.Writer{os.Stdout} |
| } |
| if runtime.GOOS == "windows" { |
| opts.Writers = append(opts.Writers, &serialPort{"COM1"}) |
| } |
| |
| // If this call to WatchConfig fails (like a metadata error) we can't continue. |
| if err := agentconfig.WatchConfig(ctx); err != nil { |
| logger.Init(ctx, opts) |
| logger.Fatalf("Error parsing metadata, agent cannot start: %v", err.Error()) |
| } |
| opts.Debug = agentconfig.Debug() |
| clog.DebugEnabled = agentconfig.Debug() |
| opts.ProjectName = agentconfig.ProjectID() |
| opts.DisableCloudLogging = agentconfig.DisableCloudLogging() |
| |
| if err := logger.Init(ctx, opts); err != nil { |
| fmt.Printf("Error initializing logger: %v", err) |
| os.Exit(1) |
| } |
| ctx = clog.WithLabels(ctx, map[string]string{"instance_name": agentconfig.Name()}) |
| |
| // Remove any existing restart file. |
| if err := os.Remove(agentconfig.RestartFile()); err != nil && !os.IsNotExist(err) { |
| clog.Errorf(ctx, "Error removing restart signal file: %v", err) |
| } |
| |
| // On shutdown if the old restart file exists, and there is nothing else in that old directory, |
| // cleanup that directory ignoring all errors. |
| // This ensures we only cleanup this directory if we were using it with an old version of the agent. |
| deferredFuncs = append(deferredFuncs, func() { |
| if runtime.GOOS == "linux" && util.Exists(agentconfig.OldRestartFile()) { |
| os.Remove(agentconfig.OldRestartFile()) |
| |
| files, err := ioutil.ReadDir(filepath.Dir(agentconfig.OldRestartFile())) |
| if err != nil || len(files) > 0 { |
| return |
| } |
| os.RemoveAll(filepath.Dir(agentconfig.OldRestartFile())) |
| } |
| }) |
| |
| deferredFuncs = append(deferredFuncs, logger.Close, func() { clog.Infof(ctx, "OSConfig Agent (version %s) shutting down.", agentconfig.Version()) }) |
| |
| obtainLock() |
| |
| // obtainLock adds functions to clear the lock at close. |
| logger.DeferredFatalFuncs = append(logger.DeferredFatalFuncs, deferredFuncs...) |
| |
| clog.Infof(ctx, "OSConfig Agent (version %s) started.", agentconfig.Version()) |
| |
| // Call RegisterAgent at least once every day, on start calling |
| // of RegisterAgent is handled in the service loop. |
| go func() { |
| c := time.Tick(24 * time.Hour) |
| for range c { |
| if agentconfig.TaskNotificationEnabled() || agentconfig.GuestPoliciesEnabled() { |
| registerAgent(ctx) |
| } |
| } |
| }() |
| |
| switch action := flag.Arg(0); action { |
| case "", "run", "noservice": |
| runServiceLoop(ctx) |
| case "inventory", "osinventory": |
| client, err := agentendpoint.NewClient(ctx) |
| if err != nil { |
| logger.Fatalf("%v", err.Error()) |
| } |
| tasker.Enqueue(ctx, "Report OSInventory", func() { |
| client.ReportInventory(ctx) |
| }) |
| tasker.Close() |
| return |
| case "gp", "policies", "guestpolicies", "ospackage": |
| policies.Run(ctx) |
| tasker.Close() |
| return |
| case "w", "waitfortasknotification", "ospatch": |
| client, err := agentendpoint.NewClient(ctx) |
| if err != nil { |
| logger.Fatalf("%v", err.Error()) |
| } |
| client.WaitForTaskNotification(ctx) |
| select { |
| case <-ctx.Done(): |
| } |
| default: |
| logger.Fatalf("Unknown arg %q", action) |
| } |
| } |
| |
| func runTaskLoop(ctx context.Context, c chan struct{}) { |
| var taskNotificationClient *agentendpoint.Client |
| var err error |
| for { |
| // Set debug logging settings so that customers don't need to restart the agent. |
| logger.SetDebugLogging(agentconfig.Debug()) |
| clog.DebugEnabled = agentconfig.Debug() |
| if agentconfig.TaskNotificationEnabled() && taskNotificationClient == nil { |
| // Call RegisterAgent now since we just either started running or were just enabled. |
| // This call is blocking until successful as we can't continue unless register agent has completed. |
| registerAgent(ctx) |
| } |
| if agentconfig.TaskNotificationEnabled() && (taskNotificationClient == nil || taskNotificationClient.Closed()) { |
| // Start WaitForTaskNotification if we need to. |
| taskNotificationClient, err = agentendpoint.NewClient(ctx) |
| if err != nil { |
| clog.Errorf(ctx, "%v", err.Error()) |
| } else { |
| taskNotificationClient.WaitForTaskNotification(ctx) |
| } |
| } else if !agentconfig.TaskNotificationEnabled() && taskNotificationClient != nil && !taskNotificationClient.Closed() { |
| // Cancel WaitForTaskNotification if we need to, this will block if there is |
| // an existing current task running. |
| if err := taskNotificationClient.Close(); err != nil { |
| clog.Errorf(ctx, "%v", err.Error()) |
| } |
| } |
| |
| // This is just to signal WaitForTaskNotification has run if needed. |
| select { |
| case c <- struct{}{}: |
| default: |
| } |
| |
| // Wait on any metadata config change. |
| if err := agentconfig.WatchConfig(ctx); err != nil { |
| clog.Errorf(ctx, "%v", err.Error()) |
| } |
| select { |
| case <-ctx.Done(): |
| return |
| default: |
| continue |
| } |
| } |
| } |
| |
| // Runs internal functions that need to run on an interval. |
| func runInternalPeriodics(ctx context.Context) { |
| ticker := time.NewTicker(10 * time.Minute) |
| defer ticker.Stop() |
| for { |
| if _, err := os.Stat(agentconfig.RestartFile()); err == nil { |
| clog.Infof(ctx, "Restart required marker file exists, beginning agent shutdown, waiting for tasks to complete.") |
| tasker.Close() |
| clog.Infof(ctx, "All tasks completed, stopping agent.") |
| for _, f := range deferredFuncs { |
| f() |
| } |
| os.Exit(2) |
| } |
| |
| select { |
| case <-ticker.C: |
| continue |
| case <-ctx.Done(): |
| return |
| } |
| } |
| } |
| |
| func runServiceLoop(ctx context.Context) { |
| go runInternalPeriodics(ctx) |
| |
| // This is just to ensure WaitForTaskNotification runs before any other tasks. |
| c := make(chan struct{}) |
| // Configures WaitForTaskNotification, waits for config changes with WatchConfig. |
| go runTaskLoop(ctx, c) |
| // Don't continue any other tasks until WaitForTaskNotification has run. |
| <-c |
| |
| // Runs functions that need to run on a set interval. |
| ticker := time.NewTicker(agentconfig.SvcPollInterval()) |
| defer ticker.Stop() |
| // First inventory run will be somewhere between 3 and 5 min. |
| firstInventory := time.After(time.Duration(rand.Intn(120)+180) * time.Second) |
| ranFirstInventory := false |
| for { |
| if agentconfig.GuestPoliciesEnabled() { |
| policies.Run(ctx) |
| } |
| |
| if agentconfig.OSInventoryEnabled() { |
| if !ranFirstInventory { |
| // Only run first inventory after the set waiting period or if the main poll ticker ticks. |
| // The default SvcPollInterval is 10min so under normal circumstances firstInventory will |
| // always fire first. |
| select { |
| case <-ticker.C: |
| case <-firstInventory: |
| case <-ctx.Done(): |
| return |
| } |
| ranFirstInventory = true |
| } |
| |
| // This should always run after ospackage.SetConfig. |
| tasker.Enqueue(ctx, "Report OSInventory", func() { |
| client, err := agentendpoint.NewClient(ctx) |
| if err != nil { |
| logger.Errorf("%v", err.Error()) |
| } |
| client.ReportInventory(ctx) |
| client.Close() |
| }) |
| } |
| |
| select { |
| case <-ticker.C: |
| continue |
| case <-ctx.Done(): |
| return |
| } |
| } |
| } |
| |
| func main() { |
| flag.Parse() |
| ctx, cncl := context.WithCancel(context.Background()) |
| ctx = clog.WithLabels(ctx, map[string]string{"agent_version": agentconfig.Version()}) |
| c := make(chan os.Signal, 1) |
| signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) |
| go func() { |
| select { |
| case <-c: |
| cncl() |
| } |
| }() |
| |
| if *profile { |
| go func() { |
| fmt.Println(http.ListenAndServe("localhost:6060", nil)) |
| }() |
| } |
| |
| switch action := flag.Arg(0); action { |
| // wuaupdates just runs the packages.WUAUpdates function and returns it's output |
| // as JSON on stdout. This avoids memory issues with the WUA api since this is |
| // called often for Windows inventory runs. |
| case "wuaupdates": |
| if err := wuaUpdates(ctx, flag.Arg(1)); err != nil { |
| fmt.Fprint(os.Stderr, err) |
| os.Exit(1) |
| } |
| os.Exit(0) |
| case "", "run": |
| runService(ctx) |
| default: |
| run(ctx) |
| } |
| |
| for _, f := range deferredFuncs { |
| f() |
| } |
| } |