Adding support for WinRM

This commit is contained in:
Sander van Harmelen 2015-04-10 20:34:46 +02:00
parent b1c6a3f63f
commit 4a29c714e5
10 changed files with 467 additions and 64 deletions

View File

@ -76,7 +76,7 @@ func (p *ResourceProvisioner) copyFiles(comm communicator.Communicator, src, dst
// If we're uploading a directory, short circuit and do that
if info.IsDir() {
if err := comm.UploadDir(dst, src, nil); err != nil {
if err := comm.UploadDir(dst, src); err != nil {
return fmt.Errorf("Upload failed: %v", err)
}
return nil

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/communicator/ssh"
"github.com/hashicorp/terraform/communicator/winrm"
"github.com/hashicorp/terraform/terraform"
)
@ -35,7 +36,7 @@ type Communicator interface {
UploadScript(string, io.Reader) error
// UploadDir is used to upload a directory
UploadDir(string, string, []string) error
UploadDir(string, string) error
}
// New returns a configured Communicator or an error if the connection type is not supported
@ -44,9 +45,9 @@ func New(s *terraform.InstanceState) (Communicator, error) {
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()
case "winrm":
return winrm.New(s)
default:
return nil, fmt.Errorf("Connection type '%s' not supported", connType)
return nil, fmt.Errorf("connection type '%s' not supported", connType)
}
}

View File

@ -17,8 +17,14 @@ func TestCommunicator_new(t *testing.T) {
if _, err := New(r); err == nil {
t.Fatalf("expected error with telnet")
}
r.Ephemeral.ConnInfo["type"] = "ssh"
if _, err := New(r); err != nil {
t.Fatalf("err: %v", err)
}
r.Ephemeral.ConnInfo["type"] = "winrm"
if _, err := New(r); err != nil {
t.Fatalf("err: %v", err)
}
}

View File

@ -23,47 +23,54 @@ const (
DefaultShebang = "#!/bin/sh\n"
)
type communicator struct {
connInfo *ConnectionInfo
config *SSHConfig
// Communicator represents the SSH communicator
type Communicator struct {
connInfo *connectionInfo
client *ssh.Client
config *sshConfig
conn net.Conn
address string
}
// SSHConfig is the structure used to configure the SSH communicator.
type SSHConfig struct {
type sshConfig struct {
// The configuration of the Go SSH connection
Config *ssh.ClientConfig
config *ssh.ClientConfig
// Connection returns a new connection. The current connection
// connection returns a new connection. The current connection
// in use will be closed as part of the Close method, or in the
// case an error occurs.
Connection func() (net.Conn, error)
connection func() (net.Conn, error)
// NoPty, if true, will not request a pty from the remote end.
NoPty bool
// noPty, if true, will not request a pty from the remote end.
noPty bool
// SSHAgentConn is a pointer to the UNIX connection for talking with the
// sshAgentConn is a pointer to the UNIX connection for talking with the
// ssh-agent.
SSHAgentConn net.Conn
sshAgentConn net.Conn
}
// New creates a new communicator implementation over SSH. This takes
// an already existing TCP connection and SSH configuration.
func New(s *terraform.InstanceState) (*communicator, error) {
connInfo, err := ParseConnectionInfo(s)
// New creates a new communicator implementation over SSH.
func New(s *terraform.InstanceState) (*Communicator, error) {
connInfo, err := parseConnectionInfo(s)
if err != nil {
return nil, err
}
comm := &communicator{connInfo: connInfo}
config, err := prepareSSHConfig(connInfo)
if err != nil {
return nil, err
}
comm := &Communicator{
connInfo: connInfo,
config: config,
}
return comm, nil
}
// Connect implementation of communicator.Communicator interface
func (c *communicator) Connect(o terraform.UIOutput) (err error) {
func (c *Communicator) Connect(o terraform.UIOutput) (err error) {
if c.conn != nil {
c.conn.Close()
}
@ -72,19 +79,14 @@ func (c *communicator) Connect(o terraform.UIOutput) (err error) {
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",
" Password: %t\n"+
" Private key: %t\n"+
" SSH Agent: %t",
c.connInfo.Host, c.connInfo.User,
c.connInfo.Password != "",
c.connInfo.KeyFile != "",
@ -93,7 +95,7 @@ func (c *communicator) Connect(o terraform.UIOutput) (err error) {
}
log.Printf("connecting to TCP connection for SSH")
c.conn, err = c.config.Connection()
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
@ -109,7 +111,7 @@ func (c *communicator) Connect(o terraform.UIOutput) (err error) {
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)
sshConn, sshChan, req, err := ssh.NewClientConn(c.conn, host, c.config.config)
if err != nil {
log.Printf("handshake error: %s", err)
return err
@ -125,26 +127,26 @@ func (c *communicator) Connect(o terraform.UIOutput) (err error) {
}
// Disconnect implementation of communicator.Communicator interface
func (c *communicator) Disconnect() error {
if c.config.SSHAgentConn != nil {
return c.config.SSHAgentConn.Close()
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 {
func (c *Communicator) Timeout() time.Duration {
return c.connInfo.TimeoutVal
}
// Timeout implementation of communicator.Communicator interface
func (c *communicator) ScriptPath() string {
// ScriptPath 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 {
func (c *Communicator) Start(cmd *remote.Cmd) error {
session, err := c.newSession()
if err != nil {
return err
@ -155,7 +157,7 @@ func (c *communicator) Start(cmd *remote.Cmd) error {
session.Stdout = cmd.Stdout
session.Stderr = cmd.Stderr
if !c.config.NoPty {
if !c.config.noPty {
// Request a PTY
termModes := ssh.TerminalModes{
ssh.ECHO: 0, // do not echo
@ -196,7 +198,7 @@ func (c *communicator) Start(cmd *remote.Cmd) error {
}
// Upload implementation of communicator.Communicator interface
func (c *communicator) Upload(path string, input io.Reader) error {
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)
@ -214,7 +216,7 @@ func (c *communicator) Upload(path string, input io.Reader) error {
}
// UploadScript implementation of communicator.Communicator interface
func (c *communicator) UploadScript(path string, input io.Reader) error {
func (c *Communicator) UploadScript(path string, input io.Reader) error {
script := bytes.NewBufferString(DefaultShebang)
script.ReadFrom(input)
@ -236,7 +238,7 @@ func (c *communicator) UploadScript(path string, input io.Reader) error {
}
// UploadDir implementation of communicator.Communicator interface
func (c *communicator) UploadDir(dst string, src string, excl []string) error {
func (c *Communicator) UploadDir(dst string, src string) error {
log.Printf("Upload dir '%s' to '%s'", src, dst)
scpFunc := func(w io.Writer, r *bufio.Reader) error {
uploadEntries := func() error {
@ -265,12 +267,7 @@ func (c *communicator) UploadDir(dst string, src string, excl []string) error {
return c.scpSession("scp -rvt "+dst, scpFunc)
}
// Download implementation of communicator.Communicator interface
func (c *communicator) Download(string, io.Writer) error {
panic("not implemented yet")
}
func (c *communicator) 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")
@ -290,7 +287,7 @@ func (c *communicator) newSession() (session *ssh.Session, err error) {
return session, nil
}
func (c *communicator) 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

View File

@ -17,7 +17,7 @@ import (
)
const (
// DefaultUser is used if there is no default user given
// DefaultUser is used if there is no user given
DefaultUser = "root"
// DefaultPort is used if there is no port given
@ -31,10 +31,10 @@ const (
DefaultTimeout = 5 * time.Minute
)
// ConnectionInfo is decoded from the ConnInfo of the resource. These are the
// 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 {
type connectionInfo struct {
User string
Password string
KeyFile string `mapstructure:"key_file"`
@ -46,10 +46,10 @@ type ConnectionInfo struct {
TimeoutVal time.Duration `mapstructure:"-"`
}
// ParseConnectionInfo is used to convert the ConnInfo of the InstanceState into
// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
// a ConnectionInfo struct
func ParseConnectionInfo(s *terraform.InstanceState) (*ConnectionInfo, error) {
connInfo := &ConnectionInfo{}
func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
connInfo := &connectionInfo{}
decConf := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: connInfo,
@ -88,9 +88,9 @@ func safeDuration(dur string, defaultDur time.Duration) time.Duration {
return d
}
// PrepareSSHConfig is used to turn the *ConnectionInfo provided into a
// prepareSSHConfig is used to turn the *ConnectionInfo provided into a
// usable *SSHConfig for client initialization.
func PrepareSSHConfig(connInfo *ConnectionInfo) (*SSHConfig, error) {
func prepareSSHConfig(connInfo *connectionInfo) (*sshConfig, error) {
var conn net.Conn
var err error
@ -154,10 +154,10 @@ func PrepareSSHConfig(connInfo *ConnectionInfo) (*SSHConfig, error) {
ssh.KeyboardInteractive(PasswordKeyboardInteractive(connInfo.Password)))
}
host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
config := &SSHConfig{
Config: sshConf,
Connection: ConnectFunc("tcp", host),
SSHAgentConn: conn,
config := &sshConfig{
config: sshConf,
connection: ConnectFunc("tcp", host),
sshAgentConn: conn,
}
return config, nil
}

View File

@ -21,7 +21,7 @@ func TestProvisioner_connInfo(t *testing.T) {
},
}
conf, err := ParseConnectionInfo(r)
conf, err := parseConnectionInfo(r)
if err != nil {
t.Fatalf("err: %v", err)
}

View File

@ -0,0 +1,186 @@
package winrm
import (
"fmt"
"io"
"log"
"time"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/masterzen/winrm/winrm"
"github.com/packer-community/winrmcp/winrmcp"
)
// Communicator represents the WinRM communicator
type Communicator struct {
connInfo *connectionInfo
client *winrm.Client
endpoint *winrm.Endpoint
}
// New creates a new communicator implementation over WinRM.
//func New(endpoint *winrm.Endpoint, user string, password string, timeout time.Duration) (*communicator, error) {
func New(s *terraform.InstanceState) (*Communicator, error) {
connInfo, err := parseConnectionInfo(s)
if err != nil {
return nil, err
}
endpoint := &winrm.Endpoint{
Host: connInfo.Host,
Port: connInfo.Port,
HTTPS: connInfo.HTTPS,
Insecure: connInfo.Insecure,
CACert: connInfo.CACert,
}
comm := &Communicator{
connInfo: connInfo,
endpoint: endpoint,
}
return comm, nil
}
// Connect implementation of communicator.Communicator interface
func (c *Communicator) Connect(o terraform.UIOutput) error {
if c.client != nil {
return nil
}
params := winrm.DefaultParameters()
params.Timeout = formatDuration(c.Timeout())
client, err := winrm.NewClientWithParameters(
c.endpoint, c.connInfo.User, c.connInfo.Password, params)
if err != nil {
return err
}
if o != nil {
o.Output(fmt.Sprintf(
"Connecting to remote host via WinRM...\n"+
" Host: %s\n"+
" Port: %d\n"+
" User: %s\n"+
" Password: %t\n"+
" HTTPS: %t\n"+
" Insecure: %t\n"+
" CACert: %t",
c.connInfo.Host,
c.connInfo.Port,
c.connInfo.User,
c.connInfo.Password != "",
c.connInfo.HTTPS,
c.connInfo.Insecure,
c.connInfo.CACert != nil,
))
}
log.Printf("connecting to remote shell using WinRM")
shell, err := client.CreateShell()
if err != nil {
log.Printf("connection error: %s", err)
return err
}
err = shell.Close()
if err != nil {
log.Printf("error closing connection: %s", err)
return err
}
if o != nil {
o.Output("Connected!")
}
c.client = client
return nil
}
// Disconnect implementation of communicator.Communicator interface
func (c *Communicator) Disconnect() error {
c.client = nil
return nil
}
// Timeout implementation of communicator.Communicator interface
func (c *Communicator) Timeout() time.Duration {
return c.connInfo.TimeoutVal
}
// ScriptPath implementation of communicator.Communicator interface
func (c *Communicator) ScriptPath() string {
return c.connInfo.ScriptPath
}
// Start implementation of communicator.Communicator interface
func (c *Communicator) Start(rc *remote.Cmd) error {
log.Printf("starting remote command: %s", rc.Command)
err := c.Connect(nil)
if err != nil {
return err
}
shell, err := c.client.CreateShell()
if err != nil {
return err
}
cmd, err := shell.Execute(rc.Command)
if err != nil {
return err
}
go runCommand(shell, cmd, rc)
return nil
}
func runCommand(shell *winrm.Shell, cmd *winrm.Command, rc *remote.Cmd) {
defer shell.Close()
go io.Copy(rc.Stdout, cmd.Stdout)
go io.Copy(rc.Stderr, cmd.Stderr)
cmd.Wait()
rc.SetExited(cmd.ExitCode())
}
// Upload implementation of communicator.Communicator interface
func (c *Communicator) Upload(path string, input io.Reader) error {
wcp, err := c.newCopyClient()
if err != nil {
return err
}
return wcp.Write(path, input)
}
// UploadScript implementation of communicator.Communicator interface
func (c *Communicator) UploadScript(path string, input io.Reader) error {
return c.Upload(path, input)
}
// UploadDir implementation of communicator.Communicator interface
func (c *Communicator) UploadDir(dst string, src string) error {
log.Printf("Upload dir '%s' to '%s'", src, dst)
wcp, err := c.newCopyClient()
if err != nil {
return err
}
return wcp.Copy(src, dst)
}
func (c *Communicator) newCopyClient() (*winrmcp.Winrmcp, error) {
addr := fmt.Sprintf("%s:%d", c.endpoint.Host, c.endpoint.Port)
return winrmcp.New(addr, &winrmcp.Config{
Auth: winrmcp.Auth{
User: c.connInfo.User,
Password: c.connInfo.Password,
},
OperationTimeout: c.Timeout(),
MaxOperationsPerShell: 15, // lowest common denominator
})
}

View File

@ -0,0 +1 @@
package winrm

View File

@ -0,0 +1,109 @@
package winrm
import (
"fmt"
"log"
"strings"
"time"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/mapstructure"
)
const (
// DefaultUser is used if there is no user given
DefaultUser = "Administrator"
// DefaultPort is used if there is no port given
DefaultPort = 5985
// DefaultScriptPath is used as the path to copy the file to
// for remote execution if not provided otherwise.
DefaultScriptPath = "C:/Temp/script.cmd"
// DefaultTimeout is used if there is no timeout given
DefaultTimeout = 5 * time.Minute
)
// connectionInfo is decoded from the ConnInfo of the resource. These are the
// only keys we look at. If a KeyFile is given, that is used instead
// of a password.
type connectionInfo struct {
User string
Password string
Host string
Port int
HTTPS bool
Insecure bool
CACert *[]byte `mapstructure:"ca_cert"`
Timeout string
ScriptPath string `mapstructure:"script_path"`
TimeoutVal time.Duration `mapstructure:"-"`
}
// parseConnectionInfo is used to convert the ConnInfo of the InstanceState into
// a ConnectionInfo struct
func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) {
connInfo := &connectionInfo{}
decConf := &mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: connInfo,
}
dec, err := mapstructure.NewDecoder(decConf)
if err != nil {
return nil, err
}
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
return nil, err
}
if connInfo.User == "" {
connInfo.User = DefaultUser
}
if connInfo.Port == 0 {
connInfo.Port = DefaultPort
}
// We also check on script paths which point to the default Windows TEMP folder because
// files which are put in there very early in the boot process could get cleaned/deleted
// before you had the change to execute them.
//
// TODO (SvH) Needs some more debugging to fully understand the exact sequence of events
// causing this...
if connInfo.ScriptPath == "" || strings.HasPrefix(connInfo.ScriptPath, "C:/Windows/Temp") {
connInfo.ScriptPath = DefaultScriptPath
}
if connInfo.Timeout != "" {
connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
} else {
connInfo.TimeoutVal = DefaultTimeout
}
return connInfo, nil
}
// safeDuration returns either the parsed duration or a default value
func safeDuration(dur string, defaultDur time.Duration) time.Duration {
d, err := time.ParseDuration(dur)
if err != nil {
log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
return defaultDur
}
return d
}
func formatDuration(duration time.Duration) string {
h := int(duration.Hours())
m := int(duration.Minutes()) - (h * 60)
s := int(duration.Seconds()) - (h*3600 + m*60)
res := "PT"
if h > 0 {
res = fmt.Sprintf("%s%dH", res, h)
}
if m > 0 {
res = fmt.Sprintf("%s%dM", res, m)
}
if s > 0 {
res = fmt.Sprintf("%s%dS", res, s)
}
return res
}

View File

@ -0,0 +1,103 @@
package winrm
import (
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestProvisioner_connInfo(t *testing.T) {
r := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"type": "winrm",
"user": "Administrator",
"password": "supersecret",
"host": "127.0.0.1",
"port": "5985",
"https": "true",
"timeout": "30s",
},
},
}
conf, err := parseConnectionInfo(r)
if err != nil {
t.Fatalf("err: %v", err)
}
if conf.User != "Administrator" {
t.Fatalf("expected: %v: got: %v", "Administrator", conf)
}
if conf.Password != "supersecret" {
t.Fatalf("expected: %v: got: %v", "supersecret", conf)
}
if conf.Host != "127.0.0.1" {
t.Fatalf("expected: %v: got: %v", "127.0.0.1", conf)
}
if conf.Port != 5985 {
t.Fatalf("expected: %v: got: %v", 5985, conf)
}
if conf.HTTPS != true {
t.Fatalf("expected: %v: got: %v", true, conf)
}
if conf.Timeout != "30s" {
t.Fatalf("expected: %v: got: %v", "30s", conf)
}
if conf.ScriptPath != DefaultScriptPath {
t.Fatalf("expected: %v: got: %v", DefaultScriptPath, conf)
}
}
func TestProvisioner_formatDuration(t *testing.T) {
cases := map[string]struct {
InstanceState *terraform.InstanceState
Result string
}{
"testSeconds": {
InstanceState: &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"timeout": "90s",
},
},
},
Result: "PT1M30S",
},
"testMinutes": {
InstanceState: &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"timeout": "5m",
},
},
},
Result: "PT5M",
},
"testHours": {
InstanceState: &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: map[string]string{
"timeout": "1h",
},
},
},
Result: "PT1H",
},
}
for name, tc := range cases {
conf, err := parseConnectionInfo(tc.InstanceState)
if err != nil {
t.Fatalf("err: %v", err)
}
result := formatDuration(conf.TimeoutVal)
if result != tc.Result {
t.Fatalf("%s: expected: %s got: %s", name, tc.Result, result)
}
}
}