Compare commits

...

25 Commits

Author SHA1 Message Date
Chris Roberts 9f1914642f When class name doesn't start with vagrant, prepend vagrant::root 2019-09-09 20:32:30 -07:00
Chris Roberts 2ad8abd57c Allow sets used within configuration to be dumped as an array 2019-09-09 20:32:08 -07:00
Chris Roberts c789fbf440 Set GRPC to use our logger 2019-09-09 20:31:26 -07:00
Chris Roberts 5c09116f53 Include parent name within config instance. Always pass all variables. 2019-09-09 20:30:55 -07:00
Chris Roberts 1651269333 Include extra error information on timeout 2019-09-09 20:30:06 -07:00
Chris Roberts 0d9ad25d69 Finish up test coverage and cleanup 2019-09-06 16:26:33 -07:00
Chris Roberts f4420f6b81 Clean tests for plugin client refactor 2019-09-06 16:26:33 -07:00
Chris Roberts fe7ac740df Remove proxy usage. Use binding for management only. 2019-09-06 16:26:33 -07:00
Chris Roberts 5605a6e843 Remove context logic from server implementations 2019-09-06 16:26:33 -07:00
Chris Roberts dcac2a167f Pass subcontext to call 2019-09-06 16:26:33 -07:00
Chris Roberts 6e9fc12b08 Remove extraneous contexts from tests 2019-09-06 16:26:33 -07:00
Chris Roberts 248f902345 Update to use errgroup for cleaner context usage 2019-09-06 16:26:33 -07:00
Chris Roberts 7b8b75593b Clean up in the plugin client and server implementations 2019-09-06 16:26:33 -07:00
Chris Roberts ca58714a03 Refactor proto usage 2019-09-06 16:26:33 -07:00
Chris Roberts a04e24378b Provide context when calling and update error handling 2019-09-06 16:26:33 -07:00
Chris Roberts bac7689b36 Clean up synced folder implementation to use updated plugin fetch 2019-09-06 16:26:33 -07:00
Chris Roberts 338cf4cc0b Ensure array type is processed when loading actions 2019-09-06 16:26:33 -07:00
Chris Roberts c441fac0db Clean up plugin lookup usage 2019-09-06 16:26:33 -07:00
Chris Roberts db249d58ac Do not re-write plugin state file
Maintain existing structure for backwards compatibility and
isolate new plugin information which can be ignored when
using previous versions.
2019-09-06 16:26:33 -07:00
Chris Roberts 8952168480 Fix up failing tests 2019-09-06 16:26:33 -07:00
Chris Roberts 893771e535 Configure travis for test coverage and configure for modules 2019-09-06 16:26:33 -07:00
Chris Roberts 9499553706 Adding test coverage 2019-09-06 16:26:33 -07:00
Chris Roberts 8aa0fd9445 Add Experimental to Util autoload entries 2019-09-06 16:26:33 -07:00
Chris Roberts 9f8d883852 Add more ignores and cleanup Vagrantfile adjustments 2019-09-06 16:26:33 -07:00
Chris Roberts b11c86528a Add basic support for go-plugin 2019-09-06 16:26:33 -07:00
96 changed files with 13794 additions and 11 deletions

5
.gitignore vendored
View File

@ -48,5 +48,10 @@ doc/
.ruby-version .ruby-version
.rvmrc .rvmrc
# Extensions
*.so
*.bundle
tmp/*
# Box storage for spec # Box storage for spec
test/vagrant-spec/boxes/*.box test/vagrant-spec/boxes/*.box

View File

@ -4,10 +4,17 @@ sudo: false
cache: bundler cache: bundler
before_install:
- which go
- sudo apt-get remove --purge golang-go
- sudo add-apt-repository ppa:gophers/archive -y
- sudo apt-get update -q
- sudo apt-get install golang-1.11-go -yq
addons: addons:
apt: apt:
packages: packages:
- bsdtar - bsdtar
rvm: rvm:
- 2.3.8 - 2.3.8
@ -22,5 +29,13 @@ branches:
env: env:
global: global:
- NOKOGIRI_USE_SYSTEM_LIBRARIES=true - NOKOGIRI_USE_SYSTEM_LIBRARIES=true
- GO111MODULE=on
- GOPATH=$HOME/go
- GOROOT=/usr/lib/go-1.11
- PATH=/usr/lib/go-1.11/bin:$PATH
script: bundle exec rake test:unit script:
- go version
- go test ./...
- bundle exec rake compile
- bundle exec rake test:unit

View File

@ -1,5 +1,10 @@
require 'rubygems' require 'rubygems'
require 'bundler/setup' require 'bundler/setup'
require 'rake/extensiontask'
Rake::ExtensionTask.new "go-plugin" do |ext|
ext.lib_dir = "lib/vagrant/go_plugin"
end
# Immediately sync all stdout so that tools like buildbot can # Immediately sync all stdout so that tools like buildbot can
# immediately load in the output. # immediately load in the output.

68
ext/go-plugin/extconf.rb Normal file
View File

@ -0,0 +1,68 @@
lib = File.expand_path('./../../../lib', File.expand_path(__FILE__))
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'mkmf'
require 'time'
find_executable('go')
go_version = /go version go(\d+\.\d+)/.match(`go version`).captures.first
raise "'go' version >=1.5.0 is required, found go #{go_version}" unless Gem::Dependency.new('', '>=1.5.0').match?('', go_version)
makefile = "Makefile"
makefile_content = <<MFEND
NAME := #{File.basename(File.dirname(File.expand_path(__FILE__)))}
BINARY := ${NAME}.so
V = 0
Q1 = $(V:1=)
Q = $(Q1:0=@)
ECHO1 = $(V:1=@:)
ECHO = $(ECHO1:0=@echo)
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -maxdepth 0 -name '*.go')
VERSION=1.0
BUILD_DATE=#{Time.now.iso8601}
cflags= $(optflags) $(warnflags)
optflags= -O3 -fno-fast-math
warnflags= -Wall -Wextra -Wno-unused-parameter -Wno-parentheses -Wno-long-long -Wno-missing-field-initializers -Wunused-variable -Wpointer-arith -Wwrite-strings -Wimplicit-function-declaration -Wdiv-by-zero -Wdeprecated-declarations
CCDLFLAGS= -fno-common
INCFLAGS= -I#{RbConfig::CONFIG['rubyhdrdir']}/ -I#{RbConfig::CONFIG['rubyarchhdrdir']}/ -I$(SOURCEDIR)
CFLAGS= $(CCDLFLAGS) $(cflags) -fno-common -pipe $(INCFLAGS)
LDFLAGS=-L#{RbConfig::CONFIG['libdir']} #{RbConfig::CONFIG['LIBRUBYARG']}
.DEFAULT_GOAL := $(BINARY)
.PHONY: help
help:
${ECHO} ${VERSION}
${ECHO} ${BUILD_DATE}
all:
make clean
$(BINARY)
$(BINARY): $(SOURCES)
CGO_CFLAGS="${CFLAGS}" CGO_LDFLAGS="${LDFLAGS}" go build -buildmode=c-shared -o ${BINARY} #{File.dirname(__FILE__)}/../../ext/${NAME}/
.PHONY: install
install:
# go install ${LDFLAGS} ./...
.PHONY: clean
clean:
if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi
if [ -f lib/${BINARY} ] ; then rm lib/${BINARY} ; fi
MFEND
puts "creating Makefile"
File.open(makefile, 'w') do |f|
f.write(makefile_content.gsub!(/(?:^|\G) {2}/m,"\t"))
end
$makefile_created = true

139
ext/go-plugin/go-plugin.go Normal file
View File

@ -0,0 +1,139 @@
package main
import (
"C"
"io/ioutil"
"os"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin"
)
var Plugins *plugin.VagrantPlugin
//export Setup
func Setup(enableLogger, timestamps bool, logLevel *C.char) bool {
lvl := to_gs(logLevel)
lopts := &hclog.LoggerOptions{Name: "vagrant"}
if enableLogger {
lopts.Output = os.Stderr
} else {
lopts.Output = ioutil.Discard
}
if !timestamps {
lopts.TimeFormat = " "
}
lopts.Level = hclog.LevelFromString(lvl)
vagrant.SetDefaultLogger(hclog.New(lopts))
if Plugins != nil {
Plugins.Logger.Error("plugins setup failure", "error", "already setup")
return false
}
Plugins = plugin.VagrantPluginInit()
return true
}
//export LoadPlugins
func LoadPlugins(plgpath *C.char) bool {
if Plugins == nil {
vagrant.DefaultLogger().Error("cannot load plugins", "error", "not setup")
return false
}
p := to_gs(plgpath)
err := Plugins.LoadPlugins(p)
if err != nil {
Plugins.Logger.Error("failed loading plugins",
"path", p, "error", err)
return false
}
Plugins.Logger.Info("plugins successfully loaded", "path", p)
return true
}
//export Reset
func Reset() {
if Plugins != nil {
Plugins.Logger.Info("resetting loaded plugins")
Teardown()
dirs := Plugins.PluginDirectories
Plugins.PluginDirectories = []string{}
for _, p := range dirs {
Plugins.LoadPlugins(p)
}
} else {
Plugins.Logger.Warn("plugin reset failure", "error", "not setup")
}
}
//export Teardown
func Teardown() {
// only teardown if setup
if Plugins == nil {
vagrant.DefaultLogger().Error("cannot teardown plugins", "error", "not setup")
return
}
Plugins.Logger.Debug("tearing down any active plugins")
Plugins.Kill()
Plugins.Logger.Info("plugins have been halted")
}
//export ListProviders
func ListProviders() *C.char {
list := map[string]interface{}{}
r := &Response{Result: list}
if Plugins == nil {
return r.Dump()
}
for n, p := range Plugins.Providers {
info := p.Provider.Info()
c := p.Client.ReattachConfig()
data := map[string]interface{}{
"network": c.Addr.Network(),
"address": c.Addr.String(),
"description": info.Description,
"priority": info.Priority,
}
list[n] = data
}
r.Result = list
return r.Dump()
}
//export ListSyncedFolders
func ListSyncedFolders() *C.char {
list := map[string]interface{}{}
r := &Response{Result: list}
if Plugins == nil {
return r.Dump()
}
for n, p := range Plugins.SyncedFolders {
info := p.SyncedFolder.Info()
c := p.Client.ReattachConfig()
data := map[string]interface{}{
"network": c.Addr.Network(),
"address": c.Addr.String(),
"description": info.Description,
"priority": info.Priority,
}
list[n] = data
}
r.Result = list
return r.Dump()
}
// stub required for build
func main() {}
// helper to convert c string to go string
func to_gs(s *C.char) string {
return C.GoString(s)
}
// helper to convert go string to c string
func to_cs(s string) *C.char {
return C.CString(s)
}

48
ext/go-plugin/response.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"C"
"encoding/json"
"errors"
"fmt"
)
type Response struct {
Error error `json:"error"`
Result interface{} `json:"result"`
}
// Serialize the response into a JSON C string
func (r *Response) Dump() *C.char {
tmp := map[string]interface{}{}
if r.Error != nil {
tmp["error"] = r.Error.Error()
} else {
tmp["error"] = nil
}
tmp["result"] = r.Result
result, err := json.Marshal(tmp)
if err != nil {
return to_cs(fmt.Sprintf(`{"error": "failed to encode response - %s"}`, err))
}
return to_cs(string(result[:]))
}
// Load a new response from a JSON C string
func LoadResponse(s *C.char) (r *Response, err error) {
tmp := map[string]interface{}{}
st := []byte(to_gs(s))
r = &Response{}
err = json.Unmarshal(st, &tmp)
if tmp["error"] != nil {
e, ok := tmp["error"].(string)
if !ok {
err = errors.New(
fmt.Sprintf("cannot load error content - %s", tmp["error"]))
return
}
r.Error = errors.New(e)
}
r.Result = tmp["result"]
return
}

View File

@ -0,0 +1,10 @@
package vagrant
type Box struct {
Name string `json:"name"`
Provider string `json:"provider"`
Version string `json:"version"`
Directory string `json:"directory"`
Metadata map[string]string `json:"metadata"`
MetadataURL string `json:"metadata_url"`
}

View File

@ -0,0 +1,61 @@
package vagrant
import (
"context"
)
type SystemCapability struct {
Name string `json:"name"`
Platform string `json:"platform"`
}
type ProviderCapability struct {
Name string `json:"name"`
Provider string `json:"provider"`
}
type GuestCapabilities interface {
GuestCapabilities() (caps []SystemCapability, err error)
GuestCapability(ctx context.Context, cap *SystemCapability, args interface{}, machine *Machine) (result interface{}, err error)
}
type HostCapabilities interface {
HostCapabilities() (caps []SystemCapability, err error)
HostCapability(ctx context.Context, cap *SystemCapability, args interface{}, env *Environment) (result interface{}, err error)
}
type ProviderCapabilities interface {
ProviderCapabilities() (caps []ProviderCapability, err error)
ProviderCapability(ctx context.Context, cap *ProviderCapability, args interface{}, machine *Machine) (result interface{}, err error)
}
type NoGuestCapabilities struct{}
type NoHostCapabilities struct{}
type NoProviderCapabilities struct{}
func (g *NoGuestCapabilities) GuestCapabilities() (caps []SystemCapability, err error) {
caps = make([]SystemCapability, 0)
return
}
func (g *NoGuestCapabilities) GuestCapability(x context.Context, c *SystemCapability, a interface{}, m *Machine) (r interface{}, err error) {
return
}
func (h *NoHostCapabilities) HostCapabilities() (caps []SystemCapability, err error) {
caps = make([]SystemCapability, 0)
return
}
func (h *NoHostCapabilities) HostCapability(x context.Context, c *SystemCapability, a interface{}, e *Environment) (r interface{}, err error) {
return
}
func (p *NoProviderCapabilities) ProviderCapabilities() (caps []ProviderCapability, err error) {
caps = make([]ProviderCapability, 0)
return
}
func (p *NoProviderCapabilities) ProviderCapability(x context.Context, cap *ProviderCapability, args interface{}, machine *Machine) (result interface{}, err error) {
return
}

View File

@ -0,0 +1,78 @@
package vagrant
import (
"context"
"testing"
)
func TestNoGuestCapabilities(t *testing.T) {
g := NoGuestCapabilities{}
caps, err := g.GuestCapabilities()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if len(caps) != 0 {
t.Fatalf("guest capabilities should be empty")
}
}
func TestNoGuestCapability(t *testing.T) {
g := NoGuestCapabilities{}
m := &Machine{}
cap := &SystemCapability{"Test", "Test"}
r, err := g.GuestCapability(context.Background(), cap, "args", m)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if r != nil {
t.Fatalf("capability returned unexpected result")
}
}
func TestNoHostCapabilities(t *testing.T) {
h := NoHostCapabilities{}
caps, err := h.HostCapabilities()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if len(caps) != 0 {
t.Fatalf("host capabilities should be empty")
}
}
func TestNoHostCapability(t *testing.T) {
h := NoHostCapabilities{}
e := &Environment{}
cap := &SystemCapability{"Test", "Test"}
r, err := h.HostCapability(context.Background(), cap, "args", e)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if r != nil {
t.Fatalf("capability returned unexpected result")
}
}
func TestNoProviderCapabilities(t *testing.T) {
p := NoProviderCapabilities{}
caps, err := p.ProviderCapabilities()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if len(caps) != 0 {
t.Fatalf("provider capabilities should be empty")
}
}
func TestNoProviderCapability(t *testing.T) {
p := NoProviderCapabilities{}
m := &Machine{}
cap := &ProviderCapability{"Test", "Test"}
r, err := p.ProviderCapability(context.Background(), cap, "args", m)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if r != nil {
t.Fatalf("capability returned unexpected result")
}
}

View File

@ -0,0 +1,202 @@
package communicator
import (
"fmt"
"io"
"strings"
"sync"
"unicode"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/mitchellh/iochan"
)
// CmdDisconnect is a sentinel value to indicate a RemoteCmd
// exited because the remote side disconnected us.
const CmdDisconnect int = 2300218
// Cmd represents a remote command being prepared or run.
type Cmd struct {
// Command is the command to run remotely. This is executed as if
// it were a shell command, so you are expected to do any shell escaping
// necessary.
Command string
// Stdin specifies the process's standard input. If Stdin is
// nil, the process reads from an empty bytes.Buffer.
Stdin io.Reader
// Stdout and Stderr represent the process's standard output and
// error.
//
// If either is nil, it will be set to ioutil.Discard.
Stdout io.Writer
Stderr io.Writer
// Once Wait returns, his will contain the exit code of the process.
exitStatus int
// Internal fields
exitCh chan struct{}
// err is used to store any error reported by the Communicator during
// execution.
err error
// This thing is a mutex, lock when making modifications concurrently
sync.Mutex
}
// Init must be called by the Communicator before executing the command.
func (c *Cmd) Init() {
c.Lock()
defer c.Unlock()
c.exitCh = make(chan struct{})
}
// SetExitStatus stores the exit status of the remote command as well as any
// communicator related error. SetExitStatus then unblocks any pending calls
// to Wait.
// This should only be called by communicators executing the remote.Cmd.
func (c *Cmd) SetExitStatus(status int, err error) {
c.Lock()
defer c.Unlock()
c.exitStatus = status
c.err = err
close(c.exitCh)
}
// StartWithUi runs the remote command and streams the output to any
// configured Writers for stdout/stderr, while also writing each line
// as it comes to a Ui.
func (r *Cmd) StartWithUi(c Communicator, ui vagrant.Ui) error {
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
defer stdout_w.Close()
defer stderr_w.Close()
// Retain the original stdout/stderr that we can replace back in.
originalStdout := r.Stdout
originalStderr := r.Stderr
defer func() {
r.Lock()
defer r.Unlock()
r.Stdout = originalStdout
r.Stderr = originalStderr
}()
// Set the writers for the output so that we get it streamed to us
if r.Stdout == nil {
r.Stdout = stdout_w
} else {
r.Stdout = io.MultiWriter(r.Stdout, stdout_w)
}
if r.Stderr == nil {
r.Stderr = stderr_w
} else {
r.Stderr = io.MultiWriter(r.Stderr, stderr_w)
}
// Start the command
if err := c.Start(r); err != nil {
return err
}
// Create the channels we'll use for data
exitCh := make(chan struct{})
stdoutCh := iochan.DelimReader(stdout_r, '\n')
stderrCh := iochan.DelimReader(stderr_r, '\n')
// Start the goroutine to watch for the exit
go func() {
defer close(exitCh)
defer stdout_w.Close()
defer stderr_w.Close()
r.Wait()
}()
// Loop and get all our output
OutputLoop:
for {
select {
case output := <-stderrCh:
if output != "" {
ui.Say(r.cleanOutputLine(output))
}
case output := <-stdoutCh:
if output != "" {
ui.Say(r.cleanOutputLine(output))
}
case <-exitCh:
break OutputLoop
}
}
// Make sure we finish off stdout/stderr because we may have gotten
// a message from the exit channel before finishing these first.
for output := range stdoutCh {
ui.Say(r.cleanOutputLine(output))
}
for output := range stderrCh {
ui.Say(r.cleanOutputLine(output))
}
return nil
}
// Wait waits for the remote command to complete.
// Wait may return an error from the communicator, or an ExitError if the
// process exits with a non-zero exit status.
func (c *Cmd) Wait() error {
<-c.exitCh
c.Lock()
defer c.Unlock()
if c.err != nil || c.exitStatus != 0 {
return &ExitError{
Command: c.Command,
ExitStatus: c.exitStatus,
Err: c.err,
}
}
return nil
}
// cleanOutputLine cleans up a line so that '\r' don't muck up the
// UI output when we're reading from a remote command.
func (r *Cmd) cleanOutputLine(line string) string {
// Trim surrounding whitespace
line = strings.TrimRightFunc(line, unicode.IsSpace)
// Trim up to the first carriage return, since that text would be
// lost anyways.
idx := strings.LastIndex(line, "\r")
if idx > -1 {
line = line[idx+1:]
}
return line
}
// ExitError is returned by Wait to indicate and error executing the remote
// command, or a non-zero exit status.
type ExitError struct {
Command string
ExitStatus int
Err error
}
func (e *ExitError) Error() string {
if e.Err != nil {
return fmt.Sprintf("error executing %q: %v", e.Command, e.Err)
}
return fmt.Sprintf("%q exit status: %d", e.Command, e.ExitStatus)
}

View File

@ -0,0 +1 @@
package communicator

View File

@ -0,0 +1,117 @@
package communicator
import (
"context"
"fmt"
"io"
"os"
"sync/atomic"
"time"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
type Communicator interface {
Connect() error
Disconnect() error
Timeout() time.Duration
Start(*Cmd) error
Download(path string, output io.Writer) error
DownloadDir(dst, src string, excludes []string) error
Upload(dst string, src io.Reader, srcinfo *os.FileInfo) error
UploadDir(dst, src string, excludes []string) error
}
// maxBackoffDelay is the maximum delay between retry attempts
var maxBackoffDelay = 20 * time.Second
var initialBackoffDelay = time.Second
var logger = vagrant.DefaultLogger().Named("communicator")
// Fatal is an interface that error values can return to halt Retry
type Fatal interface {
FatalError() error
}
// Retry retries the function f until it returns a nil error, a Fatal error, or
// the context expires.
func Retry(ctx context.Context, f func() error) error {
// container for atomic error value
type errWrap struct {
E error
}
// Try the function in a goroutine
var errVal atomic.Value
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
delay := time.Duration(0)
for {
// If our context ended, we want to exit right away.
select {
case <-ctx.Done():
return
case <-time.After(delay):
}
// Try the function call
err := f()
// return if we have no error, or a FatalError
done := false
switch e := err.(type) {
case nil:
done = true
case Fatal:
err = e.FatalError()
done = true
}
errVal.Store(errWrap{err})
if done {
return
}
logger.Warn("retryable error", "error", err)
delay *= 2
if delay == 0 {
delay = initialBackoffDelay
}
if delay > maxBackoffDelay {
delay = maxBackoffDelay
}
logger.Info("sleeping for retry", "duration", delay)
}
}()
// Wait for completion
select {
case <-ctx.Done():
case <-doneCh:
}
var lastErr error
// Check if we got an error executing
if ev, ok := errVal.Load().(errWrap); ok {
lastErr = ev.E
}
// Check if we have a context error to check if we're interrupted or timeout
switch ctx.Err() {
case context.Canceled:
return fmt.Errorf("interrupted - last error: %v", lastErr)
case context.DeadlineExceeded:
return fmt.Errorf("timeout - last error: %v", lastErr)
}
if lastErr != nil {
return lastErr
}
return nil
}

View File

@ -0,0 +1,59 @@
package none
import (
"errors"
"io"
"os"
"time"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
)
type Communicator struct {
config string
}
// Creates a null vagrant.Communicator implementation. This takes
// an already existing configuration.
func New(config string) (result *Communicator, err error) {
// Establish an initial connection and connect
result = &Communicator{
config: config,
}
return
}
func (c *Communicator) Connect() (err error) {
return
}
func (c *Communicator) Disconnect() (err error) {
return
}
func (c *Communicator) Start(cmd *communicator.Cmd) (err error) {
cmd.Init()
cmd.SetExitStatus(0, nil)
return
}
func (c *Communicator) Upload(path string, input io.Reader, fi *os.FileInfo) error {
return errors.New("Upload is not implemented when communicator = 'none'")
}
func (c *Communicator) UploadDir(dst string, src string, excl []string) error {
return errors.New("UploadDir is not implemented when communicator = 'none'")
}
func (c *Communicator) Download(path string, output io.Writer) error {
return errors.New("Download is not implemented when communicator = 'none'")
}
func (c *Communicator) DownloadDir(dst string, src string, excl []string) error {
return errors.New("DownloadDir is not implemented when communicator = 'none'")
}
func (c *Communicator) Timeout() time.Duration {
return 0
}

View File

@ -0,0 +1,12 @@
package none
import (
"testing"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
)
func TestCommIsCommunicator(t *testing.T) {
// Force failure with explanation of why it's not valid
var _ communicator.Communicator = new(Communicator)
}

View File

@ -0,0 +1,949 @@
package ssh
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
// ErrHandshakeTimeout is returned from New() whenever we're unable to establish
// an ssh connection within a certain timeframe. By default the handshake time-
// out period is 1 minute. You can change it with Config.HandshakeTimeout.
var ErrHandshakeTimeout = fmt.Errorf("Timeout during SSH handshake")
var logger = vagrant.DefaultLogger().Named("communicator.ssh")
type Communicator struct {
client *ssh.Client
config *Config
conn net.Conn
address string
}
// Config is the structure used to configure the SSH communicator.
type Config struct {
// The configuration of the Go SSH connection
SSHConfig *ssh.ClientConfig
// Connection returns a new connection. The current connection
// in use will be closed as part of the Close method, or in the
// case an error occurs.
Connection func() (net.Conn, error)
// Pty, if true, will request a pty from the remote end.
Pty bool
// DisableAgentForwarding, if true, will not forward the SSH agent.
DisableAgentForwarding bool
// HandshakeTimeout limits the amount of time we'll wait to handshake before
// saying the connection failed.
HandshakeTimeout time.Duration
// UseSftp, if true, sftp will be used instead of scp for file transfers
UseSftp bool
// KeepAliveInterval sets how often we send a channel request to the
// server. A value < 0 disables.
KeepAliveInterval time.Duration
// Timeout is how long to wait for a read or write to succeed.
Timeout time.Duration
}
// Creates a new vagrant.Communicator implementation over SSH. This takes
// an already existing TCP connection and SSH configuration.
func New(address string, config *Config) (result *Communicator, err error) {
// Establish an initial connection and connect
result = &Communicator{
config: config,
address: address,
}
// reset the logger in case custom default has been set
logger = vagrant.DefaultLogger().Named("communicator.ssh")
return
}
func (c *Communicator) Connect() error {
return c.reconnect()
}
func (c *Communicator) Disconnect() (err error) {
if c.conn != nil {
logger.Info("closing connection")
err = c.conn.Close()
} else {
err = errors.New("No connection currently established to close")
}
return
}
func (c *Communicator) Timeout() time.Duration {
return c.config.Timeout
}
func (c *Communicator) Start(cmd *communicator.Cmd) (err error) {
session, err := c.newSession()
if err != nil {
return
}
// Setup our session
session.Stdin = cmd.Stdin
session.Stdout = cmd.Stdout
session.Stderr = cmd.Stderr
if c.config.Pty {
// Request a PTY
termModes := ssh.TerminalModes{
ssh.ECHO: 0, // do not echo
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err = session.RequestPty("xterm", 40, 80, termModes); err != nil {
return
}
}
logger.Debug("starting remote command", "command", cmd.Command)
err = session.Start(cmd.Command + "\n")
if err != nil {
return
}
go func() {
if c.config.KeepAliveInterval <= 0 {
return
}
c := time.NewTicker(c.config.KeepAliveInterval)
defer c.Stop()
for range c.C {
_, err := session.SendRequest("keepalive@vagrantup.com", true, nil)
if err != nil {
return
}
}
}()
// Start a goroutine to wait for the session to end and set the
// exit boolean and status.
go func() {
defer session.Close()
err := session.Wait()
exitStatus := 0
if err != nil {
switch err.(type) {
case *ssh.ExitError:
exitStatus = err.(*ssh.ExitError).ExitStatus()
logger.Error("remote command exited non-zero",
"exitcode", exitStatus, "command", cmd.Command)
case *ssh.ExitMissingError:
logger.Error("remote command exited without exit status or exit signal",
"command", cmd.Command)
exitStatus = communicator.CmdDisconnect
default:
logger.Error("error waiting for ssh session", "error", err)
}
}
cmd.SetExitStatus(exitStatus, err)
}()
return
}
func (c *Communicator) Upload(path string, input io.Reader, fi *os.FileInfo) error {
if c.config.UseSftp {
return c.sftpUploadSession(path, input, fi)
} else {
return c.scpUploadSession(path, input, fi)
}
}
func (c *Communicator) UploadDir(dst string, src string, excl []string) error {
logger.Debug("uploading directory", "source", src, "destination", dst)
if c.config.UseSftp {
return c.sftpUploadDirSession(dst, src, excl)
} else {
return c.scpUploadDirSession(dst, src, excl)
}
}
func (c *Communicator) DownloadDir(src string, dst string, excl []string) error {
logger.Debug("downloading directory", "source", src, "destination", dst)
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
dirStack := []string{dst}
for {
fmt.Fprint(w, "\x00")
// read file info
fi, err := stdoutR.ReadString('\n')
if err != nil {
return err
}
if len(fi) < 0 {
return fmt.Errorf("empty response from server")
}
switch fi[0] {
case '\x01', '\x02':
return fmt.Errorf("%s", fi[1:])
case 'C', 'D':
break
case 'E':
dirStack = dirStack[:len(dirStack)-1]
if len(dirStack) == 0 {
fmt.Fprint(w, "\x00")
return nil
}
continue
default:
return fmt.Errorf("unexpected server response (%x)", fi[0])
}
var mode int64
var size int64
var name string
logger.Debug("download directory", "str", fi)
n, err := fmt.Sscanf(fi[1:], "%o %d %s", &mode, &size, &name)
if err != nil || n != 3 {
return fmt.Errorf("can't parse server response (%s)", fi)
}
if size < 0 {
return fmt.Errorf("negative file size")
}
logger.Debug("download directory", "mode", mode, "size", size, "name", name)
dst = filepath.Join(dirStack...)
switch fi[0] {
case 'D':
err = os.MkdirAll(filepath.Join(dst, name), os.FileMode(mode))
if err != nil {
return err
}
dirStack = append(dirStack, name)
continue
case 'C':
fmt.Fprint(w, "\x00")
err = scpDownloadFile(filepath.Join(dst, name), stdoutR, size, os.FileMode(mode))
if err != nil {
return err
}
}
if err := checkSCPStatus(stdoutR); err != nil {
return err
}
}
}
return c.scpSession("scp -vrf "+src, scpFunc)
}
func (c *Communicator) Download(path string, output io.Writer) error {
if c.config.UseSftp {
return c.sftpDownloadSession(path, output)
}
return c.scpDownloadSession(path, output)
}
func (c *Communicator) newSession() (session *ssh.Session, err error) {
logger.Debug("opening new ssh session")
if c.client == nil {
err = errors.New("client not available")
} else {
session, err = c.client.NewSession()
}
if err != nil {
logger.Error("ssh session open failure", "error", err)
if err := c.reconnect(); err != nil {
return nil, err
}
if c.client == nil {
return nil, errors.New("client not available")
} else {
return c.client.NewSession()
}
}
return session, nil
}
func (c *Communicator) reconnect() (err error) {
// Ignore errors here because we don't care if it fails
c.Disconnect()
// Set the conn and client to nil since we'll recreate it
c.conn = nil
c.client = nil
logger.Debug("reconnection to tcp connection for ssh")
c.conn, err = c.config.Connection()
if err != nil {
// Explicitly set this to the REAL nil. Connection() can return
// a nil implementation of net.Conn which will make the
// "if c.conn == nil" check fail above. Read here for more information
// on this psychotic language feature:
//
// http://golang.org/doc/faq#nil_error
c.conn = nil
logger.Error("reconnection failure", "error", err)
return
}
if c.config.Timeout > 0 {
c.conn = &timeoutConn{c.conn, c.config.Timeout, c.config.Timeout}
}
logger.Debug("handshaking with ssh")
// Default timeout to 1 minute if it wasn't specified (zero value). For
// when you need to handshake from low orbit.
var duration time.Duration
if c.config.HandshakeTimeout == 0 {
duration = 1 * time.Minute
} else {
duration = c.config.HandshakeTimeout
}
connectionEstablished := make(chan struct{}, 1)
var sshConn ssh.Conn
var sshChan <-chan ssh.NewChannel
var req <-chan *ssh.Request
go func() {
sshConn, sshChan, req, err = ssh.NewClientConn(c.conn, c.address, c.config.SSHConfig)
close(connectionEstablished)
}()
select {
case <-connectionEstablished:
// We don't need to do anything here. We just want select to block until
// we connect or timeout.
case <-time.After(duration):
if c.conn != nil {
c.conn.Close()
}
if sshConn != nil {
sshConn.Close()
}
return ErrHandshakeTimeout
}
if err != nil {
return
}
logger.Debug("handshake complete")
if sshConn != nil {
c.client = ssh.NewClient(sshConn, sshChan, req)
}
c.connectToAgent()
return
}
func (c *Communicator) connectToAgent() {
if c.client == nil {
return
}
if c.config.DisableAgentForwarding {
logger.Info("SSH agent forwarding is disabled")
return
}
// open connection to the local agent
socketLocation := os.Getenv("SSH_AUTH_SOCK")
if socketLocation == "" {
logger.Info("no local agent socket, will not connect agent")
return
}
agentConn, err := net.Dial("unix", socketLocation)
if err != nil {
logger.Error("could not connect to local agent socket", "path", socketLocation)
return
}
// create agent and add in auth
forwardingAgent := agent.NewClient(agentConn)
if forwardingAgent == nil {
logger.Error("could not create agent client")
agentConn.Close()
return
}
// add callback for forwarding agent to SSH config
// XXX - might want to handle reconnects appending multiple callbacks
auth := ssh.PublicKeysCallback(forwardingAgent.Signers)
c.config.SSHConfig.Auth = append(c.config.SSHConfig.Auth, auth)
agent.ForwardToAgent(c.client, forwardingAgent)
// Setup a session to request agent forwarding
session, err := c.newSession()
if err != nil {
return
}
defer session.Close()
err = agent.RequestAgentForwarding(session)
if err != nil {
logger.Error("request agent forwarding failed", "error", err)
return
}
logger.Info("agent forwarding enabled")
return
}
func (c *Communicator) sftpUploadSession(path string, input io.Reader, fi *os.FileInfo) error {
sftpFunc := func(client *sftp.Client) error {
return c.sftpUploadFile(path, input, client, fi)
}
return c.sftpSession(sftpFunc)
}
func (c *Communicator) sftpUploadFile(path string, input io.Reader, client *sftp.Client, fi *os.FileInfo) error {
logger.Debug("sftp uploading", "path", path)
f, err := client.Create(path)
if err != nil {
return err
}
defer f.Close()
if _, err = io.Copy(f, input); err != nil {
return err
}
if fi != nil && (*fi).Mode().IsRegular() {
mode := (*fi).Mode().Perm()
err = client.Chmod(path, mode)
if err != nil {
return err
}
}
return nil
}
func (c *Communicator) sftpUploadDirSession(dst string, src string, excl []string) error {
sftpFunc := func(client *sftp.Client) error {
rootDst := dst
if src[len(src)-1] != '/' {
logger.Debug("no trailing slash, creating the source directory name")
rootDst = filepath.Join(dst, filepath.Base(src))
}
walkFunc := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Calculate the final destination using the
// base source and root destination
relSrc, err := filepath.Rel(src, path)
if err != nil {
return err
}
finalDst := filepath.Join(rootDst, relSrc)
// In Windows, Join uses backslashes which we don't want to get
// to the sftp server
finalDst = filepath.ToSlash(finalDst)
// Skip the creation of the target destination directory since
// it should exist and we might not even own it
if finalDst == dst {
return nil
}
return c.sftpVisitFile(finalDst, path, info, client)
}
return filepath.Walk(src, walkFunc)
}
return c.sftpSession(sftpFunc)
}
func (c *Communicator) sftpMkdir(path string, client *sftp.Client, fi os.FileInfo) error {
logger.Debug("sftp create directory", "path", path)
if err := client.Mkdir(path); err != nil {
// Do not consider it an error if the directory existed
remoteFi, fiErr := client.Lstat(path)
if fiErr != nil || !remoteFi.IsDir() {
return err
}
}
mode := fi.Mode().Perm()
if err := client.Chmod(path, mode); err != nil {
return err
}
return nil
}
func (c *Communicator) sftpVisitFile(dst string, src string, fi os.FileInfo, client *sftp.Client) error {
if !fi.IsDir() {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
return c.sftpUploadFile(dst, f, client, &fi)
} else {
err := c.sftpMkdir(dst, client, fi)
return err
}
}
func (c *Communicator) sftpDownloadSession(path string, output io.Writer) error {
sftpFunc := func(client *sftp.Client) error {
f, err := client.Open(path)
if err != nil {
return err
}
defer f.Close()
if _, err = io.Copy(output, f); err != nil {
return err
}
return nil
}
return c.sftpSession(sftpFunc)
}
func (c *Communicator) sftpSession(f func(*sftp.Client) error) error {
client, err := c.newSftpClient()
if err != nil {
return fmt.Errorf("sftpSession error: %s", err.Error())
}
defer client.Close()
return f(client)
}
func (c *Communicator) newSftpClient() (*sftp.Client, error) {
session, err := c.newSession()
if err != nil {
return nil, err
}
if err := session.RequestSubsystem("sftp"); err != nil {
return nil, err
}
pw, err := session.StdinPipe()
if err != nil {
return nil, err
}
pr, err := session.StdoutPipe()
if err != nil {
return nil, err
}
// Capture stdout so we can return errors to the user
var stdout bytes.Buffer
tee := io.TeeReader(pr, &stdout)
client, err := sftp.NewClientPipe(tee, pw)
if err != nil && stdout.Len() > 0 {
logger.Error("upload failed", "error", stdout.Bytes())
}
return client, err
}
func (c *Communicator) scpUploadSession(path string, input io.Reader, fi *os.FileInfo) error {
// The target directory and file for talking the SCP protocol
target_dir := filepath.Dir(path)
target_file := filepath.Base(path)
// On windows, filepath.Dir uses backslash separators (ie. "\tmp").
// This does not work when the target host is unix. Switch to forward slash
// which works for unix and windows
target_dir = filepath.ToSlash(target_dir)
// Escape spaces in remote directory
target_dir = strings.Replace(target_dir, " ", "\\ ", -1)
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
return scpUploadFile(target_file, input, w, stdoutR, fi)
}
return c.scpSession("scp -vt "+target_dir, scpFunc)
}
func (c *Communicator) scpUploadDirSession(dst string, src string, excl []string) error {
scpFunc := func(w io.Writer, r *bufio.Reader) error {
uploadEntries := func() error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
entries, err := f.Readdir(-1)
if err != nil {
return err
}
return scpUploadDir(src, entries, w, r)
}
if src[len(src)-1] != '/' {
logger.Debug("no trailing slash, creating the source directory name")
fi, err := os.Stat(src)
if err != nil {
return err
}
return scpUploadDirProtocol(filepath.Base(src), w, r, uploadEntries, fi)
} else {
// Trailing slash, so only upload the contents
return uploadEntries()
}
}
return c.scpSession("scp -rvt "+dst, scpFunc)
}
func (c *Communicator) scpDownloadSession(path string, output io.Writer) error {
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
fmt.Fprint(w, "\x00")
// read file info
fi, err := stdoutR.ReadString('\n')
if err != nil {
return err
}
if len(fi) < 0 {
return fmt.Errorf("empty response from server")
}
switch fi[0] {
case '\x01', '\x02':
return fmt.Errorf("%s", fi[1:])
case 'C':
case 'D':
return fmt.Errorf("remote file is directory")
default:
return fmt.Errorf("unexpected server response (%x)", fi[0])
}
var mode string
var size int64
n, err := fmt.Sscanf(fi, "%6s %d ", &mode, &size)
if err != nil || n != 2 {
return fmt.Errorf("can't parse server response (%s)", fi)
}
if size < 0 {
return fmt.Errorf("negative file size")
}
fmt.Fprint(w, "\x00")
if _, err := io.CopyN(output, stdoutR, size); err != nil {
return err
}
fmt.Fprint(w, "\x00")
return checkSCPStatus(stdoutR)
}
if !strings.Contains(path, " ") {
return c.scpSession("scp -vf "+path, scpFunc)
}
return c.scpSession("scp -vf "+strconv.Quote(path), scpFunc)
}
func (c *Communicator) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error {
session, err := c.newSession()
if err != nil {
return err
}
defer session.Close()
// Get a pipe to stdin so that we can send data down
stdinW, err := session.StdinPipe()
if err != nil {
return err
}
// We only want to close once, so we nil w after we close it,
// and only close in the defer if it hasn't been closed already.
defer func() {
if stdinW != nil {
stdinW.Close()
}
}()
// Get a pipe to stdout so that we can get responses back
stdoutPipe, err := session.StdoutPipe()
if err != nil {
return err
}
stdoutR := bufio.NewReader(stdoutPipe)
// Set stderr to a bytes buffer
stderr := new(bytes.Buffer)
session.Stderr = stderr
// Start the sink mode on the other side
// TODO(mitchellh): There are probably issues with shell escaping the path
logger.Debug("starting remote scp process", "command", scpCommand)
if err := session.Start(scpCommand); err != nil {
return err
}
// Call our callback that executes in the context of SCP. We ignore
// EOF errors if they occur because it usually means that SCP prematurely
// ended on the other side.
logger.Debug("started scp session, beginning transfers")
if err := f(stdinW, stdoutR); err != nil && err != io.EOF {
return err
}
// Close the stdin, which sends an EOF, and then set w to nil so that
// our defer func doesn't close it again since that is unsafe with
// the Go SSH package.
logger.Debug("scp sessiono complete, closing stdin pipe")
stdinW.Close()
stdinW = nil
// Wait for the SCP connection to close, meaning it has consumed all
// our data and has completed. Or has errored.
logger.Debug("waiting for ssh session to complete")
err = session.Wait()
if err != nil {
if exitErr, ok := err.(*ssh.ExitError); ok {
// Otherwise, we have an ExitError, meaning we can just read
// the exit status
logger.Debug("non-zero exit status", "exitcode", exitErr.ExitStatus())
stdoutB, err := ioutil.ReadAll(stdoutR)
if err != nil {
return err
}
logger.Debug("scp output", "output", stdoutB)
// If we exited with status 127, it means SCP isn't available.
// Return a more descriptive error for that.
if exitErr.ExitStatus() == 127 {
return errors.New(
"SCP failed to start. This usually means that SCP is not\n" +
"properly installed on the remote system.")
}
}
return err
}
logger.Debug("scp stderr", "length", stderr.Len(), "content", stderr.String())
return nil
}
// checkSCPStatus checks that a prior command sent to SCP completed
// successfully. If it did not complete successfully, an error will
// be returned.
func checkSCPStatus(r *bufio.Reader) error {
code, err := r.ReadByte()
if err != nil {
return err
}
if code != 0 {
// Treat any non-zero (really 1 and 2) as fatal errors
message, _, err := r.ReadLine()
if err != nil {
return fmt.Errorf("Error reading error message: %s", err)
}
return errors.New(string(message))
}
return nil
}
func scpDownloadFile(dst string, src io.Reader, size int64, mode os.FileMode) error {
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
if err != nil {
return err
}
defer f.Close()
if _, err := io.CopyN(f, src, size); err != nil {
return err
}
return nil
}
func scpUploadFile(dst string, src io.Reader, w io.Writer, r *bufio.Reader, fi *os.FileInfo) error {
var mode os.FileMode
var size int64
if fi != nil && (*fi).Mode().IsRegular() {
mode = (*fi).Mode().Perm()
size = (*fi).Size()
} else {
// Create a temporary file where we can copy the contents of the src
// so that we can determine the length, since SCP is length-prefixed.
tf, err := ioutil.TempFile("", "vagrant-upload")
if err != nil {
return fmt.Errorf("Error creating temporary file for upload: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
mode = 0644
logger.Debug("copying input data to temporary file to read length")
if _, err := io.Copy(tf, src); err != nil {
return fmt.Errorf("Error copying input data into local temporary "+
"file. Check that TEMPDIR has enough space. Error: %s", err)
}
// Sync the file so that the contents are definitely on disk, then
// read the length of it.
if err := tf.Sync(); err != nil {
return fmt.Errorf("Error creating temporary file for upload: %s", err)
}
// Seek the file to the beginning so we can re-read all of it
if _, err := tf.Seek(0, 0); err != nil {
return fmt.Errorf("Error creating temporary file for upload: %s", err)
}
tfi, err := tf.Stat()
if err != nil {
return fmt.Errorf("Error creating temporary file for upload: %s", err)
}
size = tfi.Size()
src = tf
}
// Start the protocol
perms := fmt.Sprintf("C%04o", mode)
logger.Debug("scp uploading", "path", dst, "perms", perms, "size", size)
fmt.Fprintln(w, perms, size, dst)
if err := checkSCPStatus(r); err != nil {
return err
}
if _, err := io.CopyN(w, src, size); err != nil {
return err
}
fmt.Fprint(w, "\x00")
return checkSCPStatus(r)
}
func scpUploadDirProtocol(name string, w io.Writer, r *bufio.Reader, f func() error, fi os.FileInfo) error {
logger.Debug("scp directory upload", "path", name)
mode := fi.Mode().Perm()
perms := fmt.Sprintf("D%04o 0", mode)
fmt.Fprintln(w, perms, name)
err := checkSCPStatus(r)
if err != nil {
return err
}
if err := f(); err != nil {
return err
}
fmt.Fprintln(w, "E")
return err
}
func scpUploadDir(root string, fs []os.FileInfo, w io.Writer, r *bufio.Reader) error {
for _, fi := range fs {
realPath := filepath.Join(root, fi.Name())
// Track if this is actually a symlink to a directory. If it is
// a symlink to a file we don't do any special behavior because uploading
// a file just works. If it is a directory, we need to know so we
// treat it as such.
isSymlinkToDir := false
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
symPath, err := filepath.EvalSymlinks(realPath)
if err != nil {
return err
}
symFi, err := os.Lstat(symPath)
if err != nil {
return err
}
isSymlinkToDir = symFi.IsDir()
}
if !fi.IsDir() && !isSymlinkToDir {
// It is a regular file (or symlink to a file), just upload it
f, err := os.Open(realPath)
if err != nil {
return err
}
err = func() error {
defer f.Close()
return scpUploadFile(fi.Name(), f, w, r, &fi)
}()
if err != nil {
return err
}
continue
}
// It is a directory, recursively upload
err := scpUploadDirProtocol(fi.Name(), w, r, func() error {
f, err := os.Open(realPath)
if err != nil {
return err
}
defer f.Close()
entries, err := f.Readdir(-1)
if err != nil {
return err
}
return scpUploadDir(realPath, entries, w, r)
}, fi)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,235 @@
// +build !race
package ssh
import (
"bytes"
"fmt"
"net"
"testing"
"time"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
"golang.org/x/crypto/ssh"
)
// private key for mock server
const testServerPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA19lGVsTqIT5iiNYRgnoY1CwkbETW5cq+Rzk5v/kTlf31XpSU
70HVWkbTERECjaYdXM2gGcbb+sxpq6GtXf1M3kVomycqhxwhPv4Cr6Xp4WT/jkFx
9z+FFzpeodGJWjOH6L2H5uX1Cvr9EDdQp9t9/J32/qBFntY8GwoUI/y/1MSTmMiF
tupdMODN064vd3gyMKTwrlQ8tZM6aYuyOPsutLlUY7M5x5FwMDYvnPDSeyT/Iw0z
s3B+NCyqeeMd2T7YzQFnRATj0M7rM5LoSs7DVqVriOEABssFyLj31PboaoLhOKgc
qoM9khkNzr7FHVvi+DhYM2jD0DwvqZLN6NmnLwIDAQABAoIBAQCGVj+kuSFOV1lT
+IclQYA6bM6uY5mroqcSBNegVxCNhWU03BxlW//BE9tA/+kq53vWylMeN9mpGZea
riEMIh25KFGWXqXlOOioH8bkMsqA8S7sBmc7jljyv+0toQ9vCCtJ+sueNPhxQQxH
D2YvUjfzBQ04I9+wn30BByDJ1QA/FoPsunxIOUCcRBE/7jxuLYcpR+JvEF68yYIh
atXRld4W4in7T65YDR8jK1Uj9XAcNeDYNpT/M6oFLx1aPIlkG86aCWRO19S1jLPT
b1ZAKHHxPMCVkSYW0RqvIgLXQOR62D0Zne6/2wtzJkk5UCjkSQ2z7ZzJpMkWgDgN
ifCULFPBAoGBAPoMZ5q1w+zB+knXUD33n1J+niN6TZHJulpf2w5zsW+m2K6Zn62M
MXndXlVAHtk6p02q9kxHdgov34Uo8VpuNjbS1+abGFTI8NZgFo+bsDxJdItemwC4
KJ7L1iz39hRN/ZylMRLz5uTYRGddCkeIHhiG2h7zohH/MaYzUacXEEy3AoGBANz8
e/msleB+iXC0cXKwds26N4hyMdAFE5qAqJXvV3S2W8JZnmU+sS7vPAWMYPlERPk1
D8Q2eXqdPIkAWBhrx4RxD7rNc5qFNcQWEhCIxC9fccluH1y5g2M+4jpMX2CT8Uv+
3z+NoJ5uDTXZTnLCfoZzgZ4nCZVZ+6iU5U1+YXFJAoGBANLPpIV920n/nJmmquMj
orI1R/QXR9Cy56cMC65agezlGOfTYxk5Cfl5Ve+/2IJCfgzwJyjWUsFx7RviEeGw
64o7JoUom1HX+5xxdHPsyZ96OoTJ5RqtKKoApnhRMamau0fWydH1yeOEJd+TRHhc
XStGfhz8QNa1dVFvENczja1vAoGABGWhsd4VPVpHMc7lUvrf4kgKQtTC2PjA4xoc
QJ96hf/642sVE76jl+N6tkGMzGjnVm4P2j+bOy1VvwQavKGoXqJBRd5Apppv727g
/SM7hBXKFc/zH80xKBBgP/i1DR7kdjakCoeu4ngeGywvu2jTS6mQsqzkK+yWbUxJ
I7mYBsECgYB/KNXlTEpXtz/kwWCHFSYA8U74l7zZbVD8ul0e56JDK+lLcJ0tJffk
gqnBycHj6AhEycjda75cs+0zybZvN4x65KZHOGW/O/7OAWEcZP5TPb3zf9ned3Hl
NsZoFj52ponUM6+99A2CmezFCN16c4mbA//luWF+k3VVqR6BpkrhKw==
-----END RSA PRIVATE KEY-----`
var serverConfig = &ssh.ServerConfig{
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
if c.User() == "user" && string(pass) == "pass" {
return nil, nil
}
return nil, fmt.Errorf("password rejected for %q", c.User())
},
}
func init() {
// Parse and set the private key of the server, required to accept connections
signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey))
if err != nil {
panic("unable to parse private key: " + err.Error())
}
serverConfig.AddHostKey(signer)
}
func newMockLineServer(t *testing.T) string {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Unable to listen for connection: %s", err)
}
go func() {
defer l.Close()
c, err := l.Accept()
if err != nil {
t.Errorf("Unable to accept incoming connection: %s", err)
}
defer c.Close()
conn, chans, _, err := ssh.NewServerConn(c, serverConfig)
if err != nil {
t.Logf("Handshaking error: %v", err)
}
t.Log("Accepted SSH connection")
for newChannel := range chans {
channel, _, err := newChannel.Accept()
if err != nil {
t.Errorf("Unable to accept channel.")
}
t.Log("Accepted channel")
go func(channelType string) {
defer channel.Close()
conn.OpenChannel(channelType, nil)
}(newChannel.ChannelType())
}
conn.Close()
}()
return l.Addr().String()
}
func newMockBrokenServer(t *testing.T) string {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Unable tp listen for connection: %s", err)
}
go func() {
defer l.Close()
c, err := l.Accept()
if err != nil {
t.Errorf("Unable to accept incoming connection: %s", err)
}
defer c.Close()
// This should block for a period of time longer than our timeout in
// the test case. That way we invoke a failure scenario.
t.Log("Block on handshaking for SSH connection")
time.Sleep(5 * time.Second)
}()
return l.Addr().String()
}
func TestCommIsCommunicator(t *testing.T) {
var raw interface{}
raw = &Communicator{}
if _, ok := raw.(communicator.Communicator); !ok {
t.Fatalf("Communicator must be a communicator")
}
}
func TestNew_Invalid(t *testing.T) {
clientConfig := &ssh.ClientConfig{
User: "user",
Auth: []ssh.AuthMethod{
ssh.Password("i-am-invalid"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := newMockLineServer(t)
conn := func() (net.Conn, error) {
conn, err := net.Dial("tcp", address)
if err != nil {
t.Errorf("Unable to accept incoming connection: %v", err)
}
return conn, err
}
config := &Config{
Connection: conn,
SSHConfig: clientConfig,
}
comm, err := New(address, config)
if err != nil {
t.Fatalf("Failed to setup communicator: %s", err)
}
err = comm.Connect()
if err == nil {
t.Fatal("should have had an error connecting")
}
}
func TestStart(t *testing.T) {
clientConfig := &ssh.ClientConfig{
User: "user",
Auth: []ssh.AuthMethod{
ssh.Password("pass"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := newMockLineServer(t)
conn := func() (net.Conn, error) {
conn, err := net.Dial("tcp", address)
if err != nil {
t.Fatalf("unable to dial to remote side: %s", err)
}
return conn, err
}
config := &Config{
Connection: conn,
SSHConfig: clientConfig,
}
client, err := New(address, config)
if err != nil {
t.Fatalf("error connecting to SSH: %s", err)
}
cmd := &communicator.Cmd{
Command: "echo foo",
Stdout: new(bytes.Buffer),
}
client.Start(cmd)
}
func TestHandshakeTimeout(t *testing.T) {
clientConfig := &ssh.ClientConfig{
User: "user",
Auth: []ssh.AuthMethod{
ssh.Password("pass"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := newMockBrokenServer(t)
conn := func() (net.Conn, error) {
conn, err := net.Dial("tcp", address)
if err != nil {
t.Fatalf("unable to dial to remote side: %s", err)
}
return conn, err
}
config := &Config{
Connection: conn,
SSHConfig: clientConfig,
HandshakeTimeout: 50 * time.Millisecond,
}
comm, err := New(address, config)
if err != nil {
t.Fatalf("Failed to setup communicator: %s", err)
}
err = comm.Connect()
if err != ErrHandshakeTimeout {
// Note: there's another error that can come back from this call:
// ssh: handshake failed: EOF
// This should appear in cases where the handshake fails because of
// malformed (or no) data sent back by the server, but should not happen
// in a timeout scenario.
t.Fatalf("Expected handshake timeout, got: %s", err)
}
}

View File

@ -0,0 +1,88 @@
package ssh
import (
"fmt"
"net"
"time"
"golang.org/x/crypto/ssh"
"golang.org/x/net/proxy"
)
// ConnectFunc is a convenience method for returning a function
// that just uses net.Dial to communicate with the remote end that
// is suitable for use with the SSH communicator configuration.
func ConnectFunc(network, addr string) func() (net.Conn, error) {
return func() (net.Conn, error) {
c, err := net.DialTimeout(network, addr, 15*time.Second)
if err != nil {
return nil, err
}
if tcpConn, ok := c.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(5 * time.Second)
}
return c, nil
}
}
// ProxyConnectFunc is a convenience method for returning a function
// that connects to a host using SOCKS5 proxy
func ProxyConnectFunc(socksProxy string, socksAuth *proxy.Auth, network, addr string) func() (net.Conn, error) {
return func() (net.Conn, error) {
// create a socks5 dialer
dialer, err := proxy.SOCKS5("tcp", socksProxy, socksAuth, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("Can't connect to the proxy: %s", err)
}
c, err := dialer.Dial(network, addr)
if err != nil {
return nil, err
}
return c, nil
}
}
// BastionConnectFunc is a convenience method for returning a function
// that connects to a host over a bastion connection.
func BastionConnectFunc(
bProto string,
bAddr string,
bConf *ssh.ClientConfig,
proto string,
addr string) func() (net.Conn, error) {
return func() (net.Conn, error) {
// Connect to the bastion
bastion, err := ssh.Dial(bProto, bAddr, bConf)
if err != nil {
return nil, fmt.Errorf("Error connecting to bastion: %s", err)
}
// Connect through to the end host
conn, err := bastion.Dial(proto, addr)
if err != nil {
bastion.Close()
return nil, err
}
// Wrap it up so we close both things properly
return &bastionConn{
Conn: conn,
Bastion: bastion,
}, nil
}
}
type bastionConn struct {
net.Conn
Bastion *ssh.Client
}
func (c *bastionConn) Close() error {
c.Conn.Close()
return c.Bastion.Close()
}

View File

@ -0,0 +1,30 @@
package ssh
import (
"net"
"time"
)
// timeoutConn wraps a net.Conn, and sets a deadline for every read
// and write operation.
type timeoutConn struct {
net.Conn
ReadTimeout time.Duration
WriteTimeout time.Duration
}
func (c *timeoutConn) Read(b []byte) (int, error) {
err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout))
if err != nil {
return 0, err
}
return c.Conn.Read(b)
}
func (c *timeoutConn) Write(b []byte) (int, error) {
err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout))
if err != nil {
return 0, err
}
return c.Conn.Write(b)
}

View File

@ -0,0 +1,25 @@
package ssh
import (
"golang.org/x/crypto/ssh"
)
// An implementation of ssh.KeyboardInteractiveChallenge that simply sends
// back the password for all questions. The questions are logged.
func PasswordKeyboardInteractive(password string) ssh.KeyboardInteractiveChallenge {
return func(user, instruction string, questions []string, echos []bool) ([]string, error) {
logger.Info("keyboard interactive challenge", "user", user,
"instructions", instruction)
for i, question := range questions {
logger.Info("challenge question", "number", i+1, "question", question)
}
// Just send the password back for all questions
answers := make([]string, len(questions))
for i := range answers {
answers[i] = password
}
return answers, nil
}
}

View File

@ -0,0 +1,28 @@
package ssh
import (
"reflect"
"testing"
"golang.org/x/crypto/ssh"
)
func TestPasswordKeyboardInteractive_Impl(t *testing.T) {
var raw interface{}
raw = PasswordKeyboardInteractive("foo")
if _, ok := raw.(ssh.KeyboardInteractiveChallenge); !ok {
t.Fatal("PasswordKeyboardInteractive must implement KeyboardInteractiveChallenge")
}
}
func TestPasswordKeyboardInteractive_Challenge(t *testing.T) {
p := PasswordKeyboardInteractive("foo")
result, err := p("foo", "bar", []string{"one", "two"}, nil)
if err != nil {
t.Fatalf("err not nil: %s", err)
}
if !reflect.DeepEqual(result, []string{"foo", "foo"}) {
t.Fatalf("invalid password: %#v", result)
}
}

View File

@ -0,0 +1,257 @@
package winrm
import (
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
"github.com/masterzen/winrm"
"github.com/packer-community/winrmcp/winrmcp"
)
var logger = vagrant.DefaultLogger().Named("communicator.winrm")
// Communicator represents the WinRM communicator
type Communicator struct {
config *Config
client *winrm.Client
endpoint *winrm.Endpoint
}
// New creates a new communicator implementation over WinRM.
func New(config *Config) (*Communicator, error) {
endpoint := &winrm.Endpoint{
Host: config.Host,
Port: config.Port,
HTTPS: config.Https,
Insecure: config.Insecure,
/*
TODO
HTTPS: connInfo.HTTPS,
Insecure: connInfo.Insecure,
CACert: connInfo.CACert,
*/
}
// Create the client
params := *winrm.DefaultParameters
if config.TransportDecorator != nil {
params.TransportDecorator = config.TransportDecorator
}
params.Timeout = formatDuration(config.Timeout)
client, err := winrm.NewClientWithParameters(
endpoint, config.Username, config.Password, &params)
if err != nil {
return nil, err
}
return &Communicator{
config: config,
client: client,
endpoint: endpoint,
}, nil
}
func (c *Communicator) Connect() (err error) {
// Create the shell to verify the connection
logger.Debug("connecting to remote shell")
shell, err := c.client.CreateShell()
if err != nil {
logger.Error("connection failure", "error", err)
return
}
if err = shell.Close(); err != nil {
logger.Error("connection close failure", "error", err)
}
return
}
// Start implementation of communicator.Communicator interface
func (c *Communicator) Start(rc *communicator.Cmd) error {
shell, err := c.client.CreateShell()
if err != nil {
return err
}
logger.Info("starting remote command", "commmand", rc.Command)
rc.Init()
cmd, err := shell.Execute(rc.Command)
if err != nil {
return err
}
go runCommand(shell, cmd, rc)
return nil
}
func runCommand(shell *winrm.Shell, cmd *winrm.Command, rc *communicator.Cmd) {
defer shell.Close()
var wg sync.WaitGroup
copyFunc := func(w io.Writer, r io.Reader) {
defer wg.Done()
io.Copy(w, r)
}
if rc.Stdout != nil && cmd.Stdout != nil {
wg.Add(1)
go copyFunc(rc.Stdout, cmd.Stdout)
} else {
logger.Warn("failed to read stdout", "command", rc.Command)
}
if rc.Stderr != nil && cmd.Stderr != nil {
wg.Add(1)
go copyFunc(rc.Stderr, cmd.Stderr)
} else {
logger.Warn("failed to read stderr", "command", rc.Command)
}
cmd.Wait()
wg.Wait()
code := cmd.ExitCode()
logger.Info("command complete", "exitcode", code, "command", rc.Command)
rc.SetExitStatus(code, nil)
}
// Upload implementation of communicator.Communicator interface
func (c *Communicator) Upload(path string, input io.Reader, fi *os.FileInfo) error {
wcp, err := c.newCopyClient()
if err != nil {
return fmt.Errorf("Was unable to create winrm client: %s", err)
}
if strings.HasSuffix(path, `\`) {
// path is a directory
path += filepath.Base((*fi).Name())
}
logger.Info("uploading file", "path", path)
return wcp.Write(path, input)
}
// UploadDir implementation of communicator.Communicator interface
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
if !strings.HasSuffix(src, "/") {
dst = fmt.Sprintf("%s\\%s", dst, filepath.Base(src))
}
logger.Info("uploading directory", "source", src, "destination", dst)
wcp, err := c.newCopyClient()
if err != nil {
return err
}
return wcp.Copy(src, dst)
}
func (c *Communicator) Download(src string, dst io.Writer) error {
client, err := c.newWinRMClient()
if err != nil {
return err
}
encodeScript := `$file=[System.IO.File]::ReadAllBytes("%s"); Write-Output $([System.Convert]::ToBase64String($file))`
base64DecodePipe := &Base64Pipe{w: dst}
cmd := winrm.Powershell(fmt.Sprintf(encodeScript, src))
_, err = client.Run(cmd, base64DecodePipe, ioutil.Discard)
return err
}
func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error {
return fmt.Errorf("WinRM doesn't support download dir.")
}
func (c *Communicator) getClientConfig() *winrmcp.Config {
return &winrmcp.Config{
Auth: winrmcp.Auth{
User: c.config.Username,
Password: c.config.Password,
},
Https: c.config.Https,
Insecure: c.config.Insecure,
OperationTimeout: c.config.Timeout,
MaxOperationsPerShell: 15, // lowest common denominator
TransportDecorator: c.config.TransportDecorator,
}
}
func (c *Communicator) newCopyClient() (*winrmcp.Winrmcp, error) {
addr := fmt.Sprintf("%s:%d", c.endpoint.Host, c.endpoint.Port)
clientConfig := c.getClientConfig()
return winrmcp.New(addr, clientConfig)
}
func (c *Communicator) newWinRMClient() (*winrm.Client, error) {
conf := c.getClientConfig()
// Shamelessly borrowed from the winrmcp client to ensure
// that the client is configured using the same defaulting behaviors that
// winrmcp uses even we we aren't using winrmcp. This ensures similar
// behavior between upload, download, and copy functions. We can't use the
// one generated by winrmcp because it isn't exported.
var endpoint *winrm.Endpoint
endpoint = &winrm.Endpoint{
Host: c.endpoint.Host,
Port: c.endpoint.Port,
HTTPS: conf.Https,
Insecure: conf.Insecure,
TLSServerName: conf.TLSServerName,
CACert: conf.CACertBytes,
Timeout: conf.ConnectTimeout,
}
params := winrm.NewParameters(
winrm.DefaultParameters.Timeout,
winrm.DefaultParameters.Locale,
winrm.DefaultParameters.EnvelopeSize,
)
params.TransportDecorator = conf.TransportDecorator
params.Timeout = "PT3M"
client, err := winrm.NewClientWithParameters(
endpoint, conf.Auth.User, conf.Auth.Password, params)
return client, err
}
type Base64Pipe struct {
w io.Writer // underlying writer (file, buffer)
}
func (d *Base64Pipe) ReadFrom(r io.Reader) (int64, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return 0, err
}
var i int
i, err = d.Write(b)
if err != nil {
return 0, err
}
return int64(i), err
}
func (d *Base64Pipe) Write(p []byte) (int, error) {
dst := make([]byte, base64.StdEncoding.DecodedLen(len(p)))
decodedBytes, err := base64.StdEncoding.Decode(dst, p)
if err != nil {
return 0, err
}
return d.w.Write(dst[0:decodedBytes])
}

View File

@ -0,0 +1,123 @@
package winrm
import (
"bytes"
"io"
"strings"
"testing"
"time"
"github.com/dylanmei/winrmtest"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/communicator"
)
const PAYLOAD = "stuff"
const BASE64_ENCODED_PAYLOAD = "c3R1ZmY="
func newMockWinRMServer(t *testing.T) *winrmtest.Remote {
wrm := winrmtest.NewRemote()
wrm.CommandFunc(
winrmtest.MatchText("echo foo"),
func(out, err io.Writer) int {
out.Write([]byte("foo"))
return 0
})
wrm.CommandFunc(
winrmtest.MatchPattern(`^echo c29tZXRoaW5n >> ".*"$`),
func(out, err io.Writer) int {
return 0
})
wrm.CommandFunc(
winrmtest.MatchPattern(`^echo `+BASE64_ENCODED_PAYLOAD+` >> ".*"$`),
func(out, err io.Writer) int {
return 0
})
wrm.CommandFunc(
winrmtest.MatchPattern(`^powershell.exe -EncodedCommand .*$`),
func(out, err io.Writer) int {
out.Write([]byte(BASE64_ENCODED_PAYLOAD))
return 0
})
wrm.CommandFunc(
winrmtest.MatchText("powershell"),
func(out, err io.Writer) int {
return 0
})
wrm.CommandFunc(
winrmtest.MatchText(`powershell -Command "(Get-Item C:/Temp/vagrant.cmd) -is [System.IO.DirectoryInfo]"`),
func(out, err io.Writer) int {
out.Write([]byte("False"))
return 0
})
return wrm
}
func TestStart(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
c, err := New(&Config{
Host: wrm.Host,
Port: wrm.Port,
Username: "user",
Password: "pass",
Timeout: 30 * time.Second,
})
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
var cmd communicator.Cmd
stdout := new(bytes.Buffer)
cmd.Command = "echo foo"
cmd.Stdout = stdout
err = c.Start(&cmd)
if err != nil {
t.Fatalf("error executing remote command: %s", err)
}
cmd.Wait()
if stdout.String() != "foo" {
t.Fatalf("bad command response: expected %q, got %q", "foo", stdout.String())
}
}
func TestUpload(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
c, err := New(&Config{
Host: wrm.Host,
Port: wrm.Port,
Username: "user",
Password: "pass",
Timeout: 30 * time.Second,
})
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
file := "C:/Temp/vagrant.cmd"
err = c.Upload(file, strings.NewReader(PAYLOAD), nil)
if err != nil {
t.Fatalf("error uploading file: %s", err)
}
dest := new(bytes.Buffer)
err = c.Download(file, dest)
if err != nil {
t.Fatalf("error downloading file: %s", err)
}
downloadedPayload := dest.String()
if downloadedPayload != PAYLOAD {
t.Fatalf("files are not equal: expected [%s] length: %v, got [%s] length %v", PAYLOAD, len(PAYLOAD), downloadedPayload, len(downloadedPayload))
}
}

View File

@ -0,0 +1,19 @@
package winrm
import (
"time"
"github.com/masterzen/winrm"
)
// Config is used to configure the WinRM connection
type Config struct {
Host string
Port int
Username string
Password string
Timeout time.Duration
Https bool
Insecure bool
TransportDecorator func() winrm.Transporter
}

View File

@ -0,0 +1,32 @@
package winrm
import (
"fmt"
"time"
)
// formatDuration formats the given time.Duration into an ISO8601
// duration string.
func formatDuration(duration time.Duration) string {
// We're not supporting negative durations
if duration.Seconds() <= 0 {
return "PT0S"
}
h := int(duration.Hours())
m := int(duration.Minutes()) - (h * 60)
s := int(duration.Seconds()) - (h*3600 + m*60)
res := "PT"
if h > 0 {
res = fmt.Sprintf("%s%dH", res, h)
}
if m > 0 {
res = fmt.Sprintf("%s%dM", res, m)
}
if s > 0 {
res = fmt.Sprintf("%s%dS", res, s)
}
return res
}

View File

@ -0,0 +1,36 @@
package winrm
import (
"testing"
"time"
)
func TestFormatDuration(t *testing.T) {
// Test complex duration with hours, minutes, seconds
d := time.Duration(3701) * time.Second
s := formatDuration(d)
if s != "PT1H1M41S" {
t.Fatalf("bad ISO 8601 duration string: %s", s)
}
// Test only minutes duration
d = time.Duration(20) * time.Minute
s = formatDuration(d)
if s != "PT20M" {
t.Fatalf("bad ISO 8601 duration string for 20M: %s", s)
}
// Test only seconds
d = time.Duration(1) * time.Second
s = formatDuration(d)
if s != "PT1S" {
t.Fatalf("bad ISO 8601 duration string for 1S: %s", s)
}
// Test negative duration (unsupported)
d = time.Duration(-1) * time.Second
s = formatDuration(d)
if s != "PT0S" {
t.Fatalf("bad ISO 8601 duration string for negative: %s", s)
}
}

View File

@ -0,0 +1,25 @@
package vagrant
import (
"context"
)
type Config interface {
ConfigAttributes() (attrs []string, err error)
ConfigLoad(ctx context.Context, data map[string]interface{}) (loaddata map[string]interface{}, err error)
ConfigValidate(ctx context.Context, data map[string]interface{}, m *Machine) (errors []string, err error)
ConfigFinalize(ctx context.Context, data map[string]interface{}) (finaldata map[string]interface{}, err error)
}
type NoConfig struct{}
func (c *NoConfig) ConfigAttributes() (a []string, e error) { return }
func (c *NoConfig) ConfigLoad(context.Context, map[string]interface{}) (d map[string]interface{}, e error) {
return
}
func (c *NoConfig) ConfigValidate(context.Context, map[string]interface{}, *Machine) (es []string, e error) {
return
}
func (c *NoConfig) ConfigFinalize(context.Context, map[string]interface{}) (f map[string]interface{}, e error) {
return
}

View File

@ -0,0 +1,64 @@
package vagrant
import (
"encoding/json"
"io"
"os"
)
type Environment struct {
ActiveMachines map[string]string `json:"active_machines,omitempty"`
AliasesPath string `json:"aliases_path,omitempty"`
BoxesPath string `json:"boxes_path,omitempty"`
CWD string `json:"cwd,omitempty"`
DataDir string `json:"data_dir,omitempty"`
DefaultPrivateKeyPath string `json:"default_private_key_path,omitempty"`
GemsPath string `json:"gems_path,omitempty"`
HomePath string `json:"home_path,omitempty"`
LocalDataPath string `json:"local_data_path,omitempty"`
MachineNames []string `json:"machine_names,omitempty"`
PrimaryMachineName string `json:"primary_machine_name,omitempty"`
RootPath string `json:"root_path,omitempty"`
TmpPath string `json:"tmp_path,omitempty"`
VagrantfileName string `json:"vagrantfile_name,omitempty"`
UI Ui `json:"-"`
}
func DumpEnvironment(e *Environment) (s string, err error) {
DefaultLogger().Debug("dumping environment to serialized data")
b, err := json.Marshal(e)
if err != nil {
DefaultLogger().Error("environment dump failure", "error", err)
return
}
s = string(b)
return
}
func LoadEnvironment(edata string, ios IOServer) (e *Environment, err error) {
DefaultLogger().Debug("loading environment from serialized data")
e = &Environment{}
err = json.Unmarshal([]byte(edata), e)
if err != nil {
return
}
var stdout io.Writer
var stderr io.Writer
if ios == nil {
stdout = os.Stdout
stderr = os.Stderr
} else {
stdout = &IOWriter{target: "stdout", srv: ios}
stderr = &IOWriter{target: "stderr", srv: ios}
}
e.UI = &TargetedUi{
Target: "vagrant",
Ui: &ColoredUi{
ErrorColor: UiColorRed,
Ui: &BasicUi{
Reader: os.Stdin,
Writer: stdout,
ErrorWriter: stderr},
}}
return
}

View File

@ -0,0 +1,63 @@
package vagrant
import (
"strings"
"testing"
)
func TestLoadEnvironment(t *testing.T) {
env, err := LoadEnvironment("{}", nil)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
if env.UI == nil {
t.Fatalf("no UI configured for environment")
}
}
func TestBadLoadEnvironment(t *testing.T) {
_, err := LoadEnvironment("ack", nil)
if err == nil {
t.Fatalf("expected load error but none provided")
}
}
func TestLoadEnvironmentUIStdout(t *testing.T) {
iosrv := buildio()
env, err := LoadEnvironment("{}", iosrv)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
go func() { env.UI.Info("test string") }()
str := <-iosrv.Streams()["stdout"]
if !strings.Contains(str, "test string") {
t.Fatalf("unexpected output: %s", str)
}
}
func TestLoadEnvironmentUIStderr(t *testing.T) {
iosrv := buildio()
env, err := LoadEnvironment("{}", iosrv)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
go func() { env.UI.Error("test string") }()
str, err := iosrv.Read("stderr")
if !strings.Contains(str, "test string") {
t.Fatalf("unexpected output: %s", str)
}
}
func TestDumpEnvironment(t *testing.T) {
env, err := LoadEnvironment("{}", nil)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
d, err := DumpEnvironment(env)
if err != nil {
t.Fatalf("unexpected dump error: %s", err)
}
if d != "{}" {
t.Fatalf("unexpected dump information: %s", d)
}
}

View File

@ -0,0 +1,17 @@
package vagrant
import (
hclog "github.com/hashicorp/go-hclog"
)
var GlobalIOServer *IOServer
var defaultLogger = hclog.Default().Named("vagrant")
func DefaultLogger() hclog.Logger {
return defaultLogger
}
func SetDefaultLogger(l hclog.Logger) {
defaultLogger = l
}

View File

@ -0,0 +1,53 @@
package vagrant
import (
"errors"
)
type StreamIO interface {
Read(target string) (content string, err error)
Write(content, target string) (n int, err error)
}
type IOServer interface {
Streams() map[string]chan (string)
StreamIO
}
type IOSrv struct {
Targets map[string]chan (string)
}
func (i *IOSrv) Streams() map[string]chan (string) {
return i.Targets
}
type IOWriter struct {
target string
srv IOServer
}
func (i *IOWriter) Write(b []byte) (n int, err error) {
content := string(b)
n, err = i.srv.Write(content, i.target)
return
}
func (i *IOSrv) Read(target string) (content string, err error) {
if _, ok := i.Streams()[target]; !ok {
err = errors.New("Unknown target defined")
return
}
content = <-i.Streams()[target]
return
}
func (i *IOSrv) Write(content, target string) (n int, err error) {
if _, ok := i.Streams()[target]; !ok {
err = errors.New("Unknown target defined")
return
}
i.Streams()[target] <- content
n = len(content)
return
}

View File

@ -0,0 +1,48 @@
package vagrant
import (
"testing"
)
func buildio() IOServer {
return &IOSrv{
Targets: map[string]chan (string){
"stdout": make(chan string),
"stderr": make(chan string)}}
}
func TestIOSrvWrite(t *testing.T) {
iosrv := buildio()
var i int
go func() { i, _ = iosrv.Write("test string", "stdout") }()
_, _ = iosrv.Read("stdout")
if i != len("test string") {
t.Fatalf("unexpected write bytes %d != %d",
len("test string"), i)
}
}
func TestIOSrvRead(t *testing.T) {
iosrv := buildio()
go func() { _, _ = iosrv.Write("test string", "stdout") }()
r, _ := iosrv.Read("stdout")
if r != "test string" {
t.Fatalf("unexpected read result: %s", r)
}
}
func TestIOSrvWriteBadTarget(t *testing.T) {
iosrv := buildio()
_, err := iosrv.Write("test string", "stdno")
if err == nil {
t.Fatalf("expected error on write")
}
}
func TestIOSrvReadBadTarget(t *testing.T) {
iosrv := buildio()
_, err := iosrv.Read("stdno")
if err == nil {
t.Fatalf("expected error on read")
}
}

View File

@ -0,0 +1,59 @@
package vagrant
import (
"encoding/json"
"io"
"os"
)
type Machine struct {
Box Box `json:"box"`
Config map[string]interface{} `json:"config"`
DataDir string `json:"data_dir,omitempty"`
Env Environment `json:"environment"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
ProviderConfig map[string]interface{} `json:"provider_config"`
ProviderName string `json:"provider_name,omitempty"`
ProviderOptions map[string]string `json:"provider_options"`
UI Ui `json:"-"`
}
func DumpMachine(m *Machine) (s string, err error) {
DefaultLogger().Debug("dumping machine to serialized data")
b, err := json.Marshal(m)
if err != nil {
DefaultLogger().Debug("machine dump failure", "error", err)
return
}
s = string(b)
return
}
func LoadMachine(mdata string, ios IOServer) (m *Machine, err error) {
DefaultLogger().Debug("loading machine from serialized data")
m = &Machine{}
err = json.Unmarshal([]byte(mdata), m)
if err != nil {
return
}
var stdout io.Writer
var stderr io.Writer
if ios == nil {
stdout = os.Stdout
stderr = os.Stderr
} else {
stdout = &IOWriter{target: "stdout", srv: ios}
stderr = &IOWriter{target: "stderr", srv: ios}
}
m.UI = &TargetedUi{
Target: m.Name,
Ui: &ColoredUi{
ErrorColor: UiColorRed,
Ui: &BasicUi{
Reader: os.Stdin,
Writer: stdout,
ErrorWriter: stderr},
}}
return
}

View File

@ -0,0 +1,7 @@
package vagrant
type MachineState struct {
Id string `json:"id"`
ShortDesc string `json:"short_description"`
LongDesc string `json:"long_description"`
}

View File

@ -0,0 +1,53 @@
package vagrant
import (
"strings"
"testing"
)
func TestMachineLoad(t *testing.T) {
_, err := LoadMachine("{}", nil)
if err != nil {
t.Fatalf("failed to load machine: %s", err)
}
}
func TestMachineDump(t *testing.T) {
m, err := LoadMachine("{}", nil)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
_, err = DumpMachine(m)
if err != nil {
t.Fatalf("failed to dump machine: %s", err)
}
}
func TestMachineUI(t *testing.T) {
iosrv := buildio()
m, err := LoadMachine("{}", iosrv)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
go func() { m.UI.Info("test string") }()
r, _ := iosrv.Read("stdout")
if !strings.Contains(r, "test string") {
t.Fatalf("unexpected read result: %s", r)
}
}
func TestMachineUINamed(t *testing.T) {
iosrv := buildio()
m, err := LoadMachine("{\"name\":\"plugintest\"}", iosrv)
if err != nil {
t.Fatalf("unexpected load error: %s", err)
}
go func() { m.UI.Info("test string") }()
r, _ := iosrv.Read("stdout")
if !strings.Contains(r, "test string") {
t.Fatalf("unexpected read result: %s", r)
}
if !strings.Contains(r, "plugintest") {
t.Fatalf("output does not contain name: %s", r)
}
}

View File

@ -0,0 +1,294 @@
package plugin
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
hclog "github.com/hashicorp/go-hclog"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
var (
Handshake = go_plugin.HandshakeConfig{
MagicCookieKey: "VAGRANT_PLUGIN_MAGIC_COOKIE",
MagicCookieValue: "1561a662a76642f98df77ad025aa13a9b16225d93f90475e91090fbe577317ed",
ProtocolVersion: 1}
ErrPluginShutdown = errors.New("plugin has shutdown")
)
type RemotePlugin interface {
Impl() interface{}
}
type RemoteConfig struct {
Client *go_plugin.Client
Config vagrant.Config
}
func (r *RemoteConfig) Impl() interface{} {
return r.Config
}
type RemoteProvider struct {
Client *go_plugin.Client
Provider Provider
}
func (r *RemoteProvider) Impl() interface{} {
return r.Provider
}
type RemoteGuestCapabilities struct {
Client *go_plugin.Client
GuestCapabilities vagrant.GuestCapabilities
}
func (r *RemoteGuestCapabilities) Impl() interface{} {
return r.GuestCapabilities
}
type RemoteHostCapabilities struct {
Client *go_plugin.Client
HostCapabilities vagrant.HostCapabilities
}
func (r *RemoteHostCapabilities) Impl() interface{} {
return r.HostCapabilities
}
type RemoteProviderCapabilities struct {
Client *go_plugin.Client
ProviderCapabilities vagrant.ProviderCapabilities
}
func (r *RemoteProviderCapabilities) Impl() interface{} {
return r.ProviderCapabilities
}
type RemoteSyncedFolder struct {
Client *go_plugin.Client
SyncedFolder vagrant.SyncedFolder
}
func (r *RemoteSyncedFolder) Impl() interface{} {
return r.SyncedFolder
}
type VagrantPlugin struct {
Providers map[string]*RemoteProvider
SyncedFolders map[string]*RemoteSyncedFolder
PluginDirectories []string
PluginLookup func(name, kind string) (p interface{}, err error)
Logger hclog.Logger
}
func VagrantPluginInit() *VagrantPlugin {
v := &VagrantPlugin{
PluginDirectories: []string{},
Providers: map[string]*RemoteProvider{},
SyncedFolders: map[string]*RemoteSyncedFolder{},
Logger: vagrant.DefaultLogger().Named("go-plugin")}
v.PluginLookup = v.DefaultPluginLookup
return v
}
func (v *VagrantPlugin) DefaultPluginLookup(name, kind string) (p interface{}, err error) {
switch kind {
case "provider":
p = v.Providers[name].Impl()
case "synced_folder":
p = v.SyncedFolders[name].Impl()
default:
err = errors.New("invalid plugin type")
return
}
if p == nil {
err = errors.New(fmt.Sprintf("Failed to locate %s plugin of type %s", name, kind))
}
return
}
func (v *VagrantPlugin) LoadPlugins(pluginPath string) error {
for _, p := range v.PluginDirectories {
if p == pluginPath {
v.Logger.Error("plugin directory path already loaded", "path", pluginPath)
return errors.New("plugin directory already loaded")
}
}
v.PluginDirectories = append(v.PluginDirectories, pluginPath)
if err := v.LoadProviders(pluginPath); err != nil {
return err
}
if err := v.LoadSyncedFolders(pluginPath); err != nil {
return err
}
return nil
}
func (v *VagrantPlugin) LoadProviders(pluginPath string) error {
providerPaths, err := go_plugin.Discover("*_provider", pluginPath)
if err != nil {
v.Logger.Error("error during plugin discovery", "type", "provider",
"error", err, "path", pluginPath)
return err
}
for _, providerPath := range providerPaths {
v.Logger.Info("loading provider plugin", "path", providerPath)
client := go_plugin.NewClient(&go_plugin.ClientConfig{
AllowedProtocols: []go_plugin.Protocol{go_plugin.ProtocolGRPC},
Logger: v.Logger,
HandshakeConfig: Handshake,
Cmd: exec.Command(providerPath),
VersionedPlugins: map[int]go_plugin.PluginSet{
2: {"provider": &ProviderPlugin{}}}})
gclient, err := client.Client()
if err != nil {
v.Logger.Error("error loading provider client", "error", err, "path", providerPath)
return err
}
raw, err := gclient.Dispense("provider")
if err != nil {
v.Logger.Error("error loading provider plugin", "error", err, "path", providerPath)
return err
}
prov := raw.(Provider)
n := prov.Name()
v.Providers[n] = &RemoteProvider{
Client: client,
Provider: prov}
v.Logger.Info("plugin loaded", "type", "provider", "name", n, "path", providerPath)
go v.StreamIO("stdout", prov, n, "provider")
go v.StreamIO("stderr", prov, n, "provider")
}
return nil
}
func (v *VagrantPlugin) LoadSyncedFolders(pluginPath string) error {
folderPaths, err := go_plugin.Discover("*_synced_folder", pluginPath)
if err != nil {
v.Logger.Error("error during plugin discovery", "type", "synced_folder",
"error", err, "path", pluginPath)
return err
}
for _, folderPath := range folderPaths {
v.Logger.Info("loading synced_folder plugin", "path", folderPath)
client := go_plugin.NewClient(&go_plugin.ClientConfig{
AllowedProtocols: []go_plugin.Protocol{go_plugin.ProtocolGRPC},
Logger: v.Logger,
HandshakeConfig: Handshake,
Cmd: exec.Command(folderPath),
VersionedPlugins: map[int]go_plugin.PluginSet{
2: {"synced_folders": &SyncedFolderPlugin{}}}})
gclient, err := client.Client()
if err != nil {
v.Logger.Error("error loading synced_folder client", "error", err, "path", folderPath)
return err
}
raw, err := gclient.Dispense("synced_folder")
if err != nil {
v.Logger.Error("error loading synced_folder plugin", "error", err, "path", folderPath)
return err
}
fold := raw.(SyncedFolder)
n := fold.Name()
v.SyncedFolders[n] = &RemoteSyncedFolder{
Client: client,
SyncedFolder: fold}
v.Logger.Info("plugin loaded", "type", "synced_folder", "name", n, "path", folderPath)
go v.StreamIO("stdout", fold, n, "synced_folder")
go v.StreamIO("stderr", fold, n, "synced_folder")
}
return nil
}
func (v *VagrantPlugin) StreamIO(target string, i vagrant.IOServer, name, kind string) {
v.Logger.Info("starting plugin IO streaming", "target", target, "plugin", name, "type", kind)
for {
str, err := i.Read(target)
if err != nil {
v.Logger.Error("plugin IO streaming failure", "target", target, "plugin", name,
"type", kind, "error", err)
break
}
v.Logger.Debug("received plugin IO content", "target", target, "plugin", name,
"type", kind, "content", str)
if target == "stdout" {
os.Stdout.Write([]byte(str))
} else if target == "stderr" {
os.Stderr.Write([]byte(str))
}
}
v.Logger.Info("completed plugin IO streaming", "target", target, "plugin", name, "type", kind)
}
func (v *VagrantPlugin) Kill() {
v.Logger.Debug("killing all running plugins")
for n, p := range v.Providers {
v.Logger.Debug("killing plugin", "name", n, "type", "provider")
p.Client.Kill()
v.Logger.Info("plugin killed", "name", n, "type", "provider")
}
for n, p := range v.SyncedFolders {
v.Logger.Debug("killing plugin", "name", n, "type", "synced_folder")
p.Client.Kill()
v.Logger.Info("plugin killed", "name", n, "type", "synced_folder")
}
}
// Helper used for inspect GRPC related errors and providing "correct"
// error message
func handleGrpcError(err error, pluginCtx context.Context, reqCtx context.Context) error {
// If there was no error then nothing to process
if err == nil {
return nil
}
// If a request context is provided, check that it
// was not canceled or timed out. If no context
// provided, stub one for later.
if reqCtx != nil {
s := status.FromContextError(reqCtx.Err())
switch s.Code() {
case codes.Canceled:
return context.Canceled
case codes.DeadlineExceeded:
return context.DeadlineExceeded
}
} else {
reqCtx = context.Background()
}
s, ok := status.FromError(err)
if ok && (s.Code() == codes.Unavailable || s.Code() == codes.Canceled) {
select {
case <-pluginCtx.Done():
err = ErrPluginShutdown
case <-reqCtx.Done():
err = reqCtx.Err()
select {
case <-pluginCtx.Done():
err = ErrPluginShutdown
default:
}
case <-time.After(5):
return errors.New("exceeded context check timeout - " + err.Error())
}
return err
} else if s != nil && s.Message() != "" {
// Extract actual error message received
// and create new error
return errors.New(s.Message())
}
return err
}

View File

@ -0,0 +1,365 @@
package plugin
import (
"context"
"encoding/json"
"google.golang.org/grpc"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
"github.com/LK4D4/joincontext"
)
type GuestCapabilities interface {
vagrant.GuestCapabilities
Meta
}
type GuestCapabilitiesPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl GuestCapabilities
}
func (g *GuestCapabilitiesPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
g.Impl.Init()
vagrant_proto.RegisterGuestCapabilitiesServer(s, &GRPCGuestCapabilitiesServer{
Impl: g.Impl,
GRPCIOServer: GRPCIOServer{
Impl: g.Impl}})
return nil
}
func (g *GuestCapabilitiesPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewGuestCapabilitiesClient(c)
return &GRPCGuestCapabilitiesClient{
client: client,
doneCtx: ctx,
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx}}, nil
}
type GRPCGuestCapabilitiesServer struct {
GRPCIOServer
Impl GuestCapabilities
}
func (s *GRPCGuestCapabilitiesServer) GuestCapabilities(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.SystemCapabilityList, err error) {
resp = &vagrant_proto.SystemCapabilityList{}
r, err := s.Impl.GuestCapabilities()
if err != nil {
return
}
for _, cap := range r {
rcap := &vagrant_proto.SystemCapability{Name: cap.Name, Platform: cap.Platform}
resp.Capabilities = append(resp.Capabilities, rcap)
}
return
}
func (s *GRPCGuestCapabilitiesServer) GuestCapability(ctx context.Context, req *vagrant_proto.GuestCapabilityRequest) (resp *vagrant_proto.GenericResponse, err error) {
resp = &vagrant_proto.GenericResponse{}
var args interface{}
if err = json.Unmarshal([]byte(req.Arguments), &args); err != nil {
return
}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
cap := &vagrant.SystemCapability{
Name: req.Capability.Name,
Platform: req.Capability.Platform}
r, err := s.Impl.GuestCapability(ctx, cap, args, machine)
result, err := json.Marshal(r)
if err != nil {
return
}
resp.Result = string(result)
return
}
type GRPCGuestCapabilitiesClient struct {
GRPCCoreClient
GRPCIOClient
client vagrant_proto.GuestCapabilitiesClient
doneCtx context.Context
}
func (c *GRPCGuestCapabilitiesClient) GuestCapabilities() (caps []vagrant.SystemCapability, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.GuestCapabilities(jctx, &vagrant_proto.Empty{})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
caps = make([]vagrant.SystemCapability, len(resp.Capabilities))
for i := 0; i < len(resp.Capabilities); i++ {
cap := vagrant.SystemCapability{
Name: resp.Capabilities[i].Name,
Platform: resp.Capabilities[i].Platform}
caps[i] = cap
}
return
}
func (c *GRPCGuestCapabilitiesClient) GuestCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, machine *vagrant.Machine) (result interface{}, err error) {
a, err := json.Marshal(args)
if err != nil {
return
}
m, err := vagrant.DumpMachine(machine)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.GuestCapability(jctx, &vagrant_proto.GuestCapabilityRequest{
Capability: &vagrant_proto.SystemCapability{Name: cap.Name, Platform: cap.Platform},
Machine: m,
Arguments: string(a)})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Result), &result)
return
}
type HostCapabilities interface {
vagrant.HostCapabilities
Meta
}
type HostCapabilitiesPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl HostCapabilities
}
func (h *HostCapabilitiesPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
h.Impl.Init()
vagrant_proto.RegisterHostCapabilitiesServer(s, &GRPCHostCapabilitiesServer{
Impl: h.Impl,
GRPCIOServer: GRPCIOServer{
Impl: h.Impl}})
return nil
}
func (h *HostCapabilitiesPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewHostCapabilitiesClient(c)
return &GRPCHostCapabilitiesClient{
client: client,
doneCtx: ctx,
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx}}, nil
}
type GRPCHostCapabilitiesServer struct {
GRPCIOServer
Impl HostCapabilities
}
func (s *GRPCHostCapabilitiesServer) HostCapabilities(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.SystemCapabilityList, err error) {
resp = &vagrant_proto.SystemCapabilityList{}
r, err := s.Impl.HostCapabilities()
if err != nil {
return
}
for _, cap := range r {
rcap := &vagrant_proto.SystemCapability{Name: cap.Name, Platform: cap.Platform}
resp.Capabilities = append(resp.Capabilities, rcap)
}
return
}
func (s *GRPCHostCapabilitiesServer) HostCapability(ctx context.Context, req *vagrant_proto.HostCapabilityRequest) (resp *vagrant_proto.GenericResponse, err error) {
resp = &vagrant_proto.GenericResponse{}
var args interface{}
if err = json.Unmarshal([]byte(req.Arguments), &args); err != nil {
return
}
env, err := vagrant.LoadEnvironment(req.Environment, s.Impl)
if err != nil {
return
}
cap := &vagrant.SystemCapability{
Name: req.Capability.Name,
Platform: req.Capability.Platform}
r, err := s.Impl.HostCapability(ctx, cap, args, env)
result, err := json.Marshal(r)
if err != nil {
return
}
resp.Result = string(result)
return
}
type GRPCHostCapabilitiesClient struct {
GRPCCoreClient
GRPCIOClient
client vagrant_proto.HostCapabilitiesClient
doneCtx context.Context
}
func (c *GRPCHostCapabilitiesClient) HostCapabilities() (caps []vagrant.SystemCapability, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.HostCapabilities(jctx, &vagrant_proto.Empty{})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
caps = make([]vagrant.SystemCapability, len(resp.Capabilities))
for i := 0; i < len(resp.Capabilities); i++ {
cap := vagrant.SystemCapability{
Name: resp.Capabilities[i].Name,
Platform: resp.Capabilities[i].Platform}
caps[i] = cap
}
return
}
func (c *GRPCHostCapabilitiesClient) HostCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, env *vagrant.Environment) (result interface{}, err error) {
a, err := json.Marshal(args)
if err != nil {
return
}
e, err := vagrant.DumpEnvironment(env)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.HostCapability(jctx, &vagrant_proto.HostCapabilityRequest{
Capability: &vagrant_proto.SystemCapability{
Name: cap.Name,
Platform: cap.Platform},
Environment: e,
Arguments: string(a)})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Result), &result)
return
}
type ProviderCapabilities interface {
vagrant.ProviderCapabilities
Meta
}
type ProviderCapabilitiesPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl ProviderCapabilities
}
func (p *ProviderCapabilitiesPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
p.Impl.Init()
vagrant_proto.RegisterProviderCapabilitiesServer(s, &GRPCProviderCapabilitiesServer{
Impl: p.Impl,
GRPCIOServer: GRPCIOServer{
Impl: p.Impl}})
return nil
}
func (p *ProviderCapabilitiesPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewProviderCapabilitiesClient(c)
return &GRPCProviderCapabilitiesClient{
client: client,
doneCtx: ctx,
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx}}, nil
}
type GRPCProviderCapabilitiesServer struct {
GRPCIOServer
Impl ProviderCapabilities
}
func (s *GRPCProviderCapabilitiesServer) ProviderCapabilities(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.ProviderCapabilityList, err error) {
resp = &vagrant_proto.ProviderCapabilityList{}
r, err := s.Impl.ProviderCapabilities()
if err != nil {
return
}
for _, cap := range r {
rcap := &vagrant_proto.ProviderCapability{Name: cap.Name, Provider: cap.Provider}
resp.Capabilities = append(resp.Capabilities, rcap)
}
return
}
func (s *GRPCProviderCapabilitiesServer) ProviderCapability(ctx context.Context, req *vagrant_proto.ProviderCapabilityRequest) (resp *vagrant_proto.GenericResponse, err error) {
resp = &vagrant_proto.GenericResponse{}
var args interface{}
if err = json.Unmarshal([]byte(req.Arguments), &args); err != nil {
return
}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
cap := &vagrant.ProviderCapability{
Name: req.Capability.Name,
Provider: req.Capability.Provider}
r, err := s.Impl.ProviderCapability(ctx, cap, args, m)
if err != nil {
return
}
result, err := json.Marshal(r)
if err != nil {
return
}
resp.Result = string(result)
return
}
type GRPCProviderCapabilitiesClient struct {
GRPCCoreClient
GRPCIOClient
client vagrant_proto.ProviderCapabilitiesClient
doneCtx context.Context
}
func (c *GRPCProviderCapabilitiesClient) ProviderCapabilities() (caps []vagrant.ProviderCapability, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ProviderCapabilities(jctx, &vagrant_proto.Empty{})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
caps = make([]vagrant.ProviderCapability, len(resp.Capabilities))
for i := 0; i < len(resp.Capabilities); i++ {
cap := vagrant.ProviderCapability{
Name: resp.Capabilities[i].Name,
Provider: resp.Capabilities[i].Provider}
caps[i] = cap
}
return
}
func (c *GRPCProviderCapabilitiesClient) ProviderCapability(ctx context.Context, cap *vagrant.ProviderCapability, args interface{}, machine *vagrant.Machine) (result interface{}, err error) {
a, err := json.Marshal(args)
if err != nil {
return
}
m, err := vagrant.DumpMachine(machine)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ProviderCapability(jctx, &vagrant_proto.ProviderCapabilityRequest{
Capability: &vagrant_proto.ProviderCapability{
Name: cap.Name,
Provider: cap.Provider},
Machine: m,
Arguments: string(a)})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Result), &result)
return
}

View File

@ -0,0 +1,499 @@
package plugin
import (
"context"
"testing"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
func TestCapabilities_GuestCapabilities(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(GuestCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.GuestCapabilities()
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if len(resp) != 1 {
t.Fatalf("length %d != 1", len(resp))
}
if resp[0].Name != "test_cap" {
t.Errorf("name - %s != test_cap", resp[0].Name)
}
if resp[0].Platform != "testOS" {
t.Errorf("platform - %s != testOS", resp[0].Platform)
}
}
func TestCapabilities_GuestCapability(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(GuestCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
m := &vagrant.Machine{}
args := []string{"test_value", "next_test_value"}
resp, err := impl.GuestCapability(context.Background(), cap, args, m)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
if result[1] != "test_value" {
t.Errorf("%s != test_value", result[1])
}
}
func TestCapabilities_GuestCapability_noargs(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(GuestCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
m := &vagrant.Machine{}
var args interface{}
args = nil
resp, err := impl.GuestCapability(context.Background(), cap, args, m)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
}
func TestCapabilities_GuestCapability_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(GuestCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
m := &vagrant.Machine{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.GuestCapability(ctx, cap, args, m)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestCapabilities_GuestCapability_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &GuestCapabilitiesPlugin{Impl: &MockGuestCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(GuestCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
m := &vagrant.Machine{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.GuestCapability(ctx, cap, args, m)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestCapabilities_HostCapabilities(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(HostCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.HostCapabilities()
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if len(resp) != 1 {
t.Fatalf("length %d != 1", len(resp))
}
if resp[0].Name != "test_cap" {
t.Errorf("name - %s != test_cap", resp[0].Name)
}
if resp[0].Platform != "testOS" {
t.Errorf("platform - %s != testOS", resp[0].Platform)
}
}
func TestCapabilities_HostCapability(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(HostCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
e := &vagrant.Environment{}
args := []string{"test_value", "next_test_value"}
resp, err := impl.HostCapability(context.Background(), cap, args, e)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
if result[1] != "test_value" {
t.Errorf("%s != test_value", result[1])
}
}
func TestCapabilities_HostCapability_noargs(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(HostCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
e := &vagrant.Environment{}
var args interface{}
args = nil
resp, err := impl.HostCapability(context.Background(), cap, args, e)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
}
func TestCapabilities_HostCapability_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(HostCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
e := &vagrant.Environment{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.HostCapability(ctx, cap, args, e)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestCapabilities_HostCapability_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &HostCapabilitiesPlugin{Impl: &MockHostCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(HostCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.SystemCapability{
Name: "test_cap",
Platform: "TestOS"}
e := &vagrant.Environment{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.HostCapability(ctx, cap, args, e)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestCapabilities_ProviderCapabilities(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(ProviderCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.ProviderCapabilities()
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if len(resp) != 1 {
t.Fatalf("length %d != 1", len(resp))
}
if resp[0].Name != "test_cap" {
t.Errorf("name - %s != test_cap", resp[0].Name)
}
if resp[0].Provider != "testProvider" {
t.Errorf("provider - %s != testProvdier", resp[0].Provider)
}
}
func TestCapabilities_ProviderCapability(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(ProviderCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.ProviderCapability{
Name: "test_cap",
Provider: "test_provider"}
m := &vagrant.Machine{}
args := []string{"test_value", "next_test_value"}
resp, err := impl.ProviderCapability(context.Background(), cap, args, m)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
if result[1] != "test_value" {
t.Errorf("%s != test_value", result[1])
}
}
func TestCapabilities_ProviderCapability_noargs(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(ProviderCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.ProviderCapability{
Name: "test_cap",
Provider: "test_provider"}
m := &vagrant.Machine{}
var args interface{}
args = nil
resp, err := impl.ProviderCapability(context.Background(), cap, args, m)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result, ok := resp.([]interface{})
if !ok {
t.Fatalf("bad %#v", result)
}
if result[0] != "test_cap" {
t.Errorf("%s != test_cap", result[0])
}
}
func TestCapabilities_ProviderCapability_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(ProviderCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.ProviderCapability{
Name: "test_cap",
Provider: "test_provider"}
m := &vagrant.Machine{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.ProviderCapability(ctx, cap, args, m)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestCapabilities_ProviderCapability_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"caps": &ProviderCapabilitiesPlugin{Impl: &MockProviderCapabilities{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("caps")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(ProviderCapabilities)
if !ok {
t.Fatalf("bad %#v", raw)
}
cap := &vagrant.ProviderCapability{
Name: "test_cap",
Provider: "test_provider"}
m := &vagrant.Machine{}
args := []string{"pause", "test_value", "next_test_value"}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.ProviderCapability(ctx, cap, args, m)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}

View File

@ -0,0 +1,175 @@
package plugin
import (
"context"
"encoding/json"
"google.golang.org/grpc"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
"github.com/LK4D4/joincontext"
)
type Config interface {
vagrant.Config
Meta
}
type ConfigPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl Config
}
func (c *ConfigPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
c.Impl.Init()
vagrant_proto.RegisterConfigServer(s, &GRPCConfigServer{
Impl: c.Impl,
GRPCIOServer: GRPCIOServer{
Impl: c.Impl}})
return nil
}
func (c *ConfigPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, con *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewConfigClient(con)
return &GRPCConfigClient{
client: client,
doneCtx: ctx,
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx}}, nil
}
type GRPCConfigServer struct {
GRPCIOServer
Impl Config
}
func (s *GRPCConfigServer) ConfigAttributes(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.ListResponse, err error) {
resp = &vagrant_proto.ListResponse{}
resp.Items, err = s.Impl.ConfigAttributes()
return
}
func (s *GRPCConfigServer) ConfigLoad(ctx context.Context, req *vagrant_proto.Configuration) (resp *vagrant_proto.Configuration, err error) {
resp = &vagrant_proto.Configuration{}
var data map[string]interface{}
err = json.Unmarshal([]byte(req.Data), &data)
if err != nil {
return
}
r, err := s.Impl.ConfigLoad(ctx, data)
if err != nil {
return
}
mdata, err := json.Marshal(r)
if err != nil {
return
}
resp.Data = string(mdata)
return
}
func (s *GRPCConfigServer) ConfigValidate(ctx context.Context, req *vagrant_proto.Configuration) (resp *vagrant_proto.ListResponse, err error) {
resp = &vagrant_proto.ListResponse{}
var data map[string]interface{}
err = json.Unmarshal([]byte(req.Data), &data)
if err != nil {
return
}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
resp.Items, err = s.Impl.ConfigValidate(ctx, data, m)
return
}
func (s *GRPCConfigServer) ConfigFinalize(ctx context.Context, req *vagrant_proto.Configuration) (resp *vagrant_proto.Configuration, err error) {
resp = &vagrant_proto.Configuration{}
var data map[string]interface{}
err = json.Unmarshal([]byte(req.Data), &data)
if err != nil {
return
}
r, err := s.Impl.ConfigFinalize(ctx, data)
if err != nil {
return
}
mdata, err := json.Marshal(r)
if err != nil {
return
}
resp.Data = string(mdata)
return
}
type GRPCConfigClient struct {
GRPCCoreClient
GRPCIOClient
client vagrant_proto.ConfigClient
doneCtx context.Context
}
func (c *GRPCConfigClient) ConfigAttributes() (attrs []string, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ConfigAttributes(jctx, &vagrant_proto.Empty{})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, nil)
}
attrs = resp.Items
return
}
func (c *GRPCConfigClient) ConfigLoad(ctx context.Context, data map[string]interface{}) (loaddata map[string]interface{}, err error) {
mdata, err := json.Marshal(data)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ConfigLoad(jctx, &vagrant_proto.Configuration{
Data: string(mdata)})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Data), &loaddata)
return
}
func (c *GRPCConfigClient) ConfigValidate(ctx context.Context, data map[string]interface{}, m *vagrant.Machine) (errs []string, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
mdata, err := json.Marshal(data)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ConfigValidate(jctx, &vagrant_proto.Configuration{
Data: string(mdata),
Machine: machData})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
errs = resp.Items
return
}
func (c *GRPCConfigClient) ConfigFinalize(ctx context.Context, data map[string]interface{}) (finaldata map[string]interface{}, err error) {
mdata, err := json.Marshal(data)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.ConfigFinalize(jctx, &vagrant_proto.Configuration{
Data: string(mdata)})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Data), &finaldata)
return
}

View File

@ -0,0 +1,246 @@
package plugin
import (
"context"
"testing"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
func TestConfigPlugin_Attributes(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.ConfigAttributes()
if err != nil {
t.Fatalf("bad resp %s", err)
}
if resp[0] != "fubar" {
t.Errorf("%s != fubar", resp[0])
}
if resp[1] != "foobar" {
t.Errorf("%s != foobar", resp[1])
}
}
func TestConfigPlugin_Load(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{}
var resp map[string]interface{}
resp, err = impl.ConfigLoad(context.Background(), data)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if _, ok := resp["test_key"]; !ok {
t.Fatalf("bad resp content %#v", resp)
}
v := resp["test_key"].(string)
if v != "test_val" {
t.Errorf("%s != test_val", v)
}
}
func TestConfigPlugin_Load_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{"pause": true}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.ConfigLoad(ctx, data)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestConfigPlugin_Load_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{"pause": true}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.ConfigLoad(ctx, data)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestConfigPlugin_Validate(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{}
machine := &vagrant.Machine{}
resp, err := impl.ConfigValidate(context.Background(), data, machine)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if len(resp) != 1 {
t.Fatalf("bad size %d != 1", len(resp))
}
if resp[0] != "test error" {
t.Errorf("%s != test error", resp[0])
}
}
func TestConfigPlugin_Validate_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{"pause": true}
machine := &vagrant.Machine{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.ConfigValidate(ctx, data, machine)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestConfigPlugin_Finalize(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{
"test_key": "test_val",
"other_key": "other_val"}
resp, err := impl.ConfigFinalize(context.Background(), data)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if _, ok := resp["test_key"]; !ok {
t.Fatalf("bad resp content %#v", resp)
}
v := resp["test_key"].(string)
if v != "test_val-updated" {
t.Errorf("%s != test_val-updated", v)
}
v = resp["other_key"].(string)
if v != "other_val-updated" {
t.Errorf("%s != other_val-updated", v)
}
}
func TestConfigPlugin_Finalize_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"configs": &ConfigPlugin{Impl: &MockConfig{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("configs")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Config)
if !ok {
t.Fatalf("bad %#v", raw)
}
data := map[string]interface{}{
"pause": true,
"test_key": "test_val",
"other_key": "other_val"}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.ConfigFinalize(ctx, data)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}

View File

@ -0,0 +1,84 @@
package plugin
import (
"context"
"google.golang.org/grpc"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
"github.com/LK4D4/joincontext"
)
type IO interface {
vagrant.StreamIO
}
type IOPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl vagrant.StreamIO
}
type GRPCIOServer struct {
Impl vagrant.StreamIO
}
func (s *GRPCIOServer) Read(ctx context.Context, req *vagrant_proto.Identifier) (r *vagrant_proto.Content, err error) {
r = &vagrant_proto.Content{}
r.Value, err = s.Impl.Read(req.Name)
return
}
func (s *GRPCIOServer) Write(ctx context.Context, req *vagrant_proto.Content) (r *vagrant_proto.WriteResponse, err error) {
r = &vagrant_proto.WriteResponse{}
bytes := 0
bytes, err = s.Impl.Write(req.Value, req.Target)
if err != nil {
return
}
r.Length = int32(bytes)
return
}
type GRPCIOClient struct {
client vagrant_proto.IOClient
doneCtx context.Context
}
func (c *GRPCIOClient) Read(target string) (content string, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.Read(jctx, &vagrant_proto.Identifier{
Name: target})
if err != nil {
return content, handleGrpcError(err, c.doneCtx, ctx)
}
content = resp.Value
return
}
func (c *GRPCIOClient) Write(content, target string) (length int, err error) {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.Write(jctx, &vagrant_proto.Content{
Value: content,
Target: target})
if err != nil {
return length, handleGrpcError(err, c.doneCtx, ctx)
}
length = int(resp.Length)
return
}
func (i *IOPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
vagrant_proto.RegisterIOServer(s, &GRPCIOServer{Impl: i.Impl})
return nil
}
func (i *IOPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &GRPCIOClient{
client: vagrant_proto.NewIOClient(c),
doneCtx: ctx}, nil
}

View File

@ -0,0 +1,89 @@
package plugin
import (
"testing"
"github.com/hashicorp/go-plugin"
)
type MockIO struct {
Core
}
func TestIO_ReadWrite(t *testing.T) {
ioplugin := &MockIO{}
ioplugin.Init()
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"io": &IOPlugin{Impl: ioplugin}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("io")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(IO)
if !ok {
t.Fatalf("bad %#v", raw)
}
go func() {
length, err := impl.Write("test_message", "stdout")
if err != nil {
t.Fatalf("bad write: %s", err)
}
if length != len("test_message") {
t.Fatalf("bad length %d != %d", length, len("test_message"))
}
}()
resp, err := impl.Read("stdout")
if err != nil {
t.Fatalf("bad read: %s", err)
}
if resp != "test_message" {
t.Errorf("%s != test_message", resp)
}
}
func TestIO_Write_bad(t *testing.T) {
ioplugin := &MockIO{}
ioplugin.Init()
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"io": &IOPlugin{Impl: ioplugin}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("io")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(IO)
if !ok {
t.Fatalf("bad %#v", raw)
}
_, err = impl.Write("test_message", "bad-target")
if err == nil {
t.Fatalf("illegal write")
}
}
func TestIO_Read_bad(t *testing.T) {
ioplugin := &MockIO{}
ioplugin.Init()
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"io": &IOPlugin{Impl: ioplugin}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("io")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(IO)
if !ok {
t.Fatalf("bad %#v", raw)
}
_, err = impl.Read("bad-target")
if err == nil {
t.Fatalf("illegal read")
}
}

View File

@ -0,0 +1,39 @@
package plugin
import (
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
type Meta interface {
Init()
vagrant.IOServer
}
type GRPCCoreClient struct{}
func (c *GRPCCoreClient) Init() {}
func (c *GRPCCoreClient) Streams() (s map[string]chan (string)) { return }
type Core struct {
vagrant.IOServer
io vagrant.StreamIO
}
func (c *Core) Init() {
if c.io == nil {
c.io = &vagrant.IOSrv{
map[string]chan (string){
"stdout": make(chan string),
"stderr": make(chan string),
},
}
}
}
func (c *Core) Read(target string) (string, error) {
return c.io.Read(target)
}
func (c *Core) Write(content, target string) (int, error) {
return c.io.Write(content, target)
}

View File

@ -0,0 +1,300 @@
package plugin
import (
"context"
"errors"
"time"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
type MockGuestCapabilities struct{ Core }
func (g *MockGuestCapabilities) GuestCapabilities() (caps []vagrant.SystemCapability, err error) {
caps = []vagrant.SystemCapability{
vagrant.SystemCapability{Name: "test_cap", Platform: "testOS"}}
return
}
func (g *MockGuestCapabilities) GuestCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, m *vagrant.Machine) (result interface{}, err error) {
if args != nil {
arguments := args.([]interface{})
if arguments[0] == "pause" {
time.Sleep(1 * time.Second)
}
if len(arguments) > 0 {
result = []string{
cap.Name,
arguments[0].(string)}
return
}
}
result = []string{cap.Name}
return
}
type MockHostCapabilities struct{ Core }
func (h *MockHostCapabilities) HostCapabilities() (caps []vagrant.SystemCapability, err error) {
caps = []vagrant.SystemCapability{
vagrant.SystemCapability{Name: "test_cap", Platform: "testOS"}}
return
}
func (h *MockHostCapabilities) HostCapability(ctx context.Context, cap *vagrant.SystemCapability, args interface{}, e *vagrant.Environment) (result interface{}, err error) {
if args != nil {
arguments := args.([]interface{})
if arguments[0] == "pause" {
time.Sleep(1 * time.Second)
}
if len(arguments) > 0 {
result = []string{
cap.Name,
arguments[0].(string)}
return
}
}
result = []string{cap.Name}
return
}
type MockProviderCapabilities struct{ Core }
func (p *MockProviderCapabilities) ProviderCapabilities() (caps []vagrant.ProviderCapability, err error) {
caps = []vagrant.ProviderCapability{
vagrant.ProviderCapability{Name: "test_cap", Provider: "testProvider"}}
return
}
func (p *MockProviderCapabilities) ProviderCapability(ctx context.Context, cap *vagrant.ProviderCapability, args interface{}, m *vagrant.Machine) (result interface{}, err error) {
if args != nil {
arguments := args.([]interface{})
if arguments[0] == "pause" {
time.Sleep(1 * time.Second)
}
if len(arguments) > 0 {
result = []string{
cap.Name,
arguments[0].(string)}
return
}
}
result = []string{cap.Name}
return
}
type MockConfig struct {
Core
}
func (c *MockConfig) ConfigAttributes() (attrs []string, err error) {
attrs = []string{"fubar", "foobar"}
return
}
func (c *MockConfig) ConfigLoad(ctx context.Context, data map[string]interface{}) (loaddata map[string]interface{}, err error) {
if data["pause"] == true {
time.Sleep(1 * time.Second)
}
loaddata = map[string]interface{}{
"test_key": "test_val"}
if data["test_key"] != nil {
loaddata["sent_key"] = data["test_key"]
}
return
}
func (c *MockConfig) ConfigValidate(ctx context.Context, data map[string]interface{}, m *vagrant.Machine) (errors []string, err error) {
errors = []string{"test error"}
if data["pause"] == true {
time.Sleep(1 * time.Second)
}
return
}
func (c *MockConfig) ConfigFinalize(ctx context.Context, data map[string]interface{}) (finaldata map[string]interface{}, err error) {
finaldata = make(map[string]interface{})
for key, tval := range data {
val, ok := tval.(string)
if !ok {
continue
}
finaldata[key] = val + "-updated"
}
if data["pause"] == true {
time.Sleep(1 * time.Second)
}
return
}
type MockProvider struct {
Core
vagrant.NoConfig
vagrant.NoGuestCapabilities
vagrant.NoHostCapabilities
vagrant.NoProviderCapabilities
}
func (c *MockProvider) Action(ctx context.Context, actionName string, m *vagrant.Machine) (actions []string, err error) {
switch actionName {
case "valid":
actions = []string{"self::DoTask"}
case "pause":
time.Sleep(1 * time.Second)
default:
err = errors.New("Unknown action requested")
}
return
}
func (c *MockProvider) IsInstalled(ctx context.Context, m *vagrant.Machine) (bool, error) {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return true, nil
}
func (c *MockProvider) IsUsable(ctx context.Context, m *vagrant.Machine) (bool, error) {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return true, nil
}
func (c *MockProvider) MachineIdChanged(ctx context.Context, m *vagrant.Machine) error {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return nil
}
func (c *MockProvider) Name() string {
return "mock_provider"
}
func (c *MockProvider) RunAction(ctx context.Context, actionName string, args interface{}, m *vagrant.Machine) (r interface{}, err error) {
switch actionName {
case "send_output":
m.UI.Say("test_output_p")
case "pause":
time.Sleep(1 * time.Second)
case "valid":
default:
return nil, errors.New("invalid action name")
}
var arguments []interface{}
if args != nil {
arguments = args.([]interface{})
} else {
arguments = []interface{}{"unset"}
}
r = []string{
actionName,
arguments[0].(string)}
return
}
func (c *MockProvider) SshInfo(ctx context.Context, m *vagrant.Machine) (*vagrant.SshInfo, error) {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return &vagrant.SshInfo{
Host: "localhost",
Port: 2222}, nil
}
func (c *MockProvider) State(ctx context.Context, m *vagrant.Machine) (*vagrant.MachineState, error) {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return &vagrant.MachineState{
Id: "default",
ShortDesc: "running"}, nil
}
func (c *MockProvider) Info() *vagrant.ProviderInfo {
return &vagrant.ProviderInfo{
Description: "Custom",
Priority: 10}
}
type MockSyncedFolder struct {
Core
vagrant.NoGuestCapabilities
vagrant.NoHostCapabilities
}
func (s *MockSyncedFolder) Cleanup(ctx context.Context, m *vagrant.Machine, opts vagrant.FolderOptions) error {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
if opts != nil {
err, _ := opts["error"].(bool)
ui, _ := opts["ui"].(bool)
if err {
return errors.New("cleanup error")
}
if ui {
m.UI.Say("test_output_sf")
return nil
}
}
return nil
}
func (s *MockSyncedFolder) Disable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, opts vagrant.FolderOptions) error {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
if opts != nil && opts["error"].(bool) {
return errors.New("disable error")
}
return nil
}
func (s *MockSyncedFolder) Enable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, opts vagrant.FolderOptions) error {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
if opts != nil && opts["error"].(bool) {
return errors.New("enable error")
}
return nil
}
func (s *MockSyncedFolder) Info() *vagrant.SyncedFolderInfo {
return &vagrant.SyncedFolderInfo{
Description: "mock_folder",
Priority: 100}
}
func (s *MockSyncedFolder) IsUsable(ctx context.Context, m *vagrant.Machine) (bool, error) {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
return true, nil
}
func (s *MockSyncedFolder) Name() string {
return "mock_folder"
}
func (s *MockSyncedFolder) Prepare(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, opts vagrant.FolderOptions) error {
if m.Name == "pause" {
time.Sleep(1 * time.Second)
}
if opts != nil && opts["error"].(bool) {
return errors.New("prepare error")
}
return nil
}

View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
echo -n "Parsing proto files and generating go output... "
for i in *
do
if [ -d "${i}" ]; then
protoc --proto_path=`go env GOPATH`/src --proto_path=. --go_out=plugins=grpc:. "${i}"/*.proto;
if [ $? -ne 0 ]; then
echo "failed!"
exit 1
fi
fi
done
protoc --proto_path=`go env GOPATH`/src --proto_path=. --go_out=plugins=grpc:. *.proto;
if [ $? -ne 0 ]; then
echo "failed!"
exit 1
fi
echo "done!"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,196 @@
syntax = "proto3";
package vagrant.proto;
message Empty{}
message Machine {
string machine = 1;
}
message Valid {
bool result = 1;
}
message Identifier {
string name = 1;
}
message PluginInfo {
string description = 1;
int64 priority = 2;
}
message Content {
string target = 1;
string value = 2;
}
message WriteResponse {
int32 length = 1;
}
service IO {
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
}
message SystemCapability {
string name = 1;
string platform = 2;
}
message ProviderCapability {
string name = 1;
string provider = 2;
}
message SystemCapabilityList {
repeated SystemCapability capabilities = 1;
}
message ProviderCapabilityList {
repeated ProviderCapability capabilities = 1;
}
message GenericResponse {
string result = 1;
}
message GuestCapabilityRequest {
SystemCapability capability = 1;
string machine = 2;
string arguments = 3;
}
message HostCapabilityRequest {
SystemCapability capability = 1;
string environment = 2;
string arguments = 3;
}
message ProviderCapabilityRequest {
ProviderCapability capability = 1;
string machine = 2;
string arguments = 3;
}
service GuestCapabilities {
rpc GuestCapabilities(Empty) returns (SystemCapabilityList);
rpc GuestCapability(GuestCapabilityRequest) returns (GenericResponse);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
}
service HostCapabilities {
rpc HostCapabilities(Empty) returns (SystemCapabilityList);
rpc HostCapability(HostCapabilityRequest) returns (GenericResponse);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
}
service ProviderCapabilities {
rpc ProviderCapabilities (Empty) returns (ProviderCapabilityList);
rpc ProviderCapability (ProviderCapabilityRequest) returns (GenericResponse);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
}
message Configuration {
string data = 1;
string machine = 2;
}
message ListResponse {
repeated string items = 1;
}
service Config {
rpc ConfigAttributes(Empty) returns (ListResponse);
rpc ConfigLoad(Configuration) returns (Configuration);
rpc ConfigValidate(Configuration) returns (ListResponse);
rpc ConfigFinalize(Configuration) returns (Configuration);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
}
message SyncedFolders {
string machine = 1;
string folders = 2;
string options = 3;
}
service SyncedFolder {
rpc Cleanup(SyncedFolders) returns (Empty);
rpc Disable(SyncedFolders) returns (Empty);
rpc Enable(SyncedFolders) returns (Empty);
rpc Info(Empty) returns (PluginInfo);
rpc IsUsable(Machine) returns (Valid);
rpc Name(Empty) returns (Identifier);
rpc Prepare(SyncedFolders) returns (Empty);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
// Guest capabilities helpers (copied from GuestCapabilities service)
rpc GuestCapabilities(Empty) returns (SystemCapabilityList);
rpc GuestCapability(GuestCapabilityRequest) returns (GenericResponse);
// Host capabilities helpers (copied from GuestCapabilities service)
rpc HostCapabilities(Empty) returns (SystemCapabilityList);
rpc HostCapability(HostCapabilityRequest) returns (GenericResponse);
}
message GenericAction {
string name = 1;
string machine = 2;
}
message ExecuteAction {
string name = 1;
string data = 2;
string machine = 3;
}
message MachineSshInfo {
string host = 1;
int64 port = 2;
string private_key_path = 3;
string username = 4;
}
message MachineState {
string id = 1;
string short_description = 2;
string long_description = 3;
}
service Provider {
rpc Action(GenericAction) returns (ListResponse);
rpc Info(Empty) returns (PluginInfo);
rpc IsInstalled(Machine) returns (Valid);
rpc IsUsable(Machine) returns (Valid);
rpc MachineIdChanged(Machine) returns (Machine);
rpc Name(Empty) returns (Identifier);
rpc RunAction(ExecuteAction) returns (GenericResponse);
rpc SshInfo(Machine) returns (MachineSshInfo);
rpc State(Machine) returns (MachineState);
// IO helpers for streaming (copied from Stream service)
rpc Read(Identifier) returns (Content);
rpc Write(Content) returns (WriteResponse);
// Config helpers (copied from Config service)
rpc ConfigAttributes(Empty) returns (ListResponse);
rpc ConfigLoad(Configuration) returns (Configuration);
rpc ConfigValidate(Configuration) returns (ListResponse);
rpc ConfigFinalize(Configuration) returns (Configuration);
// Guest capabilities helpers (copied from GuestCapabilities service)
rpc GuestCapabilities(Empty) returns (SystemCapabilityList);
rpc GuestCapability(GuestCapabilityRequest) returns (GenericResponse);
// Host capabilities helpers (copied from HostCapabilities service)
rpc HostCapabilities(Empty) returns (SystemCapabilityList);
rpc HostCapability(HostCapabilityRequest) returns (GenericResponse);
// Provider capabilities helpers (copied from ProviderCapabilities service)
rpc ProviderCapabilities (Empty) returns (ProviderCapabilityList);
rpc ProviderCapability (ProviderCapabilityRequest) returns (GenericResponse);
}

View File

@ -0,0 +1,346 @@
package plugin
import (
"context"
"encoding/json"
"google.golang.org/grpc"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
"github.com/LK4D4/joincontext"
)
type Provider interface {
vagrant.Provider
Meta
}
type ProviderPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl Provider
}
type GRPCProviderClient struct {
GRPCCoreClient
GRPCConfigClient
GRPCGuestCapabilitiesClient
GRPCHostCapabilitiesClient
GRPCProviderCapabilitiesClient
GRPCIOClient
client vagrant_proto.ProviderClient
doneCtx context.Context
}
func (c *GRPCProviderClient) Action(ctx context.Context, actionName string, m *vagrant.Machine) (r []string, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.Action(jctx, &vagrant_proto.GenericAction{
Name: actionName,
Machine: machData})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
r = resp.Items
return
}
func (c *GRPCProviderClient) Info() *vagrant.ProviderInfo {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.Info(jctx, &vagrant_proto.Empty{})
if err != nil {
return &vagrant.ProviderInfo{}
}
return &vagrant.ProviderInfo{
Description: resp.Description,
Priority: resp.Priority}
}
func (c *GRPCProviderClient) IsInstalled(ctx context.Context, m *vagrant.Machine) (r bool, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.IsInstalled(jctx, &vagrant_proto.Machine{
Machine: machData})
if err != nil {
return false, handleGrpcError(err, c.doneCtx, ctx)
}
r = resp.Result
return
}
func (c *GRPCProviderClient) IsUsable(ctx context.Context, m *vagrant.Machine) (r bool, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.IsUsable(jctx, &vagrant_proto.Machine{
Machine: machData})
if err != nil {
return false, handleGrpcError(err, c.doneCtx, ctx)
}
r = resp.Result
return
}
func (c *GRPCProviderClient) MachineIdChanged(ctx context.Context, m *vagrant.Machine) (err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
_, err = c.client.MachineIdChanged(jctx, &vagrant_proto.Machine{
Machine: machData})
if err != nil {
return handleGrpcError(err, c.doneCtx, ctx)
}
return
}
func (c *GRPCProviderClient) RunAction(ctx context.Context, actName string, args interface{}, m *vagrant.Machine) (r interface{}, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
runData, err := json.Marshal(args)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.RunAction(jctx, &vagrant_proto.ExecuteAction{
Name: actName,
Data: string(runData),
Machine: machData})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
err = json.Unmarshal([]byte(resp.Result), &r)
if err != nil {
return
}
return
}
func (c *GRPCProviderClient) SshInfo(ctx context.Context, m *vagrant.Machine) (r *vagrant.SshInfo, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.SshInfo(jctx, &vagrant_proto.Machine{
Machine: machData})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
r = &vagrant.SshInfo{
Host: resp.Host,
Port: resp.Port,
PrivateKeyPath: resp.PrivateKeyPath,
Username: resp.Username}
return
}
func (c *GRPCProviderClient) State(ctx context.Context, m *vagrant.Machine) (r *vagrant.MachineState, err error) {
machData, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.State(jctx, &vagrant_proto.Machine{
Machine: machData})
if err != nil {
return nil, handleGrpcError(err, c.doneCtx, ctx)
}
r = &vagrant.MachineState{
Id: resp.Id,
ShortDesc: resp.ShortDescription,
LongDesc: resp.LongDescription}
return
}
func (c *GRPCProviderClient) Name() string {
ctx := context.Background()
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.Name(jctx, &vagrant_proto.Empty{})
if err != nil {
return ""
}
return resp.Name
}
func (p *ProviderPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewProviderClient(c)
return &GRPCProviderClient{
GRPCConfigClient: GRPCConfigClient{
client: client,
doneCtx: ctx},
GRPCGuestCapabilitiesClient: GRPCGuestCapabilitiesClient{
client: client,
doneCtx: ctx},
GRPCHostCapabilitiesClient: GRPCHostCapabilitiesClient{
client: client,
doneCtx: ctx},
GRPCProviderCapabilitiesClient: GRPCProviderCapabilitiesClient{
client: client,
doneCtx: ctx},
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx},
client: client,
doneCtx: ctx,
}, nil
}
func (p *ProviderPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
p.Impl.Init()
vagrant_proto.RegisterProviderServer(s, &GRPCProviderServer{
Impl: p.Impl,
GRPCConfigServer: GRPCConfigServer{
Impl: p.Impl},
GRPCGuestCapabilitiesServer: GRPCGuestCapabilitiesServer{
Impl: p.Impl},
GRPCHostCapabilitiesServer: GRPCHostCapabilitiesServer{
Impl: p.Impl},
GRPCProviderCapabilitiesServer: GRPCProviderCapabilitiesServer{
Impl: p.Impl},
GRPCIOServer: GRPCIOServer{
Impl: p.Impl}})
return nil
}
type GRPCProviderServer struct {
GRPCIOServer
GRPCConfigServer
GRPCGuestCapabilitiesServer
GRPCHostCapabilitiesServer
GRPCProviderCapabilitiesServer
Impl Provider
}
func (s *GRPCProviderServer) Action(ctx context.Context, req *vagrant_proto.GenericAction) (resp *vagrant_proto.ListResponse, err error) {
resp = &vagrant_proto.ListResponse{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
r, err := s.Impl.Action(ctx, req.Name, m)
if err != nil {
return
}
resp.Items = r
return
}
func (s *GRPCProviderServer) RunAction(ctx context.Context, req *vagrant_proto.ExecuteAction) (resp *vagrant_proto.GenericResponse, err error) {
resp = &vagrant_proto.GenericResponse{}
var args interface{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
if err = json.Unmarshal([]byte(req.Data), &args); err != nil {
return
}
r, err := s.Impl.RunAction(ctx, req.Name, args, m)
if err != nil {
return
}
result, err := json.Marshal(r)
if err != nil {
return
}
resp.Result = string(result)
return
}
func (s *GRPCProviderServer) Info(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.PluginInfo, err error) {
resp = &vagrant_proto.PluginInfo{}
r := s.Impl.Info()
resp.Description = r.Description
resp.Priority = r.Priority
return
}
func (s *GRPCProviderServer) IsInstalled(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Valid, err error) {
resp = &vagrant_proto.Valid{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
resp.Result, err = s.Impl.IsInstalled(ctx, m)
return
}
func (s *GRPCProviderServer) IsUsable(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Valid, err error) {
resp = &vagrant_proto.Valid{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
resp.Result, err = s.Impl.IsUsable(ctx, m)
return
}
func (s *GRPCProviderServer) SshInfo(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.MachineSshInfo, err error) {
resp = &vagrant_proto.MachineSshInfo{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
r, err := s.Impl.SshInfo(ctx, m)
if err != nil {
return
}
resp.Host = r.Host
resp.Port = r.Port
resp.Username = r.Username
resp.PrivateKeyPath = r.PrivateKeyPath
return
}
func (s *GRPCProviderServer) State(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.MachineState, err error) {
resp = &vagrant_proto.MachineState{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
r, err := s.Impl.State(ctx, m)
if err != nil {
return
}
resp.Id = r.Id
resp.ShortDescription = r.ShortDesc
resp.LongDescription = r.LongDesc
return
}
func (s *GRPCProviderServer) MachineIdChanged(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Machine, err error) {
resp = &vagrant_proto.Machine{}
m, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
if err = s.Impl.MachineIdChanged(ctx, m); err != nil {
return
}
mdata, err := vagrant.DumpMachine(m)
if err != nil {
return
}
resp = &vagrant_proto.Machine{Machine: mdata}
return
}
func (s *GRPCProviderServer) Name(ctx context.Context, req *vagrant_proto.Empty) (*vagrant_proto.Identifier, error) {
return &vagrant_proto.Identifier{Name: s.Impl.Name()}, nil
}

View File

@ -0,0 +1,667 @@
package plugin
import (
"context"
"strings"
"testing"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
func TestProvider_Action(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.Action(context.Background(), "valid", &vagrant.Machine{})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if resp[0] != "self::DoTask" {
t.Errorf("%s != self::DoTask", resp[0])
}
}
func TestProvider_Action_invalid(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
_, err = impl.Action(context.Background(), "invalid", &vagrant.Machine{})
if err == nil {
t.Errorf("illegal action")
}
}
func TestProvider_Action_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.Action(ctx, "pause", &vagrant.Machine{})
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_Action_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.Action(ctx, "pause", &vagrant.Machine{})
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_IsInstalled(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
installed, err := impl.IsInstalled(context.Background(), &vagrant.Machine{})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if !installed {
t.Errorf("bad result")
}
}
func TestProvider_IsInstalled_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.IsInstalled(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_IsInstalled_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.IsInstalled(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_IsUsable(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
usable, err := impl.IsUsable(context.Background(), &vagrant.Machine{})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if !usable {
t.Errorf("bad result")
}
}
func TestProvider_IsUsable_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_IsUsable_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_MachineIdChanged(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
err = impl.MachineIdChanged(context.Background(), &vagrant.Machine{})
if err != nil {
t.Errorf("err: %s", err)
}
}
func TestProvider_MachineIdChanged_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
err = impl.MachineIdChanged(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_MachineIdChanged_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
err = impl.MachineIdChanged(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_Name(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp := impl.Name()
if resp != "mock_provider" {
t.Errorf("%s != mock_provider", resp)
}
}
func TestProvider_RunAction(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
args := []string{"test_arg", "other_arg"}
m := &vagrant.Machine{}
resp, err := impl.RunAction(context.Background(), "valid", args, m)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
result := resp.([]interface{})
if result[0] != "valid" {
t.Errorf("%s != valid", result[0])
}
if result[1] != "test_arg" {
t.Errorf("%s != test_arg", result[1])
}
}
func TestProvider_RunAction_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
args := []string{"test_arg", "other_arg"}
m := &vagrant.Machine{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.RunAction(ctx, "pause", args, m)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_RunAction_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
args := []string{"test_arg", "other_arg"}
m := &vagrant.Machine{}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.RunAction(ctx, "pause", args, m)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestProvider_RunAction_invalid(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
args := []string{"test_arg", "other_arg"}
m := &vagrant.Machine{}
_, err = impl.RunAction(context.Background(), "invalid", args, m)
if err == nil {
t.Fatalf("illegal action run")
}
}
func TestProvider_SshInfo(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.SshInfo(context.Background(), &vagrant.Machine{})
if err != nil {
t.Fatalf("invalid resp: %s", err)
}
if resp.Host != "localhost" {
t.Errorf("%s != localhost", resp.Host)
}
if resp.Port != 2222 {
t.Errorf("%d != 2222", resp.Port)
}
}
func TestProvider_SshInfo_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.SshInfo(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("invalid resp: %s", err)
}
}
func TestProvider_SshInfo_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.SshInfo(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("invalid resp: %s", err)
}
}
func TestProvider_State(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.State(context.Background(), &vagrant.Machine{})
if err != nil {
t.Fatalf("invalid resp: %s", err)
}
if resp.Id != "default" {
t.Errorf("%s != default", resp.Id)
}
if resp.ShortDesc != "running" {
t.Errorf("%s != running", resp.ShortDesc)
}
}
func TestProvider_State_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.State(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("invalid resp: %s", err)
}
}
func TestProvider_State_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.State(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("invalid resp: %s", err)
}
}
func TestProvider_Info(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp := impl.Info()
if resp.Description != "Custom" {
t.Errorf("%s != Custom", resp.Description)
}
if resp.Priority != 10 {
t.Errorf("%d != 10", resp.Priority)
}
}
func TestProvider_MachineUI_output(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"provider": &ProviderPlugin{Impl: &MockProvider{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("provider")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(Provider)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx := context.Background()
go func() {
_, err = impl.RunAction(ctx, "send_output", nil, &vagrant.Machine{})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
}()
resp, err := impl.Read("stdout")
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if !strings.Contains(resp, "test_output_p") {
t.Errorf("%s !~ test_output_p", resp)
}
}

View File

@ -0,0 +1,283 @@
package plugin
import (
"context"
"encoding/json"
"google.golang.org/grpc"
go_plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant/plugin/proto"
"github.com/LK4D4/joincontext"
)
type SyncedFolder interface {
vagrant.SyncedFolder
Meta
}
type SyncedFolderPlugin struct {
go_plugin.NetRPCUnsupportedPlugin
Impl SyncedFolder
}
type GRPCSyncedFolderClient struct {
GRPCCoreClient
GRPCGuestCapabilitiesClient
GRPCHostCapabilitiesClient
GRPCIOClient
client vagrant_proto.SyncedFolderClient
doneCtx context.Context
}
func (c *GRPCSyncedFolderClient) Cleanup(ctx context.Context, m *vagrant.Machine, o vagrant.FolderOptions) (err error) {
machine, err := vagrant.DumpMachine(m)
if err != nil {
return
}
opts, err := json.Marshal(o)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
_, err = c.client.Cleanup(jctx, &vagrant_proto.SyncedFolders{
Machine: machine,
Options: string(opts)})
return handleGrpcError(err, c.doneCtx, ctx)
}
func (c *GRPCSyncedFolderClient) Disable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, o vagrant.FolderOptions) (err error) {
machine, err := vagrant.DumpMachine(m)
if err != nil {
return
}
folders, err := json.Marshal(f)
if err != nil {
return
}
opts, err := json.Marshal(o)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
_, err = c.client.Disable(jctx, &vagrant_proto.SyncedFolders{
Machine: machine,
Folders: string(folders),
Options: string(opts)})
return handleGrpcError(err, c.doneCtx, ctx)
}
func (c *GRPCSyncedFolderClient) Enable(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, o vagrant.FolderOptions) (err error) {
machine, err := vagrant.DumpMachine(m)
if err != nil {
return
}
folders, err := json.Marshal(f)
if err != nil {
return
}
opts, err := json.Marshal(o)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
_, err = c.client.Enable(jctx, &vagrant_proto.SyncedFolders{
Machine: machine,
Folders: string(folders),
Options: string(opts)})
return handleGrpcError(err, c.doneCtx, ctx)
}
func (c *GRPCSyncedFolderClient) Info() *vagrant.SyncedFolderInfo {
resp, err := c.client.Info(context.Background(), &vagrant_proto.Empty{})
if err != nil {
return &vagrant.SyncedFolderInfo{}
}
return &vagrant.SyncedFolderInfo{
Description: resp.Description,
Priority: resp.Priority}
}
func (c *GRPCSyncedFolderClient) IsUsable(ctx context.Context, m *vagrant.Machine) (u bool, err error) {
machine, err := vagrant.DumpMachine(m)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
resp, err := c.client.IsUsable(jctx, &vagrant_proto.Machine{
Machine: machine})
if err != nil {
return false, handleGrpcError(err, c.doneCtx, ctx)
}
u = resp.Result
return
}
func (c *GRPCSyncedFolderClient) Name() string {
resp, err := c.client.Name(context.Background(), &vagrant_proto.Empty{})
if err != nil {
return ""
}
return resp.Name
}
func (c *GRPCSyncedFolderClient) Prepare(ctx context.Context, m *vagrant.Machine, f vagrant.FolderList, o vagrant.FolderOptions) (err error) {
machine, err := vagrant.DumpMachine(m)
if err != nil {
return
}
folders, err := json.Marshal(f)
if err != nil {
return
}
opts, err := json.Marshal(o)
if err != nil {
return
}
jctx, _ := joincontext.Join(ctx, c.doneCtx)
_, err = c.client.Prepare(jctx, &vagrant_proto.SyncedFolders{
Machine: machine,
Folders: string(folders),
Options: string(opts)})
return handleGrpcError(err, c.doneCtx, ctx)
}
type GRPCSyncedFolderServer struct {
GRPCGuestCapabilitiesServer
GRPCHostCapabilitiesServer
GRPCIOServer
Impl SyncedFolder
}
func (s *GRPCSyncedFolderServer) Cleanup(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
resp = &vagrant_proto.Empty{}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
var options vagrant.FolderOptions
err = json.Unmarshal([]byte(req.Options), &options)
if err != nil {
return
}
err = s.Impl.Cleanup(ctx, machine, options)
return
}
func (s *GRPCSyncedFolderServer) Disable(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
resp = &vagrant_proto.Empty{}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
var folders vagrant.FolderList
err = json.Unmarshal([]byte(req.Folders), &folders)
if err != nil {
return
}
var options vagrant.FolderOptions
err = json.Unmarshal([]byte(req.Options), &options)
if err != nil {
return
}
err = s.Impl.Disable(ctx, machine, folders, options)
return
}
func (s *GRPCSyncedFolderServer) Enable(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
resp = &vagrant_proto.Empty{}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
var folders vagrant.FolderList
err = json.Unmarshal([]byte(req.Folders), &folders)
if err != nil {
return
}
var options vagrant.FolderOptions
err = json.Unmarshal([]byte(req.Options), &options)
if err != nil {
return
}
err = s.Impl.Enable(ctx, machine, folders, options)
return
}
func (s *GRPCSyncedFolderServer) Info(ctx context.Context, req *vagrant_proto.Empty) (resp *vagrant_proto.PluginInfo, err error) {
resp = &vagrant_proto.PluginInfo{}
r := s.Impl.Info()
resp.Description = r.Description
resp.Priority = r.Priority
return
}
func (s *GRPCSyncedFolderServer) IsUsable(ctx context.Context, req *vagrant_proto.Machine) (resp *vagrant_proto.Valid, err error) {
resp = &vagrant_proto.Valid{}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
r, err := s.Impl.IsUsable(ctx, machine)
if err != nil {
return
}
resp.Result = r
return
}
func (s *GRPCSyncedFolderServer) Name(_ context.Context, req *vagrant_proto.Empty) (*vagrant_proto.Identifier, error) {
return &vagrant_proto.Identifier{Name: s.Impl.Name()}, nil
}
func (s *GRPCSyncedFolderServer) Prepare(ctx context.Context, req *vagrant_proto.SyncedFolders) (resp *vagrant_proto.Empty, err error) {
resp = &vagrant_proto.Empty{}
machine, err := vagrant.LoadMachine(req.Machine, s.Impl)
if err != nil {
return
}
var folders vagrant.FolderList
err = json.Unmarshal([]byte(req.Folders), &folders)
if err != nil {
return
}
var options vagrant.FolderOptions
err = json.Unmarshal([]byte(req.Options), &options)
if err != nil {
return
}
err = s.Impl.Prepare(ctx, machine, folders, options)
return
}
func (f *SyncedFolderPlugin) GRPCServer(broker *go_plugin.GRPCBroker, s *grpc.Server) error {
f.Impl.Init()
vagrant_proto.RegisterSyncedFolderServer(s,
&GRPCSyncedFolderServer{
Impl: f.Impl,
GRPCIOServer: GRPCIOServer{
Impl: f.Impl},
GRPCGuestCapabilitiesServer: GRPCGuestCapabilitiesServer{
Impl: f.Impl},
GRPCHostCapabilitiesServer: GRPCHostCapabilitiesServer{
Impl: f.Impl}})
return nil
}
func (f *SyncedFolderPlugin) GRPCClient(ctx context.Context, broker *go_plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
client := vagrant_proto.NewSyncedFolderClient(c)
return &GRPCSyncedFolderClient{
GRPCIOClient: GRPCIOClient{
client: client,
doneCtx: ctx},
GRPCGuestCapabilitiesClient: GRPCGuestCapabilitiesClient{
client: client,
doneCtx: ctx},
GRPCHostCapabilitiesClient: GRPCHostCapabilitiesClient{
client: client,
doneCtx: ctx},
client: client,
doneCtx: ctx}, nil
}

View File

@ -0,0 +1,562 @@
package plugin
import (
"context"
"strings"
"testing"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/vagrant/ext/go-plugin/vagrant"
)
func TestSyncedFolder_Cleanup(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
err = impl.Cleanup(context.Background(), &vagrant.Machine{}, nil)
if err != nil {
t.Fatalf("bad resp: %#v", err)
}
}
func TestSyncedFolder_Cleanup_error(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
args := map[string]interface{}{
"error": true}
err = impl.Cleanup(context.Background(), &vagrant.Machine{}, args)
if err == nil {
t.Fatalf("illegal cleanup")
}
if err.Error() != "cleanup error" {
t.Errorf("%s != cleanup error", err.Error())
}
}
func TestSyncedFolder_Cleanup_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
err = impl.Cleanup(ctx, &vagrant.Machine{Name: "pause"}, nil)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Cleanup_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
err = impl.Cleanup(ctx, &vagrant.Machine{Name: "pause"}, nil)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Disable(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
err = impl.Disable(context.Background(), &vagrant.Machine{}, nil, nil)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Disable_error(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
folders := map[string]interface{}{
"folder_name": "options"}
args := map[string]interface{}{
"error": true}
err = impl.Disable(context.Background(), &vagrant.Machine{}, folders, args)
if err == nil {
t.Fatalf("illegal disable")
}
if err.Error() != "disable error" {
t.Errorf("%s != disable error", err.Error())
}
}
func TestSyncedFolder_Disable_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
err = impl.Disable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Disable_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
err = impl.Disable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Enable(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
err = impl.Enable(context.Background(), &vagrant.Machine{}, nil, nil)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Enable_error(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
folders := map[string]interface{}{
"folder_name": "options"}
args := map[string]interface{}{
"error": true}
err = impl.Enable(context.Background(), &vagrant.Machine{}, folders, args)
if err == nil {
t.Fatalf("illegal enable")
}
if err.Error() != "enable error" {
t.Errorf("%s != enable error", err.Error())
}
}
func TestSyncedFolder_Enable_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
err = impl.Enable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Enable_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
err = impl.Enable(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Prepare(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
err = impl.Prepare(context.Background(), &vagrant.Machine{}, nil, nil)
if err != nil {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Prepare_error(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
folders := map[string]interface{}{
"folder_name": "options"}
args := map[string]interface{}{
"error": true}
err = impl.Prepare(context.Background(), &vagrant.Machine{}, folders, args)
if err == nil {
t.Fatalf("illegal prepare")
}
if err.Error() != "prepare error" {
t.Errorf("%s != prepare error", err.Error())
}
}
func TestSyncedFolder_Prepare_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
err = impl.Prepare(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Prepare_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
err = impl.Prepare(ctx, &vagrant.Machine{Name: "pause"}, nil, nil)
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Info(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp := impl.Info()
if resp == nil {
t.Fatalf("bad resp")
}
if resp.Description != "mock_folder" {
t.Errorf("%s != mock_folder", resp.Description)
}
if resp.Priority != 100 {
t.Errorf("%d != 100", resp.Priority)
}
}
func TestSyncedFolder_IsUsable(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp, err := impl.IsUsable(context.Background(), &vagrant.Machine{})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if !resp {
t.Errorf("bad result")
}
}
func TestSyncedFolder_IsUsable_context_cancel(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
time.Sleep(2 * time.Millisecond)
cancel()
}()
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
if err != context.Canceled {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_IsUsable_context_timeout(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
_, err = impl.IsUsable(ctx, &vagrant.Machine{Name: "pause"})
if err != context.DeadlineExceeded {
t.Fatalf("bad resp: %s", err)
}
}
func TestSyncedFolder_Name(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
resp := impl.Name()
if resp != "mock_folder" {
t.Errorf("%s != mock_folder", resp)
}
}
func TestSyncedFolder_MachineUI_output(t *testing.T) {
client, server := plugin.TestPluginGRPCConn(t, map[string]plugin.Plugin{
"folder": &SyncedFolderPlugin{Impl: &MockSyncedFolder{}}})
defer server.Stop()
defer client.Close()
raw, err := client.Dispense("folder")
if err != nil {
t.Fatalf("err: %s", err)
}
impl, ok := raw.(SyncedFolder)
if !ok {
t.Fatalf("bad %#v", raw)
}
go func() {
err := impl.Cleanup(context.Background(), &vagrant.Machine{}, map[string]interface{}{"ui": true})
if err != nil {
t.Fatalf("bad resp: %s", err)
}
}()
resp, err := impl.Read("stdout")
if err != nil {
t.Fatalf("bad resp: %s", err)
}
if !strings.Contains(resp, "test_output") {
t.Errorf("%s !~ test_output", resp)
}
}

View File

@ -0,0 +1,27 @@
package vagrant
import (
"context"
)
type Provider interface {
Info() *ProviderInfo
Action(ctx context.Context, actionName string, machData *Machine) ([]string, error)
IsInstalled(ctx context.Context, machData *Machine) (bool, error)
IsUsable(ctx context.Context, machData *Machine) (bool, error)
MachineIdChanged(ctx context.Context, machData *Machine) error
Name() string
RunAction(ctx context.Context, actionName string, args interface{}, machData *Machine) (interface{}, error)
SshInfo(ctx context.Context, machData *Machine) (*SshInfo, error)
State(ctx context.Context, machData *Machine) (*MachineState, error)
Config
GuestCapabilities
HostCapabilities
ProviderCapabilities
}
type ProviderInfo struct {
Description string `json:"description"`
Priority int64 `json:"priority"`
}

View File

@ -0,0 +1,8 @@
package vagrant
type SshInfo struct {
Host string `json:"host"`
Port int64 `json:"port"`
Username string `json:"username"`
PrivateKeyPath string `json:"private_key_path"`
}

View File

@ -0,0 +1,26 @@
package vagrant
import (
"context"
)
type FolderList map[string]interface{}
type FolderOptions map[string]interface{}
type SyncedFolderInfo struct {
Description string `json:"description"`
Priority int64 `json:"priority"`
}
type SyncedFolder interface {
Cleanup(ctx context.Context, m *Machine, opts FolderOptions) error
Disable(ctx context.Context, m *Machine, f FolderList, opts FolderOptions) error
Enable(ctx context.Context, m *Machine, f FolderList, opts FolderOptions) error
Info() *SyncedFolderInfo
IsUsable(ctx context.Context, m *Machine) (bool, error)
Name() string
Prepare(ctx context.Context, m *Machine, f FolderList, opts FolderOptions) error
GuestCapabilities
HostCapabilities
}

472
ext/go-plugin/vagrant/ui.go Normal file
View File

@ -0,0 +1,472 @@
package vagrant
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"os/signal"
"runtime"
"strings"
"sync"
"syscall"
"time"
"unicode"
)
type UiColor uint
const (
UiColorRed UiColor = 31
UiColorGreen = 32
UiColorYellow = 33
UiColorBlue = 34
UiColorMagenta = 35
UiColorCyan = 36
)
type UiChannel uint
const (
UiOutput UiChannel = 1
UiError = 2
)
var logger = DefaultLogger().Named("ui")
type Options struct {
Channel UiChannel
NewLine bool
}
var defaultOptions = &Options{
Channel: UiOutput,
NewLine: true,
}
// The Ui interface handles all communication for Vagrant with the outside
// world. This sort of control allows us to strictly control how output
// is formatted and various levels of output.
type Ui interface {
Ask(string) (string, error)
Detail(string)
Info(string)
Error(string)
Machine(string, ...string)
Message(string, *Options)
Output(string)
Say(string)
Success(string)
Warn(string)
}
// The BasicUI is a UI that reads and writes from a standard Go reader
// and writer. It is safe to be called from multiple goroutines. Machine
// readable output is simply logged for this UI.
type BasicUi struct {
Reader io.Reader
Writer io.Writer
ErrorWriter io.Writer
l sync.Mutex
interrupted bool
scanner *bufio.Scanner
}
var _ Ui = new(BasicUi)
func (rw *BasicUi) Ask(query string) (string, error) {
rw.l.Lock()
defer rw.l.Unlock()
if rw.interrupted {
return "", errors.New("interrupted")
}
if rw.scanner == nil {
rw.scanner = bufio.NewScanner(rw.Reader)
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(sigCh)
logger.Info("ask", query)
if query != "" {
if _, err := fmt.Fprint(rw.Writer, query+" "); err != nil {
return "", err
}
}
result := make(chan string, 1)
go func() {
var line string
if rw.scanner.Scan() {
line = rw.scanner.Text()
}
if err := rw.scanner.Err(); err != nil {
logger.Error("scan failure", "error", err)
return
}
result <- line
}()
select {
case line := <-result:
return line, nil
case <-sigCh:
// Print a newline so that any further output starts properly
// on a new line.
fmt.Fprintln(rw.Writer)
// Mark that we were interrupted so future Ask calls fail.
rw.interrupted = true
return "", errors.New("interrupted")
}
}
func (rw *BasicUi) Detail(message string) { rw.Say(message) }
func (rw *BasicUi) Info(message string) { rw.Say(message) }
func (rw *BasicUi) Output(message string) { rw.Say(message) }
func (rw *BasicUi) Success(message string) { rw.Say(message) }
func (rw *BasicUi) Warn(message string) { rw.Say(message) }
func (rw *BasicUi) Say(message string) {
rw.Message(message, nil)
}
func (rw *BasicUi) Message(message string, opts *Options) {
rw.l.Lock()
defer rw.l.Unlock()
if opts == nil {
opts = &Options{Channel: UiOutput, NewLine: true}
}
logger.Debug("write message", "content", message, "options", opts)
target := rw.Writer
if opts.Channel == UiError {
if rw.ErrorWriter == nil {
logger.Error("error writer unset using writer")
} else {
target = rw.ErrorWriter
}
}
suffix := ""
if opts.NewLine {
suffix = "\n"
}
_, err := fmt.Fprint(target, message+suffix)
if err != nil {
logger.Error("write failure", "error", err)
}
}
func (rw *BasicUi) Error(message string) {
rw.Message(message, &Options{Channel: UiError, NewLine: true})
}
func (rw *BasicUi) Machine(t string, args ...string) {
logger.Info("machine readable", "category", t, "args", args)
}
// MachineReadableUi is a UI that only outputs machine-readable output
// to the given Writer.
type MachineReadableUi struct {
Writer io.Writer
}
var _ Ui = new(MachineReadableUi)
func (u *MachineReadableUi) Ask(query string) (string, error) {
return "", errors.New("machine-readable UI can't ask")
}
func (u *MachineReadableUi) Detail(message string) {
u.Machine("ui", "detail", message)
}
func (u *MachineReadableUi) Info(message string) {
u.Machine("ui", "info", message)
}
func (u *MachineReadableUi) Output(message string) {
u.Machine("ui", "output", message)
}
func (u *MachineReadableUi) Success(message string) {
u.Machine("ui", "success", message)
}
func (u *MachineReadableUi) Warn(message string) {
u.Machine("ui", "warn", message)
}
func (u *MachineReadableUi) Say(message string) {
u.Machine("ui", "say", message)
}
func (u *MachineReadableUi) Message(message string, opts *Options) {
u.Machine("ui", "message", message)
}
func (u *MachineReadableUi) Error(message string) {
u.Machine("ui", "error", message)
}
// TODO: Do we want to update this to match Vagrant machine style?
func (u *MachineReadableUi) Machine(category string, args ...string) {
now := time.Now().UTC()
// Determine if we have a target, and set it
target := ""
commaIdx := strings.Index(category, ",")
if commaIdx > -1 {
target = category[0:commaIdx]
category = category[commaIdx+1:]
}
// Prepare the args
for i, v := range args {
args[i] = strings.Replace(v, ",", "%!(VAGRANT_COMMA)", -1)
args[i] = strings.Replace(args[i], "\r", "\\r", -1)
args[i] = strings.Replace(args[i], "\n", "\\n", -1)
}
argsString := strings.Join(args, ",")
_, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s\n", now.Unix(), target, category, argsString)
if err != nil {
if err == syscall.EPIPE || strings.Contains(err.Error(), "broken pipe") {
// Ignore epipe errors because that just means that the file
// is probably closed or going to /dev/null or something.
} else {
panic(err)
}
}
}
type NoopUi struct{}
var _ Ui = new(NoopUi)
func (*NoopUi) Ask(string) (string, error) { return "", errors.New("this is a noop ui") }
func (*NoopUi) Detail(string) { return }
func (*NoopUi) Info(string) { return }
func (*NoopUi) Error(string) { return }
func (*NoopUi) Machine(string, ...string) { return }
func (*NoopUi) Message(string, *Options) { return }
func (*NoopUi) Output(string) { return }
func (*NoopUi) Say(string) { return }
func (*NoopUi) Success(string) { return }
func (*NoopUi) Warn(string) { return }
// ColoredUi is a UI that is colored using terminal colors.
type ColoredUi struct {
Color UiColor
ErrorColor UiColor
SuccessColor UiColor
WarnColor UiColor
Ui Ui
}
var _ Ui = new(ColoredUi)
func (u *ColoredUi) Ask(query string) (string, error) {
return u.Ui.Ask(u.colorize(query, u.Color, true))
}
func (u *ColoredUi) Detail(message string) {
u.Say(message)
}
func (u *ColoredUi) Info(message string) {
u.Say(message)
}
func (u *ColoredUi) Error(message string) {
color := u.ErrorColor
if color == 0 {
color = UiColorRed
}
u.Ui.Error(u.colorize(message, color, true))
}
func (u *ColoredUi) Machine(t string, args ...string) {
// Don't colorize machine-readable output
u.Ui.Machine(t, args...)
}
func (u *ColoredUi) Message(message string, opts *Options) {
u.Ui.Message(u.colorize(message, u.Color, false), opts)
}
func (u *ColoredUi) Output(message string) {
u.Say(message)
}
func (u *ColoredUi) Say(message string) {
u.Ui.Say(u.colorize(message, u.Color, true))
}
func (u *ColoredUi) Success(message string) {
u.Ui.Say(u.colorize(message, u.SuccessColor, true))
}
func (u *ColoredUi) Warn(message string) {
u.Ui.Say(u.colorize(message, u.WarnColor, true))
}
func (u *ColoredUi) colorize(message string, color UiColor, bold bool) string {
if !u.supportsColors() {
return message
}
attr := 0
if bold {
attr = 1
}
return fmt.Sprintf("\033[%d;%dm%s\033[0m", attr, color, message)
}
func (u *ColoredUi) supportsColors() bool {
// Never use colors if we have this environmental variable
if os.Getenv("VAGRANT_NO_COLOR") != "" {
return false
}
// For now, on non-Windows machine, just assume it does
if runtime.GOOS != "windows" {
return true
}
// On Windows, if we appear to be in Cygwin, then it does
cygwin := os.Getenv("CYGWIN") != "" ||
os.Getenv("OSTYPE") == "cygwin" ||
os.Getenv("TERM") == "cygwin"
return cygwin
}
// TargetedUi is a UI that wraps another UI implementation and modifies
// the output to indicate a specific target. Specifically, all Say output
// is prefixed with the target name. Message output is not prefixed but
// is offset by the length of the target so that output is lined up properly
// with Say output. Machine-readable output has the proper target set.
type TargetedUi struct {
Target string
Ui Ui
}
var _ Ui = new(TargetedUi)
func (u *TargetedUi) Ask(query string) (string, error) {
return u.Ui.Ask(u.prefixLines(true, query))
}
func (u *TargetedUi) Detail(message string) {
u.Ui.Detail(u.prefixLines(true, message))
}
func (u *TargetedUi) Info(message string) {
u.Ui.Info(u.prefixLines(true, message))
}
func (u *TargetedUi) Output(message string) {
u.Ui.Output(u.prefixLines(true, message))
}
func (u *TargetedUi) Success(message string) {
u.Ui.Success(u.prefixLines(true, message))
}
func (u *TargetedUi) Warn(message string) {
u.Ui.Warn(u.prefixLines(true, message))
}
func (u *TargetedUi) Say(message string) {
u.Ui.Say(u.prefixLines(true, message))
}
func (u *TargetedUi) Message(message string, opts *Options) {
u.Ui.Message(u.prefixLines(false, message), opts)
}
func (u *TargetedUi) Error(message string) {
u.Ui.Error(u.prefixLines(true, message))
}
func (u *TargetedUi) Machine(t string, args ...string) {
// Prefix in the target, then pass through
u.Ui.Machine(fmt.Sprintf("%s,%s", u.Target, t), args...)
}
func (u *TargetedUi) prefixLines(arrow bool, message string) string {
arrowText := "==>"
if !arrow {
arrowText = strings.Repeat(" ", len(arrowText))
}
var result bytes.Buffer
for _, line := range strings.Split(message, "\n") {
result.WriteString(fmt.Sprintf("%s %s: %s\n", arrowText, u.Target, line))
}
return strings.TrimRightFunc(result.String(), unicode.IsSpace)
}
// TimestampedUi is a UI that wraps another UI implementation and prefixes
// prefixes each message with an RFC3339 timestamp
type TimestampedUi struct {
Ui Ui
}
var _ Ui = new(TimestampedUi)
func (u *TimestampedUi) Ask(query string) (string, error) {
return u.Ui.Ask(query)
}
func (u *TimestampedUi) Detail(message string) {
u.Ui.Detail(u.timestampLine(message))
}
func (u *TimestampedUi) Info(message string) {
u.Ui.Info(u.timestampLine(message))
}
func (u *TimestampedUi) Output(message string) {
u.Ui.Output(u.timestampLine(message))
}
func (u *TimestampedUi) Success(message string) {
u.Ui.Success(u.timestampLine(message))
}
func (u *TimestampedUi) Warn(message string) {
u.Ui.Warn(u.timestampLine(message))
}
func (u *TimestampedUi) Say(message string) {
u.Ui.Say(u.timestampLine(message))
}
func (u *TimestampedUi) Message(message string, opts *Options) {
u.Ui.Message(u.timestampLine(message), opts)
}
func (u *TimestampedUi) Error(message string) {
u.Ui.Error(u.timestampLine(message))
}
func (u *TimestampedUi) Machine(message string, args ...string) {
u.Ui.Machine(message, args...)
}
func (u *TimestampedUi) timestampLine(string string) string {
return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string)
}

View File

@ -0,0 +1,290 @@
package vagrant
import (
"bytes"
"os"
"strings"
"testing"
)
// This reads the output from the bytes.Buffer in our test object
// and then resets the buffer.
func readWriter(ui *BasicUi) (result string) {
buffer := ui.Writer.(*bytes.Buffer)
result = buffer.String()
buffer.Reset()
return
}
// Reset the input Reader then add some input to it.
func writeReader(ui *BasicUi, input string) {
buffer := ui.Reader.(*bytes.Buffer)
buffer.WriteString(input)
}
func readErrorWriter(ui *BasicUi) (result string) {
buffer := ui.ErrorWriter.(*bytes.Buffer)
result = buffer.String()
buffer.Reset()
return
}
func testUi() *BasicUi {
return &BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
ErrorWriter: new(bytes.Buffer),
}
}
func TestColoredUi(t *testing.T) {
bufferUi := testUi()
ui := &ColoredUi{UiColorBlue, UiColorRed, UiColorGreen,
UiColorYellow, bufferUi}
if !ui.supportsColors() {
t.Skip("skipping for ui without color support")
}
ui.Say("foo")
result := readWriter(bufferUi)
if result != "\033[1;34mfoo\033[0m\n" {
t.Fatalf("invalid output: %s", result)
}
ui.Message("foo", nil)
result = readWriter(bufferUi)
if result != "\033[0;34mfoo\033[0m\n" {
t.Fatalf("invalid output: %s", result)
}
ui.Error("foo")
result = readWriter(bufferUi)
if result != "" {
t.Fatalf("invalid output: %s", result)
}
result = readErrorWriter(bufferUi)
if result != "\033[1;31mfoo\033[0m\n" {
t.Fatalf("invalid output: %s", result)
}
}
func TestColoredUi_noColorEnv(t *testing.T) {
bufferUi := testUi()
ui := &ColoredUi{UiColorBlue, UiColorRed, UiColorGreen,
UiColorYellow, bufferUi}
// Set the env var to get rid of the color
oldenv := os.Getenv("VAGRANT_NO_COLOR")
os.Setenv("VAGRANT_NO_COLOR", "1")
defer os.Setenv("VAGRANT_NO_COLOR", oldenv)
ui.Say("foo")
result := readWriter(bufferUi)
if result != "foo\n" {
t.Fatalf("invalid output: %s", result)
}
ui.Message("foo", nil)
result = readWriter(bufferUi)
if result != "foo\n" {
t.Fatalf("invalid output: %s", result)
}
ui.Error("foo")
result = readErrorWriter(bufferUi)
if result != "foo\n" {
t.Fatalf("invalid output: %s", result)
}
}
func TestTargetedUi(t *testing.T) {
bufferUi := testUi()
targetedUi := &TargetedUi{
Target: "foo",
Ui: bufferUi,
}
var actual, expected string
targetedUi.Say("foo")
actual = readWriter(bufferUi)
expected = "==> foo: foo\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
targetedUi.Message("foo", nil)
actual = readWriter(bufferUi)
expected = " foo: foo\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
targetedUi.Error("bar")
actual = readErrorWriter(bufferUi)
expected = "==> foo: bar\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
targetedUi.Say("foo\nbar")
actual = readWriter(bufferUi)
expected = "==> foo: foo\n==> foo: bar\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
}
func TestColoredUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &ColoredUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("ColoredUi must implement Ui")
}
}
func TestTargetedUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &TargetedUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("TargetedUi must implement Ui")
}
}
func TestBasicUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &BasicUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("BasicUi must implement Ui")
}
}
func TestBasicUi_Error(t *testing.T) {
bufferUi := testUi()
var actual, expected string
bufferUi.Error("foo")
actual = readErrorWriter(bufferUi)
expected = "foo\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
bufferUi.ErrorWriter = nil
bufferUi.Error("5")
actual = readWriter(bufferUi)
expected = "5\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
}
func TestBasicUi_Say(t *testing.T) {
bufferUi := testUi()
var actual, expected string
bufferUi.Say("foo")
actual = readWriter(bufferUi)
expected = "foo\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
bufferUi.Say("5")
actual = readWriter(bufferUi)
expected = "5\n"
if actual != expected {
t.Fatalf("bad: %#v", actual)
}
}
func TestBasicUi_Ask(t *testing.T) {
var actual, expected string
var err error
var testCases = []struct {
Prompt, Input, Answer string
}{
{"[c]ontinue or [a]bort", "c\n", "c"},
{"[c]ontinue or [a]bort", "c", "c"},
// Empty input shouldn't give an error
{"Name", "Joe Bloggs\n", "Joe Bloggs"},
{"Name", "Joe Bloggs", "Joe Bloggs"},
{"Name", "\n", ""},
}
for _, testCase := range testCases {
// Because of the internal bufio we can't easily reset the input, so create a new one each time
bufferUi := testUi()
writeReader(bufferUi, testCase.Input)
actual, err = bufferUi.Ask(testCase.Prompt)
if err != nil {
t.Fatal(err)
}
if actual != testCase.Answer {
t.Fatalf("bad answer: %#v", actual)
}
actual = readWriter(bufferUi)
expected = testCase.Prompt + " "
if actual != expected {
t.Fatalf("bad prompt: %#v", actual)
}
}
}
func TestMachineReadableUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &MachineReadableUi{}
if _, ok := raw.(Ui); !ok {
t.Fatalf("MachineReadableUi must implement Ui")
}
}
func TestMachineReadableUi(t *testing.T) {
var data, expected string
buf := new(bytes.Buffer)
ui := &MachineReadableUi{Writer: buf}
// No target
ui.Machine("foo", "bar", "baz")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = ",foo,bar,baz\n"
if data != expected {
t.Fatalf("bad: %s", data)
}
// Target
buf.Reset()
ui.Machine("mitchellh,foo", "bar", "baz")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = "mitchellh,foo,bar,baz\n"
if data != expected {
t.Fatalf("bad: %s", data)
}
// Commas
buf.Reset()
ui.Machine("foo", "foo,bar")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = ",foo,foo%!(VAGRANT_COMMA)bar\n"
if data != expected {
t.Fatalf("bad: %s", data)
}
// New lines
buf.Reset()
ui.Machine("foo", "foo\n")
data = strings.SplitN(buf.String(), ",", 2)[1]
expected = ",foo,foo\\n\n"
if data != expected {
t.Fatalf("bad: %#v", data)
}
}

21
go.mod Normal file
View File

@ -0,0 +1,21 @@
module github.com/hashicorp/vagrant
require (
github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5
github.com/dylanmei/iso8601 v0.1.0 // indirect
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1
github.com/golang/protobuf v1.3.0
github.com/hashicorp/go-hclog v0.8.0
github.com/hashicorp/go-plugin v0.0.0-20190220160451-3f118e8ee104
github.com/kr/fs v0.1.0 // indirect
github.com/masterzen/winrm v0.0.0-20190308153735-1d17eaf15943
github.com/mitchellh/iochan v1.0.0
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db
github.com/pkg/errors v0.8.1 // indirect
github.com/pkg/sftp v1.10.0
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4
google.golang.org/grpc v1.19.0
)

91
go.sum Normal file
View File

@ -0,0 +1,91 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4 h1:pSm8mp0T2OH2CPmPDPtwHPr3VAQaOwVF/JbllOPP4xA=
github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022 h1:y8Gs8CzNfDF5AZvjr+5UyGQvQEBL7pwo+v+wX6q9JI8=
github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4=
github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5 h1:U7q69tqXiCf6m097GRlNQB0/6SI1qWIOHYHhCEvDxF4=
github.com/LK4D4/joincontext v0.0.0-20171026170139-1724345da6d5/go.mod h1:nxQPcNPR/34g+HcK2hEsF99O+GJgIkW/OmPl8wtzhmk=
github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e h1:ptBAamGVd6CfRsUtyHD+goy2JGhv1QC32v3gqM8mYAM=
github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0 h1:JaCC8jz0zdMLk2m+qCCVLLLM/PL93p84w4pK3aJWj60=
github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dylanmei/iso8601 v0.1.0 h1:812NGQDBcqquTfH5Yeo7lwR0nzx/cKdsmf3qMjPURUI=
github.com/dylanmei/iso8601 v0.1.0/go.mod h1:w9KhXSgIyROl1DefbMYIE7UVSIvELTbMrCfx+QkYnoQ=
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1 h1:r1oACdS2XYiAWcfF8BJXkoU8l1J71KehGR+d99yWEDA=
github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1/go.mod h1:lcy9/2gH1jn/VCLouHA6tOEwLoNVd4GW6zhuKLmHC2Y=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0 h1:z3ollgGRg8RjfJH6UVBaG54R70GFd++QOkvnJH3VSBY=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-plugin v0.0.0-20190220160451-3f118e8ee104 h1:9iQ/zrTOJqzP+kH37s6xNb6T1RysiT7fnDD3DJbspVw=
github.com/hashicorp/go-plugin v0.0.0-20190220160451-3f118e8ee104/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9 h1:SmVbOZFWAlyQshuMfOkiAx1f5oUTsOGG5IXplAEYeeM=
github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc=
github.com/masterzen/winrm v0.0.0-20190308153735-1d17eaf15943 h1:Bteu9XN1gkBePnKr0v1edkUo2LJRsmK5ne2FrC6yVW4=
github.com/masterzen/winrm v0.0.0-20190308153735-1d17eaf15943/go.mod h1:bsMsaiOA3CXjbJxW0a94G4PfPDj9zUmH5JoFuJ9P4o0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db h1:9uViuKtx1jrlXLBW/pMnhOfzn3iSEdLase/But/IZRU=
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.0 h1:DGA1KlA9esU6WcicH+P8PxFZOl15O6GYtab1cIJdOlE=
github.com/pkg/sftp v1.10.0/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU=
golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,4 +1,5 @@
require "log4r" require "log4r"
require "log4r/logger"
require "vagrant/util/credential_scrubber" require "vagrant/util/credential_scrubber"
# Update the default formatter within the log4r library to ensure # Update the default formatter within the log4r library to ensure
# sensitive values are being properly scrubbed from logger data # sensitive values are being properly scrubbed from logger data
@ -53,6 +54,8 @@ if ENV["VAGRANT_LOG"] && ENV["VAGRANT_LOG"] != ""
# rest-client to write using the `<<` operator. # rest-client to write using the `<<` operator.
# See https://github.com/rest-client/rest-client/issues/34#issuecomment-290858 # See https://github.com/rest-client/rest-client/issues/34#issuecomment-290858
# for more information # for more information
Log4r::PatternFormatter::DirectiveTable["l"] =
'"[" + ' + Log4r::PatternFormatter::DirectiveTable["l"] + ' + "]"'
class VagrantLogger < Log4r::Logger class VagrantLogger < Log4r::Logger
def << (msg) def << (msg)
debug(msg.strip) debug(msg.strip)
@ -61,13 +64,10 @@ if ENV["VAGRANT_LOG"] && ENV["VAGRANT_LOG"] != ""
logger = VagrantLogger.new("vagrant") logger = VagrantLogger.new("vagrant")
logger.outputters = Log4r::Outputter.stderr logger.outputters = Log4r::Outputter.stderr
logger.level = level logger.level = level
base_formatter = Log4r::BasicFormatter.new base_formatter = Log4r::PatternFormatter.new(
if ENV["VAGRANT_LOG_TIMESTAMP"] pattern: "%d %-7l %C: %m",
base_formatter = Log4r::PatternFormatter.new( date_pattern: ENV["VAGRANT_LOG_TIMESTAMP"] ? "%FT%T.%L%z" : " "
pattern: "%d [%5l] %m", )
date_pattern: "%F %T"
)
end
# Vagrant Cloud gem uses RestClient to make HTTP requests, so # Vagrant Cloud gem uses RestClient to make HTTP requests, so
# log them if debug is enabled and use Vagrants logger # log them if debug is enabled and use Vagrants logger
require 'rest_client' require 'rest_client'
@ -118,6 +118,7 @@ module Vagrant
autoload :Driver, 'vagrant/driver' autoload :Driver, 'vagrant/driver'
autoload :Environment, 'vagrant/environment' autoload :Environment, 'vagrant/environment'
autoload :Errors, 'vagrant/errors' autoload :Errors, 'vagrant/errors'
autoload :GoPlugin, 'vagrant/go_plugin'
autoload :Guest, 'vagrant/guest' autoload :Guest, 'vagrant/guest'
autoload :Host, 'vagrant/host' autoload :Host, 'vagrant/host'
autoload :Machine, 'vagrant/machine' autoload :Machine, 'vagrant/machine'

View File

@ -221,5 +221,17 @@ module Vagrant
"#{@name}-#{@version}-#{@provider}" <=> "#{@name}-#{@version}-#{@provider}" <=>
"#{other.name}-#{other.version}-#{other.provider}" "#{other.name}-#{other.version}-#{other.provider}"
end end
# @return [String]
def to_json(*args)
{
name: name,
provider: provider,
version: version,
directory: directory.to_s,
metadata: metadata,
metadata_url: metadata_url
}.to_json(*args)
end
end end
end end

View File

@ -7,6 +7,10 @@ module Vagrant
def method_missing(name, *args, &block) def method_missing(name, *args, &block)
DummyConfig.new DummyConfig.new
end end
def to_json(*_)
"null"
end
end end
end end
end end

View File

@ -112,6 +112,14 @@ module Vagrant
@keys = state["keys"] if state.key?("keys") @keys = state["keys"] if state.key?("keys")
@missing_key_calls = state["missing_key_calls"] if state.key?("missing_key_calls") @missing_key_calls = state["missing_key_calls"] if state.key?("missing_key_calls")
end end
def to_json(*args)
Hash[
@keys.find_all { |k,v|
!k.to_s.start_with?("_")
}
].to_json(*args)
end
end end
end end
end end

View File

@ -175,6 +175,15 @@ module Vagrant
# Load any global plugins # Load any global plugins
Vagrant::Plugin::Manager.instance.load_plugins(plugins) Vagrant::Plugin::Manager.instance.load_plugins(plugins)
# Load any available go-plugins
Util::Experimental.guard_with(:go_plugin) do
begin
Vagrant::GoPlugin::Manager.instance.globalize!
rescue LoadError => err
@logger.warn("go plugin support is not available: #{err}")
end
end
plugins = process_configured_plugins plugins = process_configured_plugins
# Call the hooks that does not require configurations to be loaded # Call the hooks that does not require configurations to be loaded
@ -918,6 +927,26 @@ module Vagrant
end end
end end
# @return [String]
def to_json(*args)
{
cwd: cwd,
data_dir: data_dir,
vagrantfile_name: vagrantfile_name,
home_path: home_path,
local_data_path: local_data_path,
tmp_path: tmp_path,
aliases_path: aliases_path,
boxes_path: boxes_path,
gems_path: gems_path,
default_private_key_path: default_private_key_path,
root_path: root_path,
primary_machine_name: primary_machine_name,
machine_names: machine_names,
active_machines: Hash[active_machines]
}.to_json(*args)
end
protected protected
# Attempt to guess the configured provider in use. Will fallback # Attempt to guess the configured provider in use. Will fallback

26
lib/vagrant/go_plugin.rb Normal file
View File

@ -0,0 +1,26 @@
module Vagrant
autoload :Proto, "vagrant/go_plugin/vagrant_proto/vagrant_services_pb"
module GoPlugin
# @return [String]
INSTALL_DIRECTORY = Vagrant.user_data_path.join("go-plugins").to_s.freeze
autoload :CapabilityPlugin, "vagrant/go_plugin/capability_plugin"
autoload :ConfigPlugin, "vagrant/go_plugin/config_plugin"
autoload :Core, "vagrant/go_plugin/core"
autoload :GRPCPlugin, "vagrant/go_plugin/core"
autoload :Interface, "vagrant/go_plugin/interface"
autoload :Manager, "vagrant/go_plugin/manager"
autoload :ProviderPlugin, "vagrant/go_plugin/provider_plugin"
autoload :SyncedFolderPlugin, "vagrant/go_plugin/synced_folder_plugin"
# @return [Interface]
def self.interface
unless @_interface
@_interface = Interface.new
end
@_interface
end
end
end

View File

@ -0,0 +1,108 @@
require "vagrant/go_plugin/core"
module Vagrant
module GoPlugin
# Contains all capability functionality for go-plugin
module CapabilityPlugin
extend Vagrant::Util::Logger
# Wrapper class for go-plugin defined capabilities
class Capability
include GRPCPlugin
end
# Fetch any defined guest capabilites for given plugin and register
# capabilities within given plugin class
#
# @param [Vagrant::Proto::GuestCapabilities::Stub] client Plugin client
# @param [Class] plugin_klass Plugin class to register capabilities
# @param [Symbol] plugin_type Type of plugin
def self.generate_guest_capabilities(client, plugin_klass, plugin_type)
logger.debug("checking for guest capabilities in #{plugin_type} plugin #{plugin_klass}")
result = client.guest_capabilities(Vagrant::Proto::Empty.new)
return if result.capabilities.empty?
logger.debug("guest capabilities support detected in #{plugin_type} plugin #{plugin_klass}")
result.capabilities.each do |cap|
cap_klass = Class.new(Capability).tap do |k|
k.define_singleton_method(cap.name) { |machine, *args|
response = plugin_client.guest_capability(
Vagrant::Proto::GuestCapabilityRequest.new(
machine: JSON.dump(machine), arguments: JSON.dump(args),
capability: Vagrant::Proto::SystemCapability.new(
name: cap.name, platform: cap.platform))).result
result = JSON.load(response)
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
end
result
}
end
cap_klass.plugin_client = client
plugin_klass.guest_capability(cap.platform.to_sym, cap.name.to_sym) { cap_klass }
end
end
# Fetch any defined host capabilites for given plugin and register
# capabilities within given plugin class
#
# @param [Vagrant::Proto::HostCapabilities::Stub] client Plugin client
# @param [Class] plugin_klass Plugin class to register capabilities
# @param [Symbol] plugin_type Type of plugin
def self.generate_host_capabilities(client, plugin_klass, plugin_type)
logger.debug("checking for host capabilities in #{plugin_type} plugin #{plugin_klass}")
result = client.host_capabilities(Vagrant::Proto::Empty.new)
return if result.capabilities.empty?
logger.debug("host capabilities support detected in #{plugin_type} plugin #{plugin_klass}")
result.capabilities.each do |cap|
cap_klass = Class.new(Capability).tap do |k|
k.define_singleton_method(cap.name) { |environment, *args|
response = plugin_client.host_capability(
Vagrant::Proto::HostCapabilityRequest.new(
environment: JSON.dump(environment), arguments: JSON.dump(args),
capability: Vagrant::Proto::SystemCapability.new(
name: cap.name, platform: cap.platform))).result
result = JSON.load(response)
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
end
result
}
end
cap_klass.plugin_client = client
plugin_klass.host_capability(cap.platform.to_sym, cap.name.to_sym) { cap_klass }
end
end
# Fetch any defined provider capabilites for given plugin and register
# capabilities within given plugin class
#
# @param [Vagrant::Proto::ProviderCapabilities::Stub] client Plugin client
# @param [Class] plugin_klass Plugin class to register capabilities
# @param [Symbol] plugin_type Type of plugin
def self.generate_provider_capabilities(client, plugin_klass, plugin_type)
logger.debug("checking for provider capabilities in #{plugin_type} plugin #{plugin_klass}")
result = client.provider_capabilities(Vagrant::Proto::Empty.new)
return if result.capabilities.empty?
logger.debug("provider capabilities support detected in #{plugin_type} plugin #{plugin_klass}")
result.capabilities.each do |cap|
cap_klass = Class.new(Capability).tap do |k|
k.define_singleton_method(cap.name) { |machine, *args|
response = plugin_client.provider_capability(
Vagrant::Proto::ProviderCapabilityRequest.new(
machine: JSON.dump(machine), arguments: JSON.dump(args),
capability: Vagrant::Proto::ProviderCapability.new(
name: cap.name, provider: cap.provider))).result
result = JSON.load(response)
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
end
result
}
end
cap_klass.plugin_client = client
plugin_klass.provider_capability(cap.provider.to_sym, cap.name.to_sym) { cap_klass }
end
end
end
end
end

View File

@ -0,0 +1,64 @@
require "vagrant/go_plugin/core"
module Vagrant
module GoPlugin
# Contains all configuration functionality for go-plugin
module ConfigPlugin
# Generate configuration for the parent class
#
# @param [Vagrant::Proto::Config::Stub] client Plugin client
# @param [String] parent_name Parent plugin name
# @param [Class] parent_klass Parent class to register config
# @param [Symbol] parent_type Type of parent class (:provider, :synced_folder, etc)
def self.generate_config(client, parent_name, parent_klass, parent_type)
config_attrs = client.config_attributes(Vagrant::Proto::Empty.new).items
config_klass = Class.new(Config).tap do |c|
c.class_eval("def parent_name; '#{parent_name}'; end")
end
config_klass.plugin_client = client
Array(config_attrs).each do |att|
config_klass.instance_eval("attr_accessor :#{att}")
end
parent_klass.config(parent_name, parent_type) { config_klass }
end
# Config plugin class used with go-plugin
class Config < Vagrant.plugin("2", :config)
include GRPCPlugin
# Finalize the current configuration
def finalize!
data = local_data
response = plugin_client.config_finalize(Vagrant::Proto::Configuration.new(
data: JSON.dump(data)))
result = JSON.load(response.data)
if result && result.is_a?(Hash)
new_data = Vagrant::Util::HashWithIndifferentAccess.new(result)
new_data.each do |key, value|
next if data[key] == value
instance_variable_set("@#{key}", value)
end
end
self
end
# Validate configuration
#
# @param [Vagrant::Machine] machine Guest machine
# @return [Array<String>] list of errors
def validate(machine)
result = plugin_client.config_validate(Vagrant::Proto::Configuration.new(
machine: JSON.dump(machine),
data: JSON.dump(local_data)))
{parent_name => result.items}
end
# @return [Hash] currently defined instance variables
def local_data
Vagrant::Util::HashWithIndifferentAccess.
new(instance_variables_hash)
end
end
end
end
end

View File

@ -0,0 +1,82 @@
require "ffi"
module Vagrant
module GoPlugin
# Base module for generic setup of module/class
module Core
# Loads FFI and core helpers into given module/class
def self.included(const)
const.class_eval do
include Vagrant::Util::Logger
extend FFI::Library
ffi_lib FFI::Platform::LIBC
ffi_lib File.expand_path("./go-plugin.so", File.dirname(__FILE__))
typedef :strptr, :plugin_result
# stdlib functions
if FFI::Platform.windows?
attach_function :free, :_free, [:pointer], :void
else
attach_function :free, [:pointer], :void
end
# Load the result received from the extension. This will load
# the JSON result, raise an error if detected, and properly
# free the memory associated with the result.
def load_result(*args)
val, ptr = block_given? ? yield : args
FFI::AutoPointer.new(ptr, self.method(:free))
begin
result = JSON.load(val)
if !result.is_a?(Hash)
raise TypeError.new "Expected Hash but received `#{result.class}`"
end
if !result["error"].to_s.empty?
raise ArgumentError.new result["error"].to_s
end
result = result["result"]
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
end
result
rescue => e
# TODO: Customize to provide formatted output on error
raise
end
end
end
end
end
# Simple module to load into plugin wrapper classes
# to provide expected functionality
module GRPCPlugin
module ClassMethods
def plugin_client
@_plugin_client
end
def plugin_client=(c)
if @_plugin_client
raise ArgumentError, "Plugin client has already been set"
end
@_plugin_client = c
end
end
module InstanceMethods
def plugin_client
self.class.plugin_client
end
end
def self.included(klass)
klass.include(Vagrant::Util::Logger)
klass.include(InstanceMethods)
klass.extend(ClassMethods)
end
end
end
end

View File

@ -0,0 +1,187 @@
require "log4r"
require "vagrant/go_plugin/core"
require "vagrant/go_plugin/vagrant_proto/vagrant_services_pb"
module RubyLogger
include Vagrant::Util::Logger
end
module GRPC
extend Vagrant::Util::Logger
end
module Vagrant
module GoPlugin
# Interface for go-plugin integration
class Interface
include Core
# go plugin functions
typedef :bool, :enable_logger
typedef :bool, :timestamps
typedef :string, :log_level
typedef :string, :plugin_directory
attach_function :_setup, :Setup, [:enable_logger, :timestamps, :log_level], :bool
attach_function :_teardown, :Teardown, [], :void
attach_function :_reset, :Reset, [], :void
attach_function :_load_plugins, :LoadPlugins, [:plugin_directory], :bool
attach_function :_list_providers, :ListProviders, [], :plugin_result
attach_function :_list_synced_folders, :ListSyncedFolders, [], :plugin_result
def initialize
Vagrant::Proto.instance_eval do
::GRPC.extend(Vagrant::Util::Logger)
end
setup
end
# List of provider plugins currently available
#
# @return [Hash<String,Hash<Info>>]
def list_providers
load_result { _list_providers } || {}
end
# List of synced folder plugins currently available
#
# @return [Hash<String,Hash<Info>>]
def list_synced_folders
load_result { _list_synced_folders } || {}
end
# Load any plugins found at the given directory
#
# @param [String, Pathname] path Directory to load
def load_plugins(path)
logger.debug("loading plugins from path: #{path}")
if !File.directory?(path.to_s)
raise ArgumentError, "Directory expected for plugin loading"
end
_load_plugins(path.to_s)
end
# Register all available plugins
def register_plugins
logger.debug("registering provider plugins")
load_providers
logger.debug("registering synced folder plugins")
load_synced_folders
end
# Load the plugins found at the given directory
#
# @param [String] plugin_directory Directory containing go-plugins
def setup
if !@setup
@setup = true
Kernel.at_exit { Vagrant::GoPlugin.interface.teardown }
logger.debug("running go-plugin interface setup")
_setup(!Vagrant.log_level.to_s.empty?,
!!ENV["VAGRANT_LOG_TIMESTAMP"],
Vagrant.log_level.to_s)
else
logger.warn("go-plugin interface already setup")
end
end
def reset
logger.debug("running go-plugin interface reset")
_reset
logger.debug("completed go-plugin interface reset")
end
# Teardown any plugins that may be currently active
def teardown
logger.debug("starting teardown of go-plugin interface")
_teardown
logger.debug("teardown of go-plugin interface complete")
end
# @return [Boolean] go plugins have been setup
def configured?
!!@setup
end
# Load any detected provider plugins
def load_providers
if !@_providers_loaded
@_providers_loaded
logger.debug("provider go-plugins have not been loaded... loading")
list_providers.each do |p_name, p_details|
logger.debug("loading go-plugin provider #{p_name}. details - #{p_details}")
client = Vagrant::Proto::Provider::Stub.new(
"#{p_details[:network]}://#{p_details[:address]}",
:this_channel_is_insecure)
# Create new provider class wrapper
provider_klass = Class.new(ProviderPlugin::Provider)
provider_klass.plugin_client = client
# Create new plugin to register the provider
plugin_klass = Class.new(Vagrant.plugin("2"))
# Define the plugin
plugin_klass.class_eval do
name "#{p_name} Provider"
description p_details[:description]
end
# Register the provider
plugin_klass.provider(p_name.to_sym, priority: p_details.fetch(:priority, 0)) do
provider_klass
end
# Setup any configuration support
ConfigPlugin.generate_config(client, p_name, plugin_klass, :provider)
# Register any guest capabilities
CapabilityPlugin.generate_guest_capabilities(client, plugin_klass, :provider)
# Register any host capabilities
CapabilityPlugin.generate_host_capabilities(client, plugin_klass, :provider)
# Register any provider capabilities
CapabilityPlugin.generate_provider_capabilities(client, plugin_klass, :provider)
logger.debug("completed loading provider go-plugin #{p_name}")
logger.info("loaded go-plugin provider - #{p_name}")
end
else
logger.warn("provider go-plugins have already been loaded. ignoring load request.")
end
end
# Load any detected synced folder plugins
def load_synced_folders
if !@_synced_folders_loaded
@_synced_folders_loaded = true
logger.debug("synced folder go-plugins have not been loaded... loading")
Array(list_synced_folders).each do |f_name, f_details|
logger.debug("loading go-plugin synced folder #{f_name}. details - #{f_details}")
client = Vagrant::Proto::SyncedFolder::Stub.new(
"#{p_details[:network]}://#{p_details[:address]}",
:this_channel_is_insecure)
# Create new synced folder class wrapper
folder_klass = Class.new(SyncedFolderPlugin::SyncedFolder)
folder_klass.plugin_client = client
# Create new plugin to register the synced folder
plugin_klass = Class.new(Vagrant.plugin("2"))
# Define the plugin
plugin_klass.class_eval do
name "#{f_name} Synced Folder"
description f_details[:description]
end
# Register the synced folder
plugin_klass.synced_folder(f_name.to_sym, priority: f_details.fetch(:priority, 10)) do
folder_klass
end
# Register any guest capabilities
CapabilityPlugin.generate_guest_capabilities(client, plugin_klass, :synced_folder)
# Register any host capabilities
CapabilityPlugin.generate_host_capabilities(client, plugin_klass, :synced_folder)
# Register any provider capabilities
CapabilityPlugin.generate_provider_capabilities(client, plugin_klass, :synced_folder)
logger.debug("completed loading synced folder go-plugin #{f_name}")
logger.info("loaded go-plugin synced folder - #{f_name}")
end
else
logger.warn("synced folder go-plugins have already been loaded. ignoring load request.")
end
end
end
end
end

View File

@ -0,0 +1,87 @@
require "zip"
require "vagrant/plugin/state_file"
module Vagrant
module GoPlugin
class Manager
include Util::Logger
# @return [Manager]
def self.instance
if !@instance
@instance = self.new
end
@instance
end
# @return [StateFile] user defined plugins
attr_reader :user_file
# @return [StateFile, nil] project local defined plugins
attr_reader :local_file
def initialize
FileUtils.mkdir_p(INSTALL_DIRECTORY)
FileUtils.mkdir_p(Vagrant.user_data_path.join("tmp").to_s)
@user_file = Plugin::StateFile.new(Vagrant.user_data_path.join("plugins.json"))
end
# Load global plugins
def globalize!
Dir.glob(File.join(INSTALL_DIRECTORY, "*")).each do |entry|
next if !File.directory?(entry)
logger.debug("loading go plugins from directory: #{entry}")
GoPlugin.interface.load_plugins(entry)
end
GoPlugin.interface.register_plugins
end
# Load local plugins
def localize!
raise NotImplementedError
end
# Install a go plugin
#
# @param [String] plugin_name Name of plugin
# @param [String] remote_source Location of plugin for download
# @param [Hash] options Currently unused
def install_plugin(plugin_name, remote_source, options={})
install_dir = File.join(INSTALL_DIRECTORY, plugin_name)
FileUtils.mkdir_p(install_dir)
Dir.mktmpdir("go-plugin", Vagrant.user_data_path.join("tmp").to_s) do |dir|
dest_file = File.join(dir, "plugin.zip")
logger.debug("downloading go plugin #{plugin_name} from #{remote_source}")
Util::Downloader.new(remote_source, dest_file).download!
logger.debug("extracting go plugin #{plugin_name} from #{dest_file}")
Zip::File.open(dest_file) do |zfile|
zfile.each do |entry|
install_path = File.join(install_dir, entry.name)
if File.file?(install_path)
logger.warn("removing existing plugin path for unpacking - #{install_path}")
File.delete(install_path)
end
entry.extract(install_path)
FileUtils.chmod(0755, install_path)
end
end
end
user_file.add_go_plugin(plugin_name, source: remote_source)
end
# Uninstall a go plugin
#
# @param [String] plugin_name Name of plugin
# @param [Hash] options Currently unused
def uninstall_plugin(plugin_name, options={})
plugin_path = File.join(INSTALL_DIRECTORY, plugin_name)
if !File.directory?(plugin_path)
logger.warn("Plugin directory does not exist for #{plugin_name} - #{plugin_path}")
else
logger.debug("deleting go plugin from path #{plugin_path}")
FileUtils.rm_rf(plugin_path)
end
user_file.remove_go_plugin(plugin_name)
end
end
end
end

View File

@ -0,0 +1,171 @@
require "vagrant/go_plugin/core"
module Vagrant
module GoPlugin
module ProviderPlugin
# Helper class for wrapping actions in a go-plugin into
# something which can be used by Vagrant::Action::Builder
class Action
include GRPCPlugin
# @return [String] action name associated to this class
def self.action_name
@action_name
end
# Set the action name for this class
#
# @param [String] n action name
# @return [String]
# @note can only be set once
def self.action_name=(n)
if @action_name
raise ArgumentError.new("Class action name has already been set")
end
@action_name = n.to_s.dup.freeze
end
def initialize(app, env)
@app = app
end
# Run the action
def call(env)
if env.is_a?(Hash) && !env.is_a?(Vagrant::Util::HashWithIndifferentAccess)
env = Vagrant::Util::HashWithIndifferentAccess.new(env)
end
machine = env.fetch(:machine, {})
response = plugin_client.run_action(
Vagrant::Proto::ExecuteAction.new(
name: self.class.action_name,
data: JSON.dump(env),
machine: JSON.dump(machine)))
result = JSON.load(response.result)
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
result.each_pair do |k, v|
env[k] = v
end
end
@app.call(env)
end
end
# Helper class used to provide a wrapper around a go-plugin
# provider so that it can be interacted with normally within
# Vagrant
class Provider < Vagrant.plugin("2", :provider)
include GRPCPlugin
# @return [Vagrant::Machine]
attr_reader :machine
def initialize(machine)
@machine = machine
end
# @return [String] name of the provider plugin for this class
def name
if !@_name
@_name = plugin_client.name(Vagrant::Proto::Empty.new).name
end
@_name
end
# Get callable action by name
#
# @param [Symbol] name name of the action
# @return [Class] callable action class
def action(name)
result = plugin_client.action(
Vagrant::Proto::GenericAction.new(
name: name.to_s,
machine: JSON.dump(machine)))
klasses = result.items.map do |klass_name|
if klass_name.start_with?("self::")
action_name = klass_name.split("::", 2).last
klass = Class.new(Action)
klass.plugin_client = plugin_client
klass.action_name = action_name
klass.class_eval do
def self.name
action_name.capitalize.tr("_", "")
end
end
klass
else
klass_name.split("::").inject(Object) do |memo, const|
if memo.const_defined?(const)
memo.const_get(const)
else
raise NameError, "Unknown action class `#{klass_name}`"
end
end
end
end
Vagrant::Action::Builder.new.tap do |builder|
klasses.each do |action_class|
builder.use action_class
end
end
end
# Execute capability with given name
#
# @param [Symbol] cap_name Name of the capability
# @return [Object]
def capability(cap_name, *args)
r = plugin_client.provider_capability(
Vagrant::Proto::ProviderCapabilityRequest.new(
capability: Vagrant::Proto::ProviderCapability.new(
name: cap_name.to_s,
provider: name
),
machine: JSON.dump(machine),
arguments: JSON.dump(args)
)
)
result = JSON.load(r.result)
if result.is_a?(Hash)
result = Vagrant::Util::HashWithIndifferentAccess.new(result)
end
result
end
# @return [Boolean] provider is installed
def is_installed?
plugin_client.is_installed(Vagrant::Proto::Machine.new(
machine: JSON.dump(machine))).result
end
# @return [Boolean] provider is usable
def is_usable?
plugin_client.is_usable(Vagrant::Proto::Machine.new(
machine: JSON.dump(machine))).result
end
# @return [nil]
def machine_id_changed
plugin_client.machine_id_changed(Vagrant::Proto::Machine.new(
machine: JSON.dump(machine)))
nil
end
# @return [Hash] SSH information
def ssh_info
result = plugin_client.ssh_info(Vagrant::Proto::Machine.new(
machine: JSON.dump(machine))).to_hash
Vagrant::Util::HashWithIndifferentAccess.new(result)
end
# @return [Vagrant::MachineState]
def state
result = plugin_client.state(Vagrant::Proto::Machine.new(
machine: JSON.dump(machine)))
Vagrant::MachineState.new(result.id,
result.short_description, result.long_description)
end
end
end
end
end

View File

@ -0,0 +1,89 @@
require "vagrant/go_plugin/core"
module Vagrant
module GoPlugin
# Contains all synced folder functionality for go-plugin
module SyncedFolderPlugin
# Helper class used to provide a wrapper around a go-plugin
# synced folder so that it can be interacted with normally
# within Vagrant
class SyncedFolder < Vagrant.plugin("2", :synced_folder)
include GRPCPlugin
# Cleanup synced folders
#
# @param [Vagrant::Machine] machine Vagrant guest
# @param [Hash] opts Folder options
def cleanup(machine, opts)
plugin_client.cleanup(
Vagrant::Proto::SyncedFolders.new(
machine: JSON.dump(machine),
options: JSON.dump(opts),
folders: JSON.dump({})))
nil
end
# Disable synced folders
#
# @param [Vagrant::Machine] machine Vagrant guest
# @param [Hash] folders Folders to enable
# @param [Hash] opts Folder options
def disable(machine, folders, opts)
plugin_client.disable(
Vagrant::Proto::SyncedFolders.new(
machine: JSON.dump(machine),
folders: JSON.dump(folders),
options: JSON.dump(opts)))
nil
end
# Enable synced folders
#
# @param [Vagrant::Machine] machine Vagrant guest
# @param [Hash] folders Folders to enable
# @param [Hash] opts Folder options
def enable(machine, folders, opts)
plugin_client.enable(
Vagrant::Proto::SyncedFolders.new(
machine: JSON.dump(machine),
folders: JSON.dump(folders),
options: JSON.dump(options)))
nil
end
# Prepare synced folders
#
# @param [Vagrant::Machine] machine Vagrant guest
# @param [Hash] folders Folders to enable
# @param [Hash] opts Folder options
def prepare(machine, folders, opts)
plugin_client.prepare(
Vagrant::Proto::SyncedFolders.new(
machine: JSON.dump(machine),
folders: JSON.dump(folders),
options: JSON.dump(options)))
nil
end
# Check if plugin is usable
#
# @param [Vagrant::Machine] machine Vagrant guest
# @return [Boolean]
def usable?(machine, raise_error=false)
plugin_client.is_usable(
Vagrant::Proto::Machine.new(
machine: JSON.dump(machine))).result
end
# @return [String]
def name
if !@_name
@_name = plugin_client.name(Vagrant::Proto::Empty.new).name
end
@_name
end
end
end
end
end

View File

@ -0,0 +1,122 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: vagrant.proto
require 'google/protobuf'
Google::Protobuf::DescriptorPool.generated_pool.build do
add_file("vagrant.proto", :syntax => :proto3) do
add_message "vagrant.proto.Empty" do
end
add_message "vagrant.proto.Machine" do
optional :machine, :string, 1
end
add_message "vagrant.proto.Valid" do
optional :result, :bool, 1
end
add_message "vagrant.proto.Identifier" do
optional :name, :string, 1
end
add_message "vagrant.proto.PluginInfo" do
optional :description, :string, 1
optional :priority, :int64, 2
end
add_message "vagrant.proto.Content" do
optional :target, :string, 1
optional :value, :string, 2
end
add_message "vagrant.proto.WriteResponse" do
optional :length, :int32, 1
end
add_message "vagrant.proto.SystemCapability" do
optional :name, :string, 1
optional :platform, :string, 2
end
add_message "vagrant.proto.ProviderCapability" do
optional :name, :string, 1
optional :provider, :string, 2
end
add_message "vagrant.proto.SystemCapabilityList" do
repeated :capabilities, :message, 1, "vagrant.proto.SystemCapability"
end
add_message "vagrant.proto.ProviderCapabilityList" do
repeated :capabilities, :message, 1, "vagrant.proto.ProviderCapability"
end
add_message "vagrant.proto.GenericResponse" do
optional :result, :string, 1
end
add_message "vagrant.proto.GuestCapabilityRequest" do
optional :capability, :message, 1, "vagrant.proto.SystemCapability"
optional :machine, :string, 2
optional :arguments, :string, 3
end
add_message "vagrant.proto.HostCapabilityRequest" do
optional :capability, :message, 1, "vagrant.proto.SystemCapability"
optional :environment, :string, 2
optional :arguments, :string, 3
end
add_message "vagrant.proto.ProviderCapabilityRequest" do
optional :capability, :message, 1, "vagrant.proto.ProviderCapability"
optional :machine, :string, 2
optional :arguments, :string, 3
end
add_message "vagrant.proto.Configuration" do
optional :data, :string, 1
optional :machine, :string, 2
end
add_message "vagrant.proto.ListResponse" do
repeated :items, :string, 1
end
add_message "vagrant.proto.SyncedFolders" do
optional :machine, :string, 1
optional :folders, :string, 2
optional :options, :string, 3
end
add_message "vagrant.proto.GenericAction" do
optional :name, :string, 1
optional :machine, :string, 2
end
add_message "vagrant.proto.ExecuteAction" do
optional :name, :string, 1
optional :data, :string, 2
optional :machine, :string, 3
end
add_message "vagrant.proto.MachineSshInfo" do
optional :host, :string, 1
optional :port, :int64, 2
optional :private_key_path, :string, 3
optional :username, :string, 4
end
add_message "vagrant.proto.MachineState" do
optional :id, :string, 1
optional :short_description, :string, 2
optional :long_description, :string, 3
end
end
end
module Vagrant
module Proto
Empty = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Empty").msgclass
Machine = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Machine").msgclass
Valid = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Valid").msgclass
Identifier = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Identifier").msgclass
PluginInfo = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.PluginInfo").msgclass
Content = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Content").msgclass
WriteResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.WriteResponse").msgclass
SystemCapability = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.SystemCapability").msgclass
ProviderCapability = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ProviderCapability").msgclass
SystemCapabilityList = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.SystemCapabilityList").msgclass
ProviderCapabilityList = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ProviderCapabilityList").msgclass
GenericResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.GenericResponse").msgclass
GuestCapabilityRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.GuestCapabilityRequest").msgclass
HostCapabilityRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.HostCapabilityRequest").msgclass
ProviderCapabilityRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ProviderCapabilityRequest").msgclass
Configuration = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.Configuration").msgclass
ListResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ListResponse").msgclass
SyncedFolders = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.SyncedFolders").msgclass
GenericAction = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.GenericAction").msgclass
ExecuteAction = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.ExecuteAction").msgclass
MachineSshInfo = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.MachineSshInfo").msgclass
MachineState = Google::Protobuf::DescriptorPool.generated_pool.lookup("vagrant.proto.MachineState").msgclass
end
end

View File

@ -0,0 +1,223 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# Source: vagrant.proto for package 'vagrant.proto'
require 'grpc'
require_relative 'vagrant_pb'
module Vagrant
module Proto
module IO
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.IO'
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
end
Stub = Service.rpc_stub_class
end
module GuestCapabilities
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.GuestCapabilities'
rpc :GuestCapabilities, Empty, SystemCapabilityList
rpc :GuestCapability, GuestCapabilityRequest, GenericResponse
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
end
Stub = Service.rpc_stub_class
end
module HostCapabilities
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.HostCapabilities'
rpc :HostCapabilities, Empty, SystemCapabilityList
rpc :HostCapability, HostCapabilityRequest, GenericResponse
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
end
Stub = Service.rpc_stub_class
end
module ProviderCapabilities
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.ProviderCapabilities'
rpc :ProviderCapabilities, Empty, ProviderCapabilityList
rpc :ProviderCapability, ProviderCapabilityRequest, GenericResponse
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
end
Stub = Service.rpc_stub_class
end
module Config
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.Config'
rpc :ConfigAttributes, Empty, ListResponse
rpc :ConfigLoad, Configuration, Configuration
rpc :ConfigValidate, Configuration, ListResponse
rpc :ConfigFinalize, Configuration, Configuration
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
end
Stub = Service.rpc_stub_class
end
module SyncedFolder
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.SyncedFolder'
rpc :Cleanup, SyncedFolders, Empty
rpc :Disable, SyncedFolders, Empty
rpc :Enable, SyncedFolders, Empty
rpc :Info, Empty, PluginInfo
rpc :IsUsable, Machine, Valid
rpc :Name, Empty, Identifier
rpc :Prepare, SyncedFolders, Empty
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
# Guest capabilities helpers (copied from GuestCapabilities service)
rpc :GuestCapabilities, Empty, SystemCapabilityList
rpc :GuestCapability, GuestCapabilityRequest, GenericResponse
# Host capabilities helpers (copied from GuestCapabilities service)
rpc :HostCapabilities, Empty, SystemCapabilityList
rpc :HostCapability, HostCapabilityRequest, GenericResponse
end
Stub = Service.rpc_stub_class
end
module Provider
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'vagrant.proto.Provider'
rpc :Action, GenericAction, ListResponse
rpc :Info, Empty, PluginInfo
rpc :IsInstalled, Machine, Valid
rpc :IsUsable, Machine, Valid
rpc :MachineIdChanged, Machine, Machine
rpc :Name, Empty, Identifier
rpc :RunAction, ExecuteAction, GenericResponse
rpc :SshInfo, Machine, MachineSshInfo
rpc :State, Machine, MachineState
# IO helpers for streaming (copied from Stream service)
rpc :Read, Identifier, Content
rpc :Write, Content, WriteResponse
# Config helpers (copied from Config service)
rpc :ConfigAttributes, Empty, ListResponse
rpc :ConfigLoad, Configuration, Configuration
rpc :ConfigValidate, Configuration, ListResponse
rpc :ConfigFinalize, Configuration, Configuration
# Guest capabilities helpers (copied from GuestCapabilities service)
rpc :GuestCapabilities, Empty, SystemCapabilityList
rpc :GuestCapability, GuestCapabilityRequest, GenericResponse
# Host capabilities helpers (copied from HostCapabilities service)
rpc :HostCapabilities, Empty, SystemCapabilityList
rpc :HostCapability, HostCapabilityRequest, GenericResponse
# Provider capabilities helpers (copied from ProviderCapabilities service)
rpc :ProviderCapabilities, Empty, ProviderCapabilityList
rpc :ProviderCapability, ProviderCapabilityRequest, GenericResponse
end
Stub = Service.rpc_stub_class
end
end
end
require 'logger'
# DebugIsTruncated extends the default Logger to truncate debug messages
class DebugIsTruncated < Logger
def debug(s)
super(truncate(s, 1024))
end
# Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
#
# 'Once upon a time in a world far far away'.truncate(27)
# # => "Once upon a time in a wo..."
#
# Pass a string or regexp <tt>:separator</tt> to truncate +text+ at a natural break:
#
# 'Once upon a time in a world far far away'.truncate(27, separator: ' ')
# # => "Once upon a time in a..."
#
# 'Once upon a time in a world far far away'.truncate(27, separator: /\s/)
# # => "Once upon a time in a..."
#
# The last characters will be replaced with the <tt>:omission</tt> string (defaults to "...")
# for a total length not exceeding <tt>length</tt>:
#
# 'And they found that many people were sleeping better.'.truncate(25, omission: '... (continued)')
# # => "And they f... (continued)"
def truncate(s, truncate_at, options = {})
return s unless s.length > truncate_at
omission = options[:omission] || '...'
with_extra_room = truncate_at - omission.length
stop = \
if options[:separator]
rindex(options[:separator], with_extra_room) || with_extra_room
else
with_extra_room
end
"#{s[0, stop]}#{omission}"
end
end
# RubyLogger defines a logger for gRPC based on the standard ruby logger.
module RubyLogger
def logger
LOGGER
end
LOGGER = DebugIsTruncated.new(STDOUT)
LOGGER.level = Logger::DEBUG
end
# GRPC is the general RPC module
module GRPC
# Inject the noop #logger if no module-level logger method has been injected.
extend RubyLogger
end

View File

@ -578,6 +578,20 @@ module Vagrant
end end
end end
# @return [String]
def to_json(*args)
{
box: box,
config: config,
data_dir: data_dir,
environment: env,
id: id,
name: name,
provider_config: provider_config,
provider_name: provider_name
}.to_json(*args)
end
protected protected
# Returns the path to the file that stores the UID. # Returns the path to the file that stores the UID.

View File

@ -28,6 +28,7 @@ module Vagrant
@data["version"] ||= "1" @data["version"] ||= "1"
@data["installed"] ||= {} @data["installed"] ||= {}
@data["go_plugin"] ||= {}
end end
# Add a plugin that is installed to the state file. # Add a plugin that is installed to the state file.
@ -47,6 +48,43 @@ module Vagrant
save! save!
end end
# Add a go plugin that is installed to the state file
#
# @param [String, Symbol] name Plugin name
def add_go_plugin(name, **opts)
@data["go_plugin"][name] = {
"source" => opts[:source]
}
save!
end
# Remove a go plugin that is installed from the state file
#
# @param [String, Symbol] name Name of the plugin
def remove_go_plugin(name)
@data["go_plugin"].delete(name.to_s)
save!
end
# Check if go plugin is installed from the state file
#
# @param [String, Symbol] name Plugin name
# @return [Boolean]
def has_go_plugin?(name)
@data["go_plugin"].key?(name.to_s)
end
# This returns a hash of installed go plugins according to the state
# file. Note that this may _not_ directly match over to actually
# installed plugins
#
# @return [Hash]
def installed_go_plugins
@data["go_plugin"]
end
# Adds a RubyGems index source to look up gems. # Adds a RubyGems index source to look up gems.
# #
# @param [String] url URL of the source. # @param [String] url URL of the source.

View File

@ -138,6 +138,12 @@ module Vagrant
@__finalized = true @__finalized = true
end end
end end
class Set < ::Set
def to_json(*args)
self.to_a.to_json(*args)
end
end
end end
end end
end end

View File

@ -8,7 +8,9 @@ module Vagrant
autoload :DeepMerge, 'vagrant/util/deep_merge' autoload :DeepMerge, 'vagrant/util/deep_merge'
autoload :Env, 'vagrant/util/env' autoload :Env, 'vagrant/util/env'
autoload :HashWithIndifferentAccess, 'vagrant/util/hash_with_indifferent_access' autoload :HashWithIndifferentAccess, 'vagrant/util/hash_with_indifferent_access'
autoload :Experimental, 'vagrant/util/experimental'
autoload :GuestInspection, 'vagrant/util/guest_inspection' autoload :GuestInspection, 'vagrant/util/guest_inspection'
autoload :Logger, 'vagrant/util/logger'
autoload :LoggingFormatter, 'vagrant/util/logging_formatter' autoload :LoggingFormatter, 'vagrant/util/logging_formatter'
autoload :Platform, 'vagrant/util/platform' autoload :Platform, 'vagrant/util/platform'
autoload :Retryable, 'vagrant/util/retryable' autoload :Retryable, 'vagrant/util/retryable'

View File

@ -14,6 +14,9 @@ module Vagrant
super(&block) super(&block)
hash.each do |key, value| hash.each do |key, value|
if value.is_a?(Hash) && !value.is_a?(self.class)
value = self.class.new(value)
end
self[convert_key(key)] = value self[convert_key(key)] = value
end end
end end

View File

@ -0,0 +1,22 @@
require "log4r"
module Vagrant
module Util
# Adds a logger method which provides automatically
# namespaced logger instance
module Logger
# @return [Log4r::Logger]
def logger
if !@_logger
name = (self.is_a?(Module) ? self : self.class).name.downcase
if !name.start_with?("vagrant")
name = "vagrant::root::#{name}"
end
@_logger = Log4r::Logger.new(name)
end
@_logger
end
end
end
end

View File

@ -74,6 +74,23 @@ module VagrantPlugins
autoload :RepairPluginsLocal, action_root.join("repair_plugins") autoload :RepairPluginsLocal, action_root.join("repair_plugins")
autoload :UninstallPlugin, action_root.join("uninstall_plugin") autoload :UninstallPlugin, action_root.join("uninstall_plugin")
autoload :UpdateGems, action_root.join("update_gems") autoload :UpdateGems, action_root.join("update_gems")
Vagrant::Util::Experimental.guard_with(:go_plugin) do
autoload :UninstallGoPlugin, action_root.join("uninstall_go_plugin")
autoload :InstallGoPlugin, action_root.join("install_go_plugin")
def self.action_go_install
Vagrant::Action::Builder.new.tap do |b|
b.use InstallGoPlugin
end
end
def self.action_go_uninstall
Vagrant::Action::Builder.new.tap do |b|
b.use UninstallGoPlugin
end
end
end
end end
end end
end end

View File

@ -0,0 +1,35 @@
require "log4r"
require "vagrant/go_plugin/manager"
module VagrantPlugins
module CommandPlugin
module Action
class InstallGoPlugin
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new("vagrant::plugins::plugincommand::installgoplugin")
end
def call(env)
plugin_name = env[:plugin_name]
plugin_source = env[:plugin_source]
manager = Vagrant::GoPlugin::Manager.instance
env[:ui].info(I18n.t("vagrant.commands.plugin.installing",
name: plugin_name))
manager.install_plugin(plugin_name, plugin_source)
# Tell the user
env[:ui].success(I18n.t("vagrant.commands.plugin.installed",
name: plugin_name,
version: plugin_source))
# Continue
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,22 @@
module VagrantPlugins
module CommandPlugin
module Action
class UninstallGoPlugin
def initialize(app, env)
@app = app
end
def call(env)
# Remove it!
env[:ui].info(I18n.t("vagrant.commands.plugin.uninstalling",
name: env[:plugin_name]))
manager = Vagrant::GoPlugin::Manager.instance
manager.uninstall_plugin(env[:plugin_name])
@app.call(env)
end
end
end
end
end

View File

@ -0,0 +1,43 @@
require 'optparse'
require_relative "base"
module VagrantPlugins
module CommandPlugin
module Command
class GoInstall < Base
def execute
options = { verbose: false }
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant plugin goinstall <name> <source>"
o.separator ""
o.on("--verbose", "Enable verbose output for plugin installation") do |v|
options[:verbose] = v
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.length != 2
raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp
end
plugin_name, plugin_source = argv
action(Action.action_go_install,
plugin_name: plugin_name,
plugin_source: plugin_source
)
# Success, exit status 0
0
end
end
end
end
end

View File

@ -0,0 +1,31 @@
require 'optparse'
require_relative "base"
module VagrantPlugins
module CommandPlugin
module Command
class GoUninstall < Base
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant plugin gouninstall <name> [<name2> <name3> ...] [-h]"
end
# Parse the options
argv = parse_options(opts)
return if !argv
raise Vagrant::Errors::CLIInvalidUsage, help: opts.help.chomp if argv.length < 1
# Uninstall the plugins
argv.each do |entry|
action(Action.action_go_uninstall, plugin_name: entry)
end
# Success, exit status 0
0
end
end
end
end
end

View File

@ -48,6 +48,18 @@ module VagrantPlugins
require_relative "uninstall" require_relative "uninstall"
Uninstall Uninstall
end end
Vagrant::Util::Experimental.guard_with(:go_plugin) do
@subcommands.register(:goinstall) do
require_relative "go_install"
GoInstall
end
@subcommands.register(:gouninstall) do
require_relative "go_uninstall"
GoUninstall
end
end
end end
def execute def execute

View File

@ -30,6 +30,9 @@ describe Vagrant::CLI do
describe "#execute" do describe "#execute" do
let(:triggers) { double("triggers") } let(:triggers) { double("triggers") }
before { allow(Vagrant::Util::Experimental).to receive(:feature_enabled?) }
it "invokes help and exits with 1 if invalid command" do it "invokes help and exits with 1 if invalid command" do
subject = described_class.new(["i-dont-exist"], env) subject = described_class.new(["i-dont-exist"], env)
expect(subject).to receive(:help).once expect(subject).to receive(:help).once
@ -59,7 +62,7 @@ describe Vagrant::CLI do
it "fires triggers, if enabled" do it "fires triggers, if enabled" do
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
with("typed_triggers").and_return("true") with("typed_triggers").and_return(true)
allow(triggers).to receive(:fire_triggers) allow(triggers).to receive(:fire_triggers)
commands[:destroy] = [command_lambda("destroy", 42), {}] commands[:destroy] = [command_lambda("destroy", 42), {}]
@ -76,7 +79,7 @@ describe Vagrant::CLI do
it "does not fire triggers if disabled" do it "does not fire triggers if disabled" do
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
with("typed_triggers").and_return("false") with("typed_triggers").and_return(false)
commands[:destroy] = [command_lambda("destroy", 42), {}] commands[:destroy] = [command_lambda("destroy", 42), {}]

View File

@ -0,0 +1,215 @@
require_relative "../../base"
describe Vagrant::GoPlugin::CapabilityPlugin do
let(:client) { double("client") }
describe Vagrant::GoPlugin::CapabilityPlugin::Capability do
it "should be a GRPCPlugin" do
expect(described_class.ancestors).to include(Vagrant::GoPlugin::GRPCPlugin)
end
end
describe ".generate_guest_capabilities" do
let(:caps) {
[{platform: "dummy", name: "stub_cap"},
{platform: "dummy", name: "other_cap"}]}
let(:cap_response) {
Vagrant::Proto::SystemCapabilityList.new(
capabilities: caps.map { |i|
Vagrant::Proto::SystemCapability.new(i)})}
let(:plugin_klass) { double("plugin_klass") }
let(:plugin_type) { :testing }
before do
allow(client).to receive(:guest_capabilities).
and_return(cap_response)
allow(plugin_klass).to receive(:guest_capability)
end
it "should generate two new capability classes" do
expect(Class).to receive(:new).twice.
with(Vagrant::GoPlugin::CapabilityPlugin::Capability).
and_call_original
described_class.generate_guest_capabilities(client, plugin_klass, plugin_type)
end
it "should create capability name methods" do
c1 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
c2 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
expect(Class).to receive(:new).and_return(c1)
expect(Class).to receive(:new).and_return(c2)
described_class.generate_guest_capabilities(client, plugin_klass, plugin_type)
expect(c1).to respond_to(:stub_cap)
expect(c2).to respond_to(:other_cap)
end
it "should register guest capability" do
expect(plugin_klass).to receive(:guest_capability).with(:dummy, :stub_cap)
expect(plugin_klass).to receive(:guest_capability).with(:dummy, :other_cap)
described_class.generate_guest_capabilities(client, plugin_klass, plugin_type)
end
context "generated capability" do
let(:cap_class) { Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability) }
let(:response) { double(result: result) }
let(:result) { nil }
before {
expect(Class).to receive(:new).and_return(cap_class)
allow(Class).to receive(:new).and_call_original
described_class.generate_guest_capabilities(client, plugin_klass, plugin_type)
}
it "should call guest_capability on the plugin client" do
expect(client).to receive(:guest_capability).and_return(response)
cap_class.stub_cap(nil, nil)
end
context "when result is a hash" do
let(:result) { "{}" }
it "should convert hash to hash with indifferent access" do
expect(client).to receive(:guest_capability).and_return(response)
expect(cap_class.stub_cap(nil, nil)).to be_a(Vagrant::Util::HashWithIndifferentAccess)
end
end
end
end
describe ".generate_host_capabilities" do
let(:caps) {
[{platform: "dummy", name: "stub_cap"},
{platform: "dummy", name: "other_cap"}]}
let(:cap_response) {
Vagrant::Proto::SystemCapabilityList.new(
capabilities: caps.map { |i|
Vagrant::Proto::SystemCapability.new(i)})}
let(:plugin_klass) { double("plugin_klass") }
let(:plugin_type) { :testing }
before do
allow(client).to receive(:host_capabilities).
and_return(cap_response)
allow(plugin_klass).to receive(:host_capability)
end
it "should generate two new capability classes" do
expect(Class).to receive(:new).twice.
with(Vagrant::GoPlugin::CapabilityPlugin::Capability).
and_call_original
described_class.generate_host_capabilities(client, plugin_klass, plugin_type)
end
it "should create capability name methods" do
c1 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
c2 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
expect(Class).to receive(:new).and_return(c1)
expect(Class).to receive(:new).and_return(c2)
described_class.generate_host_capabilities(client, plugin_klass, plugin_type)
expect(c1).to respond_to(:stub_cap)
expect(c2).to respond_to(:other_cap)
end
it "should register host capability" do
expect(plugin_klass).to receive(:host_capability).with(:dummy, :stub_cap)
expect(plugin_klass).to receive(:host_capability).with(:dummy, :other_cap)
described_class.generate_host_capabilities(client, plugin_klass, plugin_type)
end
context "generated capability" do
let(:cap_class) { Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability) }
let(:response) { double(result: result) }
let(:result) { nil }
before {
expect(Class).to receive(:new).and_return(cap_class)
allow(Class).to receive(:new).and_call_original
described_class.generate_host_capabilities(client, plugin_klass, plugin_type)
}
it "should call guest_capability on the plugin client" do
expect(client).to receive(:host_capability).and_return(response)
cap_class.stub_cap(nil, nil)
end
context "when result is a hash" do
let(:result) { "{}" }
it "should convert hash to hash with indifferent access" do
expect(client).to receive(:host_capability).and_return(response)
expect(cap_class.stub_cap(nil, nil)).to be_a(Vagrant::Util::HashWithIndifferentAccess)
end
end
end
end
describe ".generate_provider_capabilities" do
let(:caps) {
[{provider: "dummy", name: "stub_cap"},
{provider: "dummy", name: "other_cap"}]}
let(:cap_response) {
Vagrant::Proto::ProviderCapabilityList.new(
capabilities: caps.map { |i|
Vagrant::Proto::ProviderCapability.new(i)})}
let(:plugin_klass) { double("plugin_klass") }
let(:plugin_type) { :testing }
before do
allow(client).to receive(:provider_capabilities).
and_return(cap_response)
allow(plugin_klass).to receive(:provider_capability)
end
it "should generate two new capability classes" do
expect(Class).to receive(:new).twice.
with(Vagrant::GoPlugin::CapabilityPlugin::Capability).
and_call_original
described_class.generate_provider_capabilities(client, plugin_klass, plugin_type)
end
it "should create capability name methods" do
c1 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
c2 = Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability)
expect(Class).to receive(:new).and_return(c1)
expect(Class).to receive(:new).and_return(c2)
described_class.generate_provider_capabilities(client, plugin_klass, plugin_type)
expect(c1).to respond_to(:stub_cap)
expect(c2).to respond_to(:other_cap)
end
it "should register provider capability" do
expect(plugin_klass).to receive(:provider_capability).with(:dummy, :stub_cap)
expect(plugin_klass).to receive(:provider_capability).with(:dummy, :other_cap)
described_class.generate_provider_capabilities(client, plugin_klass, plugin_type)
end
context "generated capability" do
let(:cap_class) { Class.new(Vagrant::GoPlugin::CapabilityPlugin::Capability) }
let(:response) { double(result: result) }
let(:result) { nil }
before {
expect(Class).to receive(:new).and_return(cap_class)
allow(Class).to receive(:new).and_call_original
described_class.generate_provider_capabilities(client, plugin_klass, plugin_type)
}
it "should call guest_capability on the plugin client" do
expect(client).to receive(:provider_capability).and_return(response)
cap_class.stub_cap(nil, nil)
end
context "when result is a hash" do
let(:result) { "{}" }
it "should convert hash to hash with indifferent access" do
expect(client).to receive(:provider_capability).and_return(response)
expect(cap_class.stub_cap(nil, nil)).to be_a(Vagrant::Util::HashWithIndifferentAccess)
end
end
end
end
end

View File

@ -0,0 +1,141 @@
require_relative "../../base"
describe Vagrant::GoPlugin::ConfigPlugin do
let(:client) { double("client") }
describe ".generate_config" do
let(:parent_name) { "test" }
let(:parent_klass) { double("parent_klass") }
let(:parent_type) { "dummy" }
let(:attributes_response) {
double("attributes_response",
items: attributes)
}
let(:attributes) { [] }
before do
allow(parent_klass).to receive(:config)
allow(client).to receive(:config_attributes).
and_return(attributes_response)
end
it "should register a new config class" do
expect(parent_klass).to receive(:config).
with(parent_name, parent_type) { |&block|
expect(block.call.ancestors).to include(Vagrant::GoPlugin::ConfigPlugin::Config)
}
described_class.generate_config(client, parent_name, parent_klass, parent_type)
end
it "should register the client within the class" do
expect(parent_klass).to receive(:config).
with(parent_name, parent_type) { |&block|
expect(block.call.plugin_client).to eq(client)
}
described_class.generate_config(client, parent_name, parent_klass, parent_type)
end
context "when attributes are provided" do
let(:attributes) { ["a_one", "a_two"] }
it "should add accessor instance methods to the class" do
expect(parent_klass).to receive(:config).
with(parent_name, parent_type) { |&block|
expect(block.call.instance_methods).to include(:a_one)
expect(block.call.instance_methods).to include(:a_two)
}
described_class.generate_config(client, parent_name, parent_klass, parent_type)
end
end
end
describe Vagrant::GoPlugin::ConfigPlugin::Config do
let(:machine) { double("machine") }
let(:subject) {
c = Class.new(described_class)
c.plugin_client = client
c.new
}
describe "#validate" do
let(:response) { double("response", items: errors) }
let(:errors) { ["errors"] }
before do
allow(machine).to receive(:to_json).and_return("{}")
allow(client).to receive(:config_validate).and_return(response)
end
it "should call client to validate" do
expect(client).to receive(:config_validate).
with(instance_of(Vagrant::Proto::Configuration)).
and_return(response)
subject.validate(machine)
end
it "should return list of validation errors" do
expect(subject.validate(machine)).to eq(errors)
end
end
describe "#finalize!" do
let(:response) { double("response", data: result) }
let(:result) { "null" }
before do
allow(client).to receive(:config_finalize).and_return(response)
end
it "should return the config instance" do
expect(subject.finalize!).to eq(subject)
end
it "should call client to finalize" do
expect(client).to receive(:config_finalize).
with(instance_of(Vagrant::Proto::Configuration)).
and_return(response)
subject.finalize!
end
context "when configuration data is returned" do
let(:result) {
{attr1: true, attr2: "value"}.to_json
}
it "should create accessor methods to configuration data" do
subject.finalize!
expect(subject).to respond_to(:attr1)
expect(subject).to respond_to(:attr2)
end
it "should return data from reader methods" do
subject.finalize!
expect(subject.attr1).to eq(true)
expect(subject.attr2).to eq("value")
end
end
describe "#local_data" do
it "should return an empty hash" do
expect(subject.local_data).to be_empty
end
context "with config attributes set" do
let(:response) { double("response", data: result) }
let(:result) { {attr1: true, attr2: "value"}.to_json }
before do
allow(client).to receive(:config_finalize).and_return(response)
subject.finalize!
end
it "should return data values" do
result = subject.local_data
expect(result[:attr1]).to eq(true)
expect(result[:attr2]).to eq("value")
end
end
end
end
end
end

View File

@ -0,0 +1,41 @@
require_relative "../../base"
describe Vagrant::GoPlugin::GRPCPlugin do
let(:client) { double("client") }
let(:subject) { Class.new.tap { |c| c.include(described_class) } }
describe ".plugin_client=" do
it "should set the plugin client" do
expect(subject.plugin_client = client).to eq(client)
expect(subject.plugin_client).to eq(client)
end
it "should error if plugin is already set" do
subject.plugin_client = client
expect { subject.plugin_client = client }.
to raise_error(ArgumentError)
end
end
describe ".plugin_client" do
it "should return nil when client has not been set" do
expect(subject.plugin_client).to be_nil
end
it "should return client when it has been set" do
subject.plugin_client = client
expect(subject.plugin_client).to eq(client)
end
end
describe "#plugin_client" do
it "should be nil when client has not been set" do
expect(subject.new.plugin_client).to be_nil
end
it "should return client when client has been set" do
subject.plugin_client = client
expect(subject.new.plugin_client).to eq(client)
end
end
end

View File

@ -0,0 +1,56 @@
require_relative "../../base"
describe Vagrant::GoPlugin::Interface do
before do
allow_any_instance_of(described_class).to receive(:_setup)
end
describe "#load_plugins" do
let(:path) { double("path", to_s: "path") }
it "should raise error if path is not a directory" do
expect(File).to receive(:directory?).with(path.to_s).and_return(false)
expect { subject.load_plugins(path) }.to raise_error(ArgumentError)
end
it "should load plugins if path is a directory" do
expect(File).to receive(:directory?).with(path.to_s).and_return(true)
expect(subject).to receive(:_load_plugins).with(path.to_s)
subject.load_plugins(path)
end
end
describe "#register_plugins" do
it "should load Provider and SyncedFolder plugins" do
expect(subject).to receive(:load_providers)
expect(subject).to receive(:load_synced_folders)
subject.register_plugins
end
end
describe "#setup" do
after { subject }
it "should register at_exit action" do
expect(Kernel).to receive(:at_exit)
subject
end
it "should run the setup action" do
expect_any_instance_of(described_class).to receive(:_setup)
end
it "should only run the setup process once" do
expect_any_instance_of(described_class).to receive(:_setup).once
expect(subject.logger).to receive(:warn)
subject.setup
end
end
describe "#teardown" do
it "should run the teardown action" do
expect(subject).to receive(:_teardown)
subject.teardown
end
end
end

View File

@ -0,0 +1,148 @@
require_relative "../../base"
describe Vagrant::GoPlugin::Manager do
include_context "unit"
let(:env) do
test_env.vagrantfile("")
test_env.create_vagrant_env
end
before do
allow(FileUtils).to receive(:mkdir_p)
end
describe ".instance" do
it "should return instance of manager" do
expect(described_class.instance).to be_a(described_class)
end
it "should cache instance" do
expect(described_class.instance).to be(described_class.instance)
end
end
describe ".new" do
it "should create the installation directory" do
expect(FileUtils).to receive(:mkdir_p).with(Vagrant::GoPlugin::INSTALL_DIRECTORY)
subject
end
it "should create installation temporary directory" do
expect(FileUtils).to receive(:mkdir_p).with(/tmp$/)
subject
end
it "should generate user state file" do
expect(subject.user_file).to be_a(Vagrant::Plugin::StateFile)
end
end
describe "#globalize!" do
let(:entries) { [double("entry1"), double("entry2")] }
before do
allow(File).to receive(:directory?).and_return(false)
allow(Dir).to receive(:glob).and_return(entries)
allow(Vagrant::GoPlugin).to receive_message_chain(:interface, :register_plugins)
end
context "when entries are not directories" do
before { allow(File).to receive(:directory?).and_return(false) }
it "should not load any plugins" do
interface = double("interface", register_plugins: nil)
allow(Vagrant::GoPlugin).to receive(:interface).and_return(interface)
expect(interface).not_to receive(:load_plugins)
subject.globalize!
end
end
context "when entries are directories" do
before { allow(File).to receive(:directory?).and_return(true) }
it "should load all entries" do
expect(Vagrant::GoPlugin).to receive_message_chain(:interface, :load_plugins).with(entries.first)
expect(Vagrant::GoPlugin).to receive_message_chain(:interface, :load_plugins).with(entries.last)
subject.globalize!
end
end
it "should register plugins after loading" do
expect(Vagrant::GoPlugin).to receive_message_chain(:interface, :register_plugins)
subject.globalize!
end
end
describe "#localize!" do
end
describe "#install_plugin" do
let(:plugin_name) { "test_plugin_name" }
let(:remote_source) { double("remote_source") }
let(:downloader) { double("downloader", download!: nil) }
before do
allow(FileUtils).to receive(:mkdir_p)
allow(Dir).to receive(:mktmpdir)
allow(Vagrant::Util::Downloader).to receive(:new).and_return(downloader)
allow(Zip::File).to receive(:open)
end
after { subject.install_plugin(plugin_name, remote_source) }
it "should create plugin directory for plugin name" do
expect(FileUtils).to receive(:mkdir_p).with(/test_plugin_name$/)
end
it "should create a temporary directory to download and unpack" do
expect(Dir).to receive(:mktmpdir).with(/go-plugin/, any_args)
end
it "should download the remote file" do
expect(Dir).to receive(:mktmpdir).with(any_args).and_yield("tmpdir")
expect(downloader).to receive(:download!)
end
it "should unzip the downloaded file" do
expect(Dir).to receive(:mktmpdir).with(any_args).and_yield("tmpdir")
expect(Zip::File).to receive(:open).with(/plugin.zip/)
end
it "should add the plugin to the user file" do
expect(subject.user_file).to receive(:add_go_plugin).and_call_original
expect(subject.user_file.has_go_plugin?("test_plugin_name")).to be_truthy
end
end
describe "#uninstall_plugin" do
let(:plugin_name) { "test_plugin_name" }
before do
allow(File).to receive(:directory?).and_call_original
allow(FileUtils).to receive(:rm_rf)
end
after { subject.uninstall_plugin(plugin_name) }
it "should remove plugin path when installed" do
expect(File).to receive(:directory?).with(/test_plugin_name/).and_return(true)
expect(FileUtils).to receive(:rm_rf).with(/test_plugin_name/)
end
it "should not remove plugin path when not installed" do
expect(File).to receive(:directory?).with(/test_plugin_name/).and_return(false)
expect(FileUtils).not_to receive(:rm_rf).with(/test_plugin_name/)
end
it "should have plugin name removed from user file when installed" do
expect(File).to receive(:directory?).with(/test_plugin_name/).and_return(true)
expect(subject.user_file).to receive(:remove_go_plugin).with(plugin_name)
end
it "should have plugin name removed from user file when not installed" do
expect(File).to receive(:directory?).with(/test_plugin_name/).and_return(false)
expect(subject.user_file).to receive(:remove_go_plugin).with(plugin_name)
end
end
end

View File

@ -0,0 +1,273 @@
require_relative "../../base"
describe Vagrant::GoPlugin::ProviderPlugin do
let(:client) { double("client") }
describe Vagrant::GoPlugin::ProviderPlugin::Action do
let(:described_class) { Class.new(Vagrant::GoPlugin::ProviderPlugin::Action) }
it "should be a GRPCPlugin" do
expect(described_class.ancestors).to include(Vagrant::GoPlugin::GRPCPlugin)
end
describe ".action_name" do
it "should return nil when unset" do
expect(described_class.action_name).to be_nil
end
it "should return defined action name when set" do
described_class.action_name = "test_action"
expect(described_class.action_name).to eq("test_action")
end
end
describe ".action_name=" do
it "should convert action name to string" do
described_class.action_name = :test_action
expect(described_class.action_name).to be_a(String)
end
it "should set the action name" do
described_class.action_name = "test_action"
expect(described_class.action_name).to eq("test_action")
end
it "should error if action name is already set" do
described_class.action_name = "test_action"
expect { described_class.action_name = "test_action" }.
to raise_error(ArgumentError)
end
it "should freeze action name" do
described_class.action_name = "test_action"
expect(described_class.action_name).to be_frozen
end
end
describe "#call" do
let(:app) { double("app") }
let(:env) { {"key1" => "value1", "key2" => "value2"} }
let(:response_env) {
{"key1" => "value1", "key2" => "value2"}
}
let(:response) { double("response", result: response_env.to_json) }
let(:subject) {
described_class.plugin_client = client
described_class.new(app, {})
}
before do
described_class.action_name = "test_action"
allow(app).to receive(:call)
allow(client).to receive(:run_action).and_return(response)
end
it "should call the next item in the middleware" do
expect(app).to receive(:call)
subject.call(env)
end
it "should call the client" do
expect(client).to receive(:run_action).and_return(response)
subject.call(env)
end
context "when new data is provided in result" do
let(:response_env) { {"new_data" => "value"} }
it "should include new data when calling next action" do
expect(app).to receive(:call).
with(hash_including("new_data" => "value", "key1" => "value1"))
subject.call(env)
end
end
end
end
describe Vagrant::GoPlugin::ProviderPlugin::Provider do
let(:described_class) {
Class.new(Vagrant::GoPlugin::ProviderPlugin::Provider).tap { |c|
c.plugin_client = client
}
}
let(:machine) { double("machine") }
let(:subject) { described_class.new(machine) }
it "should be a GRPCPlugin" do
expect(described_class.ancestors).to include(Vagrant::GoPlugin::GRPCPlugin)
end
describe "#name" do
let(:name) { double("name") }
let(:response) { double("response", name: name) }
before { allow(client).to receive(:name).and_return(response) }
it "should request name from the client" do
expect(client).to receive(:name).and_return(response)
subject.name
end
it "should return the plugin name" do
expect(subject.name).to eq(name)
end
it "should only call the client once" do
expect(client).to receive(:name).once.and_return(response)
subject.name
end
end
describe "#action" do
let(:action_name) { "test_action" }
let(:response) { double("response", items: actions) }
let(:actions) { ["self::TestAction"] }
before do
allow(client).to receive(:action).and_return(response)
end
it "should return a builder instance" do
expect(subject.action(action_name)).to be_a(Vagrant::Action::Builder)
end
it "should call the plugin client" do
expect(client).to receive(:action).
with(instance_of(Vagrant::Proto::GenericAction)).
and_return(response)
subject.action(action_name)
end
it "should create a new custom action class" do
builder = subject.action(action_name)
action = builder.stack.first.first
expect(action.ancestors).to include(Vagrant::GoPlugin::ProviderPlugin::Action)
expect(action.action_name).to eq("TestAction")
end
context "when given non-local action name" do
let(:actions) { ["Vagrant::Action::Builtin::Call"] }
it "should load the existing action class" do
builder = subject.action(action_name)
action = builder.stack.first.first
expect(action).to eq(Vagrant::Action::Builtin::Call)
end
end
end
describe "#capability" do
let(:cap_name) { "test_cap" }
let(:response) { double("response", result: result.to_json) }
let(:result) { nil }
before do
allow(subject).to receive(:name).and_return("dummy_provider")
allow(client).to receive(:provider_capability).and_return(response)
end
it "should call the plugin client" do
expect(client).to receive(:provider_capability).
with(instance_of(Vagrant::Proto::ProviderCapabilityRequest))
.and_return(response)
subject.capability(cap_name)
end
it "should deserialize result" do
expect(subject.capability(cap_name)).to eq(result)
end
context "when hash value is returned" do
let(:result) { {key: "value"} }
it "should return indifferent access hash" do
r = subject.capability(cap_name)
expect(r[:key]).to eq(result[:key])
expect(r).to be_a(Vagrant::Util::HashWithIndifferentAccess)
end
end
context "when arguments are provided" do
let(:args) { ["arg1", {"key" => "value"}] }
it "should serialize arguments when sent to plugin client" do
expect(client).to receive(:provider_capability) do |req|
expect(req.arguments).to eq(args.to_json)
response
end
subject.capability(cap_name, *args)
end
end
end
describe "#is_installed?" do
it "should call plugin client" do
expect(client).to receive(:is_installed).and_return(double("response", result: true))
expect(subject.is_installed?).to eq(true)
end
end
describe "#is_usable?" do
it "should call plugin client" do
expect(client).to receive(:is_usable).and_return(double("response", result: true))
expect(subject.is_usable?).to eq(true)
end
end
describe "#machine_id_changed" do
it "should call plugin client" do
expect(client).to receive(:machine_id_changed).and_return(double("response", result: true))
expect(subject.machine_id_changed).to be_nil
end
end
describe "#ssh_info" do
let(:response) {
Vagrant::Proto::MachineSshInfo.new(
host: "localhost",
port: 2222,
private_key_path: "/key/path",
username: "vagrant"
)
}
before { allow(client).to receive(:ssh_info).and_return(response) }
it "should return hash with indifferent access result" do
expect(subject.ssh_info).to be_a(Vagrant::Util::HashWithIndifferentAccess)
end
it "should include ssh information" do
result = subject.ssh_info
expect(result[:host]).to eq(response.host)
expect(result[:port]).to eq(response.port)
expect(result[:private_key_path]).to eq(response.private_key_path)
expect(result[:username]).to eq(response.username)
end
end
describe "#state" do
let(:response) {
Vagrant::Proto::MachineState.new(
id: "machine-id",
short_description: "running",
long_description: "it's really running"
)
}
before { allow(client).to receive(:state).and_return(response) }
it "should return a MachineState instance" do
expect(subject.state).to be_a(Vagrant::MachineState)
end
it "should set the state attributes" do
result = subject.state
expect(result.id).to eq(response.id)
expect(result.short_description).to eq(response.short_description)
expect(result.long_description).to eq(response.long_description)
end
end
end
end

View File

@ -0,0 +1,73 @@
require_relative "../../base"
describe Vagrant::GoPlugin::SyncedFolderPlugin::SyncedFolder do
let(:subject) {
Class.new(described_class).tap { |c|
c.plugin_client = client } }
let(:client) { double("client") }
let(:machine) { double("machine", to_json: "{}") }
let(:folders) { double("folders", to_json: "{}") }
let(:options) { double("options", to_json: "{}") }
it "should be a GRPCPlugin" do
expect(subject).to be_a(Vagrant::GoPlugin::GRPCPlugin)
end
describe "#cleanup" do
it "should call plugin client" do
expect(client).to receive(:cleanup).
with(instance_of(Vagrant::Proto::SyncedFolders))
subject.cleanup(machine, options)
end
end
describe "#disable" do
it "should call plugin client" do
expect(client).to receive(:disable).
with(instance_of(Vagrant::Proto::SyncedFolders))
subject.disable(machine, folders, options)
end
end
describe "#enable" do
it "should call plugin client" do
expect(client).to receive(:enable).
with(instance_of(Vagrant::Proto::SyncedFolders))
subject.enable(machine, folders, options)
end
end
describe "#prepare" do
it "should call plugin client" do
expect(client).to receive(:prepare).
with(instance_of(Vagrant::Proto::SyncedFolders))
subject.prepare(machine, folders, options)
end
end
describe "#usable?" do
let(:response) { Vagrant::Proto::Valid.new(result: true) }
it "should call the plugin client" do
expect(client).to receive(:is_usable).
with(instance_of(Vagrant::Proto::Machine)).
and_return(response)
expect(subject.usable?).to eq(true)
end
end
describe "#name" do
let(:response) { Vagrnat::Proto::Identifier.new(name: "dummy") }
it "should call the plugin client" do
expect(client).to receive(:name).and_return(response)
expect(subject.name).to eq(response.name)
end
it "should only call the plugin client once" do
expect(client).to receive(:name).once.and_return(response)
expect(subject.name).to eq(response.name)
expect(subject.name).to eq(response.name)
end
end
end

View File

@ -0,0 +1,29 @@
require_relative "../base"
describe Vagrant::GoPlugin do
describe "INSTALL_DIRECTORY constant" do
let(:subject) { described_class.const_get(:INSTALL_DIRECTORY) }
it "should be a String" do
expect(subject).to be_a(String)
end
it "should be frozen" do
expect(subject).to be_frozen
end
it "should be within the user data path" do
expect(subject).to start_with(Vagrant.user_data_path.to_s)
end
end
describe ".interface" do
it "should return an interface instance" do
expect(described_class.interface).to be_a(Vagrant::GoPlugin::Interface)
end
it "should cache the interface instance" do
expect(described_class.interface).to be(described_class.interface)
end
end
end

View File

@ -118,4 +118,56 @@ describe Vagrant::Plugin::StateFile do
to raise_error(Vagrant::Errors::PluginStateFileParseError) to raise_error(Vagrant::Errors::PluginStateFileParseError)
end end
end end
context "go plugin usage" do
describe "#add_go_plugin" do
it "should add plugin to list of installed go plugins" do
subject.add_go_plugin("foo", source: "http://localhost/foo.zip")
expect(subject.installed_go_plugins).to include("foo")
end
it "should update source when added again" do
subject.add_go_plugin("foo", source: "http://localhost/foo.zip")
expect(subject.installed_go_plugins["foo"]["source"]).to eq("http://localhost/foo.zip")
subject.add_go_plugin("foo", source: "http://localhost/foo1.zip")
expect(subject.installed_go_plugins["foo"]["source"]).to eq("http://localhost/foo1.zip")
end
end
describe "#remove_go_plugin" do
before do
subject.add_go_plugin("foo", source: "http://localhost/foo.zip")
end
it "should remove the installed plugin" do
subject.remove_go_plugin("foo")
expect(subject.installed_go_plugins).not_to include("foo")
end
it "should remove plugin not installed" do
subject.remove_go_plugin("foo")
expect(subject.installed_go_plugins).not_to include("foo")
subject.remove_go_plugin("foo")
expect(subject.installed_go_plugins).not_to include("foo")
end
end
describe "#has_go_plugin?" do
before do
subject.add_go_plugin("foo", source: "http://localhost/foo.zip")
end
it "should return true when plugin is installed" do
expect(subject.has_go_plugin?("foo")).to be_truthy
end
it "should return false when plugin is not installed" do
expect(subject.has_go_plugin?("fee")).to be_falsey
end
it "should allow symbol names" do
expect(subject.has_go_plugin?(:foo)).to be_truthy
end
end
end
end end

View File

@ -13,6 +13,7 @@ Gem::Specification.new do |s|
s.description = "Vagrant is a tool for building and distributing virtualized development environments." s.description = "Vagrant is a tool for building and distributing virtualized development environments."
s.required_ruby_version = "~> 2.2", "< 2.7" s.required_ruby_version = "~> 2.2", "< 2.7"
s.required_rubygems_version = ">= 1.3.6" s.required_rubygems_version = ">= 1.3.6"
s.rubyforge_project = "vagrant" s.rubyforge_project = "vagrant"
@ -22,6 +23,8 @@ Gem::Specification.new do |s|
s.add_dependency "erubis", "~> 2.7.0" s.add_dependency "erubis", "~> 2.7.0"
s.add_dependency "i18n", "~> 1.1.1" s.add_dependency "i18n", "~> 1.1.1"
s.add_dependency "listen", "~> 3.1.5" s.add_dependency "listen", "~> 3.1.5"
s.add_dependency "grpc", "~> 1.19.0"
s.add_dependency "grpc-tools", "~> 1.19.0"
s.add_dependency "hashicorp-checkpoint", "~> 0.1.5" s.add_dependency "hashicorp-checkpoint", "~> 0.1.5"
s.add_dependency "log4r", "~> 1.1.9", "< 1.1.11" s.add_dependency "log4r", "~> 1.1.9", "< 1.1.11"
s.add_dependency "net-ssh", "~> 5.1.0" s.add_dependency "net-ssh", "~> 5.1.0"
@ -49,6 +52,7 @@ Gem::Specification.new do |s|
s.add_development_dependency "rspec-its", "~> 1.2.0" s.add_development_dependency "rspec-its", "~> 1.2.0"
s.add_development_dependency "webmock", "~> 2.3.1" s.add_development_dependency "webmock", "~> 2.3.1"
s.add_development_dependency "fake_ftp", "~> 0.1.1" s.add_development_dependency "fake_ftp", "~> 0.1.1"
s.add_development_dependency "rake-compiler"
# The following block of code determines the files that should be included # The following block of code determines the files that should be included
# in the gem. It does this by reading all the files in the directory where # in the gem. It does this by reading all the files in the directory where

View File

@ -54,3 +54,7 @@ Enabling this feature allows all provisioners to specify `before` and `after`
options. These options allow provisioners to be configured to run before or after options. These options allow provisioners to be configured to run before or after
any given "root" provisioner. more information about these options can be found any given "root" provisioner. more information about these options can be found
on the [base provisioner documentation page](/docs/provisioning/basic_usage.html) on the [base provisioner documentation page](/docs/provisioning/basic_usage.html)
### `go_plugin`
Enabling this feature turns on support for [go-plugin](https://github.com/hashicorp/go-plugin) based plugin for Vagrant.