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 (backdoor) test

Closes issue vmware#742
  • Loading branch information
dougm committed Jul 8, 2016
1 parent fcefc12 commit 15a1fcb
Show file tree
Hide file tree
Showing 9 changed files with 487 additions and 24 deletions.
21 changes: 20 additions & 1 deletion cmd/toolbox/main.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
33 changes: 33 additions & 0 deletions pkg/vsphere/toolbox/channel.go
Original file line number Diff line number Diff line change
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 '%s': '%s'", string(request), string(reply))
}
114 changes: 114 additions & 0 deletions pkg/vsphere/toolbox/power.go
Original file line number Diff line number Diff line change
@@ -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 '%s'", 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 '%s': '%s'", 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()
}
70 changes: 57 additions & 13 deletions pkg/vsphere/toolbox/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ import (
type Service struct {
name string
in Channel
out Channel
out *ChannelOut
handlers map[string]Handler
stop chan struct{}
wg *sync.WaitGroup
delay time.Duration

Interval time.Duration
PrimaryIP func() string
}

Expand All @@ -42,12 +42,11 @@ func NewService(rpcIn Channel, rpcOut Channel) *Service {
s := &Service{
name: "toolbox", // Same name used by vmtoolsd
in: NewTraceChannel(rpcIn),
out: NewTraceChannel(rpcOut),
out: &ChannelOut{NewTraceChannel(rpcOut)},
handlers: make(map[string]Handler),
wg: new(sync.WaitGroup),
stop: make(chan struct{}, 1),

Interval: time.Second,
PrimaryIP: DefaultIP,
}

Expand All @@ -59,6 +58,24 @@ func NewService(rpcIn Channel, rpcOut Channel) *Service {
return s
}

// backoff exponentially increases the RPC poll delay up to maxDelay
func (s *Service) backoff() {
const maxDelay = 10 // rpcChannelInt.h:RPCIN_MAX_DELAY

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

// Start initializes the RPC channels and starts a goroutine to listen for incoming RPC requests
func (s *Service) Start() error {
err := s.in.Start()
Expand All @@ -71,26 +88,33 @@ func (s *Service) Start() error {
return err
}

ticker := time.NewTicker(s.Interval)

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 <-ticker.C:
_ = s.in.Send(nil) // POKE
case <-time.After(time.Millisecond * 10 * s.delay):
_ = s.in.Send(response)
response = nil

request, _ := s.in.Receive()

if len(request) > 0 {
response := s.Dispatch(request)
response = s.Dispatch(request)

_ = s.in.Send(response)
s.delay = 0
} else {
s.backoff()
}
case <-s.stop:
ticker.Stop()
return
}
}
Expand Down Expand Up @@ -176,7 +200,10 @@ func (s *Service) SetOption(args []byte) ([]byte, error) {
if val == "1" {
ip := s.PrimaryIP()
msg := fmt.Sprintf("info-set guestinfo.ip %s", ip)
return nil, s.out.Send([]byte(msg))
_, err := s.out.Request([]byte(msg))
if err != nil {
return nil, err
}
}
default:
// TODO: handle other options...
Expand All @@ -203,6 +230,23 @@ func DefaultIP() string {
}

func (s *Service) CapabilitiesRegister([]byte) ([]byte, error) {
// TODO: this is here just to make Fusion happy. ESX doesn't seem to mind if we don't support this RPC
// TOOLS_VERSION_UNMANAGED as defined in open-vm-tools/lib/include/vm_tools_version.h
const toolsVersionUnmanaged = 0x7fffffff

caps := []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",
}

for _, cap := range caps {
_, err := s.out.Request([]byte(cap))
if err != nil {
log.Printf("send '%s': %s", cap, err)
}
}

return nil, nil
}
Loading

0 comments on commit 15a1fcb

Please sign in to comment.