Skip to content

Commit

Permalink
Guest tools framework improvements
Browse files Browse the repository at this point in the history
- Implement Capabilities_Register RPC

- Use the same polling interval and backoff logic as vmtoolsd

- Add support for guest power operations

- Trace disabled by default

- Add ChannelOut.Request wrapper

- Add ESX tests

Closes issue vmware#742
  • Loading branch information
dougm committed Jul 10, 2016
1 parent fcefc12 commit 6ce14b3
Show file tree
Hide file tree
Showing 14 changed files with 977 additions and 107 deletions.
21 changes: 20 additions & 1 deletion cmd/toolbox/main.go
Expand Up @@ -15,15 +15,20 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"

"github.com/vmware/vic/pkg/vsphere/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()

Expand All @@ -38,12 +43,26 @@ func main() {
return -1, nil
}

power := toolbox.RegisterPowerCommandHandler(service)

if os.Getuid() == 0 {
power.Halt.Handler = toolbox.Halt
power.Reboot.Handler = toolbox.Reboot
}

err := service.Start()
if err != nil {
log.Fatal(err)
}

defer service.Stop()
// 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()
}
2 changes: 1 addition & 1 deletion pkg/vsphere/toolbox/backdoor.go
Expand Up @@ -65,7 +65,7 @@ func NewBackdoorChannelOut() Channel {
}
}

// NewBackdoorChannelOut creates a Channel for use with the TCLO protocol
// NewBackdoorChannelIn creates a Channel for use with the TCLO protocol
func NewBackdoorChannelIn() Channel {
return &backdoorChannel{
protocol: tcloProtocol,
Expand Down
31 changes: 31 additions & 0 deletions pkg/vsphere/toolbox/backdoor_test.go
Expand Up @@ -14,4 +14,35 @@

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")
}
}
33 changes: 33 additions & 0 deletions pkg/vsphere/toolbox/channel.go
Expand Up @@ -14,10 +14,43 @@

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)
}
114 changes: 114 additions & 0 deletions pkg/vsphere/toolbox/power.go
@@ -0,0 +1,114 @@
// Copyright 2016 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...")
return exec.Command(shutdown, "-h", "now").Run()
}

func Reboot() error {
log.Printf("Rebooting system...")
return exec.Command(shutdown, "-r", "now").Run()
}
51 changes: 51 additions & 0 deletions pkg/vsphere/toolbox/power_test.go
@@ -0,0 +1,51 @@
// Copyright 2016 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 := RegisterPowerCommandHandler(service)

// 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)
}
}

0 comments on commit 6ce14b3

Please sign in to comment.