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"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/communicator"
|
||||||
"github.com/hashicorp/terraform/helper/config"
|
"github.com/hashicorp/terraform/helper/config"
|
||||||
helper "github.com/hashicorp/terraform/helper/ssh"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResourceProvisioner represents a file provisioner
|
||||||
type ResourceProvisioner struct{}
|
type ResourceProvisioner struct{}
|
||||||
|
|
||||||
|
// Apply executes the file provisioner
|
||||||
func (p *ResourceProvisioner) Apply(
|
func (p *ResourceProvisioner) Apply(
|
||||||
o terraform.UIOutput,
|
o terraform.UIOutput,
|
||||||
s *terraform.InstanceState,
|
s *terraform.InstanceState,
|
||||||
c *terraform.ResourceConfig) error {
|
c *terraform.ResourceConfig) error {
|
||||||
// Ensure the connection type is SSH
|
// Get a new communicator
|
||||||
if err := helper.VerifySSH(s); err != nil {
|
comm, err := communicator.New(s)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the SSH configuration
|
|
||||||
conf, err := helper.ParseSSHConfig(s)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -46,9 +43,10 @@ func (p *ResourceProvisioner) Apply(
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Unsupported 'destination' type! Must be string.")
|
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) {
|
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
|
||||||
v := &config.Validator{
|
v := &config.Validator{
|
||||||
Required: []string{
|
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
|
// copyFiles is used to copy the files from a source to a destination
|
||||||
func (p *ResourceProvisioner) copyFiles(conf *helper.SSHConfig, src, dst string) error {
|
func (p *ResourceProvisioner) copyFiles(comm communicator.Communicator, src, dst string) error {
|
||||||
// Get the SSH client config
|
// Wait and retry until we establish the connection
|
||||||
config, err := helper.PrepareConfig(conf)
|
err := retryFunc(comm.Timeout(), func() error {
|
||||||
if err != nil {
|
err := comm.Connect(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)
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer comm.Disconnect()
|
||||||
|
|
||||||
info, err := os.Stat(src)
|
info, err := os.Stat(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -10,29 +10,22 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/go-linereader"
|
"github.com/mitchellh/go-linereader"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// ResourceProvisioner represents a remote exec provisioner
|
||||||
// DefaultShebang is added at the top of the script file
|
|
||||||
DefaultShebang = "#!/bin/sh"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ResourceProvisioner struct{}
|
type ResourceProvisioner struct{}
|
||||||
|
|
||||||
|
// Apply executes the remote exec provisioner
|
||||||
func (p *ResourceProvisioner) Apply(
|
func (p *ResourceProvisioner) Apply(
|
||||||
o terraform.UIOutput,
|
o terraform.UIOutput,
|
||||||
s *terraform.InstanceState,
|
s *terraform.InstanceState,
|
||||||
c *terraform.ResourceConfig) error {
|
c *terraform.ResourceConfig) error {
|
||||||
// Ensure the connection type is SSH
|
// Get a new communicator
|
||||||
if err := helper.VerifySSH(s); err != nil {
|
comm, err := communicator.New(s)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the SSH configuration
|
|
||||||
conf, err := helper.ParseSSHConfig(s)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -47,12 +40,13 @@ func (p *ResourceProvisioner) Apply(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy and execute each script
|
// 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 err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate checks if the required arguments are configured
|
||||||
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
|
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
|
||||||
num := 0
|
num := 0
|
||||||
for name := range c.Raw {
|
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
|
// generateScript takes the configuration and creates a script to be executed
|
||||||
// from the inline configs
|
// from the inline configs
|
||||||
func (p *ResourceProvisioner) generateScript(c *terraform.ResourceConfig) (string, error) {
|
func (p *ResourceProvisioner) generateScript(c *terraform.ResourceConfig) (string, error) {
|
||||||
lines := []string{DefaultShebang}
|
var lines []string
|
||||||
command, ok := c.Config["inline"]
|
command, ok := c.Config["inline"]
|
||||||
if ok {
|
if ok {
|
||||||
switch cmd := command.(type) {
|
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
|
// runScripts is used to copy and execute a set of scripts
|
||||||
func (p *ResourceProvisioner) runScripts(
|
func (p *ResourceProvisioner) runScripts(
|
||||||
o terraform.UIOutput,
|
o terraform.UIOutput,
|
||||||
conf *helper.SSHConfig,
|
comm communicator.Communicator,
|
||||||
scripts []io.ReadCloser) error {
|
scripts []io.ReadCloser) error {
|
||||||
// Get the SSH client config
|
// Wait and retry until we establish the connection
|
||||||
config, err := helper.PrepareConfig(conf)
|
err := retryFunc(comm.Timeout(), func() error {
|
||||||
if err != nil {
|
err := comm.Connect(o)
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer comm.Disconnect()
|
||||||
|
|
||||||
o.Output("Connected! Executing scripts...")
|
|
||||||
for _, script := range scripts {
|
for _, script := range scripts {
|
||||||
var cmd *helper.RemoteCmd
|
var cmd *remote.Cmd
|
||||||
outR, outW := io.Pipe()
|
outR, outW := io.Pipe()
|
||||||
errR, errW := io.Pipe()
|
errR, errW := io.Pipe()
|
||||||
outDoneCh := make(chan struct{})
|
outDoneCh := make(chan struct{})
|
||||||
|
@ -212,30 +180,20 @@ func (p *ResourceProvisioner) runScripts(
|
||||||
go p.copyOutput(o, outR, outDoneCh)
|
go p.copyOutput(o, outR, outDoneCh)
|
||||||
go p.copyOutput(o, errR, errDoneCh)
|
go p.copyOutput(o, errR, errDoneCh)
|
||||||
|
|
||||||
err := retryFunc(conf.TimeoutVal, func() error {
|
err = retryFunc(comm.Timeout(), func() error {
|
||||||
remotePath := conf.RemotePath()
|
if err := comm.UploadScript(comm.ScriptPath(), script); err != nil {
|
||||||
|
|
||||||
if err := comm.Upload(remotePath, script); err != nil {
|
|
||||||
return fmt.Errorf("Failed to upload script: %v", err)
|
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,
|
Command: comm.ScriptPath(),
|
||||||
Stdout: outW,
|
Stdout: outW,
|
||||||
Stderr: errW,
|
Stderr: errW,
|
||||||
}
|
}
|
||||||
if err := comm.Start(cmd); err != nil {
|
if err := comm.Start(cmd); err != nil {
|
||||||
return fmt.Errorf("Error starting script: %v", err)
|
return fmt.Errorf("Error starting script: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err == 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"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/communicator/remote"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemoteCmd represents a remote command being prepared or run.
|
const (
|
||||||
type RemoteCmd struct {
|
// DefaultShebang is added at the top of a SSH script file
|
||||||
// Command is the command to run remotely. This is executed as if
|
DefaultShebang = "#!/bin/sh\n"
|
||||||
// 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
|
type communicator struct {
|
||||||
// nil, the process reads from an empty bytes.Buffer.
|
connInfo *ConnectionInfo
|
||||||
Stdin io.Reader
|
config *SSHConfig
|
||||||
|
|
||||||
// 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 {
|
|
||||||
client *ssh.Client
|
client *ssh.Client
|
||||||
config *Config
|
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
address string
|
address string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config is the structure used to configure the SSH communicator.
|
// SSHConfig is the structure used to configure the SSH communicator.
|
||||||
type Config struct {
|
type SSHConfig struct {
|
||||||
// The configuration of the Go SSH connection
|
// 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
|
// in use will be closed as part of the Close method, or in the
|
||||||
|
@ -103,27 +49,105 @@ type Config struct {
|
||||||
SSHAgentConn net.Conn
|
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.
|
// an already existing TCP connection and SSH configuration.
|
||||||
func New(address string, config *Config) (result *SSHCommunicator, err error) {
|
func New(s *terraform.InstanceState) (*communicator, error) {
|
||||||
// Establish an initial connection and connect
|
connInfo, err := ParseConnectionInfo(s)
|
||||||
result = &SSHCommunicator{
|
if err != nil {
|
||||||
config: config,
|
return nil, err
|
||||||
address: address,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = result.reconnect(); err != nil {
|
comm := &communicator{connInfo: connInfo}
|
||||||
result = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
session, err := c.newSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup our session
|
// Setup our session
|
||||||
|
@ -139,15 +163,15 @@ func (c *SSHCommunicator) Start(cmd *RemoteCmd) (err error) {
|
||||||
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = session.RequestPty("xterm", 80, 40, termModes); err != nil {
|
if err := session.RequestPty("xterm", 80, 40, termModes); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("starting remote command: %s", cmd.Command)
|
log.Printf("starting remote command: %s", cmd.Command)
|
||||||
err = session.Start(cmd.Command + "\n")
|
err = session.Start(cmd.Command + "\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a goroutine to wait for the session to end and set the
|
// 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)
|
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
|
// The target directory and file for talking the SCP protocol
|
||||||
targetDir := filepath.Dir(path)
|
targetDir := filepath.Dir(path)
|
||||||
targetFile := filepath.Base(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)
|
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)
|
log.Printf("Upload dir '%s' to '%s'", src, dst)
|
||||||
scpFunc := func(w io.Writer, r *bufio.Reader) error {
|
scpFunc := func(w io.Writer, r *bufio.Reader) error {
|
||||||
uploadEntries := func() 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)
|
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")
|
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")
|
log.Println("opening new ssh session")
|
||||||
if c.client == nil {
|
if c.client == nil {
|
||||||
err = errors.New("client not available")
|
err = errors.New("client not available")
|
||||||
|
@ -231,7 +280,7 @@ func (c *SSHCommunicator) newSession() (session *ssh.Session, err error) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ssh session open error: '%s', attempting reconnect", err)
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,43 +290,7 @@ func (c *SSHCommunicator) newSession() (session *ssh.Session, err error) {
|
||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SSHCommunicator) reconnect() (err error) {
|
func (c *communicator) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) 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 {
|
|
||||||
session, err := c.newSession()
|
session, err := c.newSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
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
|
// 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.
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("Error creating temporary file for upload: %s", err)
|
return fmt.Errorf("Error creating temporary file for upload: %s", err)
|
||||||
}
|
}
|
|
@ -6,8 +6,11 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/communicator/remote"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -105,67 +108,62 @@ func newMockLineServer(t *testing.T) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNew_Invalid(t *testing.T) {
|
func TestNew_Invalid(t *testing.T) {
|
||||||
clientConfig := &ssh.ClientConfig{
|
address := newMockLineServer(t)
|
||||||
User: "user",
|
parts := strings.Split(address, ":")
|
||||||
Auth: []ssh.AuthMethod{
|
|
||||||
ssh.Password("i-am-invalid"),
|
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)
|
c, err := New(r)
|
||||||
conn := func() (net.Conn, error) {
|
|
||||||
conn, err := net.Dial("tcp", address)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unable to accept incoming connection: %v", err)
|
t.Fatalf("error creating communicator: %s", err)
|
||||||
}
|
|
||||||
return conn, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &Config{
|
err = c.Connect(nil)
|
||||||
Connection: conn,
|
|
||||||
SSHConfig: clientConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := New(address, config)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("should have had an error connecting")
|
t.Fatal("should have had an error connecting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStart(t *testing.T) {
|
func TestStart(t *testing.T) {
|
||||||
clientConfig := &ssh.ClientConfig{
|
address := newMockLineServer(t)
|
||||||
User: "user",
|
parts := strings.Split(address, ":")
|
||||||
Auth: []ssh.AuthMethod{
|
|
||||||
ssh.Password("pass"),
|
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)
|
c, err := New(r)
|
||||||
conn := func() (net.Conn, error) {
|
|
||||||
conn, err := net.Dial("tcp", address)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to dial to remote side: %s", err)
|
t.Fatalf("error creating communicator: %s", err)
|
||||||
}
|
|
||||||
return conn, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &Config{
|
var cmd remote.Cmd
|
||||||
Connection: conn,
|
|
||||||
SSHConfig: clientConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := New(address, config)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error connecting to SSH: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmd RemoteCmd
|
|
||||||
stdout := new(bytes.Buffer)
|
stdout := new(bytes.Buffer)
|
||||||
cmd.Command = "echo foo"
|
cmd.Command = "echo foo"
|
||||||
cmd.Stdout = stdout
|
cmd.Stdout = stdout
|
||||||
|
|
||||||
err = client.Start(&cmd)
|
err = c.Start(&cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error executing command: %s", err)
|
t.Fatalf("error executing remote command: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,11 +5,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
@ -34,10 +31,10 @@ const (
|
||||||
DefaultTimeout = 5 * time.Minute
|
DefaultTimeout = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
// SSHConfig is decoded from the ConnInfo of the resource. These
|
// ConnectionInfo is decoded from the ConnInfo of the resource. These are the
|
||||||
// are the only keys we look at. If a KeyFile is given, that is used
|
// only keys we look at. If a KeyFile is given, that is used instead
|
||||||
// instead of a password.
|
// of a password.
|
||||||
type SSHConfig struct {
|
type ConnectionInfo struct {
|
||||||
User string
|
User string
|
||||||
Password string
|
Password string
|
||||||
KeyFile string `mapstructure:"key_file"`
|
KeyFile string `mapstructure:"key_file"`
|
||||||
|
@ -49,31 +46,13 @@ type SSHConfig struct {
|
||||||
TimeoutVal time.Duration `mapstructure:"-"`
|
TimeoutVal time.Duration `mapstructure:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SSHConfig) RemotePath() string {
|
// ParseConnectionInfo is used to convert the ConnInfo of the InstanceState into
|
||||||
return strings.Replace(
|
// a ConnectionInfo struct
|
||||||
c.ScriptPath, "%RAND%",
|
func ParseConnectionInfo(s *terraform.InstanceState) (*ConnectionInfo, error) {
|
||||||
strconv.FormatInt(int64(rand.Int31()), 10), -1)
|
connInfo := &ConnectionInfo{}
|
||||||
}
|
|
||||||
|
|
||||||
// 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{}
|
|
||||||
decConf := &mapstructure.DecoderConfig{
|
decConf := &mapstructure.DecoderConfig{
|
||||||
WeaklyTypedInput: true,
|
WeaklyTypedInput: true,
|
||||||
Result: sshConf,
|
Result: connInfo,
|
||||||
}
|
}
|
||||||
dec, err := mapstructure.NewDecoder(decConf)
|
dec, err := mapstructure.NewDecoder(decConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -82,21 +61,21 @@ func ParseSSHConfig(s *terraform.InstanceState) (*SSHConfig, error) {
|
||||||
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
|
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if sshConf.User == "" {
|
if connInfo.User == "" {
|
||||||
sshConf.User = DefaultUser
|
connInfo.User = DefaultUser
|
||||||
}
|
}
|
||||||
if sshConf.Port == 0 {
|
if connInfo.Port == 0 {
|
||||||
sshConf.Port = DefaultPort
|
connInfo.Port = DefaultPort
|
||||||
}
|
}
|
||||||
if sshConf.ScriptPath == "" {
|
if connInfo.ScriptPath == "" {
|
||||||
sshConf.ScriptPath = DefaultScriptPath
|
connInfo.ScriptPath = DefaultScriptPath
|
||||||
}
|
}
|
||||||
if sshConf.Timeout != "" {
|
if connInfo.Timeout != "" {
|
||||||
sshConf.TimeoutVal = safeDuration(sshConf.Timeout, DefaultTimeout)
|
connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
|
||||||
} else {
|
} else {
|
||||||
sshConf.TimeoutVal = DefaultTimeout
|
connInfo.TimeoutVal = DefaultTimeout
|
||||||
}
|
}
|
||||||
return sshConf, nil
|
return connInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// safeDuration returns either the parsed duration or a default value
|
// 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
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareConfig is used to turn the *SSHConfig provided into a
|
// PrepareSSHConfig is used to turn the *ConnectionInfo provided into a
|
||||||
// usable *Config for client initialization.
|
// usable *SSHConfig for client initialization.
|
||||||
func PrepareConfig(conf *SSHConfig) (*Config, error) {
|
func PrepareSSHConfig(connInfo *ConnectionInfo) (*SSHConfig, error) {
|
||||||
var conn net.Conn
|
var conn net.Conn
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
sshConf := &ssh.ClientConfig{
|
sshConf := &ssh.ClientConfig{
|
||||||
User: conf.User,
|
User: connInfo.User,
|
||||||
}
|
}
|
||||||
if conf.Agent {
|
if connInfo.Agent {
|
||||||
sshAuthSock := os.Getenv("SSH_AUTH_SOCK")
|
sshAuthSock := os.Getenv("SSH_AUTH_SOCK")
|
||||||
|
|
||||||
if sshAuthSock == "" {
|
if sshAuthSock == "" {
|
||||||
|
@ -138,14 +117,14 @@ func PrepareConfig(conf *SSHConfig) (*Config, error) {
|
||||||
|
|
||||||
sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signers...))
|
sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signers...))
|
||||||
}
|
}
|
||||||
if conf.KeyFile != "" {
|
if connInfo.KeyFile != "" {
|
||||||
fullPath, err := homedir.Expand(conf.KeyFile)
|
fullPath, err := homedir.Expand(connInfo.KeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to expand home directory: %v", err)
|
return nil, fmt.Errorf("Failed to expand home directory: %v", err)
|
||||||
}
|
}
|
||||||
key, err := ioutil.ReadFile(fullPath)
|
key, err := ioutil.ReadFile(fullPath)
|
||||||
if err != nil {
|
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
|
// 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)
|
block, _ := pem.Decode(key)
|
||||||
if block == nil {
|
if block == nil {
|
||||||
return nil, fmt.Errorf(
|
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" {
|
if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"Failed to read key '%s': password protected keys are\n"+
|
"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)
|
signer, err := ssh.ParsePrivateKey(key)
|
||||||
if err != nil {
|
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))
|
sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signer))
|
||||||
}
|
}
|
||||||
if conf.Password != "" {
|
if connInfo.Password != "" {
|
||||||
sshConf.Auth = append(sshConf.Auth,
|
sshConf.Auth = append(sshConf.Auth,
|
||||||
ssh.Password(conf.Password))
|
ssh.Password(connInfo.Password))
|
||||||
sshConf.Auth = append(sshConf.Auth,
|
sshConf.Auth = append(sshConf.Auth,
|
||||||
ssh.KeyboardInteractive(PasswordKeyboardInteractive(conf.Password)))
|
ssh.KeyboardInteractive(PasswordKeyboardInteractive(connInfo.Password)))
|
||||||
}
|
}
|
||||||
host := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
|
host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
|
||||||
config := &Config{
|
config := &SSHConfig{
|
||||||
SSHConfig: sshConf,
|
Config: sshConf,
|
||||||
Connection: ConnectFunc("tcp", host),
|
Connection: ConnectFunc("tcp", host),
|
||||||
SSHAgentConn: conn,
|
SSHAgentConn: conn,
|
||||||
}
|
}
|
||||||
return config, nil
|
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) {
|
func TestClient_Stdin(t *testing.T) {
|
||||||
// Overwrite stdin for this test with a temporary file
|
// Overwrite stdin for this test with a temporary file
|
||||||
tf, err := ioutil.TempFile("", "packer")
|
tf, err := ioutil.TempFile("", "terraform")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue