VMware vSphere Integrated Containers provider (#206)

* Add Virtual Kubelet provider for VIC

Initial virtual kubelet provider for VMware VIC.  This provider currently
handles creating and starting of a pod VM via the VIC portlayer and persona
server.  Image store handling via the VIC persona server.  This provider
currently requires the feature/wolfpack branch of VIC.

* Added pod stop and delete.  Also added node capacity.

Added the ability to stop and delete pod VMs via VIC.  Also retrieve
node capacity information from the VCH.

* Cleanup and readme file

Some file clean up and added a Readme.md markdown file for the VIC
provider.

* Cleaned up errors, added function comments, moved operation code

1. Cleaned up error handling.  Set standard for creating errors.
2. Added method prototype comments for all interface functions.
3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder.

* Add mocking code and unit tests for podcache, podcreator, and podstarter

Used the unit test framework used in VIC to handle assertions in the provider's
unit test.  Mocking code generated using OSS project mockery, which is compatible
with the testify assertion framework.

* Vendored packages for the VIC provider

Requires feature/wolfpack branch of VIC and a few specific commit sha of
projects used within VIC.

* Implementation of POD Stopper and Deleter unit tests (#4)

* Updated files for initial PR
This commit is contained in:
Loc Nguyen
2018-06-04 15:41:32 -07:00
committed by Ria Bhatia
parent 98a111e8b7
commit 513cebe7b7
6296 changed files with 1123685 additions and 8 deletions

1
vendor/github.com/vmware/govmomi/toolbox/.gitignore generated vendored Normal file
View File

@@ -0,0 +1 @@
*.iso

158
vendor/github.com/vmware/govmomi/toolbox/README.md generated vendored Normal file
View File

@@ -0,0 +1,158 @@
# toolbox - VMware guest tools library for Go #
## Overview
The toolbox library is a lightweight, extensible framework for implementing VMware guest tools functionality.
The primary focus of the library is the implementation of VM guest RPC protocols, transport and dispatch.
These protocols are undocumented for the most part, but [open-vm-tools](https://github.com/vmware/open-vm-tools) serves
as a reference implementation. The toolbox provides default implementations of the supported RPCs, which can be
overridden and/or extended by consumers.
## Supported features
Feature list from the perspective of vSphere public API interaction. The properties, objects and methods listed are
relative to
the [VirtualMachine](http://pubs.vmware.com/vsphere-60/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvim.VirtualMachine.html)
managed object type.
### guest.toolsVersionStatus property
The toolbox reports version as `guestToolsUnmanaged`.
See [ToolsVersionStatus](http://pubs.vmware.com/vsphere-60/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvim.vm.GuestInfo.ToolsVersionStatus.html)
### guest.toolsRunningStatus and guest.guestState properties
The VMX determines these values based on the toolbox's response to the `ping` RPC.
### guest.ipAddress property
The VMX requests this value via the `Set_Option broadcastIP` RPC.
The default value can be overridden by setting the `Service.PrimaryIP` function.
See [vim.vm.GuestInfo](http://pubs.vmware.com/vsphere-60/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvim.vm.GuestInfo.html)
### guest.net property
This data is pushed to the VMX using the `SendGuestInfo(INFO_IPADDRESS_V3)` RPC.
See [GuestNicInfo](http://pubs.vmware.com/vsphere-60/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvim.vm.GuestInfo.NicInfo.html).
### ShutdownGuest and RebootGuest methods
The [PowerCommandHandler](power.go) provides power hooks for customized guest shutdown and reboot.
### GuestAuthManager object
Not supported, but authentication can be customized.
See [vim.vm.guest.AuthManager](http://pubs.vmware.com/vsphere-60/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvim.vm.guest.AuthManager.html)
### GuestFileManager object
| Method | Supported | Client Examples |
|---------------------------------|-----------|-------------------------------------------------------------------------------------|
| ChangeFileAttributesInGuest | Yes | [chmod](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/chmod.go) |
| | | [chown](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/chown.go) |
| | | [touch](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/touch.go) |
| CreateTemporaryDirectoryInGuest | Yes | [mktemp](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/mktemp.go) |
| CreateTemporaryFileInGuest | Yes | [mktemp](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/mktemp.go) |
| DeleteDirectoryInGuest | Yes | [rmdir](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/rmdir.go) |
| DeleteFileInGuest | Yes | [rm](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/rm.go) |
| InitiateFileTransferFromGuest | Yes | [download](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/download.go) |
| InitiateFileTransferToGuest | Yes | [upload](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/upload.go) |
| ListFilesInGuest | Yes | [ls](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/ls.go) |
| MakeDirectoryInGuest | Yes | [mkdir](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/mkdir.go) |
| MoveDirectoryInGuest | Yes | [mv](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/mv.go) |
| MoveFileInGuest | Yes | [mv](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/mv.go) |
See [vim.vm.guest.FileManager](http://pubs.vmware.com/vsphere-60/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvim.vm.guest.FileManager.html)
### GuestProcessManager
Currently, the `ListProcessesInGuest` and `TerminateProcessInGuest` methods only apply those processes and goroutines
started by `StartProgramInGuest`.
| Method | Supported | Client Examples |
|--------------------------------|-----------|-------------------------------------------------------------------------------------|
| ListProcessesInGuest | Yes | [ps](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/ps.go) |
| ReadEnvironmentVariableInGuest | Yes | [getenv](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/getenv.go) |
| StartProgramInGuest | Yes | [start](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/start.go) |
| TerminateProcessInGuest | Yes | [kill](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/kill.go) |
See [vim.vm.guest.ProcessManager](http://pubs.vmware.com/vsphere-60/index.jsp?topic=%2Fcom.vmware.wssdk.apiref.doc%2Fvim.vm.guest.ProcessManager.html)
## Extensions
### Authentication
Guest operations can be authenticated using the `toolbox.CommandServer.Authenticate` hook.
### Go functions
The toolbox [ProcessManager](process.go) can manage both OS processes and Go functions running as go routines.
### File handlers
The `hgfs.FileHandler` interface can be used to customize file transfer.
### Process I/O
The toolbox provides support for I/O redirection without the use of disk files within the guest.
Access to *stdin*, *stdout* and *stderr* streams is implemented as an `hgfs.FileHandler` within the `ProcessManager`.
See [toolbox.Client](https://github.com/vmware/govmomi/blob/master/guest/toolbox/client.go) and
[govc guest.run](https://github.com/vmware/govmomi/blob/master/govc/vm/guest/run.go)
### http.RoundTripper
Building on top of the process I/O functionality, `toolbox.NewProcessRoundTrip` can be used to start a Go function to
implement the [http.RoundTripper](https://golang.org/pkg/net/http/#RoundTripper) interface over vmx guest RPC. This
makes it possible to use the Go [http.Client](https://golang.org/pkg/net/http/#Client) without network access to the VM
or to a port that is bound to the guest's loopback address. It is intended for use with bootstrap configuration for
example.
### Directory archives
The toolbox provides support for transferring directories to and from guests as gzip'd tar streams, without writing the
tar file itself to the guest file system. Archive supports is implemented as an `hgfs.FileHandler` within the `hgfs`
package. See [hgfs.NewArchiveHandler](https://github.com/vmware/govmomi/blob/master/toolbox/hgfs/archive.go)
### Linux /proc file access
With standard vmware-tools, the file size is reported as returned by `stat()` and hence a `Content-Length` header of
size `0`. The toolbox reports /proc file size as `hgfs.LargePacketMax` to enable transfer of these files. Note that if
the file data fits within in `hgfs.LargePacketMax`, the `Content-Length` header will be correct as it is sent after the
first read by the vmx. However, if the file data exceeds `hgfs.LargePacketMax`, the `Content-Length` will be
`hgfs.LargePacketMax`, and client side will truncate to that size.
## Testing
The Go tests cover most of the toolbox code and can be run on any Linux or MacOSX machine, virtual or otherwise.
To test the toolbox with vSphere API interaction, it must be run inside a VM managed by vSphere without the standard
vmtoolsd running.
The [toolbox-test.sh](toolbox-test.sh) can be used to run the full suite of toolbox tests with vSphere API interaction.
Use the `-s` flag to start the standalone version of the toolbox and leave it running, to test vSphere interaction
without running the test suite.
## Consumers of the toolbox library
* [Toolbox example main](https://github.com/vmware/govmomi/blob/master/toolbox/toolbox/main.go)
* [VIC tether toolbox extension](https://github.com/vmware/vic/blob/master/lib/tether/toolbox.go)
* [VIC container VM tether](https://github.com/vmware/vic/blob/main/cmd/tether/main_linux.go)
* [VIC container host tether](https://github.com/vmware/vic/blob/master/cmd/vic-init/main_linux.go)
## Supported guests
The toolbox guest RPC implementations tend to be simple and portable thanks to the Go standard library, but are only
supported on Linux currently. Support for other guests, such as Windows, has been kept in mind but not yet tested.
## Supported vSphere Versions
The toolbox is supported with vSphere 6.0 and 6.5, but may function with older versions.

80
vendor/github.com/vmware/govmomi/toolbox/backdoor.go generated vendored Normal file
View File

@@ -0,0 +1,80 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"errors"
"github.com/vmware/vmw-guestinfo/message"
"github.com/vmware/vmw-guestinfo/vmcheck"
)
const (
rpciProtocol uint32 = 0x49435052
tcloProtocol uint32 = 0x4f4c4354
)
var (
ErrNotVirtualWorld = errors.New("not in a virtual world")
)
type backdoorChannel struct {
protocol uint32
*message.Channel
}
func (b *backdoorChannel) Start() error {
if !vmcheck.IsVirtualCPU() {
return ErrNotVirtualWorld
}
channel, err := message.NewChannel(b.protocol)
if err != nil {
return err
}
b.Channel = channel
return nil
}
func (b *backdoorChannel) Stop() error {
if b.Channel == nil {
return nil
}
err := b.Channel.Close()
b.Channel = nil
return err
}
// NewBackdoorChannelOut creates a Channel for use with the RPCI protocol
func NewBackdoorChannelOut() Channel {
return &backdoorChannel{
protocol: rpciProtocol,
}
}
// NewBackdoorChannelIn creates a Channel for use with the TCLO protocol
func NewBackdoorChannelIn() Channel {
return &backdoorChannel{
protocol: tcloProtocol,
}
}

View File

@@ -0,0 +1,50 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import "testing"
var _ Channel = new(backdoorChannel)
func TestBackdoorChannel(t *testing.T) {
in := NewBackdoorChannelIn()
out := NewBackdoorChannelOut()
funcs := []func() error{
in.Start,
out.Start,
in.Stop,
out.Stop,
}
for _, f := range funcs {
err := f()
if err != nil {
if err == ErrNotVirtualWorld {
t.SkipNow()
}
t.Fatal(err)
}
}
// expect an error if we don't specify the protocol
err := new(backdoorChannel).Start()
if err == nil {
t.Error("expected error")
}
}

58
vendor/github.com/vmware/govmomi/toolbox/channel.go generated vendored Normal file
View File

@@ -0,0 +1,58 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"bytes"
"fmt"
)
// Channel abstracts the guest<->vmx RPC transport
type Channel interface {
Start() error
Stop() error
Send([]byte) error
Receive() ([]byte, error)
}
var (
rpciOK = []byte{'1', ' '}
rpciERR = []byte{'0', ' '}
)
// ChannelOut extends Channel to provide RPCI protocol helpers
type ChannelOut struct {
Channel
}
// Request sends an RPC command to the vmx and checks the return code for success or error
func (c *ChannelOut) Request(request []byte) ([]byte, error) {
if err := c.Send(request); err != nil {
return nil, err
}
reply, err := c.Receive()
if err != nil {
return nil, err
}
if bytes.HasPrefix(reply, rpciOK) {
return reply[2:], nil
}
return nil, fmt.Errorf("request %q: %q", request, reply)
}

711
vendor/github.com/vmware/govmomi/toolbox/command.go generated vendored Normal file
View File

@@ -0,0 +1,711 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"bytes"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/vmware/govmomi/toolbox/hgfs"
"github.com/vmware/govmomi/toolbox/vix"
)
type CommandHandler func(vix.CommandRequestHeader, []byte) ([]byte, error)
type CommandServer struct {
Out *ChannelOut
ProcessManager *ProcessManager
Authenticate func(vix.CommandRequestHeader, []byte) error
ProcessStartCommand func(*ProcessManager, *vix.StartProgramRequest) (int64, error)
handlers map[uint32]CommandHandler
FileServer *hgfs.Server
}
func registerCommandServer(service *Service) *CommandServer {
server := &CommandServer{
Out: service.out,
ProcessManager: NewProcessManager(),
}
server.handlers = map[uint32]CommandHandler{
vix.CommandGetToolsState: server.GetToolsState,
vix.CommandStartProgram: server.StartCommand,
vix.CommandTerminateProcess: server.KillProcess,
vix.CommandListProcessesEx: server.ListProcesses,
vix.CommandReadEnvVariables: server.ReadEnvironmentVariables,
vix.CommandCreateTemporaryFileEx: server.CreateTemporaryFile,
vix.CommandCreateTemporaryDirectory: server.CreateTemporaryDirectory,
vix.CommandDeleteGuestFileEx: server.DeleteFile,
vix.CommandCreateDirectoryEx: server.CreateDirectory,
vix.CommandDeleteGuestDirectoryEx: server.DeleteDirectory,
vix.CommandMoveGuestFileEx: server.MoveFile,
vix.CommandMoveGuestDirectory: server.MoveDirectory,
vix.CommandListFiles: server.ListFiles,
vix.CommandSetGuestFileAttributes: server.SetGuestFileAttributes,
vix.CommandInitiateFileTransferFromGuest: server.InitiateFileTransferFromGuest,
vix.CommandInitiateFileTransferToGuest: server.InitiateFileTransferToGuest,
vix.HgfsSendPacketCommand: server.ProcessHgfsPacket,
}
server.ProcessStartCommand = DefaultStartCommand
service.RegisterHandler("Vix_1_Relayed_Command", server.Dispatch)
return server
}
func commandResult(header vix.CommandRequestHeader, rc int, err error, response []byte) []byte {
// All Foundry tools commands return results that start with a foundry error
// and a guest-OS-specific error (e.g. errno)
errno := 0
if err != nil {
// TODO: inspect err for system error, setting errno
response = []byte(err.Error())
log.Printf("[vix] op=%d error: %s", header.OpCode, err)
}
buf := bytes.NewBufferString(fmt.Sprintf("%d %d ", rc, errno))
if header.CommonFlags&vix.CommandGuestReturnsBinary != 0 {
// '#' delimits end of ascii and the start of the binary data (see ToolsDaemonTcloReceiveVixCommand)
_ = buf.WriteByte('#')
}
_, _ = buf.Write(response)
if header.CommonFlags&vix.CommandGuestReturnsBinary == 0 {
// this is not binary data, so it should be a NULL terminated string (see ToolsDaemonTcloReceiveVixCommand)
_ = buf.WriteByte(0)
}
return buf.Bytes()
}
func (c *CommandServer) Dispatch(data []byte) ([]byte, error) {
// See ToolsDaemonTcloGetQuotedString
if data[0] == '"' {
data = data[1:]
}
var name string
ix := bytes.IndexByte(data, '"')
if ix > 0 {
name = string(data[:ix])
data = data[ix+1:]
}
// skip the NULL
if data[0] == 0 {
data = data[1:]
}
if Trace {
fmt.Fprintf(os.Stderr, "vix dispatch %q...\n%s\n", name, hex.Dump(data))
}
var header vix.CommandRequestHeader
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &header)
if err != nil {
return nil, err
}
if header.Magic != vix.CommandMagicWord {
return commandResult(header, vix.InvalidMessageHeader, nil, nil), nil
}
handler, ok := c.handlers[header.OpCode]
if !ok {
return commandResult(header, vix.UnrecognizedCommandInGuest, nil, nil), nil
}
if header.OpCode != vix.CommandGetToolsState {
// Every command expect GetToolsState requires authentication
creds := buf.Bytes()[header.BodyLength:]
err = c.authenticate(header, creds[:header.CredentialLength])
if err != nil {
return commandResult(header, vix.AuthenticationFail, err, nil), nil
}
}
rc := vix.OK
response, err := handler(header, buf.Bytes())
if err != nil {
rc = vix.ErrorCode(err)
}
return commandResult(header, rc, err, response), nil
}
func (c *CommandServer) RegisterHandler(op uint32, handler CommandHandler) {
c.handlers[op] = handler
}
func (c *CommandServer) GetToolsState(_ vix.CommandRequestHeader, _ []byte) ([]byte, error) {
hostname, _ := os.Hostname()
osname := fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH)
// Note that vmtoolsd sends back 40 or so of these properties, sticking with the minimal set for now.
props := vix.PropertyList{
vix.NewStringProperty(vix.PropertyGuestOsVersion, osname),
vix.NewStringProperty(vix.PropertyGuestOsVersionShort, osname),
vix.NewStringProperty(vix.PropertyGuestToolsProductNam, "VMware Tools (Go)"),
vix.NewStringProperty(vix.PropertyGuestToolsVersion, "10.0.5 build-3227872 (Compatible)"),
vix.NewStringProperty(vix.PropertyGuestName, hostname),
vix.NewInt32Property(vix.PropertyGuestToolsAPIOptions, 0x0001), // TODO: const VIX_TOOLSFEATURE_SUPPORT_GET_HANDLE_STATE
vix.NewInt32Property(vix.PropertyGuestOsFamily, 1), // TODO: const GUEST_OS_FAMILY_*
vix.NewBoolProperty(vix.PropertyGuestStartProgramEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestTerminateProcessEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestListProcessesEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestReadEnvironmentVariableEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestMakeDirectoryEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestDeleteFileEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestDeleteDirectoryEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestMoveDirectoryEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestMoveFileEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestCreateTempFileEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestCreateTempDirectoryEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestListFilesEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestChangeFileAttributesEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestInitiateFileTransferFromGuestEnabled, true),
vix.NewBoolProperty(vix.PropertyGuestInitiateFileTransferToGuestEnabled, true),
}
src, _ := props.MarshalBinary()
enc := base64.StdEncoding
buf := make([]byte, enc.EncodedLen(len(src)))
enc.Encode(buf, src)
return buf, nil
}
func (c *CommandServer) StartCommand(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.StartProgramRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
pid, err := c.ProcessStartCommand(c.ProcessManager, r)
if err != nil {
return nil, err
}
return append([]byte(fmt.Sprintf("%d", pid)), 0), nil
}
func DefaultStartCommand(m *ProcessManager, r *vix.StartProgramRequest) (int64, error) {
p := NewProcess()
switch r.ProgramPath {
case "http.RoundTrip":
p = NewProcessRoundTrip()
default:
// Standard vmware-tools requires an absolute path,
// we'll enable IO redirection by default without an absolute path.
if !strings.Contains(r.ProgramPath, "/") {
p = p.WithIO()
}
}
return m.Start(r, p)
}
func (c *CommandServer) KillProcess(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.KillProcessRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
if c.ProcessManager.Kill(r.Body.Pid) {
return nil, err
}
// TODO: could kill process started outside of toolbox
return nil, vix.Error(vix.NoSuchProcess)
}
func (c *CommandServer) ListProcesses(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.ListProcessesRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
state := c.ProcessManager.ListProcesses(r.Pids)
return state, nil
}
func (c *CommandServer) ReadEnvironmentVariables(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.ReadEnvironmentVariablesRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
if len(r.Names) == 0 {
for _, e := range os.Environ() {
_, _ = buf.WriteString(fmt.Sprintf("<ev>%s</ev>", xmlEscape.Replace(e)))
}
} else {
for _, key := range r.Names {
val := os.Getenv(key)
if val == "" {
continue
}
_, _ = buf.WriteString(fmt.Sprintf("<ev>%s=%s</ev>", xmlEscape.Replace(key), xmlEscape.Replace(val)))
}
}
return buf.Bytes(), nil
}
func (c *CommandServer) CreateTemporaryFile(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.CreateTempFileRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
f, err := ioutil.TempFile(r.DirectoryPath, r.FilePrefix+"vmware")
if err != nil {
return nil, err
}
_ = f.Close()
return []byte(f.Name()), nil
}
func (c *CommandServer) CreateTemporaryDirectory(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.CreateTempFileRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
name, err := ioutil.TempDir(r.DirectoryPath, r.FilePrefix+"vmware")
if err != nil {
return nil, err
}
return []byte(name), nil
}
func (c *CommandServer) DeleteFile(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.FileRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
info, err := os.Stat(r.GuestPathName)
if err != nil {
return nil, err
}
if info.IsDir() {
return nil, vix.Error(vix.NotAFile)
}
err = os.Remove(r.GuestPathName)
return nil, err
}
func (c *CommandServer) DeleteDirectory(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.DirRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
info, err := os.Stat(r.GuestPathName)
if err != nil {
return nil, err
}
if !info.IsDir() {
return nil, vix.Error(vix.NotADirectory)
}
if r.Body.Recursive {
err = os.RemoveAll(r.GuestPathName)
} else {
err = os.Remove(r.GuestPathName)
}
return nil, err
}
func (c *CommandServer) CreateDirectory(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.DirRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
mkdir := os.Mkdir
if r.Body.Recursive {
mkdir = os.MkdirAll
}
err = mkdir(r.GuestPathName, 0700)
return nil, err
}
func (c *CommandServer) MoveDirectory(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.RenameFileRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
info, err := os.Stat(r.OldPathName)
if err != nil {
return nil, err
}
if !info.IsDir() {
return nil, vix.Error(vix.NotADirectory)
}
if !r.Body.Overwrite {
info, err = os.Stat(r.NewPathName)
if err == nil {
return nil, vix.Error(vix.FileAlreadyExists)
}
}
return nil, os.Rename(r.OldPathName, r.NewPathName)
}
func (c *CommandServer) MoveFile(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.RenameFileRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
info, err := os.Stat(r.OldPathName)
if err != nil {
return nil, err
}
if info.IsDir() {
return nil, vix.Error(vix.NotAFile)
}
if !r.Body.Overwrite {
info, err = os.Stat(r.NewPathName)
if err == nil {
return nil, vix.Error(vix.FileAlreadyExists)
}
}
return nil, os.Rename(r.OldPathName, r.NewPathName)
}
func (c *CommandServer) ListFiles(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.ListFilesRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
info, err := os.Lstat(r.GuestPathName)
if err != nil {
return nil, err
}
var dir string
var files []os.FileInfo
if info.IsDir() {
dir = r.GuestPathName
files, err = ioutil.ReadDir(r.GuestPathName)
if err != nil {
return nil, err
}
} else {
dir = filepath.Dir(r.GuestPathName)
files = append(files, info)
}
offset := r.Body.Offset + uint64(r.Body.Index)
total := uint64(len(files)) - offset
if int(offset) < len(files) {
files = files[offset:]
} else {
total = 0 // offset is not valid (open-vm-tools behaves the same in this case)
}
var remaining uint64
if r.Body.MaxResults > 0 && total > uint64(r.Body.MaxResults) {
remaining = total - uint64(r.Body.MaxResults)
files = files[:r.Body.MaxResults]
}
buf := new(bytes.Buffer)
buf.WriteString(fmt.Sprintf("<rem>%d</rem>", remaining))
for _, info = range files {
buf.WriteString(fileExtendedInfoFormat(dir, info))
}
return buf.Bytes(), nil
}
func chtimes(r *vix.SetGuestFileAttributesRequest) error {
var mtime, atime *time.Time
if r.IsSet(vix.FileAttributeSetModifyDate) {
t := time.Unix(r.Body.ModificationTime, 0)
mtime = &t
}
if r.IsSet(vix.FileAttributeSetAccessDate) {
t := time.Unix(r.Body.AccessTime, 0)
atime = &t
}
if mtime == nil && atime == nil {
return nil
}
info, err := os.Stat(r.GuestPathName)
if err != nil {
return err
}
if mtime == nil {
t := info.ModTime()
mtime = &t
}
if atime == nil {
t := info.ModTime()
atime = &t
}
return os.Chtimes(r.GuestPathName, *atime, *mtime)
}
func chown(r *vix.SetGuestFileAttributesRequest) error {
uid := -1
gid := -1
if r.IsSet(vix.FileAttributeSetUnixOwnerid) {
uid = int(r.Body.OwnerID)
}
if r.IsSet(vix.FileAttributeSetUnixGroupid) {
gid = int(r.Body.GroupID)
}
if uid == -1 && gid == -1 {
return nil
}
return os.Chown(r.GuestPathName, uid, gid)
}
func chmod(r *vix.SetGuestFileAttributesRequest) error {
if r.IsSet(vix.FileAttributeSetUnixPermissions) {
return os.Chmod(r.GuestPathName, os.FileMode(r.Body.Permissions).Perm())
}
return nil
}
func (c *CommandServer) SetGuestFileAttributes(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.SetGuestFileAttributesRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
for _, set := range []func(*vix.SetGuestFileAttributesRequest) error{chtimes, chown, chmod} {
err = set(r)
if err != nil {
return nil, err
}
}
return nil, nil
}
func (c *CommandServer) InitiateFileTransferFromGuest(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.ListFilesRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
info, err := c.FileServer.Stat(r.GuestPathName)
if err != nil {
return nil, err
}
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
return nil, vix.Error(vix.InvalidArg)
}
if info.IsDir() {
return nil, vix.Error(vix.NotAFile)
}
return []byte(fileExtendedInfoFormat("", info)), nil
}
func (c *CommandServer) InitiateFileTransferToGuest(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.InitiateFileTransferToGuestRequest{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
info, err := c.FileServer.Stat(r.GuestPathName)
if err == nil {
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
return nil, vix.Error(vix.InvalidArg)
}
if info.IsDir() {
return nil, vix.Error(vix.NotAFile)
}
if !r.Body.Overwrite {
return nil, vix.Error(vix.FileAlreadyExists)
}
} else {
if !os.IsNotExist(err) {
return nil, err
}
}
return nil, nil
}
func (c *CommandServer) ProcessHgfsPacket(header vix.CommandRequestHeader, data []byte) ([]byte, error) {
r := &vix.CommandHgfsSendPacket{
CommandRequestHeader: header,
}
err := r.UnmarshalBinary(data)
if err != nil {
return nil, err
}
return c.FileServer.Dispatch(r.Packet)
}
func (c *CommandServer) authenticate(r vix.CommandRequestHeader, data []byte) error {
if c.Authenticate != nil {
return c.Authenticate(r, data)
}
switch r.UserCredentialType {
case vix.UserCredentialTypeNamePassword:
var c vix.UserCredentialNamePassword
if err := c.UnmarshalBinary(data); err != nil {
return err
}
if Trace {
fmt.Fprintf(traceLog, "ignoring credentials: %q:%q\n", c.Name, c.Password)
}
return nil
default:
return fmt.Errorf("unsupported UserCredentialType=%d", r.UserCredentialType)
}
}

View File

@@ -0,0 +1,972 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"bytes"
"context"
"encoding"
"encoding/binary"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/vmware/govmomi/toolbox/hgfs"
"github.com/vmware/govmomi/toolbox/vix"
)
type CommandClient struct {
Service *Service
Header *vix.CommandRequestHeader
creds []byte
}
func NewCommandClient() *CommandClient {
Trace = testing.Verbose()
hgfs.Trace = Trace
creds, _ := (&vix.UserCredentialNamePassword{
Name: "user",
Password: "pass",
}).MarshalBinary()
header := new(vix.CommandRequestHeader)
header.Magic = vix.CommandMagicWord
header.UserCredentialType = vix.UserCredentialTypeNamePassword
header.CredentialLength = uint32(len(creds))
in := new(mockChannelIn)
out := new(mockChannelOut)
return &CommandClient{
creds: creds,
Header: header,
Service: NewService(in, out),
}
}
func (c *CommandClient) Request(op uint32, m encoding.BinaryMarshaler) []byte {
b, err := m.MarshalBinary()
if err != nil {
panic(err)
}
c.Header.OpCode = op
c.Header.BodyLength = uint32(len(b))
var buf bytes.Buffer
_, _ = buf.Write([]byte("\"reqname\"\x00"))
_ = binary.Write(&buf, binary.LittleEndian, c.Header)
_, _ = buf.Write(b)
data := append(buf.Bytes(), c.creds...)
reply, err := c.Service.Command.Dispatch(data)
if err != nil {
panic(err)
}
return reply
}
func vixRC(buf []byte) int {
args := bytes.SplitN(buf, []byte{' '}, 2)
rc, err := strconv.Atoi(string(args[0]))
if err != nil {
panic(err)
}
return rc
}
func TestVixRelayedCommandHandler(t *testing.T) {
Trace = true
if !testing.Verbose() {
// cover Trace paths but discard output
traceLog = ioutil.Discard
}
in := new(mockChannelIn)
out := new(mockChannelOut)
service := NewService(in, out)
cmd := service.Command
msg := []byte("\"reqname\"\x00")
_, err := cmd.Dispatch(msg) // io.EOF
if err == nil {
t.Fatal("expected error")
}
header := new(vix.CommandRequestHeader)
marshal := func(m ...encoding.BinaryMarshaler) []byte {
var buf bytes.Buffer
_, _ = buf.Write(msg)
_ = binary.Write(&buf, binary.LittleEndian, header)
for _, e := range m {
b, err := e.MarshalBinary()
if err != nil {
panic(err)
}
_, _ = buf.Write(b)
}
return buf.Bytes()
}
// header.Magic not set
reply, _ := cmd.Dispatch(marshal())
rc := vixRC(reply)
if rc != vix.InvalidMessageHeader {
t.Fatalf("%q", reply)
}
// header.OpCode not set
header.Magic = vix.CommandMagicWord
reply, _ = cmd.Dispatch(marshal())
rc = vixRC(reply)
if rc != vix.UnrecognizedCommandInGuest {
t.Fatalf("%q", reply)
}
// valid request for GetToolsState
header.OpCode = vix.CommandGetToolsState
reply, _ = cmd.Dispatch(marshal())
rc = vixRC(reply)
if rc != vix.OK {
t.Fatalf("%q", reply)
}
// header.UserCredentialType not set
header.OpCode = vix.CommandStartProgram
request := new(vix.StartProgramRequest)
buf := marshal(request)
reply, _ = cmd.Dispatch(marshal())
rc = vixRC(reply)
if rc != vix.AuthenticationFail {
t.Fatalf("%q", reply)
}
creds, _ := (&vix.UserCredentialNamePassword{
Name: "user",
Password: "pass",
}).MarshalBinary()
header.BodyLength = uint32(binary.Size(request.Body))
header.UserCredentialType = vix.UserCredentialTypeNamePassword
header.CredentialLength = uint32(len(creds))
// ProgramPath not set
buf = append(marshal(request), creds...)
reply, _ = cmd.Dispatch(buf)
rc = vixRC(reply)
if rc != vix.FileNotFound {
t.Fatalf("%q", reply)
}
cmd.ProcessStartCommand = func(pm *ProcessManager, r *vix.StartProgramRequest) (int64, error) {
return -1, nil
}
// valid request for StartProgram
buf = append(marshal(request), creds...)
reply, _ = cmd.Dispatch(buf)
rc = vixRC(reply)
if rc != vix.OK {
t.Fatalf("%q", reply)
}
cmd.Authenticate = func(_ vix.CommandRequestHeader, data []byte) error {
var c vix.UserCredentialNamePassword
if err := c.UnmarshalBinary(data); err != nil {
panic(err)
}
return errors.New("you shall not pass")
}
// fail auth with our own handler
buf = append(marshal(request), creds...)
reply, _ = cmd.Dispatch(buf)
rc = vixRC(reply)
if rc != vix.AuthenticationFail {
t.Fatalf("%q", reply)
}
cmd.Authenticate = nil
// cause Vix.UserCredentialNamePassword.UnmarshalBinary to error
// first by EOF reading header, second in base64 decode
for _, l := range []uint32{1, 10} {
header.CredentialLength = l
buf = append(marshal(request), creds...)
reply, _ = cmd.Dispatch(buf)
rc = vixRC(reply)
if rc != vix.AuthenticationFail {
t.Fatalf("%q", reply)
}
}
}
// cover misc error paths
func TestVixCommandErrors(t *testing.T) {
r := new(vix.StartProgramRequest)
err := r.UnmarshalBinary(nil)
if err == nil {
t.Error("expected error")
}
r.Body.NumEnvVars = 1
buf, _ := r.MarshalBinary()
err = r.UnmarshalBinary(buf)
if err == nil {
t.Error("expected error")
}
c := new(CommandServer)
_, err = c.StartCommand(r.CommandRequestHeader, nil)
if err == nil {
t.Error("expected error")
}
}
func TestVixInitiateDirTransfer(t *testing.T) {
c := NewCommandClient()
dir := os.TempDir()
for _, enable := range []bool{true, false} {
expect := vix.NotAFile
if enable {
expect = vix.OK
} else {
// validate we behave as open-vm-tools does when the directory archive feature is disabled
c.Service.Command.FileServer.RegisterFileHandler(hgfs.ArchiveScheme, nil)
}
fromGuest := &vix.ListFilesRequest{GuestPathName: dir}
toGuest := &vix.InitiateFileTransferToGuestRequest{GuestPathName: dir}
toGuest.Body.Overwrite = true
tests := []struct {
op uint32
request encoding.BinaryMarshaler
}{
{vix.CommandInitiateFileTransferFromGuest, fromGuest},
{vix.CommandInitiateFileTransferToGuest, toGuest},
}
for _, test := range tests {
reply := c.Request(test.op, test.request)
rc := vixRC(reply)
if rc != expect {
t.Errorf("rc=%d", rc)
}
}
}
}
func TestVixInitiateFileTransfer(t *testing.T) {
c := NewCommandClient()
request := new(vix.ListFilesRequest)
f, err := ioutil.TempFile("", "toolbox")
if err != nil {
t.Fatal(err)
}
for _, s := range []string{"a", "b", "c", "d", "e"} {
_, _ = f.WriteString(strings.Repeat(s, 40))
}
_ = f.Close()
name := f.Name()
// 1st pass file exists == OK, 2nd pass does not exist == FAIL
for _, fail := range []bool{false, true} {
request.GuestPathName = name
reply := c.Request(vix.CommandInitiateFileTransferFromGuest, request)
rc := vixRC(reply)
if Trace {
fmt.Fprintf(os.Stderr, "%s: %s\n", name, string(reply))
}
if fail {
if rc == vix.OK {
t.Errorf("%s: %d", name, rc)
}
} else {
if rc != vix.OK {
t.Errorf("%s: %d", name, rc)
}
err = os.Remove(name)
if err != nil {
t.Error(err)
}
}
}
}
func TestVixInitiateFileTransferWrite(t *testing.T) {
c := NewCommandClient()
request := new(vix.InitiateFileTransferToGuestRequest)
f, err := ioutil.TempFile("", "toolbox")
if err != nil {
t.Fatal(err)
}
_ = f.Close()
name := f.Name()
tests := []struct {
force bool
fail bool
}{
{false, true}, // exists == FAIL
{true, false}, // exists, but overwrite == OK
{false, false}, // does not exist == OK
}
for i, test := range tests {
request.GuestPathName = name
request.Body.Overwrite = test.force
reply := c.Request(vix.CommandInitiateFileTransferToGuest, request)
rc := vixRC(reply)
if Trace {
fmt.Fprintf(os.Stderr, "%s: %s\n", name, string(reply))
}
if test.fail {
if rc == vix.OK {
t.Errorf("%d: %d", i, rc)
}
} else {
if rc != vix.OK {
t.Errorf("%d: %d", i, rc)
}
if test.force {
_ = os.Remove(name)
}
}
}
}
func TestVixProcessHgfsPacket(t *testing.T) {
c := NewCommandClient()
c.Header.CommonFlags = vix.CommandGuestReturnsBinary
request := new(vix.CommandHgfsSendPacket)
op := new(hgfs.RequestCreateSessionV4)
packet := new(hgfs.Packet)
packet.Payload, _ = op.MarshalBinary()
packet.Header.Version = hgfs.HeaderVersion
packet.Header.Dummy = hgfs.OpNewHeader
packet.Header.HeaderSize = uint32(binary.Size(&packet.Header))
packet.Header.PacketSize = packet.Header.HeaderSize + uint32(len(packet.Payload))
packet.Header.Op = hgfs.OpCreateSessionV4
request.Packet, _ = packet.MarshalBinary()
request.Body.PacketSize = uint32(len(request.Packet))
reply := c.Request(vix.HgfsSendPacketCommand, request)
rc := vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
ix := bytes.IndexByte(reply, '#')
reply = reply[ix+1:]
err := packet.UnmarshalBinary(reply)
if err != nil {
t.Fatal(err)
}
if packet.Status != hgfs.StatusSuccess {
t.Errorf("status=%d", packet.Status)
}
if packet.Dummy != hgfs.OpNewHeader {
t.Errorf("dummy=%d", packet.Dummy)
}
session := new(hgfs.ReplyCreateSessionV4)
err = session.UnmarshalBinary(packet.Payload)
if err != nil {
t.Fatal(err)
}
if session.NumCapabilities == 0 || int(session.NumCapabilities) != len(session.Capabilities) {
t.Errorf("NumCapabilities=%d", session.NumCapabilities)
}
}
func TestVixListProcessesEx(t *testing.T) {
c := NewCommandClient()
pm := c.Service.Command.ProcessManager
c.Service.Command.ProcessStartCommand = func(pm *ProcessManager, r *vix.StartProgramRequest) (int64, error) {
var p *Process
switch r.ProgramPath {
case "foo":
p = NewProcessFunc(func(ctx context.Context, arg string) error {
return nil
})
default:
return -1, os.ErrNotExist
}
return pm.Start(r, p)
}
exec := &vix.StartProgramRequest{
ProgramPath: "foo",
}
reply := c.Request(vix.CommandStartProgram, exec)
rc := vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
r := bytes.Trim(bytes.Split(reply, []byte{' '})[2], "\x00")
pid, _ := strconv.Atoi(string(r))
exec.ProgramPath = "bar"
reply = c.Request(vix.CommandStartProgram, exec)
rc = vixRC(reply)
t.Log(vix.Error(rc).Error())
if rc != vix.FileNotFound {
t.Fatalf("rc: %d", rc)
}
if vix.ErrorCode(os.ErrNotExist) != rc {
t.Fatalf("rc: %d", rc)
}
pm.wg.Wait()
ps := new(vix.ListProcessesRequest)
ps.Pids = []int64{int64(pid)}
reply = c.Request(vix.CommandListProcessesEx, ps)
rc = vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
n := bytes.Count(reply, []byte("<proc>"))
if n != len(ps.Pids) {
t.Errorf("ps -p %d=%d", pid, n)
}
kill := new(vix.KillProcessRequest)
kill.Body.Pid = ps.Pids[0]
reply = c.Request(vix.CommandTerminateProcess, kill)
rc = vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
kill.Body.Pid = 33333
reply = c.Request(vix.CommandTerminateProcess, kill)
rc = vixRC(reply)
if rc != vix.NoSuchProcess {
t.Fatalf("rc: %d", rc)
}
}
func TestVixGetenv(t *testing.T) {
c := NewCommandClient()
env := os.Environ()
key := strings.SplitN(env[0], "=", 2)[0]
tests := []struct {
names []string
expect int
}{
{nil, len(env)}, // all env
{[]string{key, "ENOENT"}, 1}, // specific vars, 1 exists 1 does not
}
for i, test := range tests {
env := &vix.ReadEnvironmentVariablesRequest{
Names: test.names,
}
reply := c.Request(vix.CommandReadEnvVariables, env)
rc := vixRC(reply)
if rc != vix.OK {
t.Fatalf("%d) rc: %d", i, rc)
}
num := bytes.Count(reply, []byte("<ev>"))
if num != test.expect {
t.Errorf("%d) getenv(%v): %d", i, test.names, num)
}
}
}
func TestVixDirectories(t *testing.T) {
c := NewCommandClient()
mktemp := &vix.CreateTempFileRequest{
FilePrefix: "toolbox-",
}
// mktemp -d
reply := c.Request(vix.CommandCreateTemporaryDirectory, mktemp)
rc := vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
dir := strings.TrimSuffix(string(reply[4:]), "\x00")
mkdir := &vix.DirRequest{
GuestPathName: dir,
}
// mkdir $dir == EEXIST
reply = c.Request(vix.CommandCreateDirectoryEx, mkdir)
rc = vixRC(reply)
if rc != vix.FileAlreadyExists {
t.Fatalf("rc: %d", rc)
}
// mkdir $dir/ok == OK
mkdir.GuestPathName = dir + "/ok"
reply = c.Request(vix.CommandCreateDirectoryEx, mkdir)
rc = vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
// rm of a dir should fail, regardless if empty or not
reply = c.Request(vix.CommandDeleteGuestFileEx, &vix.FileRequest{
GuestPathName: mkdir.GuestPathName,
})
rc = vixRC(reply)
if rc != vix.NotAFile {
t.Errorf("rc: %d", rc)
}
// rmdir $dir/ok == OK
reply = c.Request(vix.CommandDeleteGuestDirectoryEx, mkdir)
rc = vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
// rmdir $dir/ok == ENOENT
reply = c.Request(vix.CommandDeleteGuestDirectoryEx, mkdir)
rc = vixRC(reply)
if rc != vix.FileNotFound {
t.Fatalf("rc: %d", rc)
}
// mkdir $dir/1/2 == ENOENT (parent directory does not exist)
mkdir.GuestPathName = dir + "/1/2"
reply = c.Request(vix.CommandCreateDirectoryEx, mkdir)
rc = vixRC(reply)
if rc != vix.FileNotFound {
t.Fatalf("rc: %d", rc)
}
// mkdir -p $dir/1/2 == OK
mkdir.Body.Recursive = true
reply = c.Request(vix.CommandCreateDirectoryEx, mkdir)
rc = vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
// rmdir $dir == ENOTEMPTY
mkdir.GuestPathName = dir
mkdir.Body.Recursive = false
reply = c.Request(vix.CommandDeleteGuestDirectoryEx, mkdir)
rc = vixRC(reply)
if rc != vix.DirectoryNotEmpty {
t.Fatalf("rc: %d", rc)
}
// rm -rf $dir == OK
mkdir.Body.Recursive = true
reply = c.Request(vix.CommandDeleteGuestDirectoryEx, mkdir)
rc = vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
}
func TestVixFiles(t *testing.T) {
c := NewCommandClient()
mktemp := &vix.CreateTempFileRequest{
FilePrefix: "toolbox-",
}
// mktemp -d
reply := c.Request(vix.CommandCreateTemporaryDirectory, mktemp)
rc := vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
dir := strings.TrimSuffix(string(reply[4:]), "\x00")
max := 12
var total int
// mktemp
for i := 0; i <= max; i++ {
mktemp = &vix.CreateTempFileRequest{
DirectoryPath: dir,
}
reply = c.Request(vix.CommandCreateTemporaryFileEx, mktemp)
rc = vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
}
// name of the last file temp file we created, we'll mess around with it then delete it
name := strings.TrimSuffix(string(reply[4:]), "\x00")
// for testing symlinks
link := filepath.Join(dir, "a-link")
err := os.Symlink(name, link)
if err != nil {
t.Fatal(err)
}
for _, fpath := range []string{name, link} {
// test ls of a single file
ls := &vix.ListFilesRequest{
GuestPathName: fpath,
}
reply = c.Request(vix.CommandListFiles, ls)
rc = vixRC(reply)
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
num := bytes.Count(reply, []byte("<fxi>"))
if num != 1 {
t.Errorf("ls %s: %d", name, num)
}
num = bytes.Count(reply, []byte("<rem>0</rem>"))
if num != 1 {
t.Errorf("ls %s: %d", name, num)
}
ft := 0
target := ""
if fpath == link {
target = name
ft = vix.FileAttributesSymlink
}
num = bytes.Count(reply, []byte(fmt.Sprintf("<slt>%s</slt>", target)))
if num != 1 {
t.Errorf("ls %s: %d", name, num)
}
num = bytes.Count(reply, []byte(fmt.Sprintf("<ft>%d</ft>", ft)))
if num != 1 {
t.Errorf("ls %s: %d", name, num)
}
}
mv := &vix.RenameFileRequest{
OldPathName: name,
NewPathName: name + "-new",
}
for _, expect := range []int{vix.OK, vix.FileNotFound} {
reply = c.Request(vix.CommandMoveGuestFileEx, mv)
rc = vixRC(reply)
if rc != expect {
t.Errorf("rc: %d", rc)
}
if expect == vix.OK {
// test file type is properly checked
reply = c.Request(vix.CommandMoveGuestDirectory, &vix.RenameFileRequest{
OldPathName: mv.NewPathName,
NewPathName: name,
})
rc = vixRC(reply)
if rc != vix.NotADirectory {
t.Errorf("rc: %d", rc)
}
// test Overwrite flag is properly checked
reply = c.Request(vix.CommandMoveGuestFileEx, &vix.RenameFileRequest{
OldPathName: mv.NewPathName,
NewPathName: mv.NewPathName,
})
rc = vixRC(reply)
if rc != vix.FileAlreadyExists {
t.Errorf("rc: %d", rc)
}
}
}
// rmdir of a file should fail
reply = c.Request(vix.CommandDeleteGuestDirectoryEx, &vix.DirRequest{
GuestPathName: mv.NewPathName,
})
rc = vixRC(reply)
if rc != vix.NotADirectory {
t.Errorf("rc: %d", rc)
}
file := &vix.FileRequest{
GuestPathName: mv.NewPathName,
}
for _, expect := range []int{vix.OK, vix.FileNotFound} {
reply = c.Request(vix.CommandDeleteGuestFileEx, file)
rc = vixRC(reply)
if rc != expect {
t.Errorf("rc: %d", rc)
}
}
// ls again now that file is gone
reply = c.Request(vix.CommandListFiles, &vix.ListFilesRequest{
GuestPathName: name,
})
rc = vixRC(reply)
if rc != vix.FileNotFound {
t.Errorf("rc: %d", rc)
}
// ls
ls := &vix.ListFilesRequest{
GuestPathName: dir,
}
ls.Body.MaxResults = 5 // default is 50
for i := 0; i < 5; i++ {
reply = c.Request(vix.CommandListFiles, ls)
if Trace {
fmt.Fprintf(os.Stderr, "%s: %q\n", dir, string(reply[4:]))
}
var rem int
_, err := fmt.Fscanf(bytes.NewReader(reply[4:]), "<rem>%d</rem>", &rem)
if err != nil {
t.Fatal(err)
}
num := bytes.Count(reply, []byte("<fxi>"))
total += num
ls.Body.Offset += uint64(num)
if rem == 0 {
break
}
}
if total != max+1 {
t.Errorf("expected %d, got %d", max, total)
}
// Test invalid offset, making sure it doesn't cause panic (issue #934)
ls.Body.Offset += 10
_ = c.Request(vix.CommandListFiles, ls)
// mv $dir ${dir}-old
mv = &vix.RenameFileRequest{
OldPathName: dir,
NewPathName: dir + "-old",
}
for _, expect := range []int{vix.OK, vix.FileNotFound} {
reply = c.Request(vix.CommandMoveGuestDirectory, mv)
rc = vixRC(reply)
if rc != expect {
t.Errorf("rc: %d", rc)
}
if expect == vix.OK {
// test file type is properly checked
reply = c.Request(vix.CommandMoveGuestFileEx, &vix.RenameFileRequest{
OldPathName: mv.NewPathName,
NewPathName: dir,
})
rc = vixRC(reply)
if rc != vix.NotAFile {
t.Errorf("rc: %d", rc)
}
// test Overwrite flag is properly checked
reply = c.Request(vix.CommandMoveGuestDirectory, &vix.RenameFileRequest{
OldPathName: mv.NewPathName,
NewPathName: mv.NewPathName,
})
rc = vixRC(reply)
if rc != vix.FileAlreadyExists {
t.Errorf("rc: %d", rc)
}
}
}
rmdir := &vix.DirRequest{
GuestPathName: mv.NewPathName,
}
// rm -rm $dir
for _, rmr := range []bool{false, true} {
rmdir.Body.Recursive = rmr
reply = c.Request(vix.CommandDeleteGuestDirectoryEx, rmdir)
rc = vixRC(reply)
if rmr {
if rc != vix.OK {
t.Fatalf("rc: %d", rc)
}
} else {
if rc != vix.DirectoryNotEmpty {
t.Fatalf("rc: %d", rc)
}
}
}
}
func TestVixFileChangeAttributes(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("running as root")
}
c := NewCommandClient()
f, err := ioutil.TempFile("", "toolbox-")
if err != nil {
t.Fatal(err)
}
_ = f.Close()
name := f.Name()
// touch,chown,chmod
chattr := &vix.SetGuestFileAttributesRequest{
GuestPathName: name,
}
h := &chattr.Body
tests := []struct {
expect int
f func()
}{
{
vix.OK, func() {},
},
{
vix.OK, func() {
h.FileOptions = vix.FileAttributeSetModifyDate
h.ModificationTime = time.Now().Unix()
},
},
{
vix.OK, func() {
h.FileOptions = vix.FileAttributeSetAccessDate
h.AccessTime = time.Now().Unix()
},
},
{
vix.FileAccessError, func() {
h.FileOptions = vix.FileAttributeSetUnixOwnerid
h.OwnerID = 0 // fails as we are not root
},
},
{
vix.FileAccessError, func() {
h.FileOptions = vix.FileAttributeSetUnixGroupid
h.GroupID = 0 // fails as we are not root
},
},
{
vix.OK, func() {
h.FileOptions = vix.FileAttributeSetUnixOwnerid
h.OwnerID = int32(os.Getuid())
},
},
{
vix.OK, func() {
h.FileOptions = vix.FileAttributeSetUnixGroupid
h.GroupID = int32(os.Getgid())
},
},
{
vix.OK, func() {
h.FileOptions = vix.FileAttributeSetUnixPermissions
h.Permissions = int32(os.FileMode(0755).Perm())
},
},
{
vix.FileNotFound, func() {
_ = os.Remove(name)
},
},
}
for i, test := range tests {
test.f()
reply := c.Request(vix.CommandSetGuestFileAttributes, chattr)
rc := vixRC(reply)
if rc != test.expect {
t.Errorf("%d: rc=%d", i, rc)
}
}
}

202
vendor/github.com/vmware/govmomi/toolbox/guest_info.go generated vendored Normal file
View File

@@ -0,0 +1,202 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"bytes"
"fmt"
"net"
"github.com/davecgh/go-xdr/xdr2"
)
// Defs from: open-vm-tools/lib/guestRpc/nicinfo.x
type TypedIPAddress struct {
Type int32
Address []byte
}
type IPAddressEntry struct {
Address TypedIPAddress
PrefixLength uint32
Origin *int32 `xdr:"optional"`
Status *int32 `xdr:"optional"`
}
type InetCidrRouteEntry struct {
Dest TypedIPAddress
PrefixLength uint32
NextHop *TypedIPAddress `xdr:"optional"`
IfIndex uint32
Type int32
Metric uint32
}
type DNSConfigInfo struct {
HostName *string `xdr:"optional"`
DomainName *string `xdr:"optional"`
Servers []TypedIPAddress
Search *string `xdr:"optional"`
}
type WinsConfigInfo struct {
Primary TypedIPAddress
Secondary TypedIPAddress
}
type DhcpConfigInfo struct {
Enabled bool
Settings string
}
type GuestNicV3 struct {
MacAddress string
IPs []IPAddressEntry
DNSConfigInfo *DNSConfigInfo `xdr:"optional"`
WinsConfigInfo *WinsConfigInfo `xdr:"optional"`
DhcpConfigInfov4 *DhcpConfigInfo `xdr:"optional"`
DhcpConfigInfov6 *DhcpConfigInfo `xdr:"optional"`
}
type NicInfoV3 struct {
Nics []GuestNicV3
Routes []InetCidrRouteEntry
DNSConfigInfo *DNSConfigInfo `xdr:"optional"`
WinsConfigInfo *WinsConfigInfo `xdr:"optional"`
DhcpConfigInfov4 *DhcpConfigInfo `xdr:"optional"`
DhcpConfigInfov6 *DhcpConfigInfo `xdr:"optional"`
}
type GuestNicInfo struct {
Version int32
V3 *NicInfoV3 `xdr:"optional"`
}
func EncodeXDR(val interface{}) ([]byte, error) {
var buf bytes.Buffer
_, err := xdr.Marshal(&buf, val)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func DecodeXDR(buf []byte, val interface{}) error {
r := bytes.NewReader(buf)
_, err := xdr.Unmarshal(r, val)
return err
}
func NewGuestNicInfo() *GuestNicInfo {
return &GuestNicInfo{
Version: 3,
V3: &NicInfoV3{},
}
}
func (nic *GuestNicV3) AddIP(addr net.Addr) {
ip, ok := addr.(*net.IPNet)
if !ok {
return
}
kind := int32(1) // IAT_IPV4
if ip.IP.To4() == nil {
kind = 2 // IAT_IPV6
} else {
ip.IP = ip.IP.To4() // convert to 4-byte representation
}
size, _ := ip.Mask.Size()
// nicinfo.x defines enum IpAddressStatus, but vmtoolsd only uses IAS_PREFERRED
var status int32 = 1 // IAS_PREFERRED
e := IPAddressEntry{
Address: TypedIPAddress{
Type: kind,
Address: []byte(ip.IP),
},
PrefixLength: uint32(size),
Status: &status,
}
nic.IPs = append(nic.IPs, e)
}
func GuestInfoCommand(kind int, req []byte) []byte {
request := fmt.Sprintf("SetGuestInfo %d ", kind)
return append([]byte(request), req...)
}
var (
netInterfaces = net.Interfaces
maxNics = 16 // guestRpc/nicinfo.x:NICINFO_MAX_NICS
)
//
func DefaultGuestNicInfo() *GuestNicInfo {
proto := NewGuestNicInfo()
info := proto.V3
// #nosec: Errors unhandled
ifs, _ := netInterfaces()
for _, i := range ifs {
if i.Flags&net.FlagLoopback == net.FlagLoopback {
continue
}
if len(i.HardwareAddr) == 0 {
continue // Not useful from outside the guest without a MAC
}
// #nosec: Errors unhandled
addrs, _ := i.Addrs()
if len(addrs) == 0 {
continue // Not useful from outside the guest without an IP
}
nic := GuestNicV3{
MacAddress: i.HardwareAddr.String(),
}
for _, addr := range addrs {
nic.AddIP(addr)
}
info.Nics = append(info.Nics, nic)
if len(info.Nics) >= maxNics {
break
}
}
return proto
}
func GuestInfoNicInfoRequest() ([]byte, error) {
r, err := EncodeXDR(DefaultGuestNicInfo())
if err != nil {
return nil, err
}
return GuestInfoCommand(9 /*INFO_IPADDRESS_V3*/, r), nil
}

View File

@@ -0,0 +1,73 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"net"
"reflect"
"testing"
)
func TestDefaultGuestNicProto(t *testing.T) {
p := DefaultGuestNicInfo()
info := p.V3
for _, nic := range info.Nics {
if len(nic.MacAddress) == 0 {
continue
}
_, err := net.ParseMAC(nic.MacAddress)
if err != nil {
t.Errorf("invalid MAC %s: %s", nic.MacAddress, err)
}
}
b, err := EncodeXDR(p)
if err != nil {
t.Fatal(err)
}
var dp GuestNicInfo
err = DecodeXDR(b, &dp)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(p, &dp) {
t.Error("decode mismatch")
}
}
func TestMaxGuestNic(t *testing.T) {
p := DefaultGuestNicInfo()
maxNics = len(p.V3.Nics)
a, _ := net.Interfaces()
a = append(a, a...) // double the number of interfaces returned
netInterfaces = func() ([]net.Interface, error) {
return a, nil
}
p = DefaultGuestNicInfo()
l := len(p.V3.Nics)
if l != maxNics {
t.Errorf("Nics=%d", l)
}
}

View File

@@ -0,0 +1,342 @@
/*
Copyright (c) 2017 VMware, 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 hgfs
import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"io"
"io/ioutil"
"log"
"math"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/vmware/govmomi/toolbox/vix"
)
// ArchiveScheme is the default scheme used to register the archive FileHandler
var ArchiveScheme = "archive"
// ArchiveHandler implements a FileHandler for transferring directories.
type ArchiveHandler struct {
Read func(*url.URL, *tar.Reader) error
Write func(*url.URL, *tar.Writer) error
}
// NewArchiveHandler returns a FileHandler implementation for transferring directories using gzip'd tar files.
func NewArchiveHandler() FileHandler {
return &ArchiveHandler{
Read: archiveRead,
Write: archiveWrite,
}
}
// Stat implements FileHandler.Stat
func (*ArchiveHandler) Stat(u *url.URL) (os.FileInfo, error) {
switch u.Query().Get("format") {
case "", "tar", "tgz":
// ok
default:
log.Printf("unknown archive format: %q", u)
return nil, vix.Error(vix.InvalidArg)
}
return &archive{
name: u.Path,
size: math.MaxInt64,
}, nil
}
// Open implements FileHandler.Open
func (h *ArchiveHandler) Open(u *url.URL, mode int32) (File, error) {
switch mode {
case OpenModeReadOnly:
return h.newArchiveFromGuest(u)
case OpenModeWriteOnly:
return h.newArchiveToGuest(u)
default:
return nil, os.ErrNotExist
}
}
// archive implements the hgfs.File and os.FileInfo interfaces.
type archive struct {
name string
size int64
done func() error
io.Reader
io.Writer
}
// Name implementation of the os.FileInfo interface method.
func (a *archive) Name() string {
return a.name
}
// Size implementation of the os.FileInfo interface method.
func (a *archive) Size() int64 {
return a.size
}
// Mode implementation of the os.FileInfo interface method.
func (a *archive) Mode() os.FileMode {
return 0600
}
// ModTime implementation of the os.FileInfo interface method.
func (a *archive) ModTime() time.Time {
return time.Now()
}
// IsDir implementation of the os.FileInfo interface method.
func (a *archive) IsDir() bool {
return false
}
// Sys implementation of the os.FileInfo interface method.
func (a *archive) Sys() interface{} {
return nil
}
// The trailer is required since TransferFromGuest requires a Content-Length,
// which toolbox doesn't know ahead of time as the gzip'd tarball never touches the disk.
// HTTP clients need to be aware of this and stop reading when they see the 2nd gzip header.
var gzipHeader = []byte{0x1f, 0x8b, 0x08} // rfc1952 {ID1, ID2, CM}
var gzipTrailer = true
// newArchiveFromGuest returns an hgfs.File implementation to read a directory as a gzip'd tar.
func (h *ArchiveHandler) newArchiveFromGuest(u *url.URL) (File, error) {
r, w := io.Pipe()
a := &archive{
name: u.Path,
done: r.Close,
Reader: r,
Writer: w,
}
var z io.Writer = w
var c io.Closer = ioutil.NopCloser(nil)
switch u.Query().Get("format") {
case "tgz":
gz := gzip.NewWriter(w)
z = gz
c = gz
}
tw := tar.NewWriter(z)
go func() {
err := h.Write(u, tw)
_ = tw.Close()
_ = c.Close()
if gzipTrailer {
_, _ = w.Write(gzipHeader)
}
_ = w.CloseWithError(err)
}()
return a, nil
}
// newArchiveToGuest returns an hgfs.File implementation to expand a gzip'd tar into a directory.
func (h *ArchiveHandler) newArchiveToGuest(u *url.URL) (File, error) {
r, w := io.Pipe()
buf := bufio.NewReader(r)
a := &archive{
name: u.Path,
Reader: buf,
Writer: w,
}
var cerr error
var wg sync.WaitGroup
a.done = func() error {
_ = w.Close()
// We need to wait for unpack to finish to complete its work
// and to propagate the error if any to Close.
wg.Wait()
return cerr
}
wg.Add(1)
go func() {
defer wg.Done()
c := func() error {
// Drain the pipe of tar trailer data (two null blocks)
if cerr == nil {
_, _ = io.Copy(ioutil.Discard, a.Reader)
}
return nil
}
header, _ := buf.Peek(len(gzipHeader))
if bytes.Equal(header, gzipHeader) {
gz, err := gzip.NewReader(a.Reader)
if err != nil {
_ = r.CloseWithError(err)
cerr = err
return
}
c = gz.Close
a.Reader = gz
}
tr := tar.NewReader(a.Reader)
cerr = h.Read(u, tr)
_ = c()
_ = r.CloseWithError(cerr)
}()
return a, nil
}
func (a *archive) Close() error {
return a.done()
}
// archiveRead writes the contents of the given tar.Reader to the given directory.
func archiveRead(u *url.URL, tr *tar.Reader) error {
for {
header, err := tr.Next()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
name := filepath.Join(u.Path, header.Name)
mode := os.FileMode(header.Mode)
switch header.Typeflag {
case tar.TypeDir:
err = os.MkdirAll(name, mode)
case tar.TypeReg:
_ = os.MkdirAll(filepath.Dir(name), 0755)
var f *os.File
f, err = os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, mode)
if err == nil {
_, cerr := io.Copy(f, tr)
err = f.Close()
if cerr != nil {
err = cerr
}
}
case tar.TypeSymlink:
err = os.Symlink(header.Linkname, name)
}
// TODO: Uid/Gid may not be meaningful here without some mapping.
// The other option to consider would be making use of the guest auth user ID.
// os.Lchown(name, header.Uid, header.Gid)
if err != nil {
return err
}
}
}
// archiveWrite writes the contents of the given source directory to the given tar.Writer.
func archiveWrite(u *url.URL, tw *tar.Writer) error {
info, err := os.Stat(u.Path)
if err != nil {
return err
}
// Note that the VMX will trim any trailing slash. For example:
// "/foo/bar/?prefix=bar/" will end up here as "/foo/bar/?prefix=bar"
// Escape to avoid this: "/for/bar/?prefix=bar%2F"
prefix := u.Query().Get("prefix")
dir := u.Path
f := func(file string, fi os.FileInfo, err error) error {
if err != nil {
return filepath.SkipDir
}
name := strings.TrimPrefix(file, dir)
name = strings.TrimPrefix(name, "/")
if name == "" {
return nil // this is u.Path itself (which may or may not have a trailing "/")
}
if prefix != "" {
name = prefix + name
}
header, _ := tar.FileInfoHeader(fi, name)
header.Name = name
if header.Typeflag == tar.TypeDir {
header.Name += "/"
}
var f *os.File
if header.Typeflag == tar.TypeReg && fi.Size() != 0 {
f, err = os.Open(file)
if err != nil {
if os.IsPermission(err) {
return nil
}
return err
}
}
_ = tw.WriteHeader(header)
if f != nil {
_, err = io.Copy(tw, f)
_ = f.Close()
}
return err
}
if info.IsDir() {
return filepath.Walk(u.Path, f)
}
dir = filepath.Dir(dir)
return f(u.Path, info, nil)
}

View File

@@ -0,0 +1,437 @@
/*
Copyright (c) 2017 VMware, 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 hgfs
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"log"
"net/url"
"os"
"os/exec"
"path"
"strings"
"testing"
"time"
)
func TestReadArchive(t *testing.T) {
Trace = testing.Verbose()
dir, err := ioutil.TempDir("", "toolbox-")
if err != nil {
t.Fatal(err)
}
nfiles := 5
subdir := path.Join(dir, "hangar-18")
_ = os.MkdirAll(subdir, 0755)
dirs := []string{dir, subdir}
for i := 0; i < nfiles; i++ {
for _, p := range dirs {
data := bytes.NewBufferString(strings.Repeat("X", i+1024))
f, ferr := ioutil.TempFile(p, fmt.Sprintf("file-%d-", i))
if ferr != nil {
t.Fatal(ferr)
}
_, err = io.Copy(f, data)
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
if i == 0 {
err = os.Symlink(f.Name(), path.Join(p, "first-file"))
if err != nil {
t.Fatal(err)
}
}
}
}
c := NewClient()
c.s.RegisterFileHandler(ArchiveScheme, NewArchiveHandler())
status := c.CreateSession()
if status != StatusSuccess {
t.Fatalf("status=%d", status)
}
_, status = c.GetAttr(dir)
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
handle, status := c.Open(dir + "?format=tgz")
if status != StatusSuccess {
t.Fatalf("status=%d", status)
}
var req *RequestReadV3
var offset uint64
var buf bytes.Buffer
for {
req = &RequestReadV3{
Offset: offset,
Handle: handle,
RequiredSize: 4096,
}
res := new(ReplyReadV3)
status = c.Dispatch(OpReadV3, req, res).Status
if status != StatusSuccess {
t.Fatalf("status=%d", status)
}
if Trace {
fmt.Fprintf(os.Stderr, "read %d: %q\n", res.ActualSize, string(res.Payload))
}
offset += uint64(res.ActualSize)
_, _ = buf.Write(res.Payload)
if res.ActualSize == 0 {
break
}
}
status = c.Close(handle)
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
status = c.DestroySession()
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
var files []string
gz, _ := gzip.NewReader(&buf)
tr := tar.NewReader(gz)
for {
header, terr := tr.Next()
if terr != nil {
if terr == io.EOF {
break
}
t.Fatal(terr)
}
files = append(files, header.Name)
if header.Typeflag == tar.TypeReg {
_, err = io.Copy(ioutil.Discard, tr)
if err != nil {
t.Fatal(err)
}
}
}
nfiles++ // symlink
expect := nfiles*len(dirs) + len(dirs) - 1
if len(files) != expect {
t.Errorf("expected %d, files=%d", expect, len(files))
}
err = os.RemoveAll(dir)
if err != nil {
t.Fatal(err)
}
}
func TestWriteArchive(t *testing.T) {
Trace = testing.Verbose()
dir, err := ioutil.TempDir("", "toolbox-")
if err != nil {
t.Fatal(err)
}
nfiles := 5
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
tw := tar.NewWriter(gz)
for i := 0; i < nfiles; i++ {
data := bytes.NewBufferString(strings.Repeat("X", i+1024))
header := &tar.Header{
Name: fmt.Sprintf("toolbox-file-%d", i),
ModTime: time.Now(),
Mode: 0644,
Typeflag: tar.TypeReg,
Size: int64(data.Len()),
}
err = tw.WriteHeader(header)
if err != nil {
t.Fatal(err)
}
_, _ = tw.Write(data.Bytes())
if i == 0 {
err = tw.WriteHeader(&tar.Header{
Linkname: header.Name,
Name: "first-file",
ModTime: time.Now(),
Mode: 0644,
Typeflag: tar.TypeSymlink,
})
if err != nil {
t.Fatal(err)
}
err = tw.WriteHeader(&tar.Header{
Name: "subdir",
ModTime: time.Now(),
Mode: 0755,
Typeflag: tar.TypeDir,
})
if err != nil {
t.Fatal(err)
}
}
}
_ = tw.Close()
_ = gz.Close()
c := NewClient()
c.s.RegisterFileHandler(ArchiveScheme, NewArchiveHandler())
status := c.CreateSession()
if status != StatusSuccess {
t.Fatalf("status=%d", status)
}
_, status = c.GetAttr(dir)
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
handle, status := c.OpenWrite(dir)
if status != StatusSuccess {
t.Fatalf("status=%d", status)
}
payload := buf.Bytes()
size := uint32(buf.Len())
req := &RequestWriteV3{
Handle: handle,
WriteFlags: WriteAppend,
Offset: 0,
RequiredSize: size,
Payload: payload,
}
res := new(ReplyReadV3)
status = c.Dispatch(OpWriteV3, req, res).Status
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
var attr AttrV2
info, _ := os.Stat(dir)
attr.Stat(info)
status = c.SetAttr(dir, attr)
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
status = c.Close(handle)
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
status = c.DestroySession()
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
files, err := ioutil.ReadDir(dir)
if err != nil {
t.Error(err)
}
if len(files) != nfiles+2 {
t.Errorf("files=%d", len(files))
}
err = os.RemoveAll(dir)
if err != nil {
t.Fatal(err)
}
}
func cpTar(tr *tar.Reader, tw *tar.Writer) error {
for {
header, err := tr.Next()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
if header.Typeflag != tar.TypeReg {
continue
}
if err = tw.WriteHeader(header); err != nil {
return err
}
_, err = io.Copy(tw, tr)
if err != nil {
log.Print(err)
return err
}
}
}
func TestArchiveFormat(t *testing.T) {
gzipTrailer = false
var buf bytes.Buffer
h := &ArchiveHandler{
Read: func(_ *url.URL, tr *tar.Reader) error {
tw := tar.NewWriter(&buf)
if err := cpTar(tr, tw); err != nil {
return err
}
return tw.Close()
},
Write: func(u *url.URL, tw *tar.Writer) error {
tr := tar.NewReader(&buf)
return cpTar(tr, tw)
},
}
for _, format := range []string{"tar", "tgz"} {
u := &url.URL{
Scheme: ArchiveScheme,
Path: ".",
RawQuery: "format=" + format,
}
_, err := h.Stat(u)
if err != nil {
t.Fatal(err)
}
// ToGuest: git archive | h.Read (to buf)
f, err := h.Open(u, OpenModeWriteOnly)
if err != nil {
t.Fatal(err)
}
cmd := exec.Command("git", "archive", "--format", format, "HEAD")
stdout, err := cmd.StdoutPipe()
if err != nil {
t.Fatal(err)
}
err = cmd.Start()
if err != nil {
t.Fatal(err)
}
n, err := io.Copy(f, stdout)
if err != nil {
t.Errorf("copy %d: %s", n, err)
}
err = f.Close()
if err != nil {
t.Error(err)
}
err = cmd.Wait()
if err != nil {
t.Error(err)
}
// FromGuest: h.Write (from buf) | tar -tvf-
f, err = h.Open(u, OpenModeReadOnly)
if err != nil {
t.Fatal(err)
}
cmd = exec.Command("tar", "-tvf-")
if format == "tgz" {
cmd.Args = append(cmd.Args, "-z")
}
stdin, err := cmd.StdinPipe()
if err != nil {
t.Fatal(err)
}
if testing.Verbose() {
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stderr
}
err = cmd.Start()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(stdin, f)
if err != nil {
t.Errorf("copy %d: %s", n, err)
}
_ = stdin.Close()
err = f.Close()
if err != nil {
t.Error(err)
}
err = cmd.Wait()
if err != nil {
t.Error(err)
}
buf.Reset()
}
}

View File

@@ -0,0 +1,73 @@
/*
Copyright (c) 2017 VMware, 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 hgfs
import (
"bytes"
"encoding"
"encoding/binary"
)
// MarshalBinary is a wrapper around binary.Write
func MarshalBinary(fields ...interface{}) ([]byte, error) {
buf := new(bytes.Buffer)
for _, p := range fields {
switch m := p.(type) {
case encoding.BinaryMarshaler:
data, err := m.MarshalBinary()
if err != nil {
return nil, ProtocolError(err)
}
_, _ = buf.Write(data)
case []byte:
_, _ = buf.Write(m)
case string:
_, _ = buf.WriteString(m)
default:
err := binary.Write(buf, binary.LittleEndian, p)
if err != nil {
return nil, ProtocolError(err)
}
}
}
return buf.Bytes(), nil
}
// UnmarshalBinary is a wrapper around binary.Read
func UnmarshalBinary(data []byte, fields ...interface{}) error {
buf := bytes.NewBuffer(data)
for _, p := range fields {
switch m := p.(type) {
case encoding.BinaryUnmarshaler:
return m.UnmarshalBinary(buf.Bytes())
case *[]byte:
*m = buf.Bytes()
return nil
default:
err := binary.Read(buf, binary.LittleEndian, p)
if err != nil {
return ProtocolError(err)
}
}
}
return nil
}

View File

@@ -0,0 +1,24 @@
/*
Copyright (c) 2017 VMware, 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 hgfs
import (
"os"
)
func (a *AttrV2) sysStat(info os.FileInfo) {
}

View File

@@ -0,0 +1,58 @@
/*
Copyright (c) 2017 VMware, 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 hgfs
import (
"os"
"syscall"
)
const attrMask = AttrValidAllocationSize |
AttrValidAccessTime | AttrValidWriteTime | AttrValidCreateTime | AttrValidChangeTime |
AttrValidSpecialPerms | AttrValidOwnerPerms | AttrValidGroupPerms | AttrValidOtherPerms | AttrValidEffectivePerms |
AttrValidUserID | AttrValidGroupID | AttrValidFileID | AttrValidVolID
func (a *AttrV2) sysStat(info os.FileInfo) {
sys, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return
}
a.AllocationSize = uint64(sys.Blocks * 512)
nt := func(t syscall.Timespec) uint64 {
return uint64(t.Nano()) // TODO: this is supposed to be Windows NT system time, not needed atm
}
a.AccessTime = nt(sys.Atim)
a.WriteTime = nt(sys.Mtim)
a.CreationTime = a.WriteTime // see HgfsGetCreationTime
a.AttrChangeTime = nt(sys.Ctim)
a.SpecialPerms = uint8((sys.Mode & (syscall.S_ISUID | syscall.S_ISGID | syscall.S_ISVTX)) >> 9)
a.OwnerPerms = uint8((sys.Mode & syscall.S_IRWXU) >> 6)
a.GroupPerms = uint8((sys.Mode & syscall.S_IRWXG) >> 3)
a.OtherPerms = uint8(sys.Mode & syscall.S_IRWXO)
a.UserID = sys.Uid
a.GroupID = sys.Gid
a.HostFileID = sys.Ino
a.VolumeID = uint32(sys.Dev)
a.Mask |= attrMask
}

View File

@@ -0,0 +1,24 @@
/*
Copyright (c) 2017 VMware, 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 hgfs
import (
"os"
)
func (a *AttrV2) sysStat(info os.FileInfo) {
}

View File

@@ -0,0 +1,847 @@
/*
Copyright (c) 2017 VMware, 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 hgfs
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"os"
"strings"
)
// See: https://github.com/vmware/open-vm-tools/blob/master/open-vm-tools/lib/include/hgfsProto.h
// Opcodes for server operations as defined in hgfsProto.h
const (
OpOpen = iota /* Open file */
OpRead /* Read from file */
OpWrite /* Write to file */
OpClose /* Close file */
OpSearchOpen /* Start new search */
OpSearchRead /* Get next search response */
OpSearchClose /* End a search */
OpGetattr /* Get file attributes */
OpSetattr /* Set file attributes */
OpCreateDir /* Create new directory */
OpDeleteFile /* Delete a file */
OpDeleteDir /* Delete a directory */
OpRename /* Rename a file or directory */
OpQueryVolumeInfo /* Query volume information */
OpOpenV2 /* Open file */
OpGetattrV2 /* Get file attributes */
OpSetattrV2 /* Set file attributes */
OpSearchReadV2 /* Get next search response */
OpCreateSymlink /* Create a symlink */
OpServerLockChange /* Change the oplock on a file */
OpCreateDirV2 /* Create a directory */
OpDeleteFileV2 /* Delete a file */
OpDeleteDirV2 /* Delete a directory */
OpRenameV2 /* Rename a file or directory */
OpOpenV3 /* Open file */
OpReadV3 /* Read from file */
OpWriteV3 /* Write to file */
OpCloseV3 /* Close file */
OpSearchOpenV3 /* Start new search */
OpSearchReadV3 /* Read V3 directory entries */
OpSearchCloseV3 /* End a search */
OpGetattrV3 /* Get file attributes */
OpSetattrV3 /* Set file attributes */
OpCreateDirV3 /* Create new directory */
OpDeleteFileV3 /* Delete a file */
OpDeleteDirV3 /* Delete a directory */
OpRenameV3 /* Rename a file or directory */
OpQueryVolumeInfoV3 /* Query volume information */
OpCreateSymlinkV3 /* Create a symlink */
OpServerLockChangeV3 /* Change the oplock on a file */
OpWriteWin32StreamV3 /* Write WIN32_STREAM_ID format data to file */
OpCreateSessionV4 /* Create a session and return host capabilities. */
OpDestroySessionV4 /* Destroy/close session. */
OpReadFastV4 /* Read */
OpWriteFastV4 /* Write */
OpSetWatchV4 /* Start monitoring directory changes. */
OpRemoveWatchV4 /* Stop monitoring directory changes. */
OpNotifyV4 /* Notification for a directory change event. */
OpSearchReadV4 /* Read V4 directory entries. */
OpOpenV4 /* Open file */
OpEnumerateStreamsV4 /* Enumerate alternative named streams for a file. */
OpGetattrV4 /* Get file attributes */
OpSetattrV4 /* Set file attributes */
OpDeleteV4 /* Delete a file or a directory */
OpLinkmoveV4 /* Rename/move/create hard link. */
OpFsctlV4 /* Sending FS control requests. */
OpAccessCheckV4 /* Access check. */
OpFsyncV4 /* Flush all cached data to the disk. */
OpQueryVolumeInfoV4 /* Query volume information. */
OpOplockAcquireV4 /* Acquire OPLOCK. */
OpOplockBreakV4 /* Break or downgrade OPLOCK. */
OpLockByteRangeV4 /* Acquire byte range lock. */
OpUnlockByteRangeV4 /* Release byte range lock. */
OpQueryEasV4 /* Query extended attributes. */
OpSetEasV4 /* Add or modify extended attributes. */
OpNewHeader = 0xff /* Header op, must be unique, distinguishes packet headers. */
)
// Status codes
const (
StatusSuccess = iota
StatusNoSuchFileOrDir
StatusInvalidHandle
StatusOperationNotPermitted
StatusFileExists
StatusNotDirectory
StatusDirNotEmpty
StatusProtocolError
StatusAccessDenied
StatusInvalidName
StatusGenericError
StatusSharingViolation
StatusNoSpace
StatusOperationNotSupported
StatusNameTooLong
StatusInvalidParameter
StatusNotSameDevice
StatusStaleSession
StatusTooManySessions
StatusTransportError
)
// Flags for attr mask
const (
AttrValidType = 1 << iota
AttrValidSize
AttrValidCreateTime
AttrValidAccessTime
AttrValidWriteTime
AttrValidChangeTime
AttrValidSpecialPerms
AttrValidOwnerPerms
AttrValidGroupPerms
AttrValidOtherPerms
AttrValidFlags
AttrValidAllocationSize
AttrValidUserID
AttrValidGroupID
AttrValidFileID
AttrValidVolID
AttrValidNonStaticFileID
AttrValidEffectivePerms
AttrValidExtendAttrSize
AttrValidReparsePoint
AttrValidShortName
)
// HeaderVersion for HGFS protocol version 4
const HeaderVersion = 0x1
// LargePacketMax is maximum size of an hgfs packet
const LargePacketMax = 0xf800 // HGFS_LARGE_PACKET_MAX
// Packet flags
const (
PacketFlagRequest = 1 << iota
PacketFlagReply
PacketFlagInfoExterror
PacketFlagValidFlags = 0x7
)
// Status is an error type that encapsulates an error status code and the cause
type Status struct {
Err error
Code uint32
}
func (s *Status) Error() string {
if s.Err != nil {
return s.Err.Error()
}
return fmt.Sprintf("hgfs.Status=%d", s.Code)
}
// errorStatus maps the given error type to a status code
func errorStatus(err error) uint32 {
if x, ok := err.(*Status); ok {
return x.Code
}
switch {
case os.IsNotExist(err):
return StatusNoSuchFileOrDir
case os.IsExist(err):
return StatusFileExists
case os.IsPermission(err):
return StatusOperationNotPermitted
}
return StatusGenericError
}
// ProtocolError wraps the given error as a Status type
func ProtocolError(err error) error {
return &Status{
Err: err,
Code: StatusProtocolError,
}
}
// Request as defined in hgfsProto.h:HgfsRequest
type Request struct {
Handle uint32
Op int32
}
// Reply as defined in hgfsProto.h:HgfsReply
type Reply struct {
Handle uint32
Status uint32
}
// Header as defined in hgfsProto.h:HgfsHeader
type Header struct {
Version uint8
Reserved1 [3]uint8
Dummy int32
PacketSize uint32
HeaderSize uint32
RequestID uint32
Op int32
Status uint32
Flags uint32
Information uint32
SessionID uint64
Reserved uint64
}
var (
headerSize = uint32(binary.Size(new(Header)))
packetSize = func(r *Packet) uint32 {
return headerSize + uint32(len(r.Payload))
}
)
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (h *Header) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, h)
if err != nil {
return fmt.Errorf("reading hgfs header: %s", err)
}
if h.Dummy != OpNewHeader {
return fmt.Errorf("expected hgfs header with OpNewHeader (%#x), got: %#x", OpNewHeader, h.Dummy)
}
return nil
}
// Packet encapsulates an hgfs Header and Payload
type Packet struct {
Header
Payload []byte
}
// Reply composes a new Packet with the given payload or error
func (r *Packet) Reply(payload interface{}, err error) ([]byte, error) {
p := new(Packet)
status := uint32(StatusSuccess)
if err != nil {
status = errorStatus(err)
} else {
p.Payload, err = MarshalBinary(payload)
if err != nil {
return nil, err
}
}
p.Header = Header{
Version: HeaderVersion,
Dummy: OpNewHeader,
PacketSize: headerSize + uint32(len(p.Payload)),
HeaderSize: headerSize,
RequestID: r.RequestID,
Op: r.Op,
Status: status,
Flags: PacketFlagReply,
Information: 0,
SessionID: r.SessionID,
}
if Trace {
rc := "OK"
if err != nil {
rc = err.Error()
}
fmt.Fprintf(os.Stderr, "[hgfs] response %#v [%s]\n", p.Header, rc)
} else if err != nil {
log.Printf("[hgfs] op=%d error: %s", r.Op, err)
}
return p.MarshalBinary()
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *Packet) MarshalBinary() ([]byte, error) {
r.Header.PacketSize = packetSize(r)
buf, _ := MarshalBinary(r.Header)
return append(buf, r.Payload...), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *Packet) UnmarshalBinary(data []byte) error {
err := r.Header.UnmarshalBinary(data)
if err != nil {
return err
}
r.Payload = data[r.HeaderSize:r.PacketSize]
return nil
}
// Capability as defined in hgfsProto.h:HgfsCapability
type Capability struct {
Op int32
Flags uint32
}
// RequestCreateSessionV4 as defined in hgfsProto.h:HgfsRequestCreateSessionV4
type RequestCreateSessionV4 struct {
NumCapabilities uint32
MaxPacketSize uint32
Flags uint32
Reserved uint32
Capabilities []Capability
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *RequestCreateSessionV4) MarshalBinary() ([]byte, error) {
buf := new(bytes.Buffer)
r.NumCapabilities = uint32(len(r.Capabilities))
fields := []*uint32{
&r.NumCapabilities,
&r.MaxPacketSize,
&r.Flags,
&r.Reserved,
}
for _, p := range fields {
err := binary.Write(buf, binary.LittleEndian, p)
if err != nil {
return nil, err
}
}
for i := uint32(0); i < r.NumCapabilities; i++ {
err := binary.Write(buf, binary.LittleEndian, &r.Capabilities[i])
if err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *RequestCreateSessionV4) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
fields := []*uint32{
&r.NumCapabilities,
&r.MaxPacketSize,
&r.Flags,
&r.Reserved,
}
for _, p := range fields {
err := binary.Read(buf, binary.LittleEndian, p)
if err != nil {
return err
}
}
for i := uint32(0); i < r.NumCapabilities; i++ {
var cap Capability
err := binary.Read(buf, binary.LittleEndian, &cap)
if err != nil {
return err
}
r.Capabilities = append(r.Capabilities, cap)
}
return nil
}
// ReplyCreateSessionV4 as defined in hgfsProto.h:HgfsReplyCreateSessionV4
type ReplyCreateSessionV4 struct {
SessionID uint64
NumCapabilities uint32
MaxPacketSize uint32
IdentityOffset uint32
Flags uint32
Reserved uint32
Capabilities []Capability
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *ReplyCreateSessionV4) MarshalBinary() ([]byte, error) {
buf := new(bytes.Buffer)
fields := []interface{}{
&r.SessionID,
&r.NumCapabilities,
&r.MaxPacketSize,
&r.IdentityOffset,
&r.Flags,
&r.Reserved,
}
for _, p := range fields {
err := binary.Write(buf, binary.LittleEndian, p)
if err != nil {
return nil, err
}
}
for i := uint32(0); i < r.NumCapabilities; i++ {
err := binary.Write(buf, binary.LittleEndian, &r.Capabilities[i])
if err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *ReplyCreateSessionV4) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
fields := []interface{}{
&r.SessionID,
&r.NumCapabilities,
&r.MaxPacketSize,
&r.IdentityOffset,
&r.Flags,
&r.Reserved,
}
for _, p := range fields {
err := binary.Read(buf, binary.LittleEndian, p)
if err != nil {
return err
}
}
for i := uint32(0); i < r.NumCapabilities; i++ {
var cap Capability
err := binary.Read(buf, binary.LittleEndian, &cap)
if err != nil {
return err
}
r.Capabilities = append(r.Capabilities, cap)
}
return nil
}
// RequestDestroySessionV4 as defined in hgfsProto.h:HgfsRequestDestroySessionV4
type RequestDestroySessionV4 struct {
Reserved uint64
}
// ReplyDestroySessionV4 as defined in hgfsProto.h:HgfsReplyDestroySessionV4
type ReplyDestroySessionV4 struct {
Reserved uint64
}
// FileName as defined in hgfsProto.h:HgfsFileName
type FileName struct {
Length uint32
Name string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (f *FileName) MarshalBinary() ([]byte, error) {
name := f.Name
f.Length = uint32(len(f.Name))
if f.Length == 0 {
// field is defined as 'char name[1];', this byte is required for min sizeof() validation
name = "\x00"
}
return MarshalBinary(&f.Length, name)
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (f *FileName) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
_ = binary.Read(buf, binary.LittleEndian, &f.Length)
f.Name = string(buf.Next(int(f.Length)))
return nil
}
const serverPolicyRootShareName = "root"
// FromString converts name to a FileName
func (f *FileName) FromString(name string) {
name = strings.TrimPrefix(name, "/")
cp := strings.Split(name, "/")
cp = append([]string{serverPolicyRootShareName}, cp...)
f.Name = strings.Join(cp, "\x00")
f.Length = uint32(len(f.Name))
}
// Path converts FileName to a string
func (f *FileName) Path() string {
cp := strings.Split(f.Name, "\x00")
if len(cp) == 0 || cp[0] != serverPolicyRootShareName {
return "" // TODO: not happening until if/when we handle Windows shares
}
cp[0] = ""
return strings.Join(cp, "/")
}
// FileNameV3 as defined in hgfsProto.h:HgfsFileNameV3
type FileNameV3 struct {
Length uint32
Flags uint32
CaseType int32
ID uint32
Name string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (f *FileNameV3) MarshalBinary() ([]byte, error) {
name := f.Name
f.Length = uint32(len(f.Name))
if f.Length == 0 {
// field is defined as 'char name[1];', this byte is required for min sizeof() validation
name = "\x00"
}
return MarshalBinary(&f.Length, &f.Flags, &f.CaseType, &f.ID, name)
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (f *FileNameV3) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
fields := []interface{}{
&f.Length, &f.Flags, &f.CaseType, &f.ID,
}
for _, p := range fields {
if err := binary.Read(buf, binary.LittleEndian, p); err != nil {
return err
}
}
f.Name = string(buf.Next(int(f.Length)))
return nil
}
// FromString converts name to a FileNameV3
func (f *FileNameV3) FromString(name string) {
p := new(FileName)
p.FromString(name)
f.Name = p.Name
f.Length = p.Length
}
// Path converts FileNameV3 to a string
func (f *FileNameV3) Path() string {
return (&FileName{Name: f.Name, Length: f.Length}).Path()
}
// FileType
const (
FileTypeRegular = iota
FileTypeDirectory
FileTypeSymlink
)
// AttrV2 as defined in hgfsProto.h:HgfsAttrV2
type AttrV2 struct {
Mask uint64
Type int32
Size uint64
CreationTime uint64
AccessTime uint64
WriteTime uint64
AttrChangeTime uint64
SpecialPerms uint8
OwnerPerms uint8
GroupPerms uint8
OtherPerms uint8
AttrFlags uint64
AllocationSize uint64
UserID uint32
GroupID uint32
HostFileID uint64
VolumeID uint32
EffectivePerms uint32
Reserved2 uint64
}
// RequestGetattrV2 as defined in hgfsProto.h:HgfsRequestGetattrV2
type RequestGetattrV2 struct {
Request
AttrHint uint64
Handle uint32
FileName FileName
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *RequestGetattrV2) MarshalBinary() ([]byte, error) {
return MarshalBinary(&r.Request, &r.AttrHint, &r.Handle, &r.FileName)
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *RequestGetattrV2) UnmarshalBinary(data []byte) error {
return UnmarshalBinary(data, &r.Request, &r.AttrHint, &r.Handle, &r.FileName)
}
// ReplyGetattrV2 as defined in hgfsProto.h:HgfsReplyGetattrV2
type ReplyGetattrV2 struct {
Reply
Attr AttrV2
SymlinkTarget FileName
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *ReplyGetattrV2) MarshalBinary() ([]byte, error) {
return MarshalBinary(&r.Reply, &r.Attr, &r.SymlinkTarget)
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *ReplyGetattrV2) UnmarshalBinary(data []byte) error {
return UnmarshalBinary(data, &r.Reply, &r.Attr, &r.SymlinkTarget)
}
// RequestSetattrV2 as defined in hgfsProto.h:HgfsRequestSetattrV2
type RequestSetattrV2 struct {
Request
Hints uint64
Attr AttrV2
Handle uint32
FileName FileName
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *RequestSetattrV2) MarshalBinary() ([]byte, error) {
return MarshalBinary(&r.Request, &r.Hints, &r.Attr, &r.Handle, &r.FileName)
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *RequestSetattrV2) UnmarshalBinary(data []byte) error {
return UnmarshalBinary(data, &r.Request, &r.Hints, &r.Attr, &r.Handle, &r.FileName)
}
// ReplySetattrV2 as defined in hgfsProto.h:HgfsReplySetattrV2
type ReplySetattrV2 struct {
Header Reply
}
// OpenMode
const (
OpenModeReadOnly = iota
OpenModeWriteOnly
OpenModeReadWrite
OpenModeAccmodes
)
// OpenFlags
const (
Open = iota
OpenEmpty
OpenCreate
OpenCreateSafe
OpenCreateEmpty
)
// Permissions
const (
PermRead = 4
PermWrite = 2
PermExec = 1
)
// RequestOpen as defined in hgfsProto.h:HgfsRequestOpen
type RequestOpen struct {
Request
OpenMode int32
OpenFlags int32
Permissions uint8
FileName FileName
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *RequestOpen) MarshalBinary() ([]byte, error) {
return MarshalBinary(&r.Request, &r.OpenMode, &r.OpenFlags, r.Permissions, &r.FileName)
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *RequestOpen) UnmarshalBinary(data []byte) error {
return UnmarshalBinary(data, &r.Request, &r.OpenMode, &r.OpenFlags, &r.Permissions, &r.FileName)
}
// ReplyOpen as defined in hgfsProto.h:HgfsReplyOpen
type ReplyOpen struct {
Reply
Handle uint32
}
// RequestClose as defined in hgfsProto.h:HgfsRequestClose
type RequestClose struct {
Request
Handle uint32
}
// ReplyClose as defined in hgfsProto.h:HgfsReplyClose
type ReplyClose struct {
Reply
}
// Lock type
const (
LockNone = iota
LockOpportunistic
LockExclusive
LockShared
LockBatch
LockLease
)
// RequestOpenV3 as defined in hgfsProto.h:HgfsRequestOpenV3
type RequestOpenV3 struct {
Mask uint64
OpenMode int32
OpenFlags int32
SpecialPerms uint8
OwnerPerms uint8
GroupPerms uint8
OtherPerms uint8
AttrFlags uint64
AllocationSize uint64
DesiredAccess uint32
ShareAccess uint32
DesiredLock int32
Reserved1 uint64
Reserved2 uint64
FileName FileNameV3
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *RequestOpenV3) MarshalBinary() ([]byte, error) {
return MarshalBinary(&r.Mask, &r.OpenMode, &r.OpenFlags,
&r.SpecialPerms, &r.OwnerPerms, &r.GroupPerms, &r.OtherPerms,
&r.AttrFlags, &r.AllocationSize, &r.DesiredAccess, &r.ShareAccess,
&r.DesiredLock, &r.Reserved1, &r.Reserved2, &r.FileName)
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *RequestOpenV3) UnmarshalBinary(data []byte) error {
return UnmarshalBinary(data, &r.Mask, &r.OpenMode, &r.OpenFlags,
&r.SpecialPerms, &r.OwnerPerms, &r.GroupPerms, &r.OtherPerms,
&r.AttrFlags, &r.AllocationSize, &r.DesiredAccess, &r.ShareAccess,
&r.DesiredLock, &r.Reserved1, &r.Reserved2, &r.FileName)
}
// ReplyOpenV3 as defined in hgfsProto.h:HgfsReplyOpenV3
type ReplyOpenV3 struct {
Handle uint32
AcquiredLock int32
Flags int32
Reserved uint32
}
// RequestReadV3 as defined in hgfsProto.h:HgfsRequestReadV3
type RequestReadV3 struct {
Handle uint32
Offset uint64
RequiredSize uint32
Reserved uint64
}
// ReplyReadV3 as defined in hgfsProto.h:HgfsReplyReadV3
type ReplyReadV3 struct {
ActualSize uint32
Reserved uint64
Payload []byte
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *ReplyReadV3) MarshalBinary() ([]byte, error) {
return MarshalBinary(&r.ActualSize, &r.Reserved, r.Payload)
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *ReplyReadV3) UnmarshalBinary(data []byte) error {
return UnmarshalBinary(data, &r.ActualSize, &r.Reserved, &r.Payload)
}
// Write flags
const (
WriteAppend = 1
)
// RequestWriteV3 as defined in hgfsProto.h:HgfsRequestWriteV3
type RequestWriteV3 struct {
Handle uint32
WriteFlags uint8
Offset uint64
RequiredSize uint32
Reserved uint64
Payload []byte
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *RequestWriteV3) MarshalBinary() ([]byte, error) {
return MarshalBinary(&r.Handle, &r.WriteFlags, &r.Offset, &r.RequiredSize, &r.Reserved, r.Payload)
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *RequestWriteV3) UnmarshalBinary(data []byte) error {
return UnmarshalBinary(data, &r.Handle, &r.WriteFlags, &r.Offset, &r.RequiredSize, &r.Reserved, &r.Payload)
}
// ReplyWriteV3 as defined in hgfsProto.h:HgfsReplyWriteV3
type ReplyWriteV3 struct {
ActualSize uint32
Reserved uint64
}

View File

@@ -0,0 +1,198 @@
/*
Copyright (c) 2017 VMware, 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 hgfs
import (
"bytes"
"encoding/base64"
"io"
"testing"
)
func TestProtocolEncoding(t *testing.T) {
ps := packetSize
defer func() {
packetSize = ps
}()
// a few structs have pading of some sort, leave PacketSize as-is for now with these tests
packetSize = func(r *Packet) uint32 {
return r.PacketSize
}
decode := func(s string) []byte {
b, _ := base64.StdEncoding.DecodeString(s)
return b
}
// base64 encoded packets below were captured from vmtoolsd during:
// govc guest.download /etc/hosts -
// govc guest.upload /etc/hosts /tmp/hosts
tests := []struct {
pkt string
dec interface{}
}{
{
"AQAAAP8AAABMAAAANAAAAAAAAIApAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAA==",
new(RequestCreateSessionV4),
},
{
"AQAAAP8AAABYAgAANAAAAAAAAIApAAAAAAAAAAIAAAAAAAAA//////////8AAAAAAAAAAF/NcjwZ1DYAQQAAAAD4AAAAAAAAAQAAAAAAAAAAAAAAAQAAAAEAAAABAAAAAgAAAAEAAAADAAAAAQAAAAQAAAABAAAABQAAAAEAAAAGAAAAAQAAAAcAAAABAAAACAAAAAEAAAAJAAAAAQAAAAoAAAABAAAACwAAAAEAAAAMAAAAAQAAAA0AAAABAAAADgAAAAEAAAAPAAAAAQAAABAAAAABAAAAEQAAAAEAAAASAAAAAQAAABMAAAAAAAAAFAAAAAEAAAAVAAAAAQAAABYAAAABAAAAFwAAAAEAAAAYAAAAAQAAABkAAAABAAAAGgAAAAEAAAAbAAAAAQAAABwAAAABAAAAHQAAAAEAAAAeAAAAAQAAAB8AAAABAAAAIAAAAAEAAAAhAAAAAQAAACIAAAABAAAAIwAAAAEAAAAkAAAAAQAAACUAAAABAAAAJgAAAAEAAAAnAAAAAAAAACgAAAAAAAAAKQAAAAEAAAAqAAAAAQAAACsAAAAAAAAALAAAAAAAAAAtAAAAAAAAAC4AAAAAAAAALwAAAAAAAAAwAAAAAAAAADEAAAAAAAAAMgAAAAAAAAAzAAAAAAAAADQAAAAAAAAANQAAAAAAAAA2AAAAAAAAADcAAAAAAAAAOAAAAAAAAAA5AAAAAAAAADoAAAAAAAAAOwAAAAAAAAA8AAAAAAAAAD0AAAAAAAAAPgAAAAAAAAA/AAAAAAAAAEAAAAAAAAAA",
new(ReplyCreateSessionV4),
},
{
"AQAAAP8AAABbAAAANAAAAAAAAIAPAAAAAAAAAAEAAAAAAAAAX81yPBnUNgAAAAAAAAAAAAAAAIAPAAAAAAAAAAAAAAAAAACADgAAAHJvb3QAZXRjAGhvc3RzAA==",
new(RequestGetattrV2),
},
{
"AQAAAP8AAACpAAAANAAAAAAAAIAPAAAAAAAAAAIAAAAAAAAAX81yPBnUNgAAAAAAAAAAAAAAAAAAAAAA//sCAAAAAAAAAAAAxgAAAAAAAABkP/0QmUzSAaAHo3qfz9IBZD/9EJlM0gFkP/0QmUzSAQAGBAQAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAABY9xMAAAAAAAD8AAAEAAAAAAAAAAAAAAAAAAAAAA==",
new(ReplyGetattrV2),
},
{
"AQAAAP8AAABYAAAANAAAAAAAAIAAAAAAAAAAAAEAAAAAAAAAX81yPBnUNgAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAGDgAAAHJvb3QAZXRjAGhvc3Rzcw==",
new(RequestOpen),
},
{
"AQAAAP8AAABAAAAANAAAAAAAAIAAAAAAAAAAAAIAAAAAAAAAX81yPBnUNgAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
new(ReplyOpen),
},
{
"AQAAAP8AAABMAAAANAAAAAAAAIAZAAAAAAAAAAEAAAAAAAAAX81yPBnUNgAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAA==",
new(RequestReadV3),
},
{
"AQAAAP8AAAAHAQAANAAAAAAAAIAZAAAAAAAAAAIAAAAAAAAAX81yPBnUNgAAAAAAAAAAAMYAAAAAAAAAAAAAADEyNy4wLjAuMQlsb2NhbGhvc3QKMTI3LjAuMS4xCXZhZ3JhbnQudm0JdmFncmFudAoKIyBUaGUgZm9sbG93aW5nIGxpbmVzIGFyZSBkZXNpcmFibGUgZm9yIElQdjYgY2FwYWJsZSBob3N0cwo6OjEgICAgIGxvY2FsaG9zdCBpcDYtbG9jYWxob3N0IGlwNi1sb29wYmFjawpmZjAyOjoxIGlwNi1hbGxub2RlcwpmZjAyOjoyIGlwNi1hbGxyb3V0ZXJzCgA=",
new(ReplyReadV3),
},
{
"AQAAAP8AAABAAAAANAAAAAAAAIADAAAAAAAAAAEAAAAAAAAAX81yPBnUNgAAAAAAAAAAAAAAAIADAAAAAAAAAA==",
new(RequestClose),
},
{
"AQAAAP8AAAA8AAAANAAAAAAAAIADAAAAAAAAAAIAAAAAAAAAX81yPBnUNgAAAAAAAAAAAAAAAAAAAAAA",
new(ReplyClose),
},
{
"AQAAAP8AAAA8AAAANAAAAAAAAIAqAAAAAAAAAAEAAAAAAAAAX81yPBnUNgAAAAAAAAAAAAAAAAAAAAAA",
new(RequestDestroySessionV4),
},
{
"AQAAAP8AAAA8AAAANAAAAAAAAIAqAAAAAAAAAAIAAAAAAAAAX81yPBnUNgAAAAAAAAAAAAAAAAAAAAAA",
new(ReplyDestroySessionV4),
},
{
"AQAAAP8AAACZAAAANAAAAAAAAIAYAAAAAAAAAAEAAAAAAAAAyXxnMKrzOwAAAAAAAAAAAAsIAAAAAAAAAQAAAAQAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAcm9vdAB0bXAAcmVzb2x2LmNvbmYA",
new(RequestOpenV3),
},
{
"AQAAAP8AAABEAAAANAAAAAAAAIAYAAAAAAAAAAIAAAAAAAAAyXxnMKrzOwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
new(ReplyOpenV3),
},
{
"AQAAAP8AAADJAAAANAAAAAAAAIAQAAAAAAAAAAEAAAAAAAAAyXxnMKrzOwAAAAAAAAAAAAAAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAcm9vdAB0bXAAcmVzb2x2LmNvbmYA",
new(RequestSetattrV2),
},
{
"AQAAAP8AAAA8AAAANAAAAAAAAIAQAAAAAAAAAAIAAAAAAAAAyXxnMKrzOwAAAAAAAAAAAAAAAAAAAAAA",
new(ReplySetattrV2),
},
{
"AQAAAP8AAACTAAAANAAAAAAAAIAaAAAAAAAAAAEAAAAAAAAAyXxnMKrzOwAAAAAAAAAAAAAAAAABAAAAAAAAAABGAAAAAAAAAAAAAABuYW1lc2VydmVyIDEwLjExOC42NS4xCm5hbWVzZXJ2ZXIgMTAuMTE4LjY1LjIKc2VhcmNoIGVuZy52bXdhcmUuY29tIAoK",
new(RequestWriteV3),
},
{
"AQAAAP8AAABAAAAANAAAAAAAAIAaAAAAAAAAAAIAAAAAAAAAyXxnMKrzOwAAAAAAAAAAAEYAAAAAAAAAAAAAAA==",
new(ReplyWriteV3),
},
}
for i, test := range tests {
dec := decode(test.pkt)
pkt := new(Packet)
err := pkt.UnmarshalBinary(dec)
if err != nil {
t.Fatal(err)
}
err = UnmarshalBinary(pkt.Payload, test.dec)
if err != nil {
t.Fatal(err)
}
pkt.Payload, err = MarshalBinary(test.dec)
if err != nil {
t.Fatal(err)
}
enc, err := pkt.MarshalBinary()
if err != nil {
t.Fatal(err)
}
if !bytes.HasPrefix(dec, enc) {
t.Errorf("%d: %T != %s\n", i, test.dec, test.pkt)
}
}
}
func TestFileName(t *testing.T) {
tests := []struct {
raw string
name string
}{
{
"root\x00etc\x00hosts",
"/etc/hosts",
},
}
for i, test := range tests {
fn := FileName{
Name: test.raw,
Length: uint32(len(test.raw)),
}
if fn.Path() != test.name {
t.Errorf("%d: %q != %q", i, fn.Path(), test.name)
}
var fs FileName
fs.FromString(test.name)
if fs != fn {
t.Errorf("%d: %v != %v", i, fn, fs)
}
}
}
func TestProtocolMarshal(t *testing.T) {
var buf []byte
var x uint64
err := UnmarshalBinary(buf, x)
if err == nil {
t.Fatal("expected error")
}
status := err.(*Status)
if status.Error() != io.EOF.Error() {
t.Errorf("err=%s", status.Err)
}
status.Err = nil
if status.Error() == "" {
t.Error("status")
}
}

584
vendor/github.com/vmware/govmomi/toolbox/hgfs/server.go generated vendored Normal file
View File

@@ -0,0 +1,584 @@
/*
Copyright (c) 2017 VMware, 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 hgfs
import (
"errors"
"flag"
"fmt"
"io"
"log"
"math/rand"
"net/url"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
)
// See: https://github.com/vmware/open-vm-tools/blob/master/open-vm-tools/lib/hgfsServer/hgfsServer.c
var (
// Trace enables hgfs packet tracing
Trace = false
)
// Server provides an HGFS protocol implementation to support guest tools VmxiHgfsSendPacketCommand
type Server struct {
Capabilities []Capability
handlers map[int32]func(*Packet) (interface{}, error)
schemes map[string]FileHandler
sessions map[uint64]*session
mu sync.Mutex
handle uint32
chmod func(string, os.FileMode) error
chown func(string, int, int) error
}
// NewServer creates a new Server instance with the default handlers
func NewServer() *Server {
if f := flag.Lookup("toolbox.trace"); f != nil {
Trace, _ = strconv.ParseBool(f.Value.String())
}
s := &Server{
sessions: make(map[uint64]*session),
schemes: make(map[string]FileHandler),
chmod: os.Chmod,
chown: os.Chown,
}
s.handlers = map[int32]func(*Packet) (interface{}, error){
OpCreateSessionV4: s.CreateSessionV4,
OpDestroySessionV4: s.DestroySessionV4,
OpGetattrV2: s.GetattrV2,
OpSetattrV2: s.SetattrV2,
OpOpen: s.Open,
OpClose: s.Close,
OpOpenV3: s.OpenV3,
OpReadV3: s.ReadV3,
OpWriteV3: s.WriteV3,
}
for op := range s.handlers {
s.Capabilities = append(s.Capabilities, Capability{Op: op, Flags: 0x1})
}
return s
}
// RegisterFileHandler enables dispatch to handler for the given scheme.
func (s *Server) RegisterFileHandler(scheme string, handler FileHandler) {
if handler == nil {
delete(s.schemes, scheme)
return
}
s.schemes[scheme] = handler
}
// Dispatch unpacks the given request packet and dispatches to the appropriate handler
func (s *Server) Dispatch(packet []byte) ([]byte, error) {
req := &Packet{}
err := req.UnmarshalBinary(packet)
if err != nil {
return nil, err
}
if Trace {
fmt.Fprintf(os.Stderr, "[hgfs] request %#v\n", req.Header)
}
var res interface{}
handler, ok := s.handlers[req.Op]
if ok {
res, err = handler(req)
} else {
err = &Status{
Code: StatusOperationNotSupported,
Err: fmt.Errorf("unsupported Op(%d)", req.Op),
}
}
return req.Reply(res, err)
}
// File interface abstracts standard i/o methods to support transfer
// of regular files and archives of directories.
type File interface {
io.Reader
io.Writer
io.Closer
Name() string
}
// FileHandler is the plugin interface for hgfs file transport.
type FileHandler interface {
Stat(*url.URL) (os.FileInfo, error)
Open(*url.URL, int32) (File, error)
}
// urlParse attempts to convert the given name to a URL with scheme for use as FileHandler dispatch.
func urlParse(name string) *url.URL {
var info os.FileInfo
u, err := url.Parse(name)
if err == nil && u.Scheme == "" {
info, err = os.Stat(u.Path)
if err == nil && info.IsDir() {
u.Scheme = ArchiveScheme // special case for IsDir()
return u
}
}
u, err = url.Parse(strings.TrimPrefix(name, "/")) // must appear to be an absolute path or hgfs errors
if err != nil {
u = &url.URL{Path: name}
}
if u.Scheme == "" {
ix := strings.Index(u.Path, "/")
if ix > 0 {
u.Scheme = u.Path[:ix]
u.Path = u.Path[ix:]
}
}
return u
}
// OpenFile selects the File implementation based on file type and mode.
func (s *Server) OpenFile(name string, mode int32) (File, error) {
u := urlParse(name)
if h, ok := s.schemes[u.Scheme]; ok {
f, serr := h.Open(u, mode)
if serr != os.ErrNotExist {
return f, serr
}
}
switch mode {
case OpenModeReadOnly:
return os.Open(name)
case OpenModeWriteOnly:
flag := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
return os.OpenFile(name, flag, 0600)
default:
return nil, &Status{
Err: fmt.Errorf("open mode(%d) not supported for file %q", mode, name),
Code: StatusAccessDenied,
}
}
}
// Stat wraps os.Stat such that we can report directory types as regular files to support archive streaming.
// In the case of standard vmware-tools, attempts to transfer directories result
// with a VIX_E_NOT_A_FILE (see InitiateFileTransfer{To,From}Guest).
// Note that callers on the VMX side that reach this path are only concerned with:
// - does the file exist?
// - size:
// + used for UI progress with desktop Drag-N-Drop operations, which toolbox does not support.
// + sent to as Content-Length header in response to GET of FileTransferInformation.Url,
// if the first ReadV3 size is > HGFS_LARGE_PACKET_MAX
func (s *Server) Stat(name string) (os.FileInfo, error) {
u := urlParse(name)
if h, ok := s.schemes[u.Scheme]; ok {
sinfo, serr := h.Stat(u)
if serr != os.ErrNotExist {
return sinfo, serr
}
}
return os.Stat(name)
}
type session struct {
files map[uint32]File
mu sync.Mutex
}
// TODO: we currently depend on the VMX to close files and remove sessions,
// which it does provided it can communicate with the toolbox. Let's look at
// adding session expiration when implementing OpenModeWriteOnly support.
func newSession() *session {
return &session{
files: make(map[uint32]File),
}
}
func (s *Server) getSession(p *Packet) (*session, error) {
s.mu.Lock()
session, ok := s.sessions[p.SessionID]
s.mu.Unlock()
if !ok {
return nil, &Status{
Code: StatusStaleSession,
Err: errors.New("session not found"),
}
}
return session, nil
}
func (s *Server) removeSession(id uint64) bool {
s.mu.Lock()
session, ok := s.sessions[id]
delete(s.sessions, id)
s.mu.Unlock()
if !ok {
return false
}
session.mu.Lock()
defer session.mu.Unlock()
for _, f := range session.files {
log.Printf("[hgfs] session %X removed with open file: %s", id, f.Name())
_ = f.Close()
}
return true
}
// open-vm-tools' session max is 1024, there shouldn't be more than a handful at a given time in our use cases
const maxSessions = 24
// CreateSessionV4 handls OpCreateSessionV4 requests
func (s *Server) CreateSessionV4(p *Packet) (interface{}, error) {
const SessionMaxPacketSizeValid = 0x1
req := new(RequestCreateSessionV4)
err := UnmarshalBinary(p.Payload, req)
if err != nil {
return nil, err
}
res := &ReplyCreateSessionV4{
SessionID: uint64(rand.Int63()),
NumCapabilities: uint32(len(s.Capabilities)),
MaxPacketSize: LargePacketMax,
Flags: SessionMaxPacketSizeValid,
Capabilities: s.Capabilities,
}
s.mu.Lock()
defer s.mu.Unlock()
if len(s.sessions) > maxSessions {
return nil, &Status{Code: StatusTooManySessions}
}
s.sessions[res.SessionID] = newSession()
return res, nil
}
// DestroySessionV4 handls OpDestroySessionV4 requests
func (s *Server) DestroySessionV4(p *Packet) (interface{}, error) {
if s.removeSession(p.SessionID) {
return &ReplyDestroySessionV4{}, nil
}
return nil, &Status{Code: StatusStaleSession}
}
// Stat maps os.FileInfo to AttrV2
func (a *AttrV2) Stat(info os.FileInfo) {
switch {
case info.IsDir():
a.Type = FileTypeDirectory
case info.Mode()&os.ModeSymlink == os.ModeSymlink:
a.Type = FileTypeSymlink
default:
a.Type = FileTypeRegular
}
a.Size = uint64(info.Size())
a.Mask = AttrValidType | AttrValidSize
a.sysStat(info)
}
// GetattrV2 handles OpGetattrV2 requests
func (s *Server) GetattrV2(p *Packet) (interface{}, error) {
res := &ReplyGetattrV2{}
req := new(RequestGetattrV2)
err := UnmarshalBinary(p.Payload, req)
if err != nil {
return nil, err
}
name := req.FileName.Path()
info, err := s.Stat(name)
if err != nil {
return nil, err
}
res.Attr.Stat(info)
return res, nil
}
// SetattrV2 handles OpSetattrV2 requests
func (s *Server) SetattrV2(p *Packet) (interface{}, error) {
res := &ReplySetattrV2{}
req := new(RequestSetattrV2)
err := UnmarshalBinary(p.Payload, req)
if err != nil {
return nil, err
}
name := req.FileName.Path()
_, err = os.Stat(name)
if err != nil && os.IsNotExist(err) {
// assuming this is a virtual file
return res, nil
}
uid := -1
if req.Attr.Mask&AttrValidUserID == AttrValidUserID {
uid = int(req.Attr.UserID)
}
gid := -1
if req.Attr.Mask&AttrValidGroupID == AttrValidGroupID {
gid = int(req.Attr.GroupID)
}
err = s.chown(name, uid, gid)
if err != nil {
return nil, err
}
var perm os.FileMode
if req.Attr.Mask&AttrValidOwnerPerms == AttrValidOwnerPerms {
perm |= os.FileMode(req.Attr.OwnerPerms) << 6
}
if req.Attr.Mask&AttrValidGroupPerms == AttrValidGroupPerms {
perm |= os.FileMode(req.Attr.GroupPerms) << 3
}
if req.Attr.Mask&AttrValidOtherPerms == AttrValidOtherPerms {
perm |= os.FileMode(req.Attr.OtherPerms)
}
if perm != 0 {
err = s.chmod(name, perm)
if err != nil {
return nil, err
}
}
return res, nil
}
func (s *Server) newHandle() uint32 {
return atomic.AddUint32(&s.handle, 1)
}
// Open handles OpOpen requests
func (s *Server) Open(p *Packet) (interface{}, error) {
req := new(RequestOpen)
err := UnmarshalBinary(p.Payload, req)
if err != nil {
return nil, err
}
session, err := s.getSession(p)
if err != nil {
return nil, err
}
name := req.FileName.Path()
mode := req.OpenMode
if mode != OpenModeReadOnly {
return nil, &Status{
Err: fmt.Errorf("open mode(%d) not supported for file %q", mode, name),
Code: StatusAccessDenied,
}
}
file, err := s.OpenFile(name, mode)
if err != nil {
return nil, err
}
res := &ReplyOpen{
Handle: s.newHandle(),
}
session.mu.Lock()
session.files[res.Handle] = file
session.mu.Unlock()
return res, nil
}
// Close handles OpClose requests
func (s *Server) Close(p *Packet) (interface{}, error) {
req := new(RequestClose)
err := UnmarshalBinary(p.Payload, req)
if err != nil {
return nil, err
}
session, err := s.getSession(p)
if err != nil {
return nil, err
}
session.mu.Lock()
file, ok := session.files[req.Handle]
if ok {
delete(session.files, req.Handle)
}
session.mu.Unlock()
if ok {
err = file.Close()
} else {
return nil, &Status{Code: StatusInvalidHandle}
}
return &ReplyClose{}, err
}
// OpenV3 handles OpOpenV3 requests
func (s *Server) OpenV3(p *Packet) (interface{}, error) {
req := new(RequestOpenV3)
err := UnmarshalBinary(p.Payload, req)
if err != nil {
return nil, err
}
session, err := s.getSession(p)
if err != nil {
return nil, err
}
name := req.FileName.Path()
if req.DesiredLock != LockNone {
return nil, &Status{
Err: fmt.Errorf("open lock type=%d not supported for file %q", req.DesiredLock, name),
Code: StatusOperationNotSupported,
}
}
file, err := s.OpenFile(name, req.OpenMode)
if err != nil {
return nil, err
}
res := &ReplyOpenV3{
Handle: s.newHandle(),
}
session.mu.Lock()
session.files[res.Handle] = file
session.mu.Unlock()
return res, nil
}
// ReadV3 handles OpReadV3 requests
func (s *Server) ReadV3(p *Packet) (interface{}, error) {
req := new(RequestReadV3)
err := UnmarshalBinary(p.Payload, req)
if err != nil {
return nil, err
}
session, err := s.getSession(p)
if err != nil {
return nil, err
}
session.mu.Lock()
file, ok := session.files[req.Handle]
session.mu.Unlock()
if !ok {
return nil, &Status{Code: StatusInvalidHandle}
}
buf := make([]byte, req.RequiredSize)
// Use ReadFull as Read() of an archive io.Pipe may return much smaller chunks,
// such as when we've read a tar header.
n, err := io.ReadFull(file, buf)
if err != nil && n == 0 {
if err != io.EOF {
return nil, err
}
}
res := &ReplyReadV3{
ActualSize: uint32(n),
Payload: buf[:n],
}
return res, nil
}
// WriteV3 handles OpWriteV3 requests
func (s *Server) WriteV3(p *Packet) (interface{}, error) {
req := new(RequestWriteV3)
err := UnmarshalBinary(p.Payload, req)
if err != nil {
return nil, err
}
session, err := s.getSession(p)
if err != nil {
return nil, err
}
session.mu.Lock()
file, ok := session.files[req.Handle]
session.mu.Unlock()
if !ok {
return nil, &Status{Code: StatusInvalidHandle}
}
n, err := file.Write(req.Payload)
if err != nil {
return nil, err
}
res := &ReplyWriteV3{
ActualSize: uint32(n),
}
return res, nil
}

View File

@@ -0,0 +1,443 @@
/*
Copyright (c) 2017 VMware, 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 hgfs
import (
"fmt"
"io/ioutil"
"os"
"path"
"testing"
)
type Client struct {
s *Server
SessionID uint64
}
func NewClient() *Client {
s := NewServer()
return &Client{
s: s,
}
}
func (c *Client) Dispatch(op int32, req interface{}, res interface{}) *Packet {
var err error
p := new(Packet)
p.Payload, err = MarshalBinary(req)
if err != nil {
panic(err)
}
p.Header.Version = 0x1
p.Header.Dummy = OpNewHeader
p.Header.HeaderSize = headerSize
p.Header.PacketSize = headerSize + uint32(len(p.Payload))
p.Header.SessionID = c.SessionID
p.Header.Op = op
data, err := p.MarshalBinary()
if err != nil {
panic(err)
}
data, err = c.s.Dispatch(data)
if err != nil {
panic(err)
}
p = new(Packet)
err = p.UnmarshalBinary(data)
if err != nil {
panic(err)
}
if p.Status == StatusSuccess {
err = UnmarshalBinary(p.Payload, res)
if err != nil {
panic(err)
}
}
return p
}
func (c *Client) CreateSession() uint32 {
req := new(RequestCreateSessionV4)
res := new(ReplyCreateSessionV4)
p := c.Dispatch(OpCreateSessionV4, req, res)
if p.Status == StatusSuccess {
c.SessionID = res.SessionID
}
return p.Status
}
func (c *Client) DestroySession() uint32 {
req := new(RequestDestroySessionV4)
res := new(ReplyDestroySessionV4)
return c.Dispatch(OpDestroySessionV4, req, res).Status
}
func (c *Client) GetAttr(name string) (*AttrV2, uint32) {
req := new(RequestGetattrV2)
res := new(ReplyGetattrV2)
req.FileName.FromString(name)
p := c.Dispatch(OpGetattrV2, req, res)
if p.Status != StatusSuccess {
return nil, p.Status
}
return &res.Attr, p.Status
}
func (c *Client) SetAttr(name string, attr AttrV2) uint32 {
req := new(RequestSetattrV2)
res := new(ReplySetattrV2)
req.FileName.FromString(name)
req.Attr = attr
p := c.Dispatch(OpSetattrV2, req, res)
if p.Status != StatusSuccess {
return p.Status
}
return p.Status
}
func (c *Client) Open(name string, write ...bool) (uint32, uint32) {
req := new(RequestOpen)
res := new(ReplyOpen)
if len(write) == 1 && write[0] {
req.OpenMode = OpenModeWriteOnly
}
req.FileName.FromString(name)
p := c.Dispatch(OpOpen, req, res)
if p.Status != StatusSuccess {
return 0, p.Status
}
return res.Handle, p.Status
}
func (c *Client) OpenWrite(name string) (uint32, uint32) {
req := new(RequestOpenV3)
res := new(ReplyOpenV3)
req.OpenMode = OpenModeWriteOnly
req.OpenFlags = OpenCreateEmpty
req.FileName.FromString(name)
p := c.Dispatch(OpOpenV3, req, res)
if p.Status != StatusSuccess {
return 0, p.Status
}
// cover the unsupported lock type path
req.DesiredLock = LockOpportunistic
status := c.Dispatch(OpOpenV3, req, res).Status
if status != StatusOperationNotSupported {
return 0, status
}
// cover the unsupported open mode path
req.DesiredLock = LockNone
req.OpenMode = OpenCreateSafe
status = c.Dispatch(OpOpenV3, req, res).Status
if status != StatusAccessDenied {
return 0, status
}
return res.Handle, p.Status
}
func (c *Client) Close(handle uint32) uint32 {
req := new(RequestClose)
res := new(ReplyClose)
req.Handle = handle
return c.Dispatch(OpClose, req, res).Status
}
func TestStaleSession(t *testing.T) {
c := NewClient()
// list of methods that can return StatusStaleSession
invalid := []func() uint32{
func() uint32 { _, status := c.Open("enoent"); return status },
func() uint32 { return c.Dispatch(OpReadV3, new(RequestReadV3), new(ReplyReadV3)).Status },
func() uint32 { return c.Dispatch(OpWriteV3, new(RequestWriteV3), new(ReplyWriteV3)).Status },
func() uint32 { return c.Close(0) },
c.DestroySession,
}
for i, f := range invalid {
status := f()
if status != StatusStaleSession {
t.Errorf("%d: status=%d", i, status)
}
}
}
func TestSessionMax(t *testing.T) {
c := NewClient()
var status uint32
for i := 0; i <= maxSessions+1; i++ {
status = c.CreateSession()
}
if status != StatusTooManySessions {
t.Errorf("status=%d", status)
}
}
func TestSessionDestroy(t *testing.T) {
Trace = true
c := NewClient()
c.CreateSession()
_, status := c.Open("/etc/resolv.conf")
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
c.DestroySession()
if c.s.removeSession(c.SessionID) {
t.Error("session was not removed")
}
}
func TestInvalidOp(t *testing.T) {
c := NewClient()
status := c.Dispatch(1024, new(RequestClose), new(ReplyClose)).Status
if status != StatusOperationNotSupported {
t.Errorf("status=%d", status)
}
}
func TestReadV3(t *testing.T) {
Trace = testing.Verbose()
c := NewClient()
c.CreateSession()
_, status := c.GetAttr("enoent")
if status != StatusNoSuchFileOrDir {
t.Errorf("status=%d", status)
}
_, status = c.Open("enoent")
if status != StatusNoSuchFileOrDir {
t.Errorf("status=%d", status)
}
fname := "/etc/resolv.conf"
attr, _ := c.GetAttr(path.Dir(fname))
if attr.Type != FileTypeDirectory {
t.Errorf("type=%d", attr.Type)
}
attr, _ = c.GetAttr(fname)
if attr.Type != FileTypeRegular {
t.Errorf("type=%d", attr.Type)
}
if attr.Size <= 0 {
t.Errorf("size=%d", attr.Size)
}
handle, status := c.Open(fname)
if status != StatusSuccess {
t.Fatalf("status=%d", status)
}
var req *RequestReadV3
var offset uint64
size := uint32(attr.Size / 2)
for offset = 0; offset < attr.Size; {
req = &RequestReadV3{
Offset: offset,
Handle: handle,
RequiredSize: size,
}
res := new(ReplyReadV3)
status = c.Dispatch(OpReadV3, req, res).Status
if status != StatusSuccess {
t.Fatalf("status=%d", status)
}
if Trace {
fmt.Fprintf(os.Stderr, "read %d: %q\n", res.ActualSize, string(res.Payload))
}
offset += uint64(res.ActualSize)
}
if uint64(offset) != attr.Size {
t.Errorf("size %d vs %d", offset, attr.Size)
}
status = c.Dispatch(OpReadV3, new(RequestReadV3), new(ReplyReadV3)).Status
if status != StatusInvalidHandle {
t.Fatalf("status=%d", status)
}
status = c.Close(0)
if status != StatusInvalidHandle {
t.Fatalf("status=%d", status)
}
status = c.Close(handle)
if status != StatusSuccess {
t.Fatalf("status=%d", status)
}
status = c.DestroySession()
if status != StatusSuccess {
t.Fatalf("status=%d", status)
}
}
func TestWriteV3(t *testing.T) {
Trace = testing.Verbose()
f, err := ioutil.TempFile("", "toolbox")
if err != nil {
t.Fatal(err)
}
_ = f.Close()
name := f.Name()
c := NewClient()
c.CreateSession()
_, status := c.Open("enoent", true)
// write not supported yet
if status != StatusAccessDenied {
t.Errorf("status=%d", status)
}
handle, status := c.OpenWrite(name)
if status != StatusSuccess {
t.Fatalf("status=%d", status)
}
payload := []byte("one two three\n")
size := uint32(len(payload))
req := &RequestWriteV3{
Handle: handle,
WriteFlags: WriteAppend,
Offset: 0,
RequiredSize: size,
Payload: payload,
}
res := new(ReplyReadV3)
status = c.Dispatch(OpWriteV3, req, res).Status
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
if size != res.ActualSize {
t.Errorf("%d vs %d", size, res.ActualSize)
}
status = c.Dispatch(OpWriteV3, new(RequestWriteV3), new(ReplyWriteV3)).Status
if status != StatusInvalidHandle {
t.Fatalf("status=%d", status)
}
status = c.Close(handle)
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
attr, _ := c.GetAttr(name)
if attr.Size != uint64(size) {
t.Errorf("%d vs %d", size, attr.Size)
}
attr.OwnerPerms |= PermExec
errors := []struct {
err error
status uint32
}{
{os.ErrPermission, StatusOperationNotPermitted},
{os.ErrNotExist, StatusNoSuchFileOrDir},
{os.ErrExist, StatusFileExists},
{nil, StatusSuccess},
}
for _, e := range errors {
c.s.chown = func(_ string, _ int, _ int) error {
return e.err
}
status = c.SetAttr(name, *attr)
if status != e.status {
t.Errorf("status=%d", status)
}
}
c.s.chown = func(_ string, _ int, _ int) error {
return nil
}
for _, e := range errors {
c.s.chmod = func(_ string, _ os.FileMode) error {
return e.err
}
status = c.SetAttr(name, *attr)
if status != e.status {
t.Errorf("status=%d", status)
}
}
status = c.DestroySession()
if status != StatusSuccess {
t.Errorf("status=%d", status)
}
_ = os.Remove(name)
}

118
vendor/github.com/vmware/govmomi/toolbox/power.go generated vendored Normal file
View File

@@ -0,0 +1,118 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"fmt"
"log"
"os/exec"
)
// GuestOsState enum as defined in open-vm-tools/lib/include/vmware/guestrpc/powerops.h
const (
_ = iota
powerStateHalt
powerStateReboot
powerStatePowerOn
powerStateResume
powerStateSuspend
)
var (
shutdown = "/sbin/shutdown"
)
type PowerCommand struct {
Handler func() error
out *ChannelOut
state int
name string
}
type PowerCommandHandler struct {
Halt PowerCommand
Reboot PowerCommand
PowerOn PowerCommand
Resume PowerCommand
Suspend PowerCommand
}
func registerPowerCommandHandler(service *Service) *PowerCommandHandler {
handler := new(PowerCommandHandler)
handlers := map[string]struct {
cmd *PowerCommand
state int
}{
"OS_Halt": {&handler.Halt, powerStateHalt},
"OS_Reboot": {&handler.Reboot, powerStateReboot},
"OS_PowerOn": {&handler.PowerOn, powerStatePowerOn},
"OS_Resume": {&handler.Resume, powerStateResume},
"OS_Suspend": {&handler.Suspend, powerStateSuspend},
}
for name, h := range handlers {
*h.cmd = PowerCommand{
name: name,
state: h.state,
out: service.out,
}
service.RegisterHandler(name, h.cmd.Dispatch)
}
return handler
}
func (c *PowerCommand) Dispatch([]byte) ([]byte, error) {
rc := rpciOK
log.Printf("dispatching power op %q", c.name)
if c.Handler == nil {
if c.state == powerStateHalt || c.state == powerStateReboot {
rc = rpciERR
}
}
msg := fmt.Sprintf("tools.os.statechange.status %s%d\x00", rc, c.state)
if _, err := c.out.Request([]byte(msg)); err != nil {
log.Printf("unable to send %q: %q", msg, err)
}
if c.Handler != nil {
if err := c.Handler(); err != nil {
log.Printf("%s: %s", c.name, err)
}
}
return nil, nil
}
func Halt() error {
log.Printf("Halting system...")
// #nosec: Subprocess launching with variable
return exec.Command(shutdown, "-h", "now").Run()
}
func Reboot() error {
log.Printf("Rebooting system...")
// #nosec: Subprocess launching with variable
return exec.Command(shutdown, "-r", "now").Run()
}

53
vendor/github.com/vmware/govmomi/toolbox/power_test.go generated vendored Normal file
View File

@@ -0,0 +1,53 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"errors"
"testing"
)
func TestPowerCommandHandler(t *testing.T) {
shutdown = "/bin/echo"
in := new(mockChannelIn)
out := new(mockChannelOut)
service := NewService(in, out)
power := service.Power
// cover nil Handler and out.Receive paths
_, _ = power.Halt.Dispatch(nil)
out.reply = append(out.reply, rpciOK, rpciOK)
power.Halt.Handler = Halt
power.Reboot.Handler = Reboot
power.Suspend.Handler = func() error {
return errors.New("an error")
}
commands := []PowerCommand{
power.Halt,
power.Reboot,
power.Suspend,
}
for _, cmd := range commands {
_, _ = cmd.Dispatch(nil)
}
}

630
vendor/github.com/vmware/govmomi/toolbox/process.go generated vendored Normal file
View File

@@ -0,0 +1,630 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"net"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/vmware/govmomi/toolbox/hgfs"
"github.com/vmware/govmomi/toolbox/vix"
)
var (
xmlEscape *strings.Replacer
shell = "/bin/sh"
defaultOwner = os.Getenv("USER")
)
func init() {
// See: VixToolsEscapeXMLString
chars := []string{
`"`,
"%",
"&",
"'",
"<",
">",
}
replace := make([]string, 0, len(chars)*2)
for _, c := range chars {
replace = append(replace, c)
replace = append(replace, url.QueryEscape(c))
}
xmlEscape = strings.NewReplacer(replace...)
// See procMgrPosix.c:ProcMgrStartProcess:
// Prefer bash -c as is uses exec() to replace itself,
// whereas bourne shell does a fork & exec, so two processes are started.
if sh, err := exec.LookPath("bash"); err != nil {
shell = sh
}
if defaultOwner == "" {
defaultOwner = "toolbox"
}
}
// ProcessIO encapsulates IO for Go functions and OS commands such that they can interact via the OperationsManager
// without file system disk IO.
type ProcessIO struct {
In struct {
io.Writer
io.Reader
io.Closer // Closer for the write side of the pipe, can be closed via hgfs ops (FileTranfserToGuest)
}
Out *bytes.Buffer
Err *bytes.Buffer
}
// ProcessState is the toolbox representation of the GuestProcessInfo type
type ProcessState struct {
Name string
Args string
Owner string
Pid int64
ExitCode int32
StartTime int64
EndTime int64
IO *ProcessIO
}
// WithIO enables toolbox Process IO without file system disk IO.
func (p *Process) WithIO() *Process {
p.IO = &ProcessIO{
Out: new(bytes.Buffer),
Err: new(bytes.Buffer),
}
return p
}
// ProcessFile implements the os.FileInfo interface to enable toolbox interaction with virtual files.
type ProcessFile struct {
io.Reader
io.Writer
io.Closer
name string
size int
}
// Name implementation of the os.FileInfo interface method.
func (a *ProcessFile) Name() string {
return a.name
}
// Size implementation of the os.FileInfo interface method.
func (a *ProcessFile) Size() int64 {
return int64(a.size)
}
// Mode implementation of the os.FileInfo interface method.
func (a *ProcessFile) Mode() os.FileMode {
if strings.HasSuffix(a.name, "stdin") {
return 0200
}
return 0400
}
// ModTime implementation of the os.FileInfo interface method.
func (a *ProcessFile) ModTime() time.Time {
return time.Now()
}
// IsDir implementation of the os.FileInfo interface method.
func (a *ProcessFile) IsDir() bool {
return false
}
// Sys implementation of the os.FileInfo interface method.
func (a *ProcessFile) Sys() interface{} {
return nil
}
func (s *ProcessState) toXML() string {
const format = "<proc>" +
"<cmd>%s</cmd>" +
"<name>%s</name>" +
"<pid>%d</pid>" +
"<user>%s</user>" +
"<start>%d</start>" +
"<eCode>%d</eCode>" +
"<eTime>%d</eTime>" +
"</proc>"
name := filepath.Base(s.Name)
argv := []string{s.Name}
if len(s.Args) != 0 {
argv = append(argv, xmlEscape.Replace(s.Args))
}
args := strings.Join(argv, " ")
exit := atomic.LoadInt32(&s.ExitCode)
end := atomic.LoadInt64(&s.EndTime)
return fmt.Sprintf(format, name, args, s.Pid, s.Owner, s.StartTime, exit, end)
}
// Process managed by the ProcessManager.
type Process struct {
ProcessState
Start func(*Process, *vix.StartProgramRequest) (int64, error)
Wait func() error
Kill context.CancelFunc
ctx context.Context
}
// ProcessError can be returned by the Process.Wait function to propagate ExitCode to ProcessState.
type ProcessError struct {
Err error
ExitCode int32
}
func (e *ProcessError) Error() string {
return e.Err.Error()
}
// ProcessManager manages processes within the guest.
// See: http://pubs.vmware.com/vsphere-60/topic/com.vmware.wssdk.apiref.doc/vim.vm.guest.ProcessManager.html
type ProcessManager struct {
wg sync.WaitGroup
mu sync.Mutex
expire time.Duration
entries map[int64]*Process
pids sync.Pool
}
// NewProcessManager creates a new ProcessManager instance.
func NewProcessManager() *ProcessManager {
// We use pseudo PIDs that don't conflict with OS PIDs, so they can live in the same table.
// For the pseudo PIDs, we use a sync.Pool rather than a plain old counter to avoid the unlikely,
// but possible wrapping should such a counter exceed MaxInt64.
pid := int64(32768) // TODO: /proc/sys/kernel/pid_max
return &ProcessManager{
expire: time.Minute * 5,
entries: make(map[int64]*Process),
pids: sync.Pool{
New: func() interface{} {
return atomic.AddInt64(&pid, 1)
},
},
}
}
// Start calls the Process.Start function, returning the pid on success or an error.
// A goroutine is started that calls the Process.Wait function. After Process.Wait has
// returned, the ProcessState EndTime and ExitCode fields are set. The process state can be
// queried via ListProcessesInGuest until it is removed, 5 minutes after Wait returns.
func (m *ProcessManager) Start(r *vix.StartProgramRequest, p *Process) (int64, error) {
p.Name = r.ProgramPath
p.Args = r.Arguments
// Owner is cosmetic, but useful for example with: govc guest.ps -U $uid
if p.Owner == "" {
p.Owner = defaultOwner
}
p.StartTime = time.Now().Unix()
p.ctx, p.Kill = context.WithCancel(context.Background())
pid, err := p.Start(p, r)
if err != nil {
return -1, err
}
if pid == 0 {
p.Pid = m.pids.Get().(int64) // pseudo pid for funcs
} else {
p.Pid = pid
}
m.mu.Lock()
m.entries[p.Pid] = p
m.mu.Unlock()
m.wg.Add(1)
go func() {
werr := p.Wait()
atomic.StoreInt64(&p.EndTime, time.Now().Unix())
if werr != nil {
rc := int32(1)
if xerr, ok := werr.(*ProcessError); ok {
rc = xerr.ExitCode
}
atomic.StoreInt32(&p.ExitCode, rc)
}
m.wg.Done()
p.Kill() // cancel context for those waiting on p.ctx.Done()
// See: http://pubs.vmware.com/vsphere-65/topic/com.vmware.wssdk.apiref.doc/vim.vm.guest.ProcessManager.ProcessInfo.html
// "If the process was started using StartProgramInGuest then the process completion time
// will be available if queried within 5 minutes after it completes."
<-time.After(m.expire)
m.mu.Lock()
delete(m.entries, p.Pid)
m.mu.Unlock()
if pid == 0 {
m.pids.Put(p.Pid) // pseudo pid can be reused now
}
}()
return p.Pid, nil
}
// Kill cancels the Process Context.
// Returns true if pid exists in the process table, false otherwise.
func (m *ProcessManager) Kill(pid int64) bool {
m.mu.Lock()
entry, ok := m.entries[pid]
m.mu.Unlock()
if ok {
entry.Kill()
return true
}
return false
}
// ListProcesses marshals the ProcessState for the given pids.
// If no pids are specified, all current processes are included.
// The return value can be used for responding to a VixMsgListProcessesExRequest.
func (m *ProcessManager) ListProcesses(pids []int64) []byte {
w := new(bytes.Buffer)
m.mu.Lock()
if len(pids) == 0 {
for _, p := range m.entries {
_, _ = w.WriteString(p.toXML())
}
} else {
for _, id := range pids {
p, ok := m.entries[id]
if !ok {
continue
}
_, _ = w.WriteString(p.toXML())
}
}
m.mu.Unlock()
return w.Bytes()
}
type procFileInfo struct {
os.FileInfo
}
// Size returns hgfs.LargePacketMax such that InitiateFileTransferFromGuest can download a /proc/ file from the guest.
// If we were to return the size '0' here, then a 'Content-Length: 0' header is returned by VC/ESX.
func (p procFileInfo) Size() int64 {
return hgfs.LargePacketMax // Remember, Sully, when I promised to kill you last? I lied.
}
// Stat implements hgfs.FileHandler.Stat
func (m *ProcessManager) Stat(u *url.URL) (os.FileInfo, error) {
name := path.Join("/proc", u.Path)
info, err := os.Stat(name)
if err == nil && info.Size() == 0 {
// This is a real /proc file
return &procFileInfo{info}, nil
}
dir, file := path.Split(u.Path)
pid, err := strconv.ParseInt(path.Base(dir), 10, 64)
if err != nil {
return nil, os.ErrNotExist
}
m.mu.Lock()
p := m.entries[pid]
m.mu.Unlock()
if p == nil || p.IO == nil {
return nil, os.ErrNotExist
}
pf := &ProcessFile{
name: name,
Closer: ioutil.NopCloser(nil), // via hgfs, nop for stdout and stderr
}
var r *bytes.Buffer
switch file {
case "stdin":
pf.Writer = p.IO.In.Writer
pf.Closer = p.IO.In.Closer
return pf, nil
case "stdout":
r = p.IO.Out
case "stderr":
r = p.IO.Err
default:
return nil, os.ErrNotExist
}
select {
case <-p.ctx.Done():
case <-time.After(time.Second):
// The vmx guest RPC calls are queue based, serialized on the vmx side.
// There are 5 seconds between "ping" RPC calls and after a few misses,
// the vmx considers tools as not running. In this case, the vmx would timeout
// a file transfer after 60 seconds.
//
// vix.FileAccessError is converted to a CannotAccessFile fault,
// so the client can choose to retry the transfer in this case.
// Would have preferred vix.ObjectIsBusy (EBUSY), but VC/ESX converts that
// to a general SystemErrorFault with nothing but a localized string message
// to check against: "<reason>vix error codes = (5, 0).</reason>"
// Is standard vmware-tools, EACCES is converted to a CannotAccessFile fault.
return nil, vix.Error(vix.FileAccessError)
}
pf.Reader = r
pf.size = r.Len()
return pf, nil
}
// Open implements hgfs.FileHandler.Open
func (m *ProcessManager) Open(u *url.URL, mode int32) (hgfs.File, error) {
info, err := m.Stat(u)
if err != nil {
return nil, err
}
pinfo, ok := info.(*ProcessFile)
if !ok {
return nil, os.ErrNotExist // fall through to default os.Open
}
switch path.Base(u.Path) {
case "stdin":
if mode != hgfs.OpenModeWriteOnly {
return nil, vix.Error(vix.InvalidArg)
}
case "stdout", "stderr":
if mode != hgfs.OpenModeReadOnly {
return nil, vix.Error(vix.InvalidArg)
}
}
return pinfo, nil
}
type processFunc struct {
wg sync.WaitGroup
run func(context.Context, string) error
err error
}
// NewProcessFunc creates a new Process, where the Start function calls the given run function within a goroutine.
// The Wait function waits for the goroutine to finish and returns the error returned by run.
// The run ctx param may be used to return early via the ProcessManager.Kill method.
// The run args command is that of the VixMsgStartProgramRequest.Arguments field.
func NewProcessFunc(run func(ctx context.Context, args string) error) *Process {
f := &processFunc{run: run}
return &Process{
Start: f.start,
Wait: f.wait,
}
}
// ProcessFuncIO is the Context key to access optional ProcessIO
var ProcessFuncIO = struct {
key int
}{vix.CommandMagicWord}
func (f *processFunc) start(p *Process, r *vix.StartProgramRequest) (int64, error) {
f.wg.Add(1)
var c io.Closer
if p.IO != nil {
pr, pw := io.Pipe()
p.IO.In.Reader, p.IO.In.Writer = pr, pw
c, p.IO.In.Closer = pr, pw
p.ctx = context.WithValue(p.ctx, ProcessFuncIO, p.IO)
}
go func() {
f.err = f.run(p.ctx, r.Arguments)
if p.IO != nil {
_ = c.Close()
if f.err != nil && p.IO.Err.Len() == 0 {
p.IO.Err.WriteString(f.err.Error())
}
}
f.wg.Done()
}()
return 0, nil
}
func (f *processFunc) wait() error {
f.wg.Wait()
return f.err
}
type processCmd struct {
cmd *exec.Cmd
}
// NewProcess creates a new Process, where the Start function use exec.CommandContext to create and start the process.
// The Wait function waits for the process to finish and returns the error returned by exec.Cmd.Wait().
// Prior to Wait returning, the exec.Cmd.Wait() error is used to set the ProcessState.ExitCode, if error is of type exec.ExitError.
// The ctx param may be used to kill the process via the ProcessManager.Kill method.
// The VixMsgStartProgramRequest param fields are mapped to the exec.Cmd counterpart fields.
// Processes are started within a sub-shell, allowing for i/o redirection, just as with the C version of vmware-tools.
func NewProcess() *Process {
c := new(processCmd)
return &Process{
Start: c.start,
Wait: c.wait,
}
}
func (c *processCmd) start(p *Process, r *vix.StartProgramRequest) (int64, error) {
name, err := exec.LookPath(r.ProgramPath)
if err != nil {
return -1, err
}
// #nosec: Subprocess launching with variable
// Note that processCmd is currently used only for testing.
c.cmd = exec.CommandContext(p.ctx, shell, "-c", fmt.Sprintf("%s %s", name, r.Arguments))
c.cmd.Dir = r.WorkingDir
c.cmd.Env = r.EnvVars
if p.IO != nil {
in, perr := c.cmd.StdinPipe()
if perr != nil {
return -1, perr
}
p.IO.In.Writer = in
p.IO.In.Closer = in
// Note we currently use a Buffer in addition to the os.Pipe so that:
// - Stat() can provide a size
// - FileTransferFromGuest won't block
// - Can't use the exec.Cmd.Std{out,err}Pipe methods since Wait() closes the pipes.
// We could use os.Pipe directly, but toolbox needs to take care of closing both ends,
// but also need to prevent FileTransferFromGuest from blocking.
c.cmd.Stdout = p.IO.Out
c.cmd.Stderr = p.IO.Err
}
err = c.cmd.Start()
if err != nil {
return -1, err
}
return int64(c.cmd.Process.Pid), nil
}
func (c *processCmd) wait() error {
err := c.cmd.Wait()
if err != nil {
xerr := &ProcessError{
Err: err,
ExitCode: 1,
}
if x, ok := err.(*exec.ExitError); ok {
if status, ok := x.Sys().(syscall.WaitStatus); ok {
xerr.ExitCode = int32(status.ExitStatus())
}
}
return xerr
}
return nil
}
// NewProcessRoundTrip starts a Go function to implement a toolbox backed http.RoundTripper
func NewProcessRoundTrip() *Process {
return NewProcessFunc(func(ctx context.Context, host string) error {
p, _ := ctx.Value(ProcessFuncIO).(*ProcessIO)
closers := []io.Closer{p.In.Closer}
defer func() {
for _, c := range closers {
_ = c.Close()
}
}()
c, err := new(net.Dialer).DialContext(ctx, "tcp", host)
if err != nil {
return err
}
closers = append(closers, c)
go func() {
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
_ = c.Close()
}
}()
_, err = io.Copy(c, p.In.Reader)
if err != nil {
return err
}
_, err = io.Copy(p.Out, c)
if err != nil {
return err
}
return nil
}).WithIO()
}

View File

@@ -0,0 +1,312 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"
"github.com/vmware/govmomi/toolbox/vix"
)
func TestProcessFunction(t *testing.T) {
m := NewProcessManager()
var pids []int64
for i := 0; i <= 2; i++ {
r := &vix.StartProgramRequest{
ProgramPath: "test",
Arguments: strconv.Itoa(i),
}
pid, _ := m.Start(r, NewProcessFunc(func(_ context.Context, arg string) error {
rc, _ := strconv.Atoi(arg)
if rc == 0 {
return nil
}
return &ProcessError{Err: errors.New("fail"), ExitCode: int32(rc)}
}))
if pid == 0 {
t.Fatalf("no pid")
}
pids = append(pids, pid)
}
m.wg.Wait()
_ = m.ListProcesses(pids)
for i, pid := range pids {
p := m.entries[pid]
if p.ExitCode != int32(i) {
t.Errorf("%d: %d != %d", pid, p.ExitCode, i)
}
}
}
func TestProcessCommand(t *testing.T) {
m := NewProcessManager()
var pids []int64
for i := 0; i <= 2; i++ {
r := &vix.StartProgramRequest{
ProgramPath: "/bin/bash",
Arguments: fmt.Sprintf(`-c "exit %d"`, i),
}
pid, _ := m.Start(r, NewProcess())
pids = append(pids, pid)
}
m.wg.Wait()
_ = m.ListProcesses(nil)
for i, pid := range pids {
p := m.entries[pid]
if p.ExitCode != int32(i) {
t.Errorf("%d: %d != %d", pid, p.ExitCode, i)
}
}
r := &vix.StartProgramRequest{
ProgramPath: shell,
}
shell = "/enoent/enoent"
_, err := m.Start(r, NewProcess())
if err == nil {
t.Error("expected error")
}
shell = r.ProgramPath
r.ProgramPath = "/enoent/enoent"
_, err = m.Start(r, NewProcess())
if err == nil {
t.Error("expected error")
}
}
func TestProcessKill(t *testing.T) {
m := NewProcessManager()
var pids []int64
procs := []struct {
r *vix.StartProgramRequest
p *Process
}{
{
&vix.StartProgramRequest{
ProgramPath: "test",
Arguments: "none",
},
NewProcessFunc(func(ctx context.Context, _ string) error {
select {
case <-ctx.Done():
return &ProcessError{Err: ctx.Err(), ExitCode: 42}
case <-time.After(time.Minute):
}
return nil
}),
},
{
&vix.StartProgramRequest{
ProgramPath: "/bin/bash",
Arguments: fmt.Sprintf(`-c "while true; do sleep 1; done"`),
},
NewProcess(),
},
}
for _, test := range procs {
pid, err := m.Start(test.r, test.p)
if err != nil {
t.Fatal(err)
}
pids = append(pids, pid)
}
for {
b := m.ListProcesses(pids)
if bytes.Count(b, []byte("<proc>")) == len(pids) {
break
}
<-time.After(time.Millisecond * 100)
}
for _, pid := range pids {
if !m.Kill(pid) {
t.Errorf("kill %d", pid)
}
}
m.wg.Wait()
for _, pid := range pids {
p := m.entries[pid]
if p.ExitCode == 0 {
t.Errorf("%s: exit=%d", p.Name, p.ExitCode)
}
}
if m.Kill(-1) {
t.Error("kill -1")
}
}
func TestProcessRemove(t *testing.T) {
m := NewProcessManager()
m.expire = time.Millisecond
r := &vix.StartProgramRequest{
ProgramPath: "test",
}
pid, _ := m.Start(r, NewProcessFunc(func(_ context.Context, arg string) error {
return nil
}))
m.wg.Wait()
<-time.After(m.expire * 20)
// pid should be removed by now
b := m.ListProcesses([]int64{pid})
if len(b) != 0 {
t.Error("expected 0 processes")
}
}
func TestEscapeXML(t *testing.T) {
tests := []struct {
in string
out string
}{
{`echo "foo bar" > /dev/null`, "echo %22foo bar%22 %3E /dev/null"},
}
for i, test := range tests {
e := xmlEscape.Replace(test.in)
if e != test.out {
t.Errorf("%d: %s != %s", i, e, test.out)
}
}
}
func TestProcessError(t *testing.T) {
fault := errors.New("fail")
var err error
err = &ProcessError{Err: fault}
if err.Error() != fault.Error() {
t.Fatal()
}
}
func TestProcessIO(t *testing.T) {
m := NewProcessManager()
r := &vix.StartProgramRequest{
ProgramPath: "/bin/date",
}
p := NewProcess().WithIO()
_, err := m.Start(r, p)
if err != nil {
t.Fatal(err)
}
m.wg.Wait()
var buf bytes.Buffer
_, _ = io.Copy(&buf, p.IO.Out)
if buf.Len() == 0 {
t.Error("no data")
}
}
type testRoundTripper struct {
*Process
}
func (c *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("Connection", "close") // we need the server to close the connection after 1 request
err := req.Write(c.IO.In.Writer)
if err != nil {
return nil, err
}
_ = c.IO.In.Close()
<-c.ctx.Done()
return http.ReadResponse(bufio.NewReader(c.IO.Out), req)
}
func TestProcessRoundTripper(t *testing.T) {
echo := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = r.Write(w)
}))
u, _ := url.Parse(echo.URL)
m := NewProcessManager()
r := &vix.StartProgramRequest{
ProgramPath: "http.RoundTrip",
Arguments: u.Host,
}
p := NewProcessRoundTrip()
_, err := m.Start(r, p)
if err != nil {
t.Fatal(err)
}
res, err := (&http.Client{Transport: &testRoundTripper{p}}).Get(echo.URL)
if err != nil {
t.Logf("Err: %s", p.IO.Err.String())
t.Fatal(err)
}
if res.ContentLength == 0 {
t.Errorf("len=%d", res.ContentLength)
}
}

336
vendor/github.com/vmware/govmomi/toolbox/service.go generated vendored Normal file
View File

@@ -0,0 +1,336 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"bytes"
"fmt"
"log"
"net"
"os"
"sync"
"time"
"github.com/vmware/govmomi/toolbox/hgfs"
)
const (
// TOOLS_VERSION_UNMANAGED as defined in open-vm-tools/lib/include/vm_tools_version.h
toolsVersionUnmanaged = 0x7fffffff
// RPCIN_MAX_DELAY as defined in rpcChannelInt.h:
maxDelay = 10
)
var (
capabilities = []string{
// Without tools.set.version, the UI reports Tools are "running", but "not installed"
fmt.Sprintf("tools.set.version %d", toolsVersionUnmanaged),
// Required to invoke guest power operations (shutdown, reboot)
"tools.capability.statechange",
"tools.capability.hgfs_server toolbox 1",
}
netInterfaceAddrs = net.InterfaceAddrs
// If we have an RPCI send error, the channels will be reset.
// open-vm-tools/lib/rpcChannel/rpcChannel.c:RpcChannelCheckReset also backs off in this case
resetDelay = time.Duration(500) // 500 * 10ms == 5s
)
// Service receives and dispatches incoming RPC requests from the vmx
type Service struct {
name string
in Channel
out *ChannelOut
handlers map[string]Handler
stop chan struct{}
wg *sync.WaitGroup
delay time.Duration
rpcError bool
Command *CommandServer
Power *PowerCommandHandler
PrimaryIP func() string
}
// NewService initializes a Service instance
func NewService(rpcIn Channel, rpcOut Channel) *Service {
s := &Service{
name: "toolbox", // Same name used by vmtoolsd
in: NewTraceChannel(rpcIn),
out: &ChannelOut{NewTraceChannel(rpcOut)},
handlers: make(map[string]Handler),
wg: new(sync.WaitGroup),
stop: make(chan struct{}),
PrimaryIP: DefaultIP,
}
s.RegisterHandler("reset", s.Reset)
s.RegisterHandler("ping", s.Ping)
s.RegisterHandler("Set_Option", s.SetOption)
s.RegisterHandler("Capabilities_Register", s.CapabilitiesRegister)
s.Command = registerCommandServer(s)
s.Command.FileServer = hgfs.NewServer()
s.Command.FileServer.RegisterFileHandler("proc", s.Command.ProcessManager)
s.Command.FileServer.RegisterFileHandler(hgfs.ArchiveScheme, hgfs.NewArchiveHandler())
s.Power = registerPowerCommandHandler(s)
return s
}
// backoff exponentially increases the RPC poll delay up to maxDelay
func (s *Service) backoff() {
if s.delay < maxDelay {
if s.delay > 0 {
d := s.delay * 2
if d > s.delay && d < maxDelay {
s.delay = d
} else {
s.delay = maxDelay
}
} else {
s.delay = 1
}
}
}
func (s *Service) stopChannel() {
_ = s.in.Stop()
_ = s.out.Stop()
}
func (s *Service) startChannel() error {
err := s.in.Start()
if err != nil {
return err
}
return s.out.Start()
}
func (s *Service) checkReset() error {
if s.rpcError {
s.stopChannel()
err := s.startChannel()
if err != nil {
s.delay = resetDelay
return err
}
s.rpcError = false
}
return nil
}
// Start initializes the RPC channels and starts a goroutine to listen for incoming RPC requests
func (s *Service) Start() error {
err := s.startChannel()
if err != nil {
return err
}
s.wg.Add(1)
go func() {
defer s.wg.Done()
// Same polling interval and backoff logic as vmtoolsd.
// Required in our case at startup at least, otherwise it is possible
// we miss the 1 Capabilities_Register call for example.
// Note we Send(response) even when nil, to let the VMX know we are here
var response []byte
for {
select {
case <-s.stop:
s.stopChannel()
return
case <-time.After(time.Millisecond * 10 * s.delay):
if err = s.checkReset(); err != nil {
continue
}
err = s.in.Send(response)
response = nil
if err != nil {
s.delay = resetDelay
s.rpcError = true
continue
}
request, _ := s.in.Receive()
if len(request) > 0 {
response = s.Dispatch(request)
s.delay = 0
} else {
s.backoff()
}
}
}
}()
return nil
}
// Stop cancels the RPC listener routine created via Start
func (s *Service) Stop() {
close(s.stop)
}
// Wait blocks until Start returns, allowing any current RPC in progress to complete.
func (s *Service) Wait() {
s.wg.Wait()
}
// Handler is given the raw argument portion of an RPC request and returns a response
type Handler func([]byte) ([]byte, error)
// RegisterHandler for the given RPC name
func (s *Service) RegisterHandler(name string, handler Handler) {
s.handlers[name] = handler
}
// Dispatch an incoming RPC request to a Handler
func (s *Service) Dispatch(request []byte) []byte {
msg := bytes.SplitN(request, []byte{' '}, 2)
name := msg[0]
// Trim NULL byte terminator
name = bytes.TrimRight(name, "\x00")
handler, ok := s.handlers[string(name)]
if !ok {
log.Printf("unknown command: %q\n", name)
return []byte("Unknown Command")
}
var args []byte
if len(msg) == 2 {
args = msg[1]
}
response, err := handler(args)
if err == nil {
response = append([]byte("OK "), response...)
} else {
log.Printf("error calling %s: %s\n", name, err)
response = append([]byte("ERR "), response...)
}
return response
}
// Reset is the default Handler for reset requests
func (s *Service) Reset([]byte) ([]byte, error) {
s.SendGuestInfo() // Send the IP info ASAP
return []byte("ATR " + s.name), nil
}
// Ping is the default Handler for ping requests
func (s *Service) Ping([]byte) ([]byte, error) {
return nil, nil
}
// SetOption is the default Handler for Set_Option requests
func (s *Service) SetOption(args []byte) ([]byte, error) {
opts := bytes.SplitN(args, []byte{' '}, 2)
key := string(opts[0])
val := string(opts[1])
if Trace {
fmt.Fprintf(os.Stderr, "set option %q=%q\n", key, val)
}
switch key {
case "broadcastIP": // TODO: const-ify
if val == "1" {
ip := s.PrimaryIP()
if ip == "" {
log.Printf("failed to find primary IP")
return nil, nil
}
msg := fmt.Sprintf("info-set guestinfo.ip %s", ip)
_, err := s.out.Request([]byte(msg))
if err != nil {
return nil, err
}
s.SendGuestInfo()
}
default:
// TODO: handle other options...
}
return nil, nil
}
// DefaultIP is used by default when responding to a Set_Option broadcastIP request
// It can be overridden with the Service.PrimaryIP field
func DefaultIP() string {
addrs, err := netInterfaceAddrs()
if err == nil {
for _, addr := range addrs {
if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() {
if ip.IP.To4() != nil {
return ip.IP.String()
}
}
}
}
return ""
}
func (s *Service) CapabilitiesRegister([]byte) ([]byte, error) {
for _, cap := range capabilities {
_, err := s.out.Request([]byte(cap))
if err != nil {
log.Printf("send %q: %s", cap, err)
}
}
return nil, nil
}
func (s *Service) SendGuestInfo() {
info := []func() ([]byte, error){
GuestInfoNicInfoRequest,
}
for i, r := range info {
b, err := r()
if err == nil {
_, err = s.out.Request(b)
}
if err != nil {
log.Printf("SendGuestInfo %d: %s", i, err)
}
}
}

View File

@@ -0,0 +1,465 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"bytes"
"context"
"errors"
"flag"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"sync"
"testing"
"time"
"github.com/vmware/govmomi/toolbox/hgfs"
"github.com/vmware/govmomi/toolbox/vix"
"github.com/vmware/govmomi/vim25/types"
)
func TestDefaultIP(t *testing.T) {
ip := DefaultIP()
if ip == "" {
t.Error("failed to get a default IP address")
}
}
type testRPC struct {
cmd string
expect string
}
type mockChannelIn struct {
t *testing.T
service *Service
rpc []*testRPC
wg sync.WaitGroup
start error
sendErr int
count struct {
send int
stop int
start int
}
}
func (c *mockChannelIn) Start() error {
c.count.start++
return c.start
}
func (c *mockChannelIn) Stop() error {
c.count.stop++
return nil
}
func (c *mockChannelIn) Receive() ([]byte, error) {
if len(c.rpc) == 0 {
if c.rpc != nil {
// All test RPC requests have been consumed
c.wg.Done()
c.rpc = nil
}
return nil, io.EOF
}
return []byte(c.rpc[0].cmd), nil
}
func (c *mockChannelIn) Send(buf []byte) error {
if c.sendErr > 0 {
c.count.send++
if c.count.send%c.sendErr == 0 {
c.wg.Done()
return errors.New("rpci send error")
}
}
if buf == nil {
return nil
}
expect := c.rpc[0].expect
if string(buf) != expect {
c.t.Errorf("expected %q reply for request %q, got: %q", expect, c.rpc[0].cmd, buf)
}
c.rpc = c.rpc[1:]
return nil
}
// discard rpc out for now
type mockChannelOut struct {
reply [][]byte
start error
}
func (c *mockChannelOut) Start() error {
return c.start
}
func (c *mockChannelOut) Stop() error {
return nil
}
func (c *mockChannelOut) Receive() ([]byte, error) {
if len(c.reply) == 0 {
return nil, io.EOF
}
reply := c.reply[0]
c.reply = c.reply[1:]
return reply, nil
}
func (c *mockChannelOut) Send(buf []byte) error {
if len(buf) == 0 {
return io.ErrShortBuffer
}
return nil
}
func TestServiceRun(t *testing.T) {
in := new(mockChannelIn)
out := new(mockChannelOut)
service := NewService(in, out)
in.rpc = []*testRPC{
{"reset", "OK ATR toolbox"},
{"ping", "OK "},
{"Set_Option synctime 0", "OK "},
{"Capabilities_Register", "OK "},
{"Set_Option broadcastIP 1", "OK "},
}
in.wg.Add(1)
// replies to register capabilities
for i := 0; i < len(capabilities); i++ {
out.reply = append(out.reply, rpciOK)
}
out.reply = append(out.reply,
rpciOK, // reply to SendGuestInfo call in Reset()
rpciOK, // reply to IP broadcast
)
in.service = service
in.t = t
err := service.Start()
if err != nil {
t.Fatal(err)
}
in.wg.Wait()
service.Stop()
service.Wait()
// verify we don't set delay > maxDelay
for i := 0; i <= maxDelay+1; i++ {
service.backoff()
}
if service.delay != maxDelay {
t.Errorf("delay=%d", service.delay)
}
}
func TestServiceErrors(t *testing.T) {
Trace = true
if !testing.Verbose() {
// cover TraceChannel but discard output
traceLog = ioutil.Discard
}
netInterfaceAddrs = func() ([]net.Addr, error) {
return nil, io.EOF
}
in := new(mockChannelIn)
out := new(mockChannelOut)
service := NewService(in, out)
service.RegisterHandler("Sorry", func([]byte) ([]byte, error) {
return nil, errors.New("i am so sorry")
})
ip := ""
service.PrimaryIP = func() string {
if ip == "" {
ip = "127"
} else if ip == "127" {
ip = "127.0.0.1"
} else if ip == "127.0.0.1" {
ip = ""
}
return ip
}
in.rpc = []*testRPC{
{"Capabilities_Register", "OK "},
{"Set_Option broadcastIP 1", "ERR "},
{"Set_Option broadcastIP 1", "OK "},
{"Set_Option broadcastIP 1", "OK "},
{"NOPE", "Unknown Command"},
{"Sorry", "ERR "},
}
in.wg.Add(1)
// replies to register capabilities
for i := 0; i < len(capabilities); i++ {
out.reply = append(out.reply, rpciERR)
}
foo := []byte("foo")
out.reply = append(
out.reply,
rpciERR,
rpciOK,
rpciOK,
append(rpciOK, foo...),
rpciERR,
)
in.service = service
in.t = t
err := service.Start()
if err != nil {
t.Fatal(err)
}
in.wg.Wait()
// Done serving RPCs, test ChannelOut errors
reply, err := service.out.Request(rpciOK)
if err != nil {
t.Error(err)
}
if !bytes.Equal(reply, foo) {
t.Errorf("reply=%q", foo)
}
_, err = service.out.Request(rpciOK)
if err == nil {
t.Error("expected error")
}
_, err = service.out.Request(nil)
if err == nil {
t.Error("expected error")
}
service.Stop()
service.Wait()
// cover service start error paths
start := errors.New("fail")
in.start = start
err = service.Start()
if err != start {
t.Error("expected error")
}
in.start = nil
out.start = start
err = service.Start()
if err != start {
t.Error("expected error")
}
}
func TestServiceResetChannel(t *testing.T) {
in := new(mockChannelIn)
out := new(mockChannelOut)
service := NewService(in, out)
resetDelay = maxDelay
fails := 2
in.wg.Add(fails)
in.sendErr = 10
err := service.Start()
if err != nil {
t.Fatal(err)
}
in.wg.Wait()
service.Stop()
service.Wait()
expect := fails
if in.count.start != expect || in.count.stop != expect {
t.Errorf("count=%#v", in.count)
}
}
var (
testESX = flag.Bool("toolbox.testesx", false, "Test toolbox service against ESX (vmtoolsd must not be running)")
testPID = flag.Int64("toolbox.testpid", 0, "PID to return from toolbox start command")
testOn = flag.String("toolbox.powerState", "", "Power state of VM prior to starting the test")
)
// echoHandler for testing hgfs.FileHandler
type echoHandler struct{}
func (e *echoHandler) Stat(u *url.URL) (os.FileInfo, error) {
if u.RawQuery == "" {
return nil, errors.New("no query")
}
if u.Query().Get("foo") != "bar" {
return nil, errors.New("invalid query")
}
return os.Stat(u.Path)
}
func (e *echoHandler) Open(u *url.URL, mode int32) (hgfs.File, error) {
_, err := e.Stat(u)
if err != nil {
return nil, err
}
return os.Open(u.Path)
}
func TestServiceRunESX(t *testing.T) {
if *testESX == false {
t.SkipNow()
}
Trace = testing.Verbose()
// A server that echos HTTP requests, for testing toolbox's http.RoundTripper
echo := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = r.Write(w)
}))
// Client side can use 'govc guest.getenv' to get the URL w/ random port
_ = os.Setenv("TOOLBOX_ECHO_SERVER", echo.URL)
var wg sync.WaitGroup
in := NewBackdoorChannelIn()
out := NewBackdoorChannelOut()
service := NewService(in, out)
service.Command.FileServer.RegisterFileHandler("echo", new(echoHandler))
ping := sync.NewCond(new(sync.Mutex))
service.RegisterHandler("ping", func(b []byte) ([]byte, error) {
ping.Broadcast()
return service.Ping(b)
})
// assert that reset, ping, Set_Option and Capabilities_Register are called at least once
for _, name := range []string{"reset", "ping", "Set_Option", "Capabilities_Register"} {
n := name
h := service.handlers[name]
wg.Add(1)
service.handlers[name] = func(b []byte) ([]byte, error) {
defer wg.Done()
service.handlers[n] = h // reset
return h(b)
}
}
if *testOn == string(types.VirtualMachinePowerStatePoweredOff) {
wg.Add(1)
service.Power.PowerOn.Handler = func() error {
defer wg.Done()
log.Print("power on event")
return nil
}
} else {
log.Print("skipping power on test")
}
if *testPID != 0 {
service.Command.ProcessStartCommand = func(m *ProcessManager, r *vix.StartProgramRequest) (int64, error) {
wg.Add(1)
defer wg.Done()
switch r.ProgramPath {
case "/bin/date":
return *testPID, nil
case "sleep":
p := NewProcessFunc(func(ctx context.Context, arg string) error {
d, err := time.ParseDuration(arg)
if err != nil {
return err
}
select {
case <-ctx.Done():
return &ProcessError{Err: ctx.Err(), ExitCode: 42}
case <-time.After(d):
}
return nil
})
return m.Start(r, p)
default:
return DefaultStartCommand(m, r)
}
}
}
service.PrimaryIP = func() string {
log.Print("broadcasting IP")
return DefaultIP()
}
log.Print("starting toolbox service")
err := service.Start()
if err != nil {
log.Fatal(err)
}
wg.Wait()
// wait for 1 last ping to make sure the final response has reached the client before stopping
ping.L.Lock()
ping.Wait()
ping.L.Unlock()
service.Stop()
service.Wait()
}

259
vendor/github.com/vmware/govmomi/toolbox/toolbox-test.sh generated vendored Executable file
View File

@@ -0,0 +1,259 @@
#!/bin/bash -e
# Copyright 2017 VMware, 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.
#
# Create (or reuse) a VM to run toolbox and/or toolbox.test
# Requires ESX to be configured with:
# govc host.esxcli system settings advanced set -o /Net/GuestIPHack -i 1
set -o pipefail
vm="toolbox-test-$(uuidgen)"
destroy=true
verbose=true
while getopts n:qstv flag
do
case $flag in
n)
vm=$OPTARG
unset destroy
;;
q)
verbose=false # you want this if generating lots of traffic, such as large file transfers
;;
s)
start=true
;;
t)
test=true
;;
*)
echo "unknown option" 1>&2
exit 1
;;
esac
done
echo "Building toolbox binaries..."
pushd "$(git rev-parse --show-toplevel)" >/dev/null
GOOS=linux GOARCH=amd64 go build -o "$GOPATH/bin/toolbox" -v ./toolbox/toolbox
GOOS=linux GOARCH=amd64 go test -race -i -c ./toolbox -o "$GOPATH/bin/toolbox.test"
popd >/dev/null
iso=coreos_production_iso_image.iso
govc datastore.mkdir -p images
if ! govc datastore.ls images | grep -q $iso ; then
echo "Downloading ${iso}..."
if [ ! -e $iso ] ; then
wget http://beta.release.core-os.net/amd64-usr/current/$iso
fi
echo "Uploading ${iso}..."
govc datastore.upload $iso images/$iso
fi
if [ ! -e config.iso ] ; then
echo "Generating config.iso..."
keys=$(cat ~/.ssh/id_[rd]sa.pub)
dir=$(mktemp -d toolbox.XXXXXX)
pushd "${dir}" >/dev/null
mkdir -p drive/openstack/latest
cat > drive/openstack/latest/user_data <<EOF
#!/bin/bash
# Add ${USER}'s public key(s) to .ssh/authorized_keys
echo "$keys" | update-ssh-keys -u core -a coreos-cloudinit
EOF
genisoimage=$(type -p genisoimage mkisofs | head -1)
$genisoimage -R -V config-2 -o config.iso ./drive
popd >/dev/null
mv -f "$dir/config.iso" .
rm -rf "$dir"
fi
govc datastore.mkdir -p "$vm"
vm_path="$(govc find / -type m -name "$vm")"
if [ -z "$vm_path" ] ; then
echo "Creating VM ${vm}..."
govc vm.create -g otherGuest64 -m 1024 -on=false "$vm"
device=$(govc device.cdrom.add -vm "$vm")
govc device.cdrom.insert -vm "$vm" -device "$device" images/$iso
govc datastore.upload config.iso "$vm/config.iso" >/dev/null
device=$(govc device.cdrom.add -vm "$vm")
govc device.cdrom.insert -vm "$vm" -device "$device" "$vm/config.iso"
vm_path="$(govc find / -type m -name "$vm")"
else
govc object.collect -s "$vm_path" -guest.toolsStatus toolsNot* # wait for previous toolbox to unregister
fi
state=$(govc object.collect -s "$vm_path" runtime.powerState)
if [ "$state" != "poweredOn" ] ; then
govc vm.power -on "$vm"
fi
echo -n "Waiting for ${vm} ip..."
ip=$(govc vm.ip -esxcli "$vm")
opts=(-o "UserKnownHostsFile /dev/null" -o "StrictHostKeyChecking no" -o "LogLevel error" -o "BatchMode yes")
destroy() {
if [ -n "$test" ] ; then
rm -f "$GOVC_TLS_KNOWN_HOSTS"
fi
if [ -n "$destroy" ] ; then
echo "Destroying VM ${vm}..."
govc vm.destroy "$vm"
govc datastore.rm -f "$vm"
else
ssh "${opts[@]}" "core@${ip}" pkill toolbox || true
fi
}
trap destroy EXIT
scp "${opts[@]}" "$GOPATH"/bin/toolbox{,.test} "core@${ip}:"
if [ -n "$test" ] ; then
# validate guest.FileManager adds the host thumbprint when transferring files
unset GOVC_INSECURE
GOVC_TLS_KNOWN_HOSTS=$(mktemp --tmpdir toolbox.XXXXXX)
govc about.cert -k -thumbprint > "$GOVC_TLS_KNOWN_HOSTS"
export GOVC_TLS_KNOWN_HOSTS GOVC_GUEST_LOGIN=user:pass
echo "Running toolbox tests..."
ssh "${opts[@]}" "core@${ip}" ./toolbox.test -test.v=$verbose -test.run TestServiceRunESX -toolbox.testesx \
-toolbox.testpid="$$" -toolbox.powerState="$state" &
echo "Waiting for VM ip from toolbox..."
ip=$(govc vm.ip "$vm")
echo "toolbox vm.ip=$ip"
echo "Testing guest.{start,kill,ps} operations via govc..."
export GOVC_VM="$vm"
# should be 0 procs as toolbox only lists processes it started, for now
test -z "$(govc guest.ps -e | grep -v STIME)"
out=$(govc guest.start /bin/date)
if [ "$out" != "$$" ] ; then
echo "'$out' != '$$'" 1>&2
fi
# These processes would run for 1h if we didn't kill them.
pid=$(govc guest.start sleep 1h)
echo "Killing func $pid..."
govc guest.kill -p "$pid"
govc guest.ps -e -p "$pid" -X | grep "$pid"
govc guest.ps -e -p "$pid" -json | jq -r .ProcessInfo[].ExitCode | grep -q 42
pid=$(govc guest.start /bin/sh -c "sleep 3600")
echo "Killing proc $pid..."
govc guest.kill -p "$pid"
govc guest.ps -e -p "$pid" -X | grep "$pid"
echo "Testing file copy to and from guest via govc..."
base="$(basename "$0")"
dest="/tmp/$base"
govc guest.upload -f -perm 0640 -gid 10 "$0" "$dest"
govc guest.download "$dest" - | md5sum --quiet -c <(<"$0" md5sum)
govc guest.chmod 0755 "$dest"
govc guest.ls "$dest" | grep rwxr-xr-x
echo "Testing custom hgfs.FileHandler..."
if ! govc guest.download "/echo:$dest" - 2>/dev/null ; then
govc guest.download "/echo:$dest?foo=bar" - >/dev/null
fi
home=$(govc guest.getenv HOME | cut -d= -f2)
if date | govc guest.upload -f - /tmp 2>/dev/null ; then
echo "guest.upload to directory should fail without .tgz source" 1>&2
exit 1
fi
if [ "$verbose" = "false" ] ; then # else you don't want to see this noise
# Download the $HOME directory, includes toolbox binaries (~30M total)
# Note: trailing slash is required
prefix=$(basename "$home")
govc guest.download "$home/?prefix=$prefix/&format=tgz" - | tar -tzvf - | grep "$prefix"/toolbox
govc guest.mkdir -p /tmp/toolbox-src
# Upload source files from this directory
# and validate that query string is not used as the file/dir name (see hgfs.ArchiveHandler)
git archive --format tgz HEAD | govc guest.upload -f - /tmp/toolbox-src?skip=stuff
# Upload .tar
git archive --format tar HEAD | govc guest.upload -f - /tmp/toolbox-src
# Download .tar
govc guest.download "/tmp/toolbox-src/" - | tar -tvf - | grep "$base"
# Download a single file as a .tar.gz (note: /archive: prefix is required)
govc guest.download "/archive:/tmp/toolbox-src/$base?format=tgz" - | tar -tvzf - | grep -v README.md
# Download a single file as a .tar (note: /archive: prefix is required)
govc guest.download "/archive:/tmp/toolbox-src/$base" - | tar -tvf - | grep -v README.md
govc guest.rmdir -r /tmp/toolbox-src
fi
echo "Testing we can download /proc files..."
for name in uptime diskstats net/dev ; do
test -n "$(govc guest.download /proc/$name -)"
done
addr=$(govc guest.getenv TOOLBOX_ECHO_SERVER | cut -d= -f2)
echo "Testing http.RoundTrip via $addr..."
govc guest.run GET "$addr/$vm" | grep "$vm"
echo "$vm" | govc guest.run -e Content-Type:text/plain -d - POST "$addr" | grep "$vm"
echo "Testing commands with IO..."
# Note that we don't use a pipe here, as guest.ps transitions the vm to VM_STATE_GUEST_OPERATION,
# which prevents guest.run from invoking guest operations. By letting guest.ps complete before
# guest.run, the vm will have transitioned back to VM_STATE_POWERED_ON.
data=$(govc guest.ps -json)
govc guest.run -d "$data" jq .
# test ProcessIO download retries
govc version | govc guest.run -d - bash -c "'sleep 2 && cat'"
echo "Waiting for tests to complete..."
wait
fi
if [ -n "$start" ] ; then
(
govc object.collect -s "$vm_path" -guest.toolsStatus toolsOk # wait for toolbox to register
/usr/bin/time -f "Waiting for VM ip from toolbox...%e" govc vm.ip -v4 -n ethernet-0 "$vm"
) &
echo "Starting toolbox..."
ssh "${opts[@]}" "core@${ip}" ./toolbox -toolbox.trace=$verbose
fi

View File

@@ -0,0 +1,71 @@
/*
Copyright (c) 2017 VMware, 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.
*/
// Copyright 2016-2017 VMware, 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 main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"github.com/vmware/govmomi/toolbox"
)
// This example can be run on a VM hosted by ESX, Fusion or Workstation
func main() {
flag.Parse()
in := toolbox.NewBackdoorChannelIn()
out := toolbox.NewBackdoorChannelOut()
service := toolbox.NewService(in, out)
if os.Getuid() == 0 {
service.Power.Halt.Handler = toolbox.Halt
service.Power.Reboot.Handler = toolbox.Reboot
}
err := service.Start()
if err != nil {
log.Fatal(err)
}
// handle the signals and gracefully shutdown the service
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
go func() {
log.Printf("signal %s received", <-sig)
service.Stop()
}()
service.Wait()
}

View File

@@ -0,0 +1,25 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"os"
)
func fileExtendedInfoFormat(dir string, info os.FileInfo) string {
return ""
}

View File

@@ -0,0 +1,69 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"fmt"
"os"
"path/filepath"
"syscall"
"time"
"github.com/vmware/govmomi/toolbox/vix"
)
func fileExtendedInfoFormat(dir string, info os.FileInfo) string {
const format = "<fxi>" +
"<Name>%s</Name>" +
"<ft>%d</ft>" +
"<fs>%d</fs>" +
"<mt>%d</mt>" +
"<at>%d</at>" +
"<uid>%d</uid>" +
"<gid>%d</gid>" +
"<perm>%d</perm>" +
"<slt>%s</slt>" +
"</fxi>"
props := 0
targ := ""
if info.IsDir() {
props |= vix.FileAttributesDirectory
}
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
props |= vix.FileAttributesSymlink
targ, _ = os.Readlink(filepath.Join(dir, info.Name()))
}
size := info.Size()
mtime := info.ModTime().Unix()
perm := info.Mode().Perm()
atime := mtime
uid := os.Getuid()
gid := os.Getgid()
if sys, ok := info.Sys().(*syscall.Stat_t); ok {
atime = time.Unix(sys.Atim.Unix()).Unix()
uid = int(sys.Uid)
gid = int(sys.Gid)
}
return fmt.Sprintf(format, info.Name(), props, size, mtime, atime, uid, gid, perm, targ)
}

View File

@@ -0,0 +1,41 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"fmt"
"os"
)
func fileExtendedInfoFormat(dir string, info os.FileInfo) string {
const format = "<fxi>" +
"<Name>%s</Name>" +
"<ft>%d</ft>" +
"<fs>%d</fs>" +
"<mt>%d</mt>" +
"<ct>%d</ct>" +
"<at>%d</at>" +
"</fxi>"
props := 0
size := info.Size()
mtime := info.ModTime().Unix()
ctime := 0
atime := 0
return fmt.Sprintf(format, info.Name(), props, size, mtime, ctime, atime)
}

View File

@@ -0,0 +1,83 @@
/*
Copyright (c) 2017 VMware, 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 toolbox
import (
"encoding/hex"
"flag"
"fmt"
"io"
"os"
)
var (
Trace = false
traceLog io.Writer = os.Stderr
)
func init() {
flag.BoolVar(&Trace, "toolbox.trace", Trace, "Enable toolbox trace")
}
type TraceChannel struct {
Channel
log io.Writer
}
func NewTraceChannel(c Channel) Channel {
if !Trace {
return c
}
return &TraceChannel{
Channel: c,
log: traceLog,
}
}
func (d *TraceChannel) Start() error {
err := d.Channel.Start()
return err
}
func (d *TraceChannel) Stop() error {
err := d.Channel.Stop()
return err
}
func (d *TraceChannel) Send(buf []byte) error {
if len(buf) > 0 {
fmt.Fprintf(d.log, "SEND %d...\n%s\n", len(buf), hex.Dump(buf))
}
err := d.Channel.Send(buf)
return err
}
func (d *TraceChannel) Receive() ([]byte, error) {
buf, err := d.Channel.Receive()
if err == nil && len(buf) > 0 {
fmt.Fprintf(d.log, "RECV %d...\n%s\n", len(buf), hex.Dump(buf))
}
return buf, err
}

View File

@@ -0,0 +1,236 @@
/*
Copyright (c) 2017 VMware, 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 vix
import (
"bytes"
"encoding/binary"
"errors"
)
// Property type enum as defined in open-vm-tools/lib/include/vix.h
const (
_ = iota // ANY type not supported
vixPropertyTypeInt32
vixPropertyTypeString
vixPropertyTypeBool
_ // HANDLE type not supported
vixPropertyTypeInt64
vixPropertyTypeBlob
)
// Property ID enum as defined in open-vm-tools/lib/include/vixOpenSource.h
const (
PropertyGuestToolsAPIOptions = 4501
PropertyGuestOsFamily = 4502
PropertyGuestOsVersion = 4503
PropertyGuestToolsProductNam = 4511
PropertyGuestToolsVersion = 4500
PropertyGuestName = 4505
PropertyGuestOsVersionShort = 4520
PropertyGuestStartProgramEnabled = 4540
PropertyGuestListProcessesEnabled = 4541
PropertyGuestTerminateProcessEnabled = 4542
PropertyGuestReadEnvironmentVariableEnabled = 4543
PropertyGuestMakeDirectoryEnabled = 4547
PropertyGuestDeleteFileEnabled = 4548
PropertyGuestDeleteDirectoryEnabled = 4549
PropertyGuestMoveDirectoryEnabled = 4550
PropertyGuestMoveFileEnabled = 4551
PropertyGuestCreateTempFileEnabled = 4552
PropertyGuestCreateTempDirectoryEnabled = 4553
PropertyGuestListFilesEnabled = 4554
PropertyGuestChangeFileAttributesEnabled = 4555
PropertyGuestInitiateFileTransferFromGuestEnabled = 4556
PropertyGuestInitiateFileTransferToGuestEnabled = 4557
)
type Property struct {
header struct {
ID int32
Kind int32
Length int32
}
data struct {
Int32 int32
String string
Bool uint8
Int64 int64
Blob []byte
}
}
var int32Size int32
func init() {
var i int32
int32Size = int32(binary.Size(&i))
}
type PropertyList []*Property
func NewInt32Property(ID int32, val int32) *Property {
p := new(Property)
p.header.ID = ID
p.header.Kind = vixPropertyTypeInt32
p.header.Length = int32Size
p.data.Int32 = val
return p
}
func NewStringProperty(ID int32, val string) *Property {
p := new(Property)
p.header.ID = ID
p.header.Kind = vixPropertyTypeString
p.header.Length = int32(len(val) + 1)
p.data.String = val
return p
}
func NewBoolProperty(ID int32, val bool) *Property {
p := new(Property)
p.header.ID = ID
p.header.Kind = vixPropertyTypeBool
p.header.Length = 1
if val {
p.data.Bool = 1
}
return p
}
func NewInt64Property(ID int32, val int64) *Property {
p := new(Property)
p.header.ID = ID
p.header.Kind = vixPropertyTypeInt64
p.header.Length = int32Size * 2
p.data.Int64 = val
return p
}
func NewBlobProperty(ID int32, val []byte) *Property {
p := new(Property)
p.header.ID = ID
p.header.Kind = vixPropertyTypeBlob
p.header.Length = int32(len(val))
p.data.Blob = val
return p
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (p *Property) MarshalBinary() ([]byte, error) {
buf := new(bytes.Buffer)
// #nosec: Errors unhandled
_ = binary.Write(buf, binary.LittleEndian, &p.header)
switch p.header.Kind {
case vixPropertyTypeBool:
// #nosec: Errors unhandled
_ = binary.Write(buf, binary.LittleEndian, p.data.Bool)
case vixPropertyTypeInt32:
// #nosec: Errors unhandled
_ = binary.Write(buf, binary.LittleEndian, p.data.Int32)
case vixPropertyTypeInt64:
// #nosec: Errors unhandled
_ = binary.Write(buf, binary.LittleEndian, p.data.Int64)
case vixPropertyTypeString:
// #nosec: Errors unhandled
_, _ = buf.WriteString(p.data.String)
// #nosec: Errors unhandled
_ = buf.WriteByte(0)
case vixPropertyTypeBlob:
// #nosec: Errors unhandled
_, _ = buf.Write(p.data.Blob)
}
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (p *Property) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &p.header)
if err != nil {
return err
}
switch p.header.Kind {
case vixPropertyTypeBool:
return binary.Read(buf, binary.LittleEndian, &p.data.Bool)
case vixPropertyTypeInt32:
return binary.Read(buf, binary.LittleEndian, &p.data.Int32)
case vixPropertyTypeInt64:
return binary.Read(buf, binary.LittleEndian, &p.data.Int64)
case vixPropertyTypeString:
s := make([]byte, p.header.Length)
if _, err := buf.Read(s); err != nil {
return err
}
p.data.String = string(bytes.TrimRight(s, "\x00"))
case vixPropertyTypeBlob:
p.data.Blob = make([]byte, p.header.Length)
if _, err := buf.Read(p.data.Blob); err != nil {
return err
}
default:
return errors.New("VIX_E_UNRECOGNIZED_PROPERTY")
}
return nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (l *PropertyList) UnmarshalBinary(data []byte) error {
headerSize := int32Size * 3
for {
p := new(Property)
err := p.UnmarshalBinary(data)
if err != nil {
return err
}
*l = append(*l, p)
offset := headerSize + p.header.Length
data = data[offset:]
if len(data) == 0 {
return nil
}
}
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (l *PropertyList) MarshalBinary() ([]byte, error) {
var buf bytes.Buffer
for _, p := range *l {
// #nosec: Errors unhandled
b, _ := p.MarshalBinary()
// #nosec: Errors unhandled
_, _ = buf.Write(b)
}
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,107 @@
/*
Copyright (c) 2017 VMware, 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 vix
import (
"encoding/base64"
"encoding/binary"
"math"
"reflect"
"testing"
)
func TestToolsStateProperties(t *testing.T) {
// captured from vmtoolsd
str := `lxEAAAIAAAAoAAAATGludXggNC40LjAtMjEtZ2VuZXJpYyBVYnVudHUgMTYuMDQgTFRTAKgRAAACAAAACgAAAHVidW50dS02NACfEQAAAgAAAA0AAABWTXdhcmUgVG9vbHMAlBEAAAIAAAAVAAAAMTAuMC41IGJ1aWxkLTMyMjc4NzIAmREAAAIAAAATAAAAdWJ1bnR1LTE2MDQtdm13YXJlAJURAAABAAAABAAAAAEAAACWEQAAAQAAAAQAAAABAAAAmBEAAAIAAAABAAAAAMsAAAACAAAAEQAAAC90bXAvdm13YXJlLXJvb3QApxEAAAEAAAAEAAAAQAAAAK0RAAACAAAACgAAAC9tbnQvaGdmcwC8EQAAAwAAAAEAAAAAvREAAAMAAAABAAAAAL4RAAADAAAAAQAAAAC/EQAAAwAAAAEAAAAAwBEAAAMAAAABAAAAAMERAAADAAAAAQAAAADCEQAAAwAAAAEAAAAAwxEAAAMAAAABAAAAAMQRAAADAAAAAQAAAADFEQAAAwAAAAEAAAAAxhEAAAMAAAABAAAAAMcRAAADAAAAAQAAAADIEQAAAwAAAAEAAAAAyREAAAMAAAABAAAAAMoRAAADAAAAAQAAAADLEQAAAwAAAAEAAAAAzBEAAAMAAAABAAAAAM0RAAADAAAAAQAAAADOEQAAAwAAAAEAAAAAzxEAAAMAAAABAAAAANARAAADAAAAAQAAAADREQAAAwAAAAEAAAAA0hEAAAMAAAABAAAAANMRAAADAAAAAQAAAADUEQAAAwAAAAEAAAAA1REAAAMAAAABAAAAANYRAAADAAAAAQAAAADXEQAAAwAAAAEAAAAA2BEAAAMAAAABAAAAAA==`
data, err := base64.StdEncoding.DecodeString(str)
if err != nil {
t.Fatal(err)
}
var props PropertyList
err = props.UnmarshalBinary(data)
if err != nil {
t.Error(err)
}
data2, err := props.MarshalBinary()
if err != nil {
t.Fatal(err)
}
str2 := base64.StdEncoding.EncodeToString(data2)
if str != str2 {
t.Error("encoding mismatch")
}
}
func TestMarshalProperties(t *testing.T) {
in := PropertyList{
NewInt32Property(1, math.MaxInt32),
NewStringProperty(2, "foo"),
NewBoolProperty(3, true),
NewInt64Property(4, math.MaxInt64),
NewBlobProperty(5, []byte("deadbeef")),
}
buf, err := in.MarshalBinary()
if err != nil {
t.Fatal(err)
}
var out PropertyList
err = out.UnmarshalBinary(buf)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(in, out) {
t.Error("marshal mismatch")
}
}
// hit unmarshal error paths
func TestUnmarshalPropertiesErrors(t *testing.T) {
props := PropertyList{
NewBoolProperty(1, true),
NewStringProperty(1, "foo"),
NewBlobProperty(2, []byte("deadbeef")),
}
props[0].header.Kind = 0xff
for _, prop := range props {
buf, _ := prop.MarshalBinary()
for i, l := range []int{1, binary.Size(prop.header)} {
err := prop.UnmarshalBinary(buf[:l])
if err == nil {
t.Errorf("test %d (len=%d) expected error", i, l)
}
}
}
buf, _ := props.MarshalBinary()
err := props.UnmarshalBinary(buf)
if err == nil {
t.Error("expected error")
}
}

View File

@@ -0,0 +1,847 @@
/*
Copyright (c) 2017 VMware, 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 vix
import (
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"os"
"os/exec"
"syscall"
)
const (
CommandMagicWord = 0xd00d0001
CommandGetToolsState = 62
CommandStartProgram = 185
CommandListProcessesEx = 186
CommandReadEnvVariables = 187
CommandTerminateProcess = 193
CommandCreateDirectoryEx = 178
CommandMoveGuestFileEx = 179
CommandMoveGuestDirectory = 180
CommandCreateTemporaryFileEx = 181
CommandCreateTemporaryDirectory = 182
CommandSetGuestFileAttributes = 183
CommandDeleteGuestFileEx = 194
CommandDeleteGuestDirectoryEx = 195
CommandListFiles = 177
HgfsSendPacketCommand = 84
CommandInitiateFileTransferFromGuest = 188
CommandInitiateFileTransferToGuest = 189
// VIX_USER_CREDENTIAL_NAME_PASSWORD
UserCredentialTypeNamePassword = 1
// VIX_E_* constants from vix.h
OK = 0
Fail = 1
InvalidArg = 3
FileNotFound = 4
FileAlreadyExists = 12
FileAccessError = 13
AuthenticationFail = 35
UnrecognizedCommandInGuest = 3025
InvalidMessageHeader = 10000
InvalidMessageBody = 10001
NotAFile = 20001
NotADirectory = 20002
NoSuchProcess = 20003
DirectoryNotEmpty = 20006
// VIX_COMMAND_* constants from Commands.h
CommandGuestReturnsBinary = 0x80
// VIX_FILE_ATTRIBUTES_ constants from vix.h
FileAttributesDirectory = 0x0001
FileAttributesSymlink = 0x0002
)
// SetGuestFileAttributes flags as defined in vixOpenSource.h
const (
FileAttributeSetAccessDate = 0x0001
FileAttributeSetModifyDate = 0x0002
FileAttributeSetReadonly = 0x0004
FileAttributeSetHidden = 0x0008
FileAttributeSetUnixOwnerid = 0x0010
FileAttributeSetUnixGroupid = 0x0020
FileAttributeSetUnixPermissions = 0x0040
)
type Error int
func (err Error) Error() string {
return fmt.Sprintf("vix error=%d", err)
}
// ErrorCode does its best to map the given error to a VIX error code.
// See also: Vix_TranslateErrno
func ErrorCode(err error) int {
switch t := err.(type) {
case Error:
return int(t)
case *os.PathError:
if errno, ok := t.Err.(syscall.Errno); ok {
switch errno {
case syscall.ENOTEMPTY:
return DirectoryNotEmpty
}
}
case *exec.Error:
if t.Err == exec.ErrNotFound {
return FileNotFound
}
}
switch {
case os.IsNotExist(err):
return FileNotFound
case os.IsExist(err):
return FileAlreadyExists
case os.IsPermission(err):
return FileAccessError
default:
return Fail
}
}
type Header struct {
Magic uint32
MessageVersion uint16
TotalMessageLength uint32
HeaderLength uint32
BodyLength uint32
CredentialLength uint32
CommonFlags uint8
}
type CommandRequestHeader struct {
Header
OpCode uint32
RequestFlags uint32
TimeOut uint32
Cookie uint64
ClientHandleID uint32
UserCredentialType uint32
}
type StartProgramRequest struct {
CommandRequestHeader
Body struct {
StartMinimized uint8
ProgramPathLength uint32
ArgumentsLength uint32
WorkingDirLength uint32
NumEnvVars uint32
EnvVarLength uint32
}
ProgramPath string
Arguments string
WorkingDir string
EnvVars []string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *StartProgramRequest) MarshalBinary() ([]byte, error) {
var env bytes.Buffer
if n := len(r.EnvVars); n != 0 {
for _, e := range r.EnvVars {
_, _ = env.Write([]byte(e))
_ = env.WriteByte(0)
}
r.Body.NumEnvVars = uint32(n)
r.Body.EnvVarLength = uint32(env.Len())
}
var fields []string
add := func(s string, l *uint32) {
if n := len(s); n != 0 {
*l = uint32(n) + 1
fields = append(fields, s)
}
}
add(r.ProgramPath, &r.Body.ProgramPathLength)
add(r.Arguments, &r.Body.ArgumentsLength)
add(r.WorkingDir, &r.Body.WorkingDirLength)
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
for _, val := range fields {
_, _ = buf.Write([]byte(val))
_ = buf.WriteByte(0)
}
if r.Body.EnvVarLength != 0 {
_, _ = buf.Write(env.Bytes())
}
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *StartProgramRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
fields := []struct {
len uint32
val *string
}{
{r.Body.ProgramPathLength, &r.ProgramPath},
{r.Body.ArgumentsLength, &r.Arguments},
{r.Body.WorkingDirLength, &r.WorkingDir},
}
for _, field := range fields {
if field.len == 0 {
continue
}
x := buf.Next(int(field.len))
*field.val = string(bytes.TrimRight(x, "\x00"))
}
for i := 0; i < int(r.Body.NumEnvVars); i++ {
env, rerr := buf.ReadString(0)
if rerr != nil {
return rerr
}
env = env[:len(env)-1] // discard NULL terminator
r.EnvVars = append(r.EnvVars, env)
}
return nil
}
type KillProcessRequest struct {
CommandRequestHeader
Body struct {
Pid int64
Options uint32
}
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *KillProcessRequest) MarshalBinary() ([]byte, error) {
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *KillProcessRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
return binary.Read(buf, binary.LittleEndian, &r.Body)
}
type ListProcessesRequest struct {
CommandRequestHeader
Body struct {
Key uint32
Offset uint32
NumPids uint32
}
Pids []int64
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *ListProcessesRequest) MarshalBinary() ([]byte, error) {
r.Body.NumPids = uint32(len(r.Pids))
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
for _, pid := range r.Pids {
_ = binary.Write(buf, binary.LittleEndian, &pid)
}
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *ListProcessesRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
r.Pids = make([]int64, r.Body.NumPids)
for i := uint32(0); i < r.Body.NumPids; i++ {
err := binary.Read(buf, binary.LittleEndian, &r.Pids[i])
if err != nil {
return err
}
}
return nil
}
type ReadEnvironmentVariablesRequest struct {
CommandRequestHeader
Body struct {
NumNames uint32
NamesLength uint32
}
Names []string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *ReadEnvironmentVariablesRequest) MarshalBinary() ([]byte, error) {
var env bytes.Buffer
if n := len(r.Names); n != 0 {
for _, e := range r.Names {
_, _ = env.Write([]byte(e))
_ = env.WriteByte(0)
}
r.Body.NumNames = uint32(n)
r.Body.NamesLength = uint32(env.Len())
}
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
if r.Body.NamesLength != 0 {
_, _ = buf.Write(env.Bytes())
}
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *ReadEnvironmentVariablesRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
for i := 0; i < int(r.Body.NumNames); i++ {
env, rerr := buf.ReadString(0)
if rerr != nil {
return rerr
}
env = env[:len(env)-1] // discard NULL terminator
r.Names = append(r.Names, env)
}
return nil
}
type CreateTempFileRequest struct {
CommandRequestHeader
Body struct {
Options int32
FilePrefixLength uint32
FileSuffixLength uint32
DirectoryPathLength uint32
PropertyListLength uint32
}
FilePrefix string
FileSuffix string
DirectoryPath string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *CreateTempFileRequest) MarshalBinary() ([]byte, error) {
var fields []string
add := func(s string, l *uint32) {
*l = uint32(len(s)) // NOTE: NULL byte is not included in the length fields on the wire
fields = append(fields, s)
}
add(r.FilePrefix, &r.Body.FilePrefixLength)
add(r.FileSuffix, &r.Body.FileSuffixLength)
add(r.DirectoryPath, &r.Body.DirectoryPathLength)
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
for _, val := range fields {
_, _ = buf.Write([]byte(val))
_ = buf.WriteByte(0)
}
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *CreateTempFileRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
fields := []struct {
len uint32
val *string
}{
{r.Body.FilePrefixLength, &r.FilePrefix},
{r.Body.FileSuffixLength, &r.FileSuffix},
{r.Body.DirectoryPathLength, &r.DirectoryPath},
}
for _, field := range fields {
field.len++ // NOTE: NULL byte is not included in the length fields on the wire
x := buf.Next(int(field.len))
*field.val = string(bytes.TrimRight(x, "\x00"))
}
return nil
}
type FileRequest struct {
CommandRequestHeader
Body struct {
FileOptions int32
GuestPathNameLength uint32
}
GuestPathName string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *FileRequest) MarshalBinary() ([]byte, error) {
buf := new(bytes.Buffer)
r.Body.GuestPathNameLength = uint32(len(r.GuestPathName))
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
_, _ = buf.WriteString(r.GuestPathName)
_ = buf.WriteByte(0)
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *FileRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
name := buf.Next(int(r.Body.GuestPathNameLength))
r.GuestPathName = string(bytes.TrimRight(name, "\x00"))
return nil
}
type DirRequest struct {
CommandRequestHeader
Body struct {
FileOptions int32
GuestPathNameLength uint32
FilePropertiesLength uint32
Recursive bool
}
GuestPathName string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *DirRequest) MarshalBinary() ([]byte, error) {
buf := new(bytes.Buffer)
r.Body.GuestPathNameLength = uint32(len(r.GuestPathName))
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
_, _ = buf.WriteString(r.GuestPathName)
_ = buf.WriteByte(0)
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *DirRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
name := buf.Next(int(r.Body.GuestPathNameLength))
r.GuestPathName = string(bytes.TrimRight(name, "\x00"))
return nil
}
type RenameFileRequest struct {
CommandRequestHeader
Body struct {
CopyFileOptions int32
OldPathNameLength uint32
NewPathNameLength uint32
FilePropertiesLength uint32
Overwrite bool
}
OldPathName string
NewPathName string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *RenameFileRequest) MarshalBinary() ([]byte, error) {
var fields []string
add := func(s string, l *uint32) {
*l = uint32(len(s)) // NOTE: NULL byte is not included in the length fields on the wire
fields = append(fields, s)
}
add(r.OldPathName, &r.Body.OldPathNameLength)
add(r.NewPathName, &r.Body.NewPathNameLength)
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
for _, val := range fields {
_, _ = buf.Write([]byte(val))
_ = buf.WriteByte(0)
}
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *RenameFileRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
fields := []struct {
len uint32
val *string
}{
{r.Body.OldPathNameLength, &r.OldPathName},
{r.Body.NewPathNameLength, &r.NewPathName},
}
for _, field := range fields {
field.len++ // NOTE: NULL byte is not included in the length fields on the wire
x := buf.Next(int(field.len))
*field.val = string(bytes.TrimRight(x, "\x00"))
}
return nil
}
type ListFilesRequest struct {
CommandRequestHeader
Body struct {
FileOptions int32
GuestPathNameLength uint32
PatternLength uint32
Index int32
MaxResults int32
Offset uint64
}
GuestPathName string
Pattern string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *ListFilesRequest) MarshalBinary() ([]byte, error) {
var fields []string
add := func(s string, l *uint32) {
if n := len(s); n != 0 {
*l = uint32(n) + 1
fields = append(fields, s)
}
}
add(r.GuestPathName, &r.Body.GuestPathNameLength)
add(r.Pattern, &r.Body.PatternLength)
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
for _, val := range fields {
_, _ = buf.Write([]byte(val))
_ = buf.WriteByte(0)
}
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *ListFilesRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
fields := []struct {
len uint32
val *string
}{
{r.Body.GuestPathNameLength, &r.GuestPathName},
{r.Body.PatternLength, &r.Pattern},
}
for _, field := range fields {
if field.len == 0 {
continue
}
x := buf.Next(int(field.len))
*field.val = string(bytes.TrimRight(x, "\x00"))
}
return nil
}
type SetGuestFileAttributesRequest struct {
CommandRequestHeader
Body struct {
FileOptions int32
AccessTime int64
ModificationTime int64
OwnerID int32
GroupID int32
Permissions int32
Hidden bool
ReadOnly bool
GuestPathNameLength uint32
}
GuestPathName string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *SetGuestFileAttributesRequest) MarshalBinary() ([]byte, error) {
buf := new(bytes.Buffer)
r.Body.GuestPathNameLength = uint32(len(r.GuestPathName))
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
_, _ = buf.WriteString(r.GuestPathName)
_ = buf.WriteByte(0)
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *SetGuestFileAttributesRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
name := buf.Next(int(r.Body.GuestPathNameLength))
r.GuestPathName = string(bytes.TrimRight(name, "\x00"))
return nil
}
func (r *SetGuestFileAttributesRequest) IsSet(opt int32) bool {
return r.Body.FileOptions&opt == opt
}
type CommandHgfsSendPacket struct {
CommandRequestHeader
Body struct {
PacketSize uint32
Timeout int32
}
Packet []byte
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *CommandHgfsSendPacket) MarshalBinary() ([]byte, error) {
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
_, _ = buf.Write(r.Packet)
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *CommandHgfsSendPacket) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
r.Packet = buf.Next(int(r.Body.PacketSize))
return nil
}
type InitiateFileTransferToGuestRequest struct {
CommandRequestHeader
Body struct {
Options int32
GuestPathNameLength uint32
Overwrite bool
}
GuestPathName string
}
// MarshalBinary implements the encoding.BinaryMarshaler interface
func (r *InitiateFileTransferToGuestRequest) MarshalBinary() ([]byte, error) {
buf := new(bytes.Buffer)
r.Body.GuestPathNameLength = uint32(len(r.GuestPathName))
_ = binary.Write(buf, binary.LittleEndian, &r.Body)
_, _ = buf.WriteString(r.GuestPathName)
_ = buf.WriteByte(0)
return buf.Bytes(), nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface
func (r *InitiateFileTransferToGuestRequest) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(data)
err := binary.Read(buf, binary.LittleEndian, &r.Body)
if err != nil {
return err
}
name := buf.Next(int(r.Body.GuestPathNameLength))
r.GuestPathName = string(bytes.TrimRight(name, "\x00"))
return nil
}
type UserCredentialNamePassword struct {
Body struct {
NameLength uint32
PasswordLength uint32
}
Name string
Password string
}
func (c *UserCredentialNamePassword) UnmarshalBinary(data []byte) error {
buf := bytes.NewBuffer(bytes.TrimRight(data, "\x00"))
err := binary.Read(buf, binary.LittleEndian, &c.Body)
if err != nil {
return err
}
str, err := base64.StdEncoding.DecodeString(string(buf.Bytes()))
if err != nil {
return err
}
c.Name = string(str[0:c.Body.NameLength])
c.Password = string(str[c.Body.NameLength+1 : len(str)-1])
return nil
}
func (c *UserCredentialNamePassword) MarshalBinary() ([]byte, error) {
buf := new(bytes.Buffer)
c.Body.NameLength = uint32(len(c.Name))
c.Body.PasswordLength = uint32(len(c.Password))
_ = binary.Write(buf, binary.LittleEndian, &c.Body)
src := append([]byte(c.Name+"\x00"), []byte(c.Password+"\x00")...)
enc := base64.StdEncoding
pwd := make([]byte, enc.EncodedLen(len(src)))
enc.Encode(pwd, src)
_, _ = buf.Write(pwd)
_ = buf.WriteByte(0)
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,67 @@
/*
Copyright (c) 2017 VMware, 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 vix
import (
"reflect"
"testing"
)
func TestMarshalVixMsgStartProgramRequest(t *testing.T) {
requests := []*StartProgramRequest{
{},
{
ProgramPath: "/bin/date",
},
{
ProgramPath: "/bin/date",
Arguments: "--date=@2147483647",
},
{
ProgramPath: "/bin/date",
WorkingDir: "/tmp",
},
{
ProgramPath: "/bin/date",
WorkingDir: "/tmp",
EnvVars: []string{"FOO=bar"},
},
{
ProgramPath: "/bin/date",
WorkingDir: "/tmp",
EnvVars: []string{"FOO=bar", "BAR=foo"},
},
}
for i, in := range requests {
buf, err := in.MarshalBinary()
if err != nil {
t.Fatal(err)
}
out := new(StartProgramRequest)
err = out.UnmarshalBinary(buf)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(in, out) {
t.Errorf("%d marshal mismatch", i)
}
}
}