Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: add WinRM support #1483

Merged
merged 10 commits into from
May 1, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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