Merge pull request #1483 from svanharmelen/f-winrm-support

core: add WinRM support
This commit is contained in:
Paul Hinze 2015-05-01 15:56:53 -05:00
commit 15c75c501f
23 changed files with 1121 additions and 463 deletions

View File

@ -6,25 +6,22 @@ import (
"os"
"time"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/helper/config"
helper "github.com/hashicorp/terraform/helper/ssh"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-homedir"
)
// ResourceProvisioner represents a file provisioner
type ResourceProvisioner struct{}
// Apply executes the file provisioner
func (p *ResourceProvisioner) Apply(
o terraform.UIOutput,
s *terraform.InstanceState,
c *terraform.ResourceConfig) error {
// Ensure the connection type is SSH
if err := helper.VerifySSH(s); err != nil {
return err
}
// Get the SSH configuration
conf, err := helper.ParseSSHConfig(s)
// Get a new communicator
comm, err := communicator.New(s)
if err != nil {
return err
}
@ -46,9 +43,10 @@ func (p *ResourceProvisioner) Apply(
if !ok {
return fmt.Errorf("Unsupported 'destination' type! Must be string.")
}
return p.copyFiles(conf, src, dst)
return p.copyFiles(comm, src, dst)
}
// Validate checks if the required arguments are configured
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
v := &config.Validator{
Required: []string{
@ -60,24 +58,16 @@ func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string
}
// copyFiles is used to copy the files from a source to a destination
func (p *ResourceProvisioner) copyFiles(conf *helper.SSHConfig, src, dst string) error {
// Get the SSH client config
config, err := helper.PrepareConfig(conf)
if err != nil {
return err
}
defer config.CleanupConfig()
// Wait and retry until we establish the SSH connection
var comm *helper.SSHCommunicator
err = retryFunc(conf.TimeoutVal, func() error {
host := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
comm, err = helper.New(host, config)
func (p *ResourceProvisioner) copyFiles(comm communicator.Communicator, src, dst string) error {
// Wait and retry until we establish the connection
err := retryFunc(comm.Timeout(), func() error {
err := comm.Connect(nil)
return err
})
if err != nil {
return err
}
defer comm.Disconnect()
info, err := os.Stat(src)
if err != nil {
@ -86,7 +76,7 @@ func (p *ResourceProvisioner) copyFiles(conf *helper.SSHConfig, src, dst string)
// If we're uploading a directory, short circuit and do that
if info.IsDir() {
if err := comm.UploadDir(dst, src, nil); err != nil {
if err := comm.UploadDir(dst, src); err != nil {
return fmt.Errorf("Upload failed: %v", err)
}
return nil

View File

@ -10,29 +10,22 @@ import (
"strings"
"time"
helper "github.com/hashicorp/terraform/helper/ssh"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-linereader"
)
const (
// DefaultShebang is added at the top of the script file
DefaultShebang = "#!/bin/sh"
)
// ResourceProvisioner represents a remote exec provisioner
type ResourceProvisioner struct{}
// Apply executes the remote exec provisioner
func (p *ResourceProvisioner) Apply(
o terraform.UIOutput,
s *terraform.InstanceState,
c *terraform.ResourceConfig) error {
// Ensure the connection type is SSH
if err := helper.VerifySSH(s); err != nil {
return err
}
// Get the SSH configuration
conf, err := helper.ParseSSHConfig(s)
// Get a new communicator
comm, err := communicator.New(s)
if err != nil {
return err
}
@ -47,12 +40,13 @@ func (p *ResourceProvisioner) Apply(
}
// Copy and execute each script
if err := p.runScripts(o, conf, scripts); err != nil {
if err := p.runScripts(o, comm, scripts); err != nil {
return err
}
return nil
}
// Validate checks if the required arguments are configured
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
num := 0
for name := range c.Raw {
@ -76,7 +70,7 @@ func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string
// generateScript takes the configuration and creates a script to be executed
// from the inline configs
func (p *ResourceProvisioner) generateScript(c *terraform.ResourceConfig) (string, error) {
lines := []string{DefaultShebang}
var lines []string
command, ok := c.Config["inline"]
if ok {
switch cmd := command.(type) {
@ -165,46 +159,20 @@ func (p *ResourceProvisioner) collectScripts(c *terraform.ResourceConfig) ([]io.
// runScripts is used to copy and execute a set of scripts
func (p *ResourceProvisioner) runScripts(
o terraform.UIOutput,
conf *helper.SSHConfig,
comm communicator.Communicator,
scripts []io.ReadCloser) error {
// Get the SSH client config
config, err := helper.PrepareConfig(conf)
if err != nil {
return err
}
defer config.CleanupConfig()
o.Output(fmt.Sprintf(
"Connecting to remote host via SSH...\n"+
" Host: %s\n"+
" User: %s\n"+
" Password: %v\n"+
" Private key: %v"+
" SSH Agent: %v",
conf.Host, conf.User,
conf.Password != "",
conf.KeyFile != "",
conf.Agent,
))
// Wait and retry until we establish the SSH connection
var comm *helper.SSHCommunicator
err = retryFunc(conf.TimeoutVal, func() error {
host := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
comm, err = helper.New(host, config)
if err != nil {
o.Output(fmt.Sprintf("Connection error, will retry: %s", err))
}
// Wait and retry until we establish the connection
err := retryFunc(comm.Timeout(), func() error {
err := comm.Connect(o)
return err
})
if err != nil {
return err
}
defer comm.Disconnect()
o.Output("Connected! Executing scripts...")
for _, script := range scripts {
var cmd *helper.RemoteCmd
var cmd *remote.Cmd
outR, outW := io.Pipe()
errR, errW := io.Pipe()
outDoneCh := make(chan struct{})
@ -212,23 +180,14 @@ func (p *ResourceProvisioner) runScripts(
go p.copyOutput(o, outR, outDoneCh)
go p.copyOutput(o, errR, errDoneCh)
err := retryFunc(conf.TimeoutVal, func() error {
remotePath := conf.RemotePath()
err = retryFunc(comm.Timeout(), func() error {
remotePath := comm.ScriptPath()
if err := comm.Upload(remotePath, script); err != nil {
if err := comm.UploadScript(remotePath, script); err != nil {
return fmt.Errorf("Failed to upload script: %v", err)
}
cmd = &helper.RemoteCmd{
Command: fmt.Sprintf("chmod 0777 %s", remotePath),
}
if err := comm.Start(cmd); err != nil {
return fmt.Errorf(
"Error chmodding script file to 0777 in remote "+
"machine: %s", err)
}
cmd.Wait()
cmd = &helper.RemoteCmd{
cmd = &remote.Cmd{
Command: remotePath,
Stdout: outW,
Stderr: errW,
@ -236,6 +195,7 @@ func (p *ResourceProvisioner) runScripts(
if err := comm.Start(cmd); err != nil {
return fmt.Errorf("Error starting script: %v", err)
}
return nil
})
if err == nil {

View File

@ -41,6 +41,11 @@ func TestResourceProvider_Validate_bad(t *testing.T) {
}
}
var expectedScriptOut = `cd /tmp
wget http://foobar
exit 0
`
func TestResourceProvider_generateScript(t *testing.T) {
p := new(ResourceProvisioner)
conf := testConfig(t, map[string]interface{}{
@ -60,12 +65,6 @@ func TestResourceProvider_generateScript(t *testing.T) {
}
}
var expectedScriptOut = `#!/bin/sh
cd /tmp
wget http://foobar
exit 0
`
func TestResourceProvider_CollectScripts_inline(t *testing.T) {
p := new(ResourceProvisioner)
conf := testConfig(t, map[string]interface{}{
@ -91,8 +90,8 @@ func TestResourceProvider_CollectScripts_inline(t *testing.T) {
t.Fatalf("err: %v", err)
}
if string(out.Bytes()) != expectedScriptOut {
t.Fatalf("bad: %v", out.Bytes())
if out.String() != expectedScriptOut {
t.Fatalf("bad: %v", out.String())
}
}
@ -117,8 +116,8 @@ func TestResourceProvider_CollectScripts_script(t *testing.T) {
t.Fatalf("err: %v", err)
}
if string(out.Bytes()) != expectedScriptOut {
t.Fatalf("bad: %v", out.Bytes())
if out.String() != expectedScriptOut {
t.Fatalf("bad: %v", out.String())
}
}
@ -148,8 +147,8 @@ func TestResourceProvider_CollectScripts_scripts(t *testing.T) {
t.Fatalf("err: %v", err)
}
if string(out.Bytes()) != expectedScriptOut {
t.Fatalf("bad: %v", out.Bytes())
if out.String() != expectedScriptOut {
t.Fatalf("bad: %v", out.String())
}
}
}

View File

@ -1,4 +1,3 @@
#!/bin/sh
cd /tmp
wget http://foobar
exit 0

View File

@ -0,0 +1,53 @@
package communicator
import (
"fmt"
"io"
"time"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/communicator/ssh"
"github.com/hashicorp/terraform/communicator/winrm"
"github.com/hashicorp/terraform/terraform"
)
// Communicator is an interface that must be implemented by all communicators
// used for any of the provisioners
type Communicator interface {
// Connect is used to setup the connection
Connect(terraform.UIOutput) error
// Disconnect is used to terminate the connection
Disconnect() error
// Timeout returns the configured connection timeout
Timeout() time.Duration
// ScriptPath returns the configured script path
ScriptPath() string
// Start executes a remote command in a new session
Start(*remote.Cmd) error
// Upload is used to upload a single file
Upload(string, io.Reader) error
// UploadScript is used to upload a file as a executable script
UploadScript(string, io.Reader) error
// UploadDir is used to upload a directory
UploadDir(string, string) error
}
// New returns a configured Communicator or an error if the connection type is not supported
func New(s *terraform.InstanceState) (Communicator, error) {
connType := s.Ephemeral.ConnInfo["type"]
switch connType {
case "ssh", "": // The default connection type is ssh, so if connType is empty use ssh
return ssh.New(s)
case "winrm":
return winrm.New(s)
default:
return nil, fmt.Errorf("connection type '%s' not supported", connType)
}
}

View File

@ -0,0 +1,30 @@
package communicator
import (
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestCommunicator_new(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "telnet",
},
},
}
if _, err := New(r); err == nil {
t.Fatalf("expected error with telnet")
}
r.Ephemeral.ConnInfo["type"] = "ssh"
if _, err := New(r); err != nil {
t.Fatalf("err: %v", err)
}
r.Ephemeral.ConnInfo["type"] = "winrm"
if _, err := New(r); err != nil {
t.Fatalf("err: %v", err)
}
}

View File

@ -0,0 +1,67 @@
package remote
import (
"io"
"sync"
)
// 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
// This will be set to true when the remote command has exited. It
// shouldn't be set manually by the user, but there is no harm in
// doing so.
Exited bool
// Once Exited is true, this will contain the exit code of the process.
ExitStatus int
// Internal fields
exitCh chan struct{}
// This thing is a mutex, lock when making modifications concurrently
sync.Mutex
}
// SetExited is a helper for setting that this process is exited. This
// should be called by communicators who are running a remote command in
// order to set that the command is done.
func (r *Cmd) SetExited(status int) {
r.Lock()
defer r.Unlock()
if r.exitCh == nil {
r.exitCh = make(chan struct{})
}
r.Exited = true
r.ExitStatus = status
close(r.exitCh)
}
// Wait waits for the remote command to complete.
func (r *Cmd) Wait() {
// Make sure our condition variable is initialized.
r.Lock()
if r.exitCh == nil {
r.exitCh = make(chan struct{})
}
r.Unlock()
<-r.exitCh
}

View File

@ -0,0 +1 @@
package remote

View File

@ -8,122 +8,153 @@ import (
"io"
"io/ioutil"
"log"
"math/rand"
"net"
"os"
"path/filepath"
"sync"
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/terraform"
"golang.org/x/crypto/ssh"
)
// RemoteCmd represents a remote command being prepared or run.
type RemoteCmd 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
const (
// DefaultShebang is added at the top of a SSH script file
DefaultShebang = "#!/bin/sh\n"
)
// 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
// This will be set to true when the remote command has exited. It
// shouldn't be set manually by the user, but there is no harm in
// doing so.
Exited bool
// Once Exited is true, this will contain the exit code of the process.
ExitStatus int
// Internal fields
exitCh chan struct{}
// This thing is a mutex, lock when making modifications concurrently
sync.Mutex
// Communicator represents the SSH communicator
type Communicator struct {
connInfo *connectionInfo
client *ssh.Client
config *sshConfig
conn net.Conn
address string
}
// SetExited is a helper for setting that this process is exited. This
// should be called by communicators who are running a remote command in
// order to set that the command is done.
func (r *RemoteCmd) SetExited(status int) {
r.Lock()
defer r.Unlock()
if r.exitCh == nil {
r.exitCh = make(chan struct{})
}
r.Exited = true
r.ExitStatus = status
close(r.exitCh)
}
// Wait waits for the remote command to complete.
func (r *RemoteCmd) Wait() {
// Make sure our condition variable is initialized.
r.Lock()
if r.exitCh == nil {
r.exitCh = make(chan struct{})
}
r.Unlock()
<-r.exitCh
}
type SSHCommunicator struct {
client *ssh.Client
config *Config
conn net.Conn
address string
}
// Config is the structure used to configure the SSH communicator.
type Config struct {
type sshConfig struct {
// The configuration of the Go SSH connection
SSHConfig *ssh.ClientConfig
config *ssh.ClientConfig
// Connection returns a new connection. The current connection
// 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)
connection func() (net.Conn, error)
// NoPty, if true, will not request a pty from the remote end.
NoPty bool
// noPty, if true, will not request a pty from the remote end.
noPty bool
// SSHAgentConn is a pointer to the UNIX connection for talking with the
// sshAgentConn is a pointer to the UNIX connection for talking with the
// ssh-agent.
SSHAgentConn net.Conn
sshAgentConn net.Conn
}
// New creates a new packer.Communicator implementation over SSH. This takes
// an already existing TCP connection and SSH configuration.
func New(address string, config *Config) (result *SSHCommunicator, err error) {
// Establish an initial connection and connect
result = &SSHCommunicator{
config: config,
address: address,
// New creates a new communicator implementation over SSH.
func New(s *terraform.InstanceState) (*Communicator, error) {
connInfo, err := parseConnectionInfo(s)
if err != nil {
return nil, err
}
if err = result.reconnect(); err != nil {
result = nil
return
config, err := prepareSSHConfig(connInfo)
if err != nil {
return nil, err
}
return
comm := &Communicator{
connInfo: connInfo,
config: config,
}
return comm, nil
}
func (c *SSHCommunicator) Start(cmd *RemoteCmd) (err error) {
// Connect implementation of communicator.Communicator interface
func (c *Communicator) Connect(o terraform.UIOutput) (err error) {
if c.conn != nil {
c.conn.Close()
}
// Set the conn and client to nil since we'll recreate it
c.conn = nil
c.client = nil
if o != nil {
o.Output(fmt.Sprintf(
"Connecting to remote host via SSH...\n"+
" Host: %s\n"+
" User: %s\n"+
" Password: %t\n"+
" Private key: %t\n"+
" SSH Agent: %t",
c.connInfo.Host, c.connInfo.User,
c.connInfo.Password != "",
c.connInfo.KeyFile != "",
c.connInfo.Agent,
))
}
log.Printf("connecting 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
log.Printf("connection error: %s", err)
return err
}
log.Printf("handshaking with SSH")
host := fmt.Sprintf("%s:%d", c.connInfo.Host, c.connInfo.Port)
sshConn, sshChan, req, err := ssh.NewClientConn(c.conn, host, c.config.config)
if err != nil {
log.Printf("handshake error: %s", err)
return err
}
c.client = ssh.NewClient(sshConn, sshChan, req)
if o != nil {
o.Output("Connected!")
}
return err
}
// Disconnect implementation of communicator.Communicator interface
func (c *Communicator) Disconnect() error {
if c.config.sshAgentConn != nil {
return c.config.sshAgentConn.Close()
}
return nil
}
// Timeout implementation of communicator.Communicator interface
func (c *Communicator) Timeout() time.Duration {
return c.connInfo.TimeoutVal
}
// ScriptPath implementation of communicator.Communicator interface
func (c *Communicator) ScriptPath() string {
return strings.Replace(
c.connInfo.ScriptPath, "%RAND%",
strconv.FormatInt(int64(rand.Int31()), 10), -1)
}
// Start implementation of communicator.Communicator interface
func (c *Communicator) Start(cmd *remote.Cmd) error {
session, err := c.newSession()
if err != nil {
return
return err
}
// Setup our session
@ -131,7 +162,7 @@ func (c *SSHCommunicator) Start(cmd *RemoteCmd) (err error) {
session.Stdout = cmd.Stdout
session.Stderr = cmd.Stderr
if !c.config.NoPty {
if !c.config.noPty {
// Request a PTY
termModes := ssh.TerminalModes{
ssh.ECHO: 0, // do not echo
@ -139,15 +170,15 @@ func (c *SSHCommunicator) Start(cmd *RemoteCmd) (err error) {
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err = session.RequestPty("xterm", 80, 40, termModes); err != nil {
return
if err := session.RequestPty("xterm", 80, 40, termModes); err != nil {
return err
}
}
log.Printf("starting remote command: %s", cmd.Command)
err = session.Start(cmd.Command + "\n")
if err != nil {
return
return err
}
// Start a goroutine to wait for the session to end and set the
@ -168,10 +199,11 @@ func (c *SSHCommunicator) Start(cmd *RemoteCmd) (err error) {
cmd.SetExited(exitStatus)
}()
return
return nil
}
func (c *SSHCommunicator) Upload(path string, input io.Reader) error {
// Upload implementation of communicator.Communicator interface
func (c *Communicator) Upload(path string, input io.Reader) error {
// The target directory and file for talking the SCP protocol
targetDir := filepath.Dir(path)
targetFile := filepath.Base(path)
@ -188,7 +220,30 @@ func (c *SSHCommunicator) Upload(path string, input io.Reader) error {
return c.scpSession("scp -vt "+targetDir, scpFunc)
}
func (c *SSHCommunicator) UploadDir(dst string, src string, excl []string) error {
// UploadScript implementation of communicator.Communicator interface
func (c *Communicator) UploadScript(path string, input io.Reader) error {
script := bytes.NewBufferString(DefaultShebang)
script.ReadFrom(input)
if err := c.Upload(path, script); err != nil {
return err
}
cmd := &remote.Cmd{
Command: fmt.Sprintf("chmod 0777 %s", c.connInfo.ScriptPath),
}
if err := c.Start(cmd); err != nil {
return fmt.Errorf(
"Error chmodding script file to 0777 in remote "+
"machine: %s", err)
}
cmd.Wait()
return nil
}
// UploadDir implementation of communicator.Communicator interface
func (c *Communicator) UploadDir(dst string, src string) error {
log.Printf("Upload dir '%s' to '%s'", src, dst)
scpFunc := func(w io.Writer, r *bufio.Reader) error {
uploadEntries := func() error {
@ -217,11 +272,7 @@ func (c *SSHCommunicator) UploadDir(dst string, src string, excl []string) error
return c.scpSession("scp -rvt "+dst, scpFunc)
}
func (c *SSHCommunicator) Download(string, io.Writer) error {
panic("not implemented yet")
}
func (c *SSHCommunicator) newSession() (session *ssh.Session, err error) {
func (c *Communicator) newSession() (session *ssh.Session, err error) {
log.Println("opening new ssh session")
if c.client == nil {
err = errors.New("client not available")
@ -231,7 +282,7 @@ func (c *SSHCommunicator) newSession() (session *ssh.Session, err error) {
if err != nil {
log.Printf("ssh session open error: '%s', attempting reconnect", err)
if err := c.reconnect(); err != nil {
if err := c.Connect(nil); err != nil {
return nil, err
}
@ -241,43 +292,7 @@ func (c *SSHCommunicator) newSession() (session *ssh.Session, err error) {
return session, nil
}
func (c *SSHCommunicator) reconnect() (err error) {
if c.conn != nil {
c.conn.Close()
}
// Set the conn and client to nil since we'll recreate it
c.conn = nil
c.client = nil
log.Printf("reconnecting 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
log.Printf("reconnection error: %s", err)
return
}
log.Printf("handshaking with SSH")
sshConn, sshChan, req, err := ssh.NewClientConn(c.conn, c.address, c.config.SSHConfig)
if err != nil {
log.Printf("handshake error: %s", err)
}
if sshConn != nil {
c.client = ssh.NewClient(sshConn, sshChan, req)
}
return
}
func (c *SSHCommunicator) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error {
func (c *Communicator) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error {
session, err := c.newSession()
if err != nil {
return err
@ -382,7 +397,7 @@ func checkSCPStatus(r *bufio.Reader) error {
func scpUploadFile(dst string, src io.Reader, w io.Writer, r *bufio.Reader) error {
// 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("", "packer-upload")
tf, err := ioutil.TempFile("", "terraform-upload")
if err != nil {
return fmt.Errorf("Error creating temporary file for upload: %s", err)
}

View File

@ -6,8 +6,12 @@ import (
"bytes"
"fmt"
"net"
"regexp"
"strings"
"testing"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/terraform"
"golang.org/x/crypto/ssh"
)
@ -105,67 +109,91 @@ func newMockLineServer(t *testing.T) string {
}
func TestNew_Invalid(t *testing.T) {
clientConfig := &ssh.ClientConfig{
User: "user",
Auth: []ssh.AuthMethod{
ssh.Password("i-am-invalid"),
address := newMockLineServer(t)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "i-am-invalid",
"host": parts[0],
"port": parts[1],
"timeout": "30s",
},
},
}
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
c, err := New(r)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
config := &Config{
Connection: conn,
SSHConfig: clientConfig,
}
_, err := New(address, config)
err = c.Connect(nil)
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"),
address := newMockLineServer(t)
parts := strings.Split(address, ":")
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "user",
"password": "pass",
"host": parts[0],
"port": parts[1],
"timeout": "30s",
},
},
}
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)
c, err := New(r)
if err != nil {
t.Fatalf("error connecting to SSH: %s", err)
t.Fatalf("error creating communicator: %s", err)
}
var cmd RemoteCmd
var cmd remote.Cmd
stdout := new(bytes.Buffer)
cmd.Command = "echo foo"
cmd.Stdout = stdout
err = client.Start(&cmd)
err = c.Start(&cmd)
if err != nil {
t.Fatalf("error executing command: %s", err)
t.Fatalf("error executing remote command: %s", err)
}
}
func TestScriptPath(t *testing.T) {
cases := []struct {
Input string
Pattern string
}{
{
"/tmp/script.sh",
`^/tmp/script\.sh$`,
},
{
"/tmp/script_%RAND%.sh",
`^/tmp/script_(\d+)\.sh$`,
},
}
for _, tc := range cases {
comm := &Communicator{connInfo: &connectionInfo{ScriptPath: tc.Input}}
output := comm.ScriptPath()
match, err := regexp.Match(tc.Pattern, []byte(output))
if err != nil {
t.Fatalf("bad: %s\n\nerr: %s", tc.Input, err)
}
if !match {
t.Fatalf("bad: %s\n\n%s", tc.Input, output)
}
}
}

View File

@ -5,11 +5,8 @@ import (
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform/terraform"
@ -20,7 +17,7 @@ import (
)
const (
// DefaultUser is used if there is no default user given
// DefaultUser is used if there is no user given
DefaultUser = "root"
// DefaultPort is used if there is no port given
@ -28,16 +25,16 @@ const (
// DefaultScriptPath is used as the path to copy the file to
// for remote execution if not provided otherwise.
DefaultScriptPath = "/tmp/script_%RAND%.sh"
DefaultScriptPath = "/tmp/terraform_%RAND%.sh"
// DefaultTimeout is used if there is no timeout given
DefaultTimeout = 5 * time.Minute
)
// SSHConfig is decoded from the ConnInfo of the resource. These
// are the only keys we look at. If a KeyFile is given, that is used
// instead of a password.
type SSHConfig struct {
// connectionInfo is decoded from the ConnInfo of the resource. These are the
// only keys we look at. If a KeyFile is given, that is used instead
// of a password.
type connectionInfo struct {
User string
Password string
KeyFile string `mapstructure:"key_file"`
@ -49,31 +46,13 @@ type SSHConfig struct {
TimeoutVal time.Duration `mapstructure:"-"`
}
func (c *SSHConfig) RemotePath() string {
return strings.Replace(
c.ScriptPath, "%RAND%",
strconv.FormatInt(int64(rand.Int31()), 10), -1)
}
// VerifySSH is used to verify the ConnInfo is usable by remote-exec
func VerifySSH(s *terraform.InstanceState) error {
connType := s.Ephemeral.ConnInfo["type"]
switch connType {
case "":
case "ssh":
default:
return fmt.Errorf("Connection type '%s' not supported", connType)
}
return nil
}
// ParseSSHConfig is used to convert the ConnInfo of the InstanceState into
// a SSHConfig struct
func ParseSSHConfig(s *terraform.InstanceState) (*SSHConfig, error) {
sshConf := &SSHConfig{}
// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
// a ConnectionInfo struct
func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
connInfo := &connectionInfo{}
decConf := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: sshConf,
Result: connInfo,
}
dec, err := mapstructure.NewDecoder(decConf)
if err != nil {
@ -82,21 +61,23 @@ func ParseSSHConfig(s *terraform.InstanceState) (*SSHConfig, error) {
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
return nil, err
}
if sshConf.User == "" {
sshConf.User = DefaultUser
if connInfo.User == "" {
connInfo.User = DefaultUser
}
if sshConf.Port == 0 {
sshConf.Port = DefaultPort
if connInfo.Port == 0 {
connInfo.Port = DefaultPort
}
if sshConf.ScriptPath == "" {
sshConf.ScriptPath = DefaultScriptPath
if connInfo.ScriptPath == "" {
connInfo.ScriptPath = DefaultScriptPath
}
if sshConf.Timeout != "" {
sshConf.TimeoutVal = safeDuration(sshConf.Timeout, DefaultTimeout)
if connInfo.Timeout != "" {
connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
} else {
sshConf.TimeoutVal = DefaultTimeout
connInfo.TimeoutVal = DefaultTimeout
}
return sshConf, nil
return connInfo, nil
}
// safeDuration returns either the parsed duration or a default value
@ -109,16 +90,16 @@ func safeDuration(dur string, defaultDur time.Duration) time.Duration {
return d
}
// PrepareConfig is used to turn the *SSHConfig provided into a
// usable *Config for client initialization.
func PrepareConfig(conf *SSHConfig) (*Config, error) {
// prepareSSHConfig is used to turn the *ConnectionInfo provided into a
// usable *SSHConfig for client initialization.
func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
var conn net.Conn
var err error
sshConf := &ssh.ClientConfig{
User: conf.User,
User: connInfo.User,
}
if conf.Agent {
if connInfo.Agent {
sshAuthSock := os.Getenv("SSH_AUTH_SOCK")
if sshAuthSock == "" {
@ -138,14 +119,14 @@ func PrepareConfig(conf *SSHConfig) (*Config, error) {
sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signers...))
}
if conf.KeyFile != "" {
fullPath, err := homedir.Expand(conf.KeyFile)
if connInfo.KeyFile != "" {
fullPath, err := homedir.Expand(connInfo.KeyFile)
if err != nil {
return nil, fmt.Errorf("Failed to expand home directory: %v", err)
}
key, err := ioutil.ReadFile(fullPath)
if err != nil {
return nil, fmt.Errorf("Failed to read key file '%s': %v", conf.KeyFile, err)
return nil, fmt.Errorf("Failed to read key file '%s': %v", connInfo.KeyFile, err)
}
// We parse the private key on our own first so that we can
@ -153,40 +134,32 @@ func PrepareConfig(conf *SSHConfig) (*Config, error) {
block, _ := pem.Decode(key)
if block == nil {
return nil, fmt.Errorf(
"Failed to read key '%s': no key found", conf.KeyFile)
"Failed to read key '%s': no key found", connInfo.KeyFile)
}
if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
return nil, fmt.Errorf(
"Failed to read key '%s': password protected keys are\n"+
"not supported. Please decrypt the key prior to use.", conf.KeyFile)
"not supported. Please decrypt the key prior to use.", connInfo.KeyFile)
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return nil, fmt.Errorf("Failed to parse key file '%s': %v", conf.KeyFile, err)
return nil, fmt.Errorf("Failed to parse key file '%s': %v", connInfo.KeyFile, err)
}
sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signer))
}
if conf.Password != "" {
if connInfo.Password != "" {
sshConf.Auth = append(sshConf.Auth,
ssh.Password(conf.Password))
ssh.Password(connInfo.Password))
sshConf.Auth = append(sshConf.Auth,
ssh.KeyboardInteractive(PasswordKeyboardInteractive(conf.Password)))
ssh.KeyboardInteractive(PasswordKeyboardInteractive(connInfo.Password)))
}
host := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
config := &Config{
SSHConfig: sshConf,
Connection: ConnectFunc("tcp", host),
SSHAgentConn: conn,
host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
config := &sshConfig{
config: sshConf,
connection: ConnectFunc("tcp", host),
sshAgentConn: conn,
}
return config, nil
}
func (c *Config) CleanupConfig() error {
if c.SSHAgentConn != nil {
return c.SSHAgentConn.Close()
}
return nil
}

View File

@ -0,0 +1,50 @@
package ssh
import (
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestProvisioner_connInfo(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"key_file": "/my/key/file.pem",
"host": "127.0.0.1",
"port": "22",
"timeout": "30s",
},
},
}
conf, err := parseConnectionInfo(r)
if err != nil {
t.Fatalf("err: %v", err)
}
if conf.User != "root" {
t.Fatalf("bad: %v", conf)
}
if conf.Password != "supersecret" {
t.Fatalf("bad: %v", conf)
}
if conf.KeyFile != "/my/key/file.pem" {
t.Fatalf("bad: %v", conf)
}
if conf.Host != "127.0.0.1" {
t.Fatalf("bad: %v", conf)
}
if conf.Port != 22 {
t.Fatalf("bad: %v", conf)
}
if conf.Timeout != "30s" {
t.Fatalf("bad: %v", conf)
}
if conf.ScriptPath != DefaultScriptPath {
t.Fatalf("bad: %v", conf)
}
}

View File

@ -0,0 +1,193 @@
package winrm
import (
"fmt"
"io"
"log"
"math/rand"
"strconv"
"strings"
"time"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/masterzen/winrm/winrm"
"github.com/packer-community/winrmcp/winrmcp"
// This import is a bit strange, but it's needed so `make updatedeps` can see and download it
_ "github.com/dylanmei/winrmtest"
)
// Communicator represents the WinRM communicator
type Communicator struct {
connInfo *connectionInfo
client *winrm.Client
endpoint *winrm.Endpoint
}
// New creates a new communicator implementation over WinRM.
func New(s *terraform.InstanceState) (*Communicator, error) {
connInfo, err := parseConnectionInfo(s)
if err != nil {
return nil, err
}
endpoint := &winrm.Endpoint{
Host: connInfo.Host,
Port: connInfo.Port,
HTTPS: connInfo.HTTPS,
Insecure: connInfo.Insecure,
CACert: connInfo.CACert,
}
comm := &Communicator{
connInfo: connInfo,
endpoint: endpoint,
}
return comm, nil
}
// Connect implementation of communicator.Communicator interface
func (c *Communicator) Connect(o terraform.UIOutput) error {
if c.client != nil {
return nil
}
params := winrm.DefaultParameters()
params.Timeout = formatDuration(c.Timeout())
client, err := winrm.NewClientWithParameters(
c.endpoint, c.connInfo.User, c.connInfo.Password, params)
if err != nil {
return err
}
if o != nil {
o.Output(fmt.Sprintf(
"Connecting to remote host via WinRM...\n"+
" Host: %s\n"+
" Port: %d\n"+
" User: %s\n"+
" Password: %t\n"+
" HTTPS: %t\n"+
" Insecure: %t\n"+
" CACert: %t",
c.connInfo.Host,
c.connInfo.Port,
c.connInfo.User,
c.connInfo.Password != "",
c.connInfo.HTTPS,
c.connInfo.Insecure,
c.connInfo.CACert != nil,
))
}
log.Printf("connecting to remote shell using WinRM")
shell, err := client.CreateShell()
if err != nil {
log.Printf("connection error: %s", err)
return err
}
err = shell.Close()
if err != nil {
log.Printf("error closing connection: %s", err)
return err
}
if o != nil {
o.Output("Connected!")
}
c.client = client
return nil
}
// Disconnect implementation of communicator.Communicator interface
func (c *Communicator) Disconnect() error {
c.client = nil
return nil
}
// Timeout implementation of communicator.Communicator interface
func (c *Communicator) Timeout() time.Duration {
return c.connInfo.TimeoutVal
}
// ScriptPath implementation of communicator.Communicator interface
func (c *Communicator) ScriptPath() string {
return strings.Replace(
c.connInfo.ScriptPath, "%RAND%",
strconv.FormatInt(int64(rand.Int31()), 10), -1)
}
// Start implementation of communicator.Communicator interface
func (c *Communicator) Start(rc *remote.Cmd) error {
log.Printf("starting remote command: %s", rc.Command)
err := c.Connect(nil)
if err != nil {
return err
}
shell, err := c.client.CreateShell()
if err != nil {
return err
}
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 *remote.Cmd) {
defer shell.Close()
go io.Copy(rc.Stdout, cmd.Stdout)
go io.Copy(rc.Stderr, cmd.Stderr)
cmd.Wait()
rc.SetExited(cmd.ExitCode())
}
// Upload implementation of communicator.Communicator interface
func (c *Communicator) Upload(path string, input io.Reader) error {
wcp, err := c.newCopyClient()
if err != nil {
return err
}
return wcp.Write(path, input)
}
// UploadScript implementation of communicator.Communicator interface
func (c *Communicator) UploadScript(path string, input io.Reader) error {
return c.Upload(path, input)
}
// UploadDir implementation of communicator.Communicator interface
func (c *Communicator) UploadDir(dst string, src string) error {
log.Printf("Upload dir '%s' to '%s'", src, dst)
wcp, err := c.newCopyClient()
if err != nil {
return err
}
return wcp.Copy(src, dst)
}
func (c *Communicator) newCopyClient() (*winrmcp.Winrmcp, error) {
addr := fmt.Sprintf("%s:%d", c.endpoint.Host, c.endpoint.Port)
return winrmcp.New(addr, &winrmcp.Config{
Auth: winrmcp.Auth{
User: c.connInfo.User,
Password: c.connInfo.Password,
},
OperationTimeout: c.Timeout(),
MaxOperationsPerShell: 15, // lowest common denominator
})
}

View File

@ -0,0 +1,145 @@
package winrm
import (
"bytes"
"io"
"regexp"
"strconv"
"testing"
"github.com/dylanmei/winrmtest"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/terraform"
)
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(`^powershell.exe -EncodedCommand .*$`),
func(out, err io.Writer) int {
return 0
})
wrm.CommandFunc(
winrmtest.MatchText("powershell"),
func(out, err io.Writer) int {
return 0
})
return wrm
}
func TestStart(t *testing.T) {
wrm := newMockWinRMServer(t)
defer wrm.Close()
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "user",
"password": "pass",
"host": wrm.Host,
"port": strconv.Itoa(wrm.Port),
"timeout": "30s",
},
},
}
c, err := New(r)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
var cmd remote.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()
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "user",
"password": "pass",
"host": wrm.Host,
"port": strconv.Itoa(wrm.Port),
"timeout": "30s",
},
},
}
c, err := New(r)
if err != nil {
t.Fatalf("error creating communicator: %s", err)
}
err = c.Connect(nil)
if err != nil {
t.Fatalf("error connecting communicator: %s", err)
}
defer c.Disconnect()
err = c.Upload("C:/Temp/terraform.cmd", bytes.NewReader([]byte("something")))
if err != nil {
t.Fatalf("error uploading file: %s", err)
}
}
func TestScriptPath(t *testing.T) {
cases := []struct {
Input string
Pattern string
}{
{
"/tmp/script.sh",
`^/tmp/script\.sh$`,
},
{
"/tmp/script_%RAND%.sh",
`^/tmp/script_(\d+)\.sh$`,
},
}
for _, tc := range cases {
comm := &Communicator{connInfo: &connectionInfo{ScriptPath: tc.Input}}
output := comm.ScriptPath()
match, err := regexp.Match(tc.Pattern, []byte(output))
if err != nil {
t.Fatalf("bad: %s\n\nerr: %s", tc.Input, err)
}
if !match {
t.Fatalf("bad: %s\n\n%s", tc.Input, output)
}
}
}

View File

@ -0,0 +1,117 @@
package winrm
import (
"fmt"
"log"
"path/filepath"
"strings"
"time"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
)
const (
// DefaultUser is used if there is no user given
DefaultUser = "Administrator"
// DefaultPort is used if there is no port given
DefaultPort = 5985
// DefaultScriptPath is used as the path to copy the file to
// for remote execution if not provided otherwise.
DefaultScriptPath = "C:/Temp/terraform_%RAND%.cmd"
// DefaultTimeout is used if there is no timeout given
DefaultTimeout = 5 * time.Minute
)
// connectionInfo is decoded from the ConnInfo of the resource. These are the
// only keys we look at. If a KeyFile is given, that is used instead
// of a password.
type connectionInfo struct {
User string
Password string
Host string
Port int
HTTPS bool
Insecure bool
CACert *[]byte `mapstructure:"ca_cert"`
Timeout string
ScriptPath string `mapstructure:"script_path"`
TimeoutVal time.Duration `mapstructure:"-"`
}
// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
// a ConnectionInfo struct
func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
connInfo := &connectionInfo{}
decConf := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: connInfo,
}
dec, err := mapstructure.NewDecoder(decConf)
if err != nil {
return nil, err
}
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
return nil, err
}
// Check on script paths which point to the default Windows TEMP folder because files
// which are put in there very early in the boot process could get cleaned/deleted
// before you had the change to execute them.
//
// TODO (SvH) Needs some more debugging to fully understand the exact sequence of events
// causing this...
if strings.HasPrefix(filepath.ToSlash(connInfo.ScriptPath), "C:/Windows/Temp") {
return nil, fmt.Errorf(
`Using the C:\Windows\Temp folder is not supported. Please use a different 'script_path'.`)
}
if connInfo.User == "" {
connInfo.User = DefaultUser
}
if connInfo.Port == 0 {
connInfo.Port = DefaultPort
}
if connInfo.ScriptPath == "" {
connInfo.ScriptPath = DefaultScriptPath
}
if connInfo.Timeout != "" {
connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
} else {
connInfo.TimeoutVal = DefaultTimeout
}
return connInfo, nil
}
// safeDuration returns either the parsed duration or a default value
func safeDuration(dur string, defaultDur time.Duration) time.Duration {
d, err := time.ParseDuration(dur)
if err != nil {
log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
return defaultDur
}
return d
}
func formatDuration(duration time.Duration) string {
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,103 @@
package winrm
import (
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestProvisioner_connInfo(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "Administrator",
"password": "supersecret",
"host": "127.0.0.1",
"port": "5985",
"https": "true",
"timeout": "30s",
},
},
}
conf, err := parseConnectionInfo(r)
if err != nil {
t.Fatalf("err: %v", err)
}
if conf.User != "Administrator" {
t.Fatalf("expected: %v: got: %v", "Administrator", conf)
}
if conf.Password != "supersecret" {
t.Fatalf("expected: %v: got: %v", "supersecret", conf)
}
if conf.Host != "127.0.0.1" {
t.Fatalf("expected: %v: got: %v", "127.0.0.1", conf)
}
if conf.Port != 5985 {
t.Fatalf("expected: %v: got: %v", 5985, conf)
}
if conf.HTTPS != true {
t.Fatalf("expected: %v: got: %v", true, conf)
}
if conf.Timeout != "30s" {
t.Fatalf("expected: %v: got: %v", "30s", conf)
}
if conf.ScriptPath != DefaultScriptPath {
t.Fatalf("expected: %v: got: %v", DefaultScriptPath, conf)
}
}
func TestProvisioner_formatDuration(t *testing.T) {
cases := map[string]struct {
InstanceState *terraform.InstanceState
Result string
}{
"testSeconds": {
InstanceState: &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"timeout": "90s",
},
},
},
Result: "PT1M30S",
},
"testMinutes": {
InstanceState: &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"timeout": "5m",
},
},
},
Result: "PT5M",
},
"testHours": {
InstanceState: &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"timeout": "1h",
},
},
},
Result: "PT1H",
},
}
for name, tc := range cases {
conf, err := parseConnectionInfo(tc.InstanceState)
if err != nil {
t.Fatalf("err: %v", err)
}
result := formatDuration(conf.TimeoutVal)
if result != tc.Result {
t.Fatalf("%s: expected: %s got: %s", name, tc.Result, result)
}
}
}

View File

@ -1,97 +0,0 @@
package ssh
import (
"regexp"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestSSHConfig_RemotePath(t *testing.T) {
cases := []struct {
Input string
Pattern string
}{
{
"/tmp/script.sh",
`^/tmp/script\.sh$`,
},
{
"/tmp/script_%RAND%.sh",
`^/tmp/script_(\d+)\.sh$`,
},
}
for _, tc := range cases {
config := &SSHConfig{ScriptPath: tc.Input}
output := config.RemotePath()
match, err := regexp.Match(tc.Pattern, []byte(output))
if err != nil {
t.Fatalf("bad: %s\n\nerr: %s", tc.Input, err)
}
if !match {
t.Fatalf("bad: %s\n\n%s", tc.Input, output)
}
}
}
func TestResourceProvider_verifySSH(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "telnet",
},
},
}
if err := VerifySSH(r); err == nil {
t.Fatalf("expected error with telnet")
}
r.Ephemeral.ConnInfo["type"] = "ssh"
if err := VerifySSH(r); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestResourceProvider_sshConfig(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "ssh",
"user": "root",
"password": "supersecret",
"key_file": "/my/key/file.pem",
"host": "127.0.0.1",
"port": "22",
"timeout": "30s",
},
},
}
conf, err := ParseSSHConfig(r)
if err != nil {
t.Fatalf("err: %v", err)
}
if conf.User != "root" {
t.Fatalf("bad: %v", conf)
}
if conf.Password != "supersecret" {
t.Fatalf("bad: %v", conf)
}
if conf.KeyFile != "/my/key/file.pem" {
t.Fatalf("bad: %v", conf)
}
if conf.Host != "127.0.0.1" {
t.Fatalf("bad: %v", conf)
}
if conf.Port != 22 {
t.Fatalf("bad: %v", conf)
}
if conf.Timeout != "30s" {
t.Fatalf("bad: %v", conf)
}
if conf.ScriptPath != DefaultScriptPath {
t.Fatalf("bad: %v", conf)
}
}

View File

@ -99,7 +99,7 @@ func TestClient_Stderr(t *testing.T) {
func TestClient_Stdin(t *testing.T) {
// Overwrite stdin for this test with a temporary file
tf, err := ioutil.TempFile("", "packer")
tf, err := ioutil.TempFile("", "terraform")
if err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -3,13 +3,13 @@ layout: "docs"
page_title: "Provisioner Connections"
sidebar_current: "docs-provisioners-connection"
description: |-
Many provisioners require access to the remote resource. For example, a provisioner may need to use ssh to connect to the resource.
Many provisioners require access to the remote resource. For example, a provisioner may need to use SSH or WinRM to connect to the resource.
---
# Provisioner Connections
Many provisioners require access to the remote resource. For example,
a provisioner may need to use ssh to connect to the resource.
a provisioner may need to use SSH or WinRM to connect to the resource.
Terraform uses a number of defaults when connecting to a resource, but these
can be overridden using `connection` block in either a `resource` or `provisioner`.
@ -21,7 +21,7 @@ subsequent provisioners connect as a user with more limited permissions.
## Example usage
```
# Copies the file as the root user using a password
# Copies the file as the root user using SSH
provisioner "file" {
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
@ -30,28 +30,53 @@ provisioner "file" {
password = "${var.root_password}"
}
}
# Copies the file as the Administrator user using WinRM
provisioner "file" {
source = "conf/myapp.conf"
destination = "C:/App/myapp.conf"
connection {
type = "winrm"
user = "Administrator"
password = "${var.admin_password}"
}
}
```
## Argument Reference
The following arguments are supported:
**The following arguments are supported by all connection types:**
* `type` - The connection type that should be used. This defaults to "ssh". The type
of connection supported depends on the provisioner.
* `type` - The connection type that should be used. Valid types are "ssh" and "winrm"
This defaults to "ssh".
* `user` - The user that we should use for the connection. This defaults to "root".
* `user` - The user that we should use for the connection. Defaults to "root" when
using type "ssh" and defaults to "Administrator" when using type "winrm".
* `password` - The password we should use for the connection.
* `key_file` - The SSH key to use for the connection. This takes preference over the
password if provided.
* `agent` - Set to true to enable using ssh-agent to authenticate.
* `password` - The password we should use for the connection. In some cases this is
provided by the provider.
* `host` - The address of the resource to connect to. This is provided by the provider.
* `port` - The port to connect to. This defaults to 22.
* `port` - The port to connect to. Defaults to 22 when using type "ssh" and defaults
to 5985 when using type "winrm".
* `timeout` - The timeout to wait for the connection to become available. This defaults
to 5 minutes. Should be provided as a string like "30s" or "5m".
to 5 minutes. Should be provided as a string like "30s" or "5m".
* `script_path` - The path used to copy scripts to meant for remote execution.
**Additional arguments only supported by the "ssh" connection type:**
* `key_file` - The SSH key to use for the connection. This takes preference over the
password if provided.
* `agent` - Set to true to enable using ssh-agent to authenticate.
**Additional arguments only supported by the "winrm" connection type:**
* `https` - Set to true to connect using HTTPS instead of HTTP.
* `insecure` - Set to true to not validate the HTTPS certificate chain.
* `cacert` - The CA certificate to validate against.

View File

@ -9,8 +9,8 @@ description: |-
# File Provisioner
The `file` provisioner is used to copy files or directories from the machine
executing Terraform to the newly created resource. The `file` provisioner only
supports `ssh` type [connections](/docs/provisioners/connection.html).
executing Terraform to the newly created resource. The `file` provisioner
supports both `ssh` and `winrm` type [connections](/docs/provisioners/connection.html).
## Example usage
@ -29,6 +29,12 @@ resource "aws_instance" "web" {
source = "conf/configs.d"
destination = "/etc"
}
# Copies all files and folders in apps/app1 to D:/IIS/webapp1
provisioner "file" {
source = "apps/app1/"
destination = "D:/IIS/webapp1"
}
}
```
@ -47,8 +53,10 @@ The following arguments are supported:
The file provisioner is also able to upload a complete directory to the remote machine.
When uploading a directory, there are a few important things you should know.
First, the destination directory must already exist. If you need to create it,
use a remote-exec provisioner just prior to the file provisioner in order to create the directory.
First, when using the `ssh` connection type the destination directory must already exist.
If you need to create it, use a remote-exec provisioner just prior to the file provisioner
in order to create the directory. When using the `winrm` connection type the destination
directory will be created for you if it doesn't already exist.
Next, the existence of a trailing slash on the source path will determine whether the
directory name will be embedded within the destination, or whether the destination will
@ -63,4 +71,3 @@ If the source, however, is `/foo/` (a trailing slash is present), and the destin
This behavior was adopted from the standard behavior of rsync. Note that under the covers,
rsync may or may not be used.

View File

@ -12,7 +12,7 @@ The `remote-exec` provisioner invokes a script on a remote resource after it
is created. This can be used to run a configuration management tool, bootstrap
into a cluster, etc. To invoke a local process, see the `local-exec`
[provisioner](/docs/provisioners/local-exec.html) instead. The `remote-exec`
provisioner only supports `ssh` type [connections](/docs/provisioners/connection.html).
provisioner supports both `ssh` and `winrm` type [connections](/docs/provisioners/connection.html).
## Example usage