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  }