github.com/vmware/govmomi@v0.51.0/guest/toolbox/client.go (about) 1 // © Broadcom. All Rights Reserved. 2 // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. 3 // SPDX-License-Identifier: Apache-2.0 4 5 package toolbox 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "io" 12 "log" 13 "net/url" 14 "os" 15 "os/exec" 16 "strings" 17 "time" 18 19 "github.com/vmware/govmomi/guest" 20 "github.com/vmware/govmomi/internal" 21 "github.com/vmware/govmomi/property" 22 "github.com/vmware/govmomi/vim25" 23 "github.com/vmware/govmomi/vim25/mo" 24 "github.com/vmware/govmomi/vim25/soap" 25 "github.com/vmware/govmomi/vim25/types" 26 ) 27 28 // Client attempts to expose guest.OperationsManager as idiomatic Go interfaces 29 type Client struct { 30 ProcessManager *guest.ProcessManager 31 FileManager *guest.FileManager 32 Authentication types.BaseGuestAuthentication 33 GuestFamily types.VirtualMachineGuestOsFamily 34 } 35 36 // NewClient initializes a Client's ProcessManager, FileManager and GuestFamily 37 func NewClient(ctx context.Context, c *vim25.Client, vm mo.Reference, auth types.BaseGuestAuthentication) (*Client, error) { 38 m := guest.NewOperationsManager(c, vm.Reference()) 39 40 pm, err := m.ProcessManager(ctx) 41 if err != nil { 42 return nil, err 43 } 44 45 fm, err := m.FileManager(ctx) 46 if err != nil { 47 return nil, err 48 } 49 50 family := "" 51 var props mo.VirtualMachine 52 pc := property.DefaultCollector(c) 53 err = pc.RetrieveOne(context.Background(), vm.Reference(), []string{"guest.guestFamily", "guest.toolsInstallType"}, &props) 54 if err != nil { 55 return nil, err 56 } 57 58 if props.Guest != nil { 59 family = props.Guest.GuestFamily 60 if family == string(types.VirtualMachineGuestOsFamilyOtherGuestFamily) { 61 if props.Guest.ToolsInstallType == string(types.VirtualMachineToolsInstallTypeGuestToolsTypeMSI) { 62 // The case of Windows version not supported by the ESX version 63 family = string(types.VirtualMachineGuestOsFamilyWindowsGuest) 64 } 65 } 66 } 67 68 return &Client{ 69 ProcessManager: pm, 70 FileManager: fm, 71 Authentication: auth, 72 GuestFamily: types.VirtualMachineGuestOsFamily(family), 73 }, nil 74 } 75 76 func (c *Client) rm(ctx context.Context, path string) { 77 err := c.FileManager.DeleteFile(ctx, c.Authentication, path) 78 if err != nil { 79 log.Printf("rm %q: %s", path, err) 80 } 81 } 82 83 func (c *Client) mktemp(ctx context.Context) (string, error) { 84 return c.FileManager.CreateTemporaryFile(ctx, c.Authentication, "govmomi-", "", "") 85 } 86 87 type exitError struct { 88 error 89 exitCode int 90 } 91 92 func (e *exitError) ExitCode() int { 93 return e.exitCode 94 } 95 96 // Run implements exec.Cmd.Run over vmx guest RPC against standard vmware-tools or toolbox. 97 func (c *Client) Run(ctx context.Context, cmd *exec.Cmd) error { 98 if cmd.Stdin != nil { 99 dst, err := c.mktemp(ctx) 100 if err != nil { 101 return err 102 } 103 104 defer c.rm(ctx, dst) 105 106 var buf bytes.Buffer 107 size, err := io.Copy(&buf, cmd.Stdin) 108 if err != nil { 109 return err 110 } 111 112 p := soap.DefaultUpload 113 p.ContentLength = size 114 attr := new(types.GuestPosixFileAttributes) 115 116 err = c.Upload(ctx, &buf, dst, p, attr, true) 117 if err != nil { 118 return err 119 } 120 121 cmd.Args = append(cmd.Args, "<", dst) 122 } 123 124 output := []struct { 125 io.Writer 126 fd string 127 path string 128 }{ 129 {cmd.Stdout, "1", ""}, 130 {cmd.Stderr, "2", ""}, 131 } 132 133 for i, out := range output { 134 if out.Writer == nil { 135 continue 136 } 137 138 dst, err := c.mktemp(ctx) 139 if err != nil { 140 return err 141 } 142 143 defer c.rm(ctx, dst) 144 145 cmd.Args = append(cmd.Args, out.fd+">", dst) 146 output[i].path = dst 147 } 148 149 path := cmd.Path 150 args := cmd.Args 151 152 switch c.GuestFamily { 153 case types.VirtualMachineGuestOsFamilyWindowsGuest: 154 // Using 'cmd.exe /c' is required on Windows for i/o redirection 155 path = "c:\\Windows\\System32\\cmd.exe" 156 args = append([]string{"/c", cmd.Path}, args...) 157 default: 158 if !strings.ContainsAny(cmd.Path, "/") { 159 // vmware-tools requires an absolute ProgramPath 160 // Default to 'bash -c' as a convenience 161 path = "/bin/bash" 162 arg := "'" + strings.Join(append([]string{cmd.Path}, args...), " ") + "'" 163 args = []string{"-c", arg} 164 } 165 } 166 167 spec := types.GuestProgramSpec{ 168 ProgramPath: path, 169 Arguments: strings.Join(args, " "), 170 EnvVariables: cmd.Env, 171 WorkingDirectory: cmd.Dir, 172 } 173 174 pid, err := c.ProcessManager.StartProgram(ctx, c.Authentication, &spec) 175 if err != nil { 176 return err 177 } 178 179 rc := 0 180 for { 181 procs, err := c.ProcessManager.ListProcesses(ctx, c.Authentication, []int64{pid}) 182 if err != nil { 183 return err 184 } 185 186 p := procs[0] 187 if p.EndTime == nil { 188 <-time.After(time.Second / 2) 189 continue 190 } 191 192 rc = int(p.ExitCode) 193 194 break 195 } 196 197 for _, out := range output { 198 if out.Writer == nil { 199 continue 200 } 201 202 f, _, err := c.Download(ctx, out.path) 203 if err != nil { 204 return err 205 } 206 207 _, err = io.Copy(out.Writer, f) 208 _ = f.Close() 209 if err != nil { 210 return err 211 } 212 } 213 214 if rc != 0 { 215 return &exitError{fmt.Errorf("%s: exit %d", cmd.Path, rc), rc} 216 } 217 218 return nil 219 } 220 221 // archiveReader wraps an io.ReadCloser to support streaming download 222 // of a guest directory, stops reading once it sees the stream trailer. 223 // This is only useful when guest tools is the Go toolbox. 224 // The trailer is required since TransferFromGuest requires a Content-Length, 225 // which toolbox doesn't know ahead of time as the gzip'd tarball never touches the disk. 226 // We opted to wrap this here for now rather than guest.FileManager so 227 // DownloadFile can be also be used as-is to handle this use case. 228 type archiveReader struct { 229 io.ReadCloser 230 } 231 232 var ( 233 gzipHeader = []byte{0x1f, 0x8b, 0x08} // rfc1952 {ID1, ID2, CM} 234 gzipHeaderLen = len(gzipHeader) 235 ) 236 237 func (r *archiveReader) Read(buf []byte) (int, error) { 238 nr, err := r.ReadCloser.Read(buf) 239 240 // Stop reading if the last N bytes are the gzipTrailer 241 if nr >= gzipHeaderLen { 242 if bytes.Equal(buf[nr-gzipHeaderLen:nr], gzipHeader) { 243 nr -= gzipHeaderLen 244 err = io.EOF 245 } 246 } 247 248 return nr, err 249 } 250 251 func isDir(src string) bool { 252 u, err := url.Parse(src) 253 if err != nil { 254 return false 255 } 256 257 return strings.HasSuffix(u.Path, "/") 258 } 259 260 // Download initiates a file transfer from the guest 261 func (c *Client) Download(ctx context.Context, src string) (io.ReadCloser, int64, error) { 262 vc := c.ProcessManager.Client() 263 264 info, err := c.FileManager.InitiateFileTransferFromGuest(ctx, c.Authentication, src) 265 if err != nil { 266 return nil, 0, err 267 } 268 269 u, err := c.FileManager.TransferURL(ctx, info.Url) 270 if err != nil { 271 return nil, 0, err 272 } 273 274 p := soap.DefaultDownload 275 p.Close = true // disable Keep-Alive connection to ESX 276 277 if internal.UsingEnvoySidecar(c.ProcessManager.Client()) { 278 vc = internal.ClientWithEnvoyHostGateway(vc) 279 } 280 281 f, n, err := vc.Download(ctx, u, &p) 282 if err != nil { 283 return nil, n, err 284 } 285 286 if strings.HasPrefix(src, "/archive:/") || isDir(src) { 287 f = &archiveReader{ReadCloser: f} // look for the gzip trailer 288 } 289 290 return f, n, nil 291 } 292 293 // Upload transfers a file to the guest 294 func (c *Client) Upload(ctx context.Context, src io.Reader, dst string, p soap.Upload, attr types.BaseGuestFileAttributes, force bool) error { 295 vc := c.ProcessManager.Client() 296 297 var err error 298 299 if p.ContentLength == 0 { // Content-Length is required 300 switch r := src.(type) { 301 case *bytes.Buffer: 302 p.ContentLength = int64(r.Len()) 303 case *bytes.Reader: 304 p.ContentLength = int64(r.Len()) 305 case *strings.Reader: 306 p.ContentLength = int64(r.Len()) 307 case *os.File: 308 info, serr := r.Stat() 309 if serr != nil { 310 return serr 311 } 312 313 p.ContentLength = info.Size() 314 } 315 316 if p.ContentLength == 0 { // os.File for example could be a device (stdin) 317 buf := new(bytes.Buffer) 318 319 p.ContentLength, err = io.Copy(buf, src) 320 if err != nil { 321 return err 322 } 323 324 src = buf 325 } 326 } 327 328 url, err := c.FileManager.InitiateFileTransferToGuest(ctx, c.Authentication, dst, attr, p.ContentLength, force) 329 if err != nil { 330 return err 331 } 332 333 u, err := c.FileManager.TransferURL(ctx, url) 334 if err != nil { 335 return err 336 } 337 338 p.Close = true // disable Keep-Alive connection to ESX 339 340 if internal.UsingEnvoySidecar(c.ProcessManager.Client()) { 341 vc = internal.ClientWithEnvoyHostGateway(vc) 342 } 343 344 return vc.Client.Upload(ctx, src, u, &p) 345 }