Adding some abstractions for the communicators
This is needed as preperation for adding WinRM support. There is still one error in the tests which needs another look, but other than that it seems like were now ready to start working on the WinRM part…
This commit is contained in:
parent
5d07394971
commit
c9e9e374bb
|
@ -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 {
|
||||
|
|
|
@ -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,30 +180,20 @@ 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()
|
||||
|
||||
if err := comm.Upload(remotePath, script); err != nil {
|
||||
err = retryFunc(comm.Timeout(), func() error {
|
||||
if err := comm.UploadScript(comm.ScriptPath(), 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{
|
||||
Command: remotePath,
|
||||
cmd = &remote.Cmd{
|
||||
Command: comm.ScriptPath(),
|
||||
Stdout: outW,
|
||||
Stderr: errW,
|
||||
}
|
||||
if err := comm.Start(cmd); err != nil {
|
||||
return fmt.Errorf("Error starting script: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package communicator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/communicator/remote"
|
||||
"github.com/hashicorp/terraform/communicator/ssh"
|
||||
"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, []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()
|
||||
default:
|
||||
return nil, fmt.Errorf("Connection type '%s' not supported", connType)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package remote
|
|
@ -11,84 +11,30 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
type communicator struct {
|
||||
connInfo *ConnectionInfo
|
||||
config *SSHConfig
|
||||
client *ssh.Client
|
||||
config *Config
|
||||
conn net.Conn
|
||||
address string
|
||||
}
|
||||
|
||||
// Config is the structure used to configure the SSH communicator.
|
||||
type Config struct {
|
||||
// SSHConfig is the structure used to configure the SSH communicator.
|
||||
type SSHConfig struct {
|
||||
// The configuration of the Go SSH connection
|
||||
SSHConfig *ssh.ClientConfig
|
||||
Config *ssh.ClientConfig
|
||||
|
||||
// Connection returns a new connection. The current connection
|
||||
// in use will be closed as part of the Close method, or in the
|
||||
|
@ -103,27 +49,105 @@ type Config struct {
|
|||
SSHAgentConn net.Conn
|
||||
}
|
||||
|
||||
// New creates a new packer.Communicator implementation over SSH. This takes
|
||||
// New creates a new 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,
|
||||
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
|
||||
}
|
||||
comm := &communicator{connInfo: connInfo}
|
||||
|
||||
return
|
||||
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
|
||||
|
||||
c.config, err = PrepareSSHConfig(c.connInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o != nil {
|
||||
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",
|
||||
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
|
||||
}
|
||||
|
||||
// Timeout implementation of communicator.Communicator interface
|
||||
func (c *communicator) ScriptPath() string {
|
||||
return c.connInfo.ScriptPath
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -139,15 +163,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 +192,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 +213,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, excl []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 +265,12 @@ 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 {
|
||||
// Download implementation of communicator.Communicator interface
|
||||
func (c *communicator) 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 +280,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 +290,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 +395,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)
|
||||
}
|
|
@ -6,8 +6,11 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/communicator/remote"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
|
@ -105,67 +108,62 @@ 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)
|
||||
c, err := New(r)
|
||||
if err != nil {
|
||||
t.Errorf("Unable to accept incoming connection: %v", err)
|
||||
}
|
||||
return conn, err
|
||||
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)
|
||||
c, err := New(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to dial to remote side: %s", err)
|
||||
}
|
||||
return conn, err
|
||||
t.Fatalf("error creating communicator: %s", err)
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
Connection: conn,
|
||||
SSHConfig: clientConfig,
|
||||
}
|
||||
|
||||
client, err := New(address, config)
|
||||
if err != nil {
|
||||
t.Fatalf("error connecting to SSH: %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)
|
||||
}
|
||||
}
|
|
@ -5,11 +5,8 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
|
@ -34,10 +31,10 @@ const (
|
|||
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,21 @@ 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 +88,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 +117,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 +132,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,
|
||||
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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue