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)

Reply via email to