github.com/vmware/govmomi@v0.37.2/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  }