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