Skip to content

Commit

Permalink
Merge pull request #1483 from svanharmelen/f-winrm-support
Browse files Browse the repository at this point in the history
core: add WinRM support
  • Loading branch information
phinze committed May 1, 2015
2 parents 8d953ea + e55169b commit 15c75c5
Show file tree
Hide file tree
Showing 23 changed files with 1,116 additions and 458 deletions.
36 changes: 13 additions & 23 deletions builtin/provisioners/file/resource_provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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{
Expand All @@ -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 {
Expand All @@ -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
Expand Down
80 changes: 20 additions & 60 deletions builtin/provisioners/remote-exec/resource_provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -165,77 +159,43 @@ 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{})
errDoneCh := make(chan struct{})
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,
}
if err := comm.Start(cmd); err != nil {
return fmt.Errorf("Error starting script: %v", err)
}

return nil
})
if err == nil {
Expand Down
23 changes: 11 additions & 12 deletions builtin/provisioners/remote-exec/resource_provisioner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}{
Expand All @@ -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{}{
Expand All @@ -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())
}
}

Expand All @@ -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())
}
}

Expand Down Expand Up @@ -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())
}
}
}
Expand Down
1 change: 0 additions & 1 deletion builtin/provisioners/remote-exec/test-fixtures/script1.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/bin/sh
cd /tmp
wget http://foobar
exit 0
53 changes: 53 additions & 0 deletions communicator/communicator.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
30 changes: 30 additions & 0 deletions communicator/communicator_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 15c75c5

Please sign in to comment.