Skip to content

Commit

Permalink
toolbox: add http and exec round trippers
Browse files Browse the repository at this point in the history
Add guest/toolbox.Client that implements http.RoundTripper and
Run method that behaves like exec.Cmd.Run
  • Loading branch information
dougm committed Jul 4, 2017
1 parent 2eadc60 commit ba6720c
Show file tree
Hide file tree
Showing 10 changed files with 796 additions and 19 deletions.
168 changes: 168 additions & 0 deletions govc/vm/guest/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
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 guest

import (
"bytes"
"context"
"flag"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strings"

"github.com/vmware/govmomi/govc/cli"
"github.com/vmware/govmomi/guest/toolbox"
)

type run struct {
*GuestFlag

data string
verbose bool
dir string
vars env
}

func init() {
cli.Register("guest.run", &run{})
}

func (cmd *run) Register(ctx context.Context, f *flag.FlagSet) {
cmd.GuestFlag, ctx = newGuestFlag(ctx)
cmd.GuestFlag.Register(ctx, f)

f.StringVar(&cmd.data, "d", "", "Input data")
f.BoolVar(&cmd.verbose, "v", false, "Verbose")
f.StringVar(&cmd.dir, "C", "", "The absolute path of the working directory for the program to start")
f.Var(&cmd.vars, "e", "Set environment variable or HTTP header")
}

func (cmd *run) Process(ctx context.Context) error {
if err := cmd.GuestFlag.Process(ctx); err != nil {
return err
}
return nil
}

func (cmd *run) Usage() string {
return "NAME [ARG]..."
}

func (cmd *run) Description() string {
return `Run program NAME in VM and display output.
This command depends on govmomi/toolbox running in the VM guest and does not work with standard VMware tools.
If the program NAME is an HTTP verb, the toolbox's http.RoundTripper will be used as the HTTP transport.
Examples:
govc guest.run -vm $name kubectl get pods
govc guest.run -vm $name -d - kubectl create -f - <svc.json
govc guest.run -vm $name kubectl delete pod,service my-service
govc guest.run -vm $name GET http://localhost:8080/api/v1/nodes
govc guest.run -vm $name -e Content-Type:application/json -d - POST http://localhost:8080/api/v1/namespaces/default/pods <svc.json
govc guest.run -vm $name DELETE http://localhost:8080/api/v1/namespaces/default/services/my-service`
}

func (cmd *run) do(c *http.Client, req *http.Request) error {
for _, v := range cmd.vars {
h := strings.SplitN(v, ":", 2)
if len(h) != 2 {
return fmt.Errorf("invalid header: %q", v)
}

req.Header.Set(strings.TrimSpace(h[0]), strings.TrimSpace(h[1]))
}

res, err := c.Do(req)
if err != nil {
return err
}

if cmd.verbose {
return res.Write(cmd.Out)
}

_, err = io.Copy(cmd.Out, res.Body)

_ = res.Body.Close()

return err
}

func (cmd *run) Run(ctx context.Context, f *flag.FlagSet) error {
name := f.Arg(0)

pm, err := cmd.ProcessManager()
if err != nil {
return err
}

fm, err := cmd.FileManager()
if err != nil {
return err
}

tc := &toolbox.Client{
ProcessManager: pm,
FileManager: fm,
Authentication: cmd.Auth(),
}

hc := &http.Client{
Transport: tc,
}

switch name {
case "HEAD", "GET", "DELETE":
req, err := http.NewRequest(name, f.Arg(1), nil)
if err != nil {
return err
}

return cmd.do(hc, req)
case "POST":
req, err := http.NewRequest("POST", f.Arg(1), os.Stdin)
if err != nil {
return err
}

return cmd.do(hc, req)
default:
ecmd := &exec.Cmd{
Path: name,
Args: f.Args()[1:],
Env: cmd.vars,
Dir: cmd.dir,
Stdout: os.Stdout,
Stderr: os.Stderr,
}

switch cmd.data {
case "":
case "-":
ecmd.Stdin = os.Stdin
default:
ecmd.Stdin = bytes.NewBuffer([]byte(cmd.data))
}

return tc.Run(ctx, ecmd)
}
}
4 changes: 4 additions & 0 deletions guest/process_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ type ProcessManager struct {
c *vim25.Client
}

func (m ProcessManager) Client() *vim25.Client {
return m.c
}

func (m ProcessManager) Reference() types.ManagedObjectReference {
return m.ManagedObjectReference
}
Expand Down
204 changes: 204 additions & 0 deletions guest/toolbox/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
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"
"fmt"
"io"
"net/http"
"os/exec"
"strings"

"github.com/vmware/govmomi/guest"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
)

// Client attempts to expose guest.OperationsManager as idiomatic Go interfaces
type Client struct {
ProcessManager *guest.ProcessManager
FileManager *guest.FileManager
Authentication types.BaseGuestAuthentication
}

// RoundTrip implements http.RoundTripper over vmx guest RPC.
// This transport depends on govmomi/toolbox running in the VM guest and does not work with standard VMware tools.
// Using this transport makes it is possible to connect to HTTP endpoints that are bound to the VM's loopback address.
// Note that the toolbox's http.RoundTripper only supports the "http" scheme, "https" is not supported.
func (c *Client) RoundTrip(req *http.Request) (*http.Response, error) {
if req.URL.Scheme != "http" {
return nil, fmt.Errorf("%q scheme not supported", req.URL.Scheme)
}

ctx := req.Context()

req.Header.Set("Connection", "close") // we need the server to close the connection after 1 request

spec := types.GuestProgramSpec{
ProgramPath: "http.RoundTrip",
Arguments: req.URL.Host,
}

pid, err := c.ProcessManager.StartProgram(ctx, c.Authentication, &spec)
if err != nil {
return nil, err
}

dst := fmt.Sprintf("/proc/%d/stdin", pid)
src := fmt.Sprintf("/proc/%d/stdout", pid)

var buf bytes.Buffer
err = req.Write(&buf)
if err != nil {
return nil, err
}

attr := new(types.GuestPosixFileAttributes)
size := int64(buf.Len())

url, err := c.FileManager.InitiateFileTransferToGuest(ctx, c.Authentication, dst, attr, size, true)
if err != nil {
return nil, err
}

vc := c.ProcessManager.Client()

u, err := vc.ParseURL(url)
if err != nil {
return nil, err
}

p := soap.DefaultUpload
p.ContentLength = size

err = vc.Client.Upload(&buf, u, &p)
if err != nil {
return nil, err
}

info, err := c.FileManager.InitiateFileTransferFromGuest(ctx, c.Authentication, src)
if err != nil {
return nil, err
}

u, err = vc.ParseURL(info.Url)
if err != nil {
return nil, err
}

f, _, err := vc.Client.Download(u, &soap.DefaultDownload)
if err != nil {
return nil, err
}

return http.ReadResponse(bufio.NewReader(f), req)
}

// Run implements exec.Cmd.Run over vmx guest RPC.
func (c *Client) Run(ctx context.Context, cmd *exec.Cmd) error {
vc := c.ProcessManager.Client()

spec := types.GuestProgramSpec{
ProgramPath: cmd.Path,
Arguments: strings.Join(cmd.Args, " "),
EnvVariables: cmd.Env,
WorkingDirectory: cmd.Dir,
}

pid, serr := c.ProcessManager.StartProgram(ctx, c.Authentication, &spec)
if serr != nil {
return serr
}

if cmd.Stdin != nil {
dst := fmt.Sprintf("/proc/%d/stdin", pid)

var buf bytes.Buffer
size, err := io.Copy(&buf, cmd.Stdin)
if err != nil {
return err
}

attr := new(types.GuestPosixFileAttributes)

url, err := c.FileManager.InitiateFileTransferToGuest(ctx, c.Authentication, dst, attr, size, true)
if err != nil {
return err
}

u, err := vc.ParseURL(url)
if err != nil {
return err
}

p := soap.DefaultUpload
p.ContentLength = size

err = vc.Client.Upload(&buf, u, &p)
if err != nil {
return err
}
}

names := []string{"out", "err"}

for i, w := range []io.Writer{cmd.Stdout, cmd.Stderr} {
if w == nil {
continue
}

src := fmt.Sprintf("/proc/%d/std%s", pid, names[i])

info, err := c.FileManager.InitiateFileTransferFromGuest(ctx, c.Authentication, src)
if err != nil {
return err
}

u, err := vc.ParseURL(info.Url)
if err != nil {
return err
}

f, _, err := vc.Client.Download(u, &soap.DefaultDownload)
if err != nil {
return err
}

_, err = io.Copy(w, f)
_ = f.Close()
if err != nil {
return err
}
}

procs, err := c.ProcessManager.ListProcesses(ctx, c.Authentication, []int64{pid})
if err != nil {
return err
}

if len(procs) == 1 {
rc := procs[0].ExitCode
if rc != 0 {
return fmt.Errorf("%s: exit %d", cmd.Path, rc)
}
}

return nil
}
Loading

0 comments on commit ba6720c

Please sign in to comment.