Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package google-guest-agent for openSUSE:Factory checked in at 2024-01-10 21:51:35 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/google-guest-agent (Old) and /work/SRC/openSUSE:Factory/.google-guest-agent.new.21961 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "google-guest-agent" Wed Jan 10 21:51:35 2024 rev:31 rq:1137800 version:20231214.00 Changes: -------- --- /work/SRC/openSUSE:Factory/google-guest-agent/google-guest-agent.changes 2023-11-14 21:43:53.782538458 +0100 +++ /work/SRC/openSUSE:Factory/.google-guest-agent.new.21961/google-guest-agent.changes 2024-01-10 21:51:55.875388470 +0100 @@ -1,0 +2,13 @@ +Thu Jan 4 11:32:21 UTC 2024 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to version 20231214.00 + * Fix snapshot test failure (#336) +- from version 20231212.00 + * Implement json-based command messaging system for guest-agent (#326) +- from version 20231118.00 + * sshca: Remove certificate caching (#334) +- from version 20231115.00 + * revert: 3ddd9d4a496f7a9c591ded58c3f541fd9cc7e317 (#333) + * Update script runner to use common cfg package (#331) + +------------------------------------------------------------------- Old: ---- guest-agent-20231110.00.tar.gz New: ---- guest-agent-20231214.00.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ google-guest-agent.spec ++++++ --- /var/tmp/diff_new_pack.inVPyX/_old 2024-01-10 21:51:57.587450643 +0100 +++ /var/tmp/diff_new_pack.inVPyX/_new 2024-01-10 21:51:57.587450643 +0100 @@ -1,7 +1,7 @@ # # spec file for package google-guest-agent # -# Copyright (c) 2023 SUSE LLC +# Copyright (c) 2024 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -24,7 +24,7 @@ %global import_path %{provider_prefix} Name: google-guest-agent -Version: 20231110.00 +Version: 20231214.00 Release: 0 Summary: Google Cloud Guest Agent License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.inVPyX/_old 2024-01-10 21:51:57.619451805 +0100 +++ /var/tmp/diff_new_pack.inVPyX/_new 2024-01-10 21:51:57.623451951 +0100 @@ -3,8 +3,8 @@ <param name="url">https://github.com/GoogleCloudPlatform/guest-agent/</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="versionformat">20231110.00</param> - <param name="revision">20231110.00</param> + <param name="versionformat">20231214.00</param> + <param name="revision">20231214.00</param> <param name="changesgenerate">enable</param> </service> <service name="recompress" mode="disabled"> @@ -15,7 +15,7 @@ <param name="basename">guest-agent</param> </service> <service name="go_modules" mode="disabled"> - <param name="archive">guest-agent-20231110.00.tar.gz</param> + <param name="archive">guest-agent-20231214.00.tar.gz</param> </service> </services> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.inVPyX/_old 2024-01-10 21:51:57.647452821 +0100 +++ /var/tmp/diff_new_pack.inVPyX/_new 2024-01-10 21:51:57.651452967 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/GoogleCloudPlatform/guest-agent/</param> - <param name="changesrevision">94cae3c6bcdc11c7461abb94783f3a52146d6729</param></service></servicedata> + <param name="changesrevision">b1c6ecf632c2f5ebc20935139a2650202561b324</param></service></servicedata> (No newline at EOF) ++++++ guest-agent-20231110.00.tar.gz -> guest-agent-20231214.00.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/go.mod new/guest-agent-20231214.00/go.mod --- old/guest-agent-20231110.00/go.mod 2023-11-03 22:38:21.000000000 +0100 +++ new/guest-agent-20231214.00/go.mod 2023-12-15 00:56:53.000000000 +0100 @@ -31,6 +31,7 @@ cloud.google.com/go/iam v1.1.1 // indirect cloud.google.com/go/logging v1.7.0 // indirect cloud.google.com/go/longrunning v0.5.1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect github.com/google/go-sev-guest v0.7.0 // indirect github.com/google/logger v1.1.1 // indirect github.com/google/s2a-go v0.1.4 // indirect @@ -40,10 +41,12 @@ github.com/pborman/uuid v1.2.1 // indirect github.com/pkg/errors v0.9.1 // indirect go.opencensus.io v0.24.0 // indirect + golang.org/x/mod v0.8.0 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/text v0.11.0 // indirect + golang.org/x/tools v0.6.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.134.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/go.sum new/guest-agent-20231214.00/go.sum --- old/guest-agent-20231110.00/go.sum 2023-11-03 22:38:21.000000000 +0100 +++ new/guest-agent-20231214.00/go.sum 2023-12-15 00:56:53.000000000 +0100 @@ -17,6 +17,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/GoogleCloudPlatform/guest-logging-go v0.0.0-20230710215706-450679fd88a9 h1:b3geIwOPAShYtR4F0XFt+2NJXTHVTfbxUFmrpiZXHdQ= github.com/GoogleCloudPlatform/guest-logging-go v0.0.0-20230710215706-450679fd88a9/go.mod h1:6ZqSUIZRAPR5dNMWJ+FwIarFFQ9t5qalaKQs20o6h+I= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -94,8 +96,10 @@ github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/kardianos/service v1.2.1 h1:AYndMsehS+ywIS6RB9KOlcXzteWUzxgMgBymJD7+BYk= github.com/kardianos/service v1.2.1/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -134,6 +138,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -192,6 +198,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/cfg/cfg.go new/guest-agent-20231214.00/google_guest_agent/cfg/cfg.go --- old/guest-agent-20231110.00/google_guest_agent/cfg/cfg.go 2023-11-03 22:38:21.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/cfg/cfg.go 2023-12-15 00:56:53.000000000 +0100 @@ -101,6 +101,10 @@ timeout_in_seconds = 60 [Unstable] +command_monitor_enabled = false +command_pipe_mode = 0770 +command_pipe_group = +command_request_timeout = 10s ` ) @@ -271,6 +275,11 @@ // is guaranteed for configurations defined in the Unstable section. By default all flags defined // in this section is disabled and is intended to isolate under development features. type Unstable struct { + CommandMonitorEnabled bool `ini:"command_monitor_enabled,omitempty"` + CommandPipePath string `ini:"command_pipe_path,omitempty"` + CommandRequestTimeout string `ini:"command_request_timeout,omitempty"` + CommandPipeMode string `ini:"command_pipe_mode,omitempty"` + CommandPipeGroup string `ini:"command_pipe_group,omitempty"` } // WSFC contains the configurations of WSFC section. @@ -296,9 +305,9 @@ } return append(res, []interface{}{ + config, config + ".distro", config + ".template", - config, }...) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/cfg/cfg_test.go new/guest-agent-20231214.00/google_guest_agent/cfg/cfg_test.go --- old/guest-agent-20231110.00/google_guest_agent/cfg/cfg_test.go 2023-11-03 22:38:21.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/cfg/cfg_test.go 2023-12-15 00:56:53.000000000 +0100 @@ -15,8 +15,6 @@ package cfg import ( - "os" - "path" "testing" ) @@ -96,82 +94,3 @@ t.Errorf("Get() should return always the same pointer, expected: %p, got: %p", firstCfg, secondCfg) } } - -func TestConfigLoadOrder(t *testing.T) { - config := path.Join(t.TempDir(), "config.cfg") - configFile = func(string) string { return config } - t.Cleanup(func() { configFile = defaultConfigFile }) - testcases := []struct { - name string - extraDefault string - distroConfig string - templateConfig string - userConfig string - output bool - }{ - { - name: "user config override", - extraDefault: "[NetworkInterfaces]\nSetup = true\n", - distroConfig: "[NetworkInterfaces]\nSetup = true\n", - templateConfig: "[NetworkInterfaces]\nSetup = true\n", - userConfig: "[NetworkInterfaces]\nSetup = false\n", - output: false, - }, - { - name: "template config override", - extraDefault: "[NetworkInterfaces]\nSetup = true\n", - distroConfig: "[NetworkInterfaces]\nSetup = true\n", - templateConfig: "[NetworkInterfaces]\nSetup = false\n", - userConfig: "", - output: false, - }, - { - name: "distro config override", - extraDefault: "[NetworkInterfaces]\nSetup = true\n", - distroConfig: "[NetworkInterfaces]\nSetup = false\n", - templateConfig: "", - userConfig: "", - output: false, - }, - { - name: "extra default override", - extraDefault: "[NetworkInterfaces]\nSetup = false\n", - distroConfig: "", - templateConfig: "", - userConfig: "", - output: false, - }, - { - // If this fails, other test case results are not valid - name: "default is true", - extraDefault: "", - distroConfig: "", - templateConfig: "", - userConfig: "", - output: true, - }, - } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - err := os.WriteFile(config+".distro", []byte(tc.distroConfig), 0777) - if err != nil { - t.Fatal(err) - } - err = os.WriteFile(config+".template", []byte(tc.templateConfig), 0777) - if err != nil { - t.Fatal(err) - } - err = os.WriteFile(config, []byte(tc.userConfig), 0777) - if err != nil { - t.Fatal(err) - } - err = Load([]byte(tc.extraDefault)) - if err != nil { - t.Fatal(err) - } - if Get().NetworkInterfaces.Setup != tc.output { - t.Errorf("unexpected config value for NetworkInterfaces.Setup, wanted %v but got %v", Get().NetworkInterfaces.Setup, tc.output) - } - }) - } -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/command/Readme.md new/guest-agent-20231214.00/google_guest_agent/command/Readme.md --- old/guest-agent-20231110.00/google_guest_agent/command/Readme.md 1970-01-01 01:00:00.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/command/Readme.md 2023-12-15 00:56:53.000000000 +0100 @@ -0,0 +1,24 @@ +# Guest Agent Command Monitor +## Overview +The Guest Agent command monitor is a system used for executing commands in the guest agent on behalf of components in the guest os. + +The events layer is formed of a **Monitor**, a **Server** and a **Handler** where the **Monitor** handles command registration for guest agent components, the **Server** is the component which listens for events from the gueest os, and the **Handler** is the function executed by the agent. + +Each **Handler** is identified by a string ID, provided when sending commands to the server. Requests and response to and from the server are structured in JSON format. A request must contain the name field, specifying the handler to be executed. A request may contain arbitrary other fields to be passed to the handler. An example request is below: + +``` +{"Name":"agent.ExampleCommand","ArbitraryArgument":123} +``` + +A response will be valid JSON and has two required fields: Status and StatusMessage. Status is an int which follows unix status code conventions (ie zero is success, status codes are arbitrary and meaning is defined by the function called) and StatusMessage is an explanatory string accompanying the Status. Two example responses are below. + +``` +{"Status":0,"StatusMessage":""} + +{"Status":7,"StatusMessage":"Failure message"} +``` + +By default, the Server listens on a unix socket or a named pipe, depending on platform. Permissions for the pipe and the pipe path can be set in the guest-agent [configuration](https://github.com/GoogleCloudPlatform/guest-agent#configuration). The default pipe path for windows and linux systems are `\\.\pipe\google-guest-agent-commands` non-windows and `/run/google-guest-agent/commands.sock` respectively. + +## Implementing a command handler +Registering a command handler will expose the handler function to be called by anyone with write permission to the underlying socket. To do so, call `command.Get().RegisterHandler(name, handerFunc)` to get the current command monitor and register the handlerFunc with it. Note that if the command system is disabled by user configuration, handler registration will succeed but the server will not be available for callers to send commands to. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/command/command.go new/guest-agent-20231214.00/google_guest_agent/command/command.go --- old/guest-agent-20231110.00/google_guest_agent/command/command.go 1970-01-01 01:00:00.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/command/command.go 2023-12-15 00:56:53.000000000 +0100 @@ -0,0 +1,146 @@ +// Copyright 2023 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. + +// Package command facilitates calling commands within the guest-agent. +package command + +import ( + "context" + "encoding/json" + "fmt" + "io" + + "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg" +) + +// Get returns the current command monitor which can be used to register command handlers. +func Get() *Monitor { + return cmdMonitor +} + +// Handler functions are the business logic of commands. They must process json +// encoded as a byte slice which contains a Command field and optional arbitrary +// data, and return json which contains a Status, StatusMessage, and optional +// arbitrary data (again encoded as a byte slice). Returned errors will be +// passed onto the command requester. +type Handler func([]byte) ([]byte, error) + +// Request is the basic request structure. Command determines which handler the +// request is routed to. Callers may set additional arbitrary fields. +type Request struct { + Command string +} + +// Response is the basic response structure. Handlers may set additional +// arbitrary fields. +type Response struct { + // Status code for the request. Meaning is defined by the caller, but + // conventially zero is success. + Status int + // StatusMessage is an optional message defined by the caller. Should generally + // help a human understand what happened. + StatusMessage string +} + +var ( + // CmdNotFoundError is return when there is no handler for the request command + CmdNotFoundError = Response{ + Status: 101, + StatusMessage: "Could not find a handler for the requested command", + } + // BadRequestError is returned for invalid or unparseable JSON + BadRequestError = Response{ + Status: 102, + StatusMessage: "Could not parse valid JSON from request", + } + // ConnError is returned for errors from the underlying communication protocol + ConnError = Response{ + Status: 103, + StatusMessage: "Connection error", + } + // TimeoutError is returned when the timeout period elapses before valid JSON is receieved + TimeoutError = Response{ + Status: 104, + StatusMessage: "Connection timeout before reading valid request", + } + // HandlerError is returned when the handler function returns an non-nil error. The status message will be replaced with the returnd error string. + HandlerError = Response{ + Status: 105, + StatusMessage: "The command handler encountered an error processing your request", + } + // InternalErrorCode is the error code for internal command server errors. Returned when failing to marshal a response. + InternalErrorCode = 106 + internalError = []byte(`{"Status":106,"StatusMessage":"The command server encountered an internal error trying to respond to your request"}`) +) + +// RegisterHandler registers f as the handler for cmd. If a command.Server has +// been initialized, it will be signalled to start listening for commands. +func (m *Monitor) RegisterHandler(cmd string, f Handler) error { + m.handlersMu.Lock() + defer m.handlersMu.Unlock() + if _, ok := m.handlers[cmd]; ok { + return fmt.Errorf("cmd %s is already handled", cmd) + } + m.handlers[cmd] = f + return nil +} + +// UnregisterHandler clears the handlers for cmd. If a command.Server has been +// intialized and there are no more handlers registered, the server will be +// signalled to stop listening for commands. +func (m *Monitor) UnregisterHandler(cmd string) error { + m.handlersMu.Lock() + defer m.handlersMu.Unlock() + if _, ok := m.handlers[cmd]; !ok { + return fmt.Errorf("cmd %s is not registered", cmd) + } + delete(m.handlers, cmd) + return nil +} + +// SendCommand sends a command request over the configured pipe. +func SendCommand(ctx context.Context, req []byte) []byte { + pipe := cfg.Get().Unstable.CommandPipePath + if pipe == "" { + pipe = DefaultPipePath + } + return SendCmdPipe(ctx, pipe, req) +} + +// SendCmdPipe sends a command request over a specific pipe. Most callers +// should use SendCommand() instead. +func SendCmdPipe(ctx context.Context, pipe string, req []byte) []byte { + conn, err := dialPipe(ctx, pipe) + if err != nil { + if b, err := json.Marshal(ConnError); err != nil { + return b + } + return internalError + } + i, err := conn.Write(req) + if err != nil || i != len(req) { + if b, err := json.Marshal(ConnError); err != nil { + return b + } + return internalError + } + data, err := io.ReadAll(conn) + if err != nil { + if b, err := json.Marshal(ConnError); err != nil { + return b + } + return internalError + } + return data +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/command/command_linux.go new/guest-agent-20231214.00/google_guest_agent/command/command_linux.go --- old/guest-agent-20231110.00/google_guest_agent/command/command_linux.go 1970-01-01 01:00:00.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/command/command_linux.go 2023-12-15 00:56:53.000000000 +0100 @@ -0,0 +1,140 @@ +// Copyright 2023 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. + +package command + +import ( + "context" + "fmt" + "net" + "os" + "os/user" + "path" + "runtime" + "strconv" + "syscall" + + "github.com/GoogleCloudPlatform/guest-logging-go/logger" +) + +// DefaultPipePath is the default unix socket path for linux. +const DefaultPipePath = "/run/google-guest-agent/commands.sock" + +func mkdirpWithPerms(dir string, p os.FileMode, uid, gid int) error { + stat, err := os.Stat(dir) + if err == nil { + statT, ok := stat.Sys().(*syscall.Stat_t) + if !ok { + return fmt.Errorf("could not determine owner of %s", dir) + } + if !stat.IsDir() { + return fmt.Errorf("%s exists and is not a directory", dir) + } + if morePermissive(int(stat.Mode()), int(p)) { + if err := os.Chmod(dir, p); err != nil { + return fmt.Errorf("could not correct %s permissions to %d: %v", dir, p, err) + } + } + if statT.Uid != 0 && statT.Uid != uint32(uid) { + if err := os.Chown(dir, uid, -1); err != nil { + return fmt.Errorf("could not correct %s owner to %d: %v", dir, uid, err) + } + } + if statT.Gid != 0 && statT.Gid != uint32(gid) { + if err := os.Chown(dir, -1, gid); err != nil { + return fmt.Errorf("could not correct %s group to %d: %v", dir, gid, err) + } + } + } else { + parent, _ := path.Split(dir) + if parent != "/" && parent != "" { + if err := mkdirpWithPerms(parent, p, uid, gid); err != nil { + return err + } + } + if err := os.Mkdir(dir, p); err != nil { + return err + } + } + return nil +} + +func morePermissive(i, j int) bool { + for k := 0; k < 3; k++ { + if (i % 010) > (j % 10) { + return true + } + i = i / 010 + j = j / 010 + } + return false +} + +func listen(ctx context.Context, pipe string, filemode int, grp string) (net.Listener, error) { + // If grp is an int, use it as a GID + gid, err := strconv.Atoi(grp) + if err != nil { + // Otherwise lookup GID + group, err := user.LookupGroup(grp) + if err != nil { + logger.Errorf("guest agent command pipe group %s is not a GID nor a valid group, not changing socket ownership", grp) + gid = -1 + } else { + gid, err = strconv.Atoi(group.Gid) + if err != nil { + logger.Errorf("os reported group %s has gid %s which is not a valid int, not changing socket ownership. this should never happen", grp, group.Gid) + gid = -1 + } + } + } + // socket owner group does not need to have permissions to everything in the directory containing it, whatever user and group we are should own that + user, err := user.Current() + if err != nil { + return nil, fmt.Errorf("could not lookup current user") + } + currentuid, err := strconv.Atoi(user.Uid) + if err != nil { + return nil, fmt.Errorf("os reported user %s has uid %s which is not a valid int, can't determine directory owner. this should never happen", user.Username, user.Uid) + } + currentgid, err := strconv.Atoi(user.Gid) + if err != nil { + return nil, fmt.Errorf("os reported user %s has gid %s which is not a valid int, can't determine directory owner. this should never happen", user.Username, user.Gid) + } + if err := mkdirpWithPerms(path.Dir(pipe), os.FileMode(filemode), currentuid, currentgid); err != nil { + return nil, err + } + // Mutating the umask of the process for this is not ideal, but tightening permissions with chown after creation is not really secure. + // Lock OS thread while mutating umask so we don't lose a thread with a mutated mask. + runtime.LockOSThread() + oldmask := syscall.Umask(777 - filemode) + var lc net.ListenConfig + l, err := lc.Listen(ctx, "unix", pipe) + syscall.Umask(oldmask) + runtime.UnlockOSThread() + if err != nil { + return nil, err + } + // But we need to chown anyway to loosen permissions to include whatever group the user has configured + err = os.Chown(pipe, int(currentuid), gid) + if err != nil { + l.Close() + return nil, err + } + return l, nil +} + +func dialPipe(ctx context.Context, pipe string) (net.Conn, error) { + var dialer net.Dialer + return dialer.DialContext(ctx, "unix", pipe) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/command/command_monitor.go new/guest-agent-20231214.00/google_guest_agent/command/command_monitor.go --- old/guest-agent-20231110.00/google_guest_agent/command/command_monitor.go 1970-01-01 01:00:00.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/command/command_monitor.go 2023-12-15 00:56:53.000000000 +0100 @@ -0,0 +1,228 @@ +// Copyright 2023 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. + +/* + * This file contains the details of command's internal communication protocol + * listener. Most callers should not need to call anything in this file. The + * command handler and caller API is contained in command.go. + */ + +package command + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "net" + "os" + "strconv" + "sync" + "time" + + "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg" + "github.com/GoogleCloudPlatform/guest-logging-go/logger" +) + +var cmdMonitor *Monitor = &Monitor{ + handlersMu: new(sync.RWMutex), + handlers: make(map[string]Handler), +} + +// Init starts an internally managed command server. The agent configuration +// will decide the server options. Returns a reference to the internally managed +// command monitor which the caller can Close() when appropriate. +func Init(ctx context.Context) { + if cmdMonitor.srv != nil { + return + } + pipe := cfg.Get().Unstable.CommandPipePath + if pipe == "" { + pipe = DefaultPipePath + } + to, err := time.ParseDuration(cfg.Get().Unstable.CommandRequestTimeout) + if err != nil { + logger.Errorf("commmand request timeout configuration is not a valid duration string, falling back to 10s timeout") + to = time.Duration(10) * time.Second + } + var pipemode int64 = 0770 + pipemode, err = strconv.ParseInt(cfg.Get().Unstable.CommandPipeMode, 8, 32) + if err != nil { + logger.Errorf("could not parse command_pipe_mode as octal integer: %v falling back to mode 0770", err) + } + cmdMonitor.srv = &Server{ + pipe: pipe, + pipeMode: int(pipemode), + pipeGroup: cfg.Get().Unstable.CommandPipeGroup, + timeout: to, + monitor: cmdMonitor, + } + err = cmdMonitor.srv.start(ctx) + if err != nil { + logger.Errorf("failed to start command server: %s", err) + } +} + +// Close will close the internally managed command server, if it was initialized. +func Close() error { + if cmdMonitor.srv != nil { + return cmdMonitor.srv.Close() + } + return nil +} + +// Monitor is the structure which handles command registration and deregistration. +type Monitor struct { + srv *Server + handlersMu *sync.RWMutex + handlers map[string]Handler +} + +// Close stops the server from listening to commands. +func (m *Monitor) Close() error { return m.srv.Close() } + +// Start begins listening for commands. +func (m *Monitor) Start(ctx context.Context) error { return m.srv.start(ctx) } + +// Server is the server structure which will listen for command requests and +// route them to handlers. Most callers should not interact with this directly. +type Server struct { + pipe string + pipeMode int + pipeGroup string + timeout time.Duration + srv net.Listener + monitor *Monitor +} + +// Close signals the server to stop listening for commands and stop waiting to +// listen. +func (c *Server) Close() error { + if c.srv != nil { + return c.srv.Close() + } + return nil +} + +func (c *Server) start(ctx context.Context) error { + if c.srv != nil { + return errors.New("server already listening") + } + srv, err := listen(ctx, c.pipe, c.pipeMode, c.pipeGroup) + if err != nil { + return err + } + go func() { + defer srv.Close() + for { + if ctx.Err() != nil { + return + } + conn, err := srv.Accept() + if err != nil { + if err == net.ErrClosed { + break + } + logger.Infof("error on connection to pipe %s: %v", c.pipe, err) + continue + } + go func(conn net.Conn) { + defer conn.Close() + // Go has lots of helpers to do this for us but none of them return the byte + // slice afterwards, and we need it for the handler + var b []byte + r := bufio.NewReader(conn) + var depth int + deadline := time.Now().Add(c.timeout) + e := conn.SetReadDeadline(deadline) + if e != nil { + logger.Infof("could not set read deadline on command request: %v", e) + return + } + for { + if time.Now().After(deadline) { + if b, err := json.Marshal(TimeoutError); err != nil { + conn.Write(internalError) + } else { + conn.Write(b) + } + return + } + rune, _, err := r.ReadRune() + if err != nil { + logger.Debugf("connection read error: %v", err) + if errors.Is(err, os.ErrDeadlineExceeded) { + if b, err := json.Marshal(TimeoutError); err != nil { + conn.Write(internalError) + } else { + conn.Write(b) + } + } else { + if b, err := json.Marshal(ConnError); err != nil { + conn.Write(internalError) + } else { + conn.Write(b) + } + } + return + } + b = append(b, byte(rune)) + switch rune { + case '{': + depth++ + case '}': + depth-- + } + // Must check here because the first pass always depth = 0 + if depth == 0 { + break + } + } + var req Request + err := json.Unmarshal(b, &req) + if err != nil { + if b, err := json.Marshal(BadRequestError); err != nil { + conn.Write(internalError) + } else { + conn.Write(b) + } + return + } + c.monitor.handlersMu.RLock() + defer c.monitor.handlersMu.RUnlock() + handler, ok := c.monitor.handlers[req.Command] + if !ok { + if b, err := json.Marshal(CmdNotFoundError); err != nil { + conn.Write(internalError) + } else { + conn.Write(b) + } + return + } + resp, err := handler(b) + if err != nil { + re := Response{Status: HandlerError.Status, StatusMessage: err.Error()} + if b, err := json.Marshal(re); err != nil { + resp = internalError + } else { + resp = b + } + } + conn.Write(resp) + }(conn) + } + }() + c.srv = srv + return nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/command/command_test.go new/guest-agent-20231214.00/google_guest_agent/command/command_test.go --- old/guest-agent-20231110.00/google_guest_agent/command/command_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/command/command_test.go 2023-12-15 00:56:53.000000000 +0100 @@ -0,0 +1,209 @@ +// Copyright 2023 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. + +package command + +import ( + "context" + "encoding/json" + "fmt" + "io" + "math/rand" + "os/user" + "path" + "runtime" + "sync" + "testing" + "time" + + "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg" +) + +func cmdServerForTest(t *testing.T, pipeMode int, pipeGroup string, timeout time.Duration) *Server { + cs := &Server{ + pipe: getTestPipePath(t), + pipeMode: pipeMode, + pipeGroup: pipeGroup, + timeout: timeout, + monitor: &Monitor{ + handlersMu: new(sync.RWMutex), + handlers: make(map[string]Handler), + }, + } + cs.monitor.srv = cs + err := cs.start(testctx(t)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + err := cs.Close() + if err != nil { + t.Errorf("error closing command server: %v", err) + } + }) + return cs +} + +func getTestPipePath(t *testing.T) string { + if runtime.GOOS == "windows" { + return `\\.\pipe\google-guest-agent-commands-test-` + t.Name() + } + return path.Join(t.TempDir(), "run", "pipe") +} + +func testctx(t *testing.T) context.Context { + d, ok := t.Deadline() + if !ok { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + return ctx + } + ctx, cancel := context.WithDeadline(context.Background(), d) + t.Cleanup(cancel) + return ctx +} + +type testRequest struct { + Command string + ArbitraryData int +} + +func TestInit(t *testing.T) { + cfg.Load(nil) + cfg.Get().Unstable.CommandPipePath = getTestPipePath(t) + if cmdMonitor.srv != nil { + t.Fatal("internal command server already exists") + } + Init(testctx(t)) + if cmdMonitor.srv == nil { + t.Errorf("could not start internally managed command server") + } + if err := Close(); err != nil { + t.Errorf("could not close managed command server: %s", err) + } +} + +func TestListen(t *testing.T) { + cu, err := user.Current() + if err != nil { + t.Fatalf("could not get current user: %v", err) + } + ug, err := cu.GroupIds() + if err != nil { + t.Fatalf("could not get user groups for %s: %v", cu.Name, err) + } + resp := []byte(`{"Status":0,"StatusMessage":"OK"}`) + errresp := []byte(`{"Status":1,"StatusMessage":"ERR"}`) + req := []byte(`{"ArbitraryData":1234,"Command":"TestListen"}`) + h := func(b []byte) ([]byte, error) { + var r testRequest + err := json.Unmarshal(b, &r) + if err != nil || r.ArbitraryData != 1234 { + return errresp, nil + } + return resp, nil + } + + testcases := []struct { + name string + filemode int + group string + }{ + { + name: "world read/writeable", + filemode: 0777, + group: "-1", + }, + { + name: "group read/writeable", + filemode: 0770, + group: "-1", + }, + { + name: "user read/writeable", + filemode: 0700, + group: "-1", + }, + { + name: "additional user group as group owner", + filemode: 0770, + group: ug[rand.Intn(len(ug))], + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + cs := cmdServerForTest(t, tc.filemode, tc.group, time.Second) + err := cs.monitor.RegisterHandler("TestListen", h) + if err != nil { + t.Errorf("could not register handler: %v", err) + } + d := SendCmdPipe(testctx(t), cs.pipe, req) + var r Response + err = json.Unmarshal(d, &r) + if err != nil { + t.Error(err) + } + if r.Status != 0 || r.StatusMessage != "OK" { + t.Errorf("unexpected status from test-cmd, want 0, \"OK\" but got %d, %q", r.Status, r.StatusMessage) + } + }) + } +} + +func TestHandlerFailure(t *testing.T) { + req := []byte(`{"Command":"TestHandlerFailure"}`) + h := func(b []byte) ([]byte, error) { + return nil, fmt.Errorf("always fail") + } + + cs := cmdServerForTest(t, 0777, "-1", time.Second) + cs.monitor.RegisterHandler("TestHandlerFailure", h) + d := SendCmdPipe(testctx(t), cs.pipe, req) + var r Response + err := json.Unmarshal(d, &r) + if err != nil { + t.Error(err) + } + if r.Status != HandlerError.Status || r.StatusMessage != "always fail" { + t.Errorf("unexpected status from TestHandlerFailure, want %d, \"always fail\" but got %d, %q", HandlerError.Status, r.Status, r.StatusMessage) + } +} + +func TestListenTimeout(t *testing.T) { + expect, err := json.Marshal(TimeoutError) + if err != nil { + t.Fatal(err) + } + if runtime.GOOS == "windows" { + // winio library does not surface timeouts from the underlying net.Conn as + // timeouts, but as generic errors. Timeouts still work they just can't be + // detected as timeouts, so they are generic connErrors here. + expect, err = json.Marshal(ConnError) + if err != nil { + t.Fatal(err) + } + } + cs := cmdServerForTest(t, 0770, "-1", time.Millisecond) + conn, err := dialPipe(testctx(t), cs.pipe) + if err != nil { + t.Errorf("could not connect to command server: %v", err) + } + data, err := io.ReadAll(conn) + if err != nil { + t.Errorf("error reading response from command server: %v", err) + } + if string(data) != string(expect) { + t.Errorf("unexpected response from timed out connection, got %s but want %s", data, expect) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/command/command_windows.go new/guest-agent-20231214.00/google_guest_agent/command/command_windows.go --- old/guest-agent-20231110.00/google_guest_agent/command/command_windows.go 1970-01-01 01:00:00.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/command/command_windows.go 2023-12-15 00:56:53.000000000 +0100 @@ -0,0 +1,104 @@ +// Copyright 2023 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. + +package command + +import ( + "context" + "fmt" + "net" + "os/user" + + "github.com/GoogleCloudPlatform/guest-logging-go/logger" + "github.com/Microsoft/go-winio" +) + +const ( + // DefaultPipePath is the default named pipe path for windows. + DefaultPipePath = `\\.\pipe\google-guest-agent-commands` + nullSID = "S-1-0-0" + worldSID = "S-1-1-0" + creatorOwnerSID = "S-1-3-0" + creatorGroupSID = "S-1-3-1" +) + +func genSecurityDescriptor(filemode int, grp string) string { + // This function translates the intention of a unix file mode and owner group into an appropriate SDDL security descriptor for a windows named pipe. + owner := creatorOwnerSID + group := creatorGroupSID + + wPerm := filemode % 010 + filemode /= 010 + gPerm := filemode % 010 + filemode /= 010 + uPerm := filemode % 010 + + // Having only read or only write access to a bidirectional pipe is pointless so we treat access for user/group as yes or no based on whether the permission grants RW access + if uPerm < 06 { + owner = nullSID + } + if gPerm < 06 { + group = nullSID + } + // If permissions grant world RW, make world the owner + if wPerm > 05 { + owner = worldSID + group = worldSID + } + + // Group is handled as supplemental DACL, but ignore it if user specified no group rw permission + var dacl string + if gPerm > 05 { + g, err := user.LookupGroup(grp) + if err != nil { + logger.Errorf("Could not lookup group %s SID, this group will not be included in the command server security descriptor: %v", grp, err) + } else { + // Allow access;Protected DACL;Allow all general access;Empty object guid;Empty inherit object guid;group sid from lookup + dacl = fmt.Sprintf("D:(A;P;GA;;;%s)", g.Gid) + } + } + + sddl := "O:%sG:%s%s" + return fmt.Sprintf(sddl, owner, group, dacl) +} + +func listen(ctx context.Context, path string, filemode int, group string) (net.Listener, error) { + // Winio library does not provide any method to listen on context. Failing to + // specify a pipeconfig (or using the zero value) results in flaky ACCESS_DENIED + // errors when re-opening the same pipe (~1/10). + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea#remarks + // Even with a pipeconfig, this flakes ~1/200 runs, hence the retry until the + // context is expired or listen is successful. + var l net.Listener + var lastError error + for { + if ctx.Err() != nil { + return nil, fmt.Errorf("context expired: %v before successful listen (last error: %v)", ctx.Err(), lastError) + } + config := &winio.PipeConfig{ + MessageMode: false, + InputBufferSize: 1024, + OutputBufferSize: 1024, + SecurityDescriptor: genSecurityDescriptor(filemode, group), + } + l, lastError = winio.ListenPipe(path, config) + if lastError == nil { + return l, lastError + } + } +} + +func dialPipe(ctx context.Context, pipe string) (net.Conn, error) { + return winio.DialPipeContext(ctx, pipe) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/command/command_windows_test.go new/guest-agent-20231214.00/google_guest_agent/command/command_windows_test.go --- old/guest-agent-20231110.00/google_guest_agent/command/command_windows_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/command/command_windows_test.go 2023-12-15 00:56:53.000000000 +0100 @@ -0,0 +1,73 @@ +//go:build windows + +// Copyright 2023 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. +package command + +import ( + "os/user" + "testing" +) + +func TestGenSecurityDescriptor(t *testing.T) { + guest, err := user.LookupGroup("Guests") + if err != nil { + t.Fatal(err) + } + testcases := []struct { + name string + filemode int + group string + output string + }{ + { + name: "world writeable", + filemode: 0777, + group: nullSID, + output: "O:" + worldSID + "G:" + worldSID, + }, + { + name: "user+group writable", + filemode: 0770, + group: "", + output: "O:" + creatorOwnerSID + "G:" + creatorGroupSID, + }, + { + name: "user writable", + filemode: 0700, + group: nullSID, + output: "O:" + creatorOwnerSID + "G:" + nullSID, + }, + { + name: "no write permissions", + filemode: 000, + group: nullSID, + output: "O:" + nullSID + "G:" + nullSID, + }, + { + name: "custom named group", + filemode: 0770, + group: "Guests", + output: "O:" + creatorOwnerSID + "G:" + creatorGroupSID + "D:(A;P;GA;;;" + guest.Gid + ")", + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + sd := genSecurityDescriptor(tc.filemode, tc.group) + if sd != tc.output { + t.Errorf("unexpected output from genSecurityDescriptor(%d, %s), got %s want %s", tc.filemode, tc.group, sd, tc.output) + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/main.go new/guest-agent-20231214.00/google_guest_agent/main.go --- old/guest-agent-20231110.00/google_guest_agent/main.go 2023-11-03 22:38:21.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/main.go 2023-12-15 00:56:53.000000000 +0100 @@ -26,6 +26,7 @@ "time" "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg" + "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/command" "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/events" mdsEvent "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/events/metadata" "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/osinfo" @@ -175,6 +176,11 @@ agentInit(ctx) + if cfg.Get().Unstable.CommandMonitorEnabled { + command.Init(ctx) + defer command.Close() + } + // Previous request to metadata *may* not have worked becasue routes don't get added until agentInit. var err error if newMetadata == nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/oslogin.go new/guest-agent-20231214.00/google_guest_agent/oslogin.go --- old/guest-agent-20231110.00/google_guest_agent/oslogin.go 2023-11-03 22:38:21.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/oslogin.go 2023-12-15 00:56:53.000000000 +0100 @@ -69,6 +69,11 @@ } func enableDisableOSLoginCertAuth(ctx context.Context) error { + if newMetadata == nil { + logger.Infof("Could not enable/disable OSLogin Cert Auth, metadata is not initialized.") + return nil + } + eventManager := events.Get() osLoginEnabled, _, _ := getOSLoginEnabled(newMetadata) if osLoginEnabled { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/snapshot_listener.go new/guest-agent-20231214.00/google_guest_agent/snapshot_listener.go --- old/guest-agent-20231110.00/google_guest_agent/snapshot_listener.go 2023-11-03 22:38:21.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/snapshot_listener.go 2023-12-15 00:56:53.000000000 +0100 @@ -66,7 +66,7 @@ } func listenForSnapshotRequests(ctx context.Context, address string, requestChan chan<- *sspb.GuestMessage) { - for leaving := false; !leaving; { + for context.Cause(ctx) == nil { // Start hanging connection on server that feeds to channel logger.Infof("Attempting to connect to snapshot service at %s.", address) conn, err := grpc.Dial(address, grpc.WithInsecure()) @@ -76,22 +76,22 @@ } c := sspb.NewSnapshotServiceClient(conn) - ctx, cancel := context.WithCancel(ctx) + guestReady := sspb.GuestReady{ RequestServerInfo: false, } + r, err := c.CreateConnection(ctx, &guestReady) if err != nil { - logger.Errorf("Error creating connection: %v.", err) - leaving = errors.Is(err, context.Canceled) - cancel() + if !errors.Is(err, context.Canceled) { + logger.Errorf("Error creating connection: %v.", err) + } continue } for { request, err := r.Recv() if err != nil { logger.Errorf("Error reading snapshot request: %v.", err) - cancel() break } logger.Infof("Received snapshot request.") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_guest_agent/sshca/sshca.go new/guest-agent-20231214.00/google_guest_agent/sshca/sshca.go --- old/guest-agent-20231110.00/google_guest_agent/sshca/sshca.go 2023-11-03 22:38:21.000000000 +0100 +++ new/guest-agent-20231214.00/google_guest_agent/sshca/sshca.go 2023-12-15 00:56:53.000000000 +0100 @@ -37,9 +37,6 @@ } var ( - // cachedCertificate stores the previously retrieved certificate to be cached in case mds fails. - cachedCertificate string - // mdsClient is the metadata's client, used to query oslogin certificates. mdsClient *metadata.Client ) @@ -74,18 +71,13 @@ pipeData.Finished() }() - // The certificates key/endpoint is not cached, we can't rely on the metadata watcher data because of that. certificate, err := mdsClient.GetKey(ctx, "oslogin/certificates", nil) - if err != nil && cachedCertificate != "" { - certificate = cachedCertificate - logger.Warningf("Failed to get certificate, assuming/using previously cached one.") - } else if err != nil { + if err != nil { logger.Errorf("Failed to get certificate from metadata server: %+v", err) return true } // Keep a copy of the returned certificate for error fallback caching. - cachedCertificate = certificate var certs Certificates var outData []string diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_metadata_script_runner/main.go new/guest-agent-20231214.00/google_metadata_script_runner/main.go --- old/guest-agent-20231110.00/google_metadata_script_runner/main.go 2023-11-03 22:38:21.000000000 +0100 +++ new/guest-agent-20231214.00/google_metadata_script_runner/main.go 2023-12-15 00:56:53.000000000 +0100 @@ -38,15 +38,13 @@ "time" "cloud.google.com/go/storage" + "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg" "github.com/GoogleCloudPlatform/guest-agent/metadata" "github.com/GoogleCloudPlatform/guest-agent/utils" "github.com/GoogleCloudPlatform/guest-logging-go/logger" - "github.com/go-ini/ini" ) const ( - winConfigPath = `C:\Program Files\Google\Compute Engine\instance_configs.cfg` - configPath = "/etc/default/instance_configs.cfg" storageURL = "storage.googleapis.com" bucket = "([a-z0-9][-_.a-z0-9]*)" object = "(.+)" @@ -58,7 +56,6 @@ programName = path.Base(os.Args[0]) powerShellArgs = []string{"-NoProfile", "-NoLogo", "-ExecutionPolicy", "Unrestricted", "-File"} errUsage = fmt.Errorf("no valid arguments specified. Specify one of \"startup\", \"shutdown\" or \"specialize\"") - config *ini.File // Many of the Google Storage URLs are supported below. // It is preferred that customers specify their object using @@ -268,7 +265,7 @@ } // Make temp directory. - tmpDir, err := os.MkdirTemp(config.Section("MetadataScripts").Key("run_dir").String(), "metadata-scripts") + tmpDir, err := os.MkdirTemp(cfg.Get().MetadataScripts.RunDir, "metadata-scripts") if err != nil { return err } @@ -278,7 +275,10 @@ if runtime.GOOS == "windows" { tmpFile = normalizeFilePathForWindows(tmpFile, metadataKey, gcsScriptURL) } - writeScriptToFile(ctx, value, tmpFile, gcsScriptURL) + + if err := writeScriptToFile(ctx, value, tmpFile, gcsScriptURL); err != nil { + return fmt.Errorf("unable to write script to file: %v", err) + } return runScript(tmpFile, metadataKey) } @@ -292,7 +292,7 @@ if runtime.GOOS == "windows" { cmd = exec.Command(filePath) } else { - cmd = exec.Command(config.Section("MetadataScripts").Key("default_shell").MustString("/bin/bash"), "-c", filePath) + cmd = exec.Command(cfg.Get().MetadataScripts.DefaultShell, "-c", filePath) } } return runCmd(cmd, metadataKey) @@ -341,12 +341,27 @@ switch prefix { case "specialize": prefix = "sysprep-specialize" - case "startup", "shutdown": + case "startup": if os == "windows" { prefix = "windows-" + prefix + if !cfg.Get().MetadataScripts.StartupWindows { + return nil, fmt.Errorf("windows startup scripts disabled in instance config") + } + } else { + if !cfg.Get().MetadataScripts.Startup { + return nil, fmt.Errorf("startup scripts disabled in instance config") + } } - if !config.Section("MetadataScripts").Key(prefix).MustBool(true) { - return nil, fmt.Errorf("%s scripts disabled in instance config", prefix) + case "shutdown": + if os == "windows" { + prefix = "windows-" + prefix + if !cfg.Get().MetadataScripts.ShutdownWindows { + return nil, fmt.Errorf("windows shutdown scripts disabled in instance config") + } + } else { + if !cfg.Get().MetadataScripts.Shutdown { + return nil, fmt.Errorf("shutdown scripts disabled in instance config") + } } default: return nil, errUsage @@ -401,23 +416,12 @@ return fmt.Sprintf("%s %s: %s", now, programName, e.Message) } -func parseConfig(file string) (*ini.File, error) { - // Priority: file.cfg, file.cfg.distro, file.cfg.template - cfg, err := ini.LoadSources(ini.LoadOptions{Loose: true, Insensitive: true}, file+".template", file+".distro", file) - if err != nil { - return nil, err - } - return cfg, nil -} - func main() { ctx := context.Background() opts := logger.LogOpts{LoggerName: programName} - cfgfile := configPath if runtime.GOOS == "windows" { - cfgfile = winConfigPath opts.Writers = []io.Writer{&utils.SerialPort{Port: "COM1"}, os.Stdout} opts.FormatFunction = logFormatWindows } else { @@ -428,9 +432,9 @@ } var err error - config, err = parseConfig(cfgfile) - if err != nil && !os.IsNotExist(err) { - fmt.Printf("Error parsing instance config %s: %s\n", cfgfile, err.Error()) + if err := cfg.Load(nil); err != nil { + fmt.Fprintf(os.Stderr, "Failed to load instance configuration: %+v", err) + os.Exit(1) } // The keys to check vary based on the argument and the OS. Also functions to validate arguments. @@ -466,7 +470,7 @@ } logger.Infof("Found %s in metadata.", wantedKey) if err := setupAndRunScript(ctx, wantedKey, value); err != nil { - logger.Infof("%s %s", wantedKey, err) + logger.Warningf("Script %q failed with error: %v", wantedKey, err) continue } logger.Infof("%s exit status 0", wantedKey) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/guest-agent-20231110.00/google_metadata_script_runner/main_test.go new/guest-agent-20231214.00/google_metadata_script_runner/main_test.go --- old/guest-agent-20231110.00/google_metadata_script_runner/main_test.go 2023-11-03 22:38:21.000000000 +0100 +++ new/guest-agent-20231214.00/google_metadata_script_runner/main_test.go 2023-12-15 00:56:53.000000000 +0100 @@ -19,15 +19,17 @@ "fmt" "net/url" "os" - "path/filepath" "reflect" "testing" + "github.com/GoogleCloudPlatform/guest-agent/google_guest_agent/cfg" "github.com/GoogleCloudPlatform/guest-agent/metadata" ) func TestMain(m *testing.M) { - config, _ = parseConfig("") + if err := cfg.Load(nil); err != nil { + os.Exit(1) + } os.Exit(m.Run()) } @@ -249,40 +251,49 @@ } } -func TestParseConfig(t *testing.T) { - dir := t.TempDir() - file := filepath.Join(dir, "cfg") - - s1 := ` - [Section] - key = value1 - ` - s2 := ` - [Section] - key = value2 - ` - s3 := ` - [Section] - key = value3 - ` - - if err := os.WriteFile(file, []byte(s1), 0644); err != nil { - t.Fatalf("os.WriteFile(%s) failed unexpectedly with error: %v", file, err) - } - if err := os.WriteFile(file+".distro", []byte(s2), 0644); err != nil { - t.Fatalf("os.WriteFile(%s) failed unexpectedly with error: %v", file+".distro", err) - } - if err := os.WriteFile(file+".template", []byte(s3), 0644); err != nil { - t.Fatalf("os.WriteFile(%s) failed unexpectedly with error: %v", file+".template", err) - } - - i, err := parseConfig(file) - if err != nil { - t.Errorf("parseConfig(%s) failed unexpectedly with error: %v", file, err) +func TestGetWantedKeysError(t *testing.T) { + // Reset original value. + defer cfg.Load(nil) + + tests := []struct { + cfg string + arg string + os string + }{ + { + cfg: `[MetadataScripts] + shutdown = false`, + arg: "shutdown", + os: "linux", + }, + { + cfg: `[MetadataScripts] + startup = false`, + arg: "startup", + os: "linux", + }, + { + cfg: `[MetadataScripts] + shutdown-windows = false`, + arg: "shutdown", + os: "windows", + }, + { + cfg: `[MetadataScripts] + startup-windows = false`, + arg: "startup", + os: "windows", + }, } - want := "value1" - if got := i.Section("Section").Key("key").String(); got != want { - t.Errorf("parseConfig(%s) = %s, want %s", file, got, want) + for _, test := range tests { + t.Run(test.os+"-"+test.arg, func(t *testing.T) { + if err := cfg.Load([]byte(test.cfg)); err != nil { + t.Errorf("cfg.Load(%s) failed unexpectedly with error: %v", test.cfg, err) + } + if _, err := getWantedKeys([]string{"", test.arg}, test.os); err == nil { + t.Errorf("getWantedKeys(%s, %s) succeeded for disabled config, want error", test.arg, test.os) + } + }) } } ++++++ vendor.tar.gz ++++++ ++++ 21202 lines of diff (skipped)