commit
ea225488d1
|
@ -0,0 +1,129 @@
|
|||
package winrm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/masterzen/winrm/winrm"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/packer-community/winrmcp/winrmcp"
|
||||
|
||||
// This import is a bit strange, but it's needed so `make updatedeps`
|
||||
// can see and download it
|
||||
_ "github.com/dylanmei/winrmtest"
|
||||
)
|
||||
|
||||
// Communicator represents the WinRM communicator
|
||||
type Communicator struct {
|
||||
config *Config
|
||||
client *winrm.Client
|
||||
endpoint *winrm.Endpoint
|
||||
}
|
||||
|
||||
// New creates a new communicator implementation over WinRM.
|
||||
func New(config *Config) (*Communicator, error) {
|
||||
endpoint := &winrm.Endpoint{
|
||||
Host: config.Host,
|
||||
Port: config.Port,
|
||||
|
||||
/*
|
||||
TODO
|
||||
HTTPS: connInfo.HTTPS,
|
||||
Insecure: connInfo.Insecure,
|
||||
CACert: connInfo.CACert,
|
||||
*/
|
||||
}
|
||||
|
||||
// Create the client
|
||||
params := winrm.DefaultParameters()
|
||||
params.Timeout = formatDuration(config.Timeout)
|
||||
client, err := winrm.NewClientWithParameters(
|
||||
endpoint, config.Username, config.Password, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the shell to verify the connection
|
||||
log.Printf("[DEBUG] connecting to remote shell using WinRM")
|
||||
shell, err := client.CreateShell()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] connection error: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := shell.Close(); err != nil {
|
||||
log.Printf("[ERROR] error closing connection: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Communicator{
|
||||
config: config,
|
||||
client: client,
|
||||
endpoint: endpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start implementation of communicator.Communicator interface
|
||||
func (c *Communicator) Start(rc *packer.RemoteCmd) error {
|
||||
shell, err := c.client.CreateShell()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[INFO] starting remote command: %s", rc.Command)
|
||||
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 *packer.RemoteCmd) {
|
||||
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, _ *os.FileInfo) error {
|
||||
wcp, err := c.newCopyClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Uploading file to '%s'", path)
|
||||
return wcp.Write(path, input)
|
||||
}
|
||||
|
||||
// UploadDir implementation of communicator.Communicator interface
|
||||
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
|
||||
log.Printf("Uploading dir '%s' to '%s'", src, dst)
|
||||
wcp, err := c.newCopyClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return wcp.Copy(src, dst)
|
||||
}
|
||||
|
||||
func (c *Communicator) Download(src string, dst io.Writer) error {
|
||||
panic("download not implemented")
|
||||
}
|
||||
|
||||
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.config.Username,
|
||||
Password: c.config.Password,
|
||||
},
|
||||
OperationTimeout: c.config.Timeout,
|
||||
MaxOperationsPerShell: 15, // lowest common denominator
|
||||
})
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package winrm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dylanmei/winrmtest"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func newMockWinRMServer(t *testing.T) *winrmtest.Remote {
|
||||
wrm := winrmtest.NewRemote()
|
||||
|
||||
wrm.CommandFunc(
|
||||
winrmtest.MatchText("echo foo"),
|
||||
func(out, err io.Writer) int {
|
||||
out.Write([]byte("foo"))
|
||||
return 0
|
||||
})
|
||||
|
||||
wrm.CommandFunc(
|
||||
winrmtest.MatchPattern(`^echo c29tZXRoaW5n >> ".*"$`),
|
||||
func(out, err io.Writer) int {
|
||||
return 0
|
||||
})
|
||||
|
||||
wrm.CommandFunc(
|
||||
winrmtest.MatchPattern(`^powershell.exe -EncodedCommand .*$`),
|
||||
func(out, err io.Writer) int {
|
||||
return 0
|
||||
})
|
||||
|
||||
wrm.CommandFunc(
|
||||
winrmtest.MatchText("powershell"),
|
||||
func(out, err io.Writer) int {
|
||||
return 0
|
||||
})
|
||||
|
||||
return wrm
|
||||
}
|
||||
|
||||
func TestStart(t *testing.T) {
|
||||
wrm := newMockWinRMServer(t)
|
||||
defer wrm.Close()
|
||||
|
||||
c, err := New(&Config{
|
||||
Host: wrm.Host,
|
||||
Port: wrm.Port,
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
Timeout: 30 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("error creating communicator: %s", err)
|
||||
}
|
||||
|
||||
var cmd packer.RemoteCmd
|
||||
stdout := new(bytes.Buffer)
|
||||
cmd.Command = "echo foo"
|
||||
cmd.Stdout = stdout
|
||||
|
||||
err = c.Start(&cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("error executing remote command: %s", err)
|
||||
}
|
||||
cmd.Wait()
|
||||
|
||||
if stdout.String() != "foo" {
|
||||
t.Fatalf("bad command response: expected %q, got %q", "foo", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpload(t *testing.T) {
|
||||
wrm := newMockWinRMServer(t)
|
||||
defer wrm.Close()
|
||||
|
||||
c, err := New(&Config{
|
||||
Host: wrm.Host,
|
||||
Port: wrm.Port,
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
Timeout: 30 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("error creating communicator: %s", err)
|
||||
}
|
||||
|
||||
err = c.Upload("C:/Temp/terraform.cmd", bytes.NewReader([]byte("something")), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("error uploading file: %s", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package winrm
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config is used to configure the WinRM connection
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Timeout time.Duration
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package winrm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// formatDuration formats the given time.Duration into an ISO8601
|
||||
// duration string.
|
||||
func formatDuration(duration time.Duration) string {
|
||||
// We're not supporting negative durations
|
||||
if duration.Seconds() <= 0 {
|
||||
return "PT0S"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package winrm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFormatDuration(t *testing.T) {
|
||||
// Test complex duration with hours, minutes, seconds
|
||||
d := time.Duration(3701) * time.Second
|
||||
s := formatDuration(d)
|
||||
if s != "PT1H1M41S" {
|
||||
t.Fatalf("bad ISO 8601 duration string: %s", s)
|
||||
}
|
||||
|
||||
// Test only minutes duration
|
||||
d = time.Duration(20) * time.Minute
|
||||
s = formatDuration(d)
|
||||
if s != "PT20M" {
|
||||
t.Fatalf("bad ISO 8601 duration string for 20M: %s", s)
|
||||
}
|
||||
|
||||
// Test only seconds
|
||||
d = time.Duration(1) * time.Second
|
||||
s = formatDuration(d)
|
||||
if s != "PT1S" {
|
||||
t.Fatalf("bad ISO 8601 duration string for 1S: %s", s)
|
||||
}
|
||||
|
||||
// Test negative duration (unsupported)
|
||||
d = time.Duration(-1) * time.Second
|
||||
s = formatDuration(d)
|
||||
if s != "PT0S" {
|
||||
t.Fatalf("bad ISO 8601 duration string for negative: %s", s)
|
||||
}
|
||||
}
|
|
@ -12,7 +12,9 @@ import (
|
|||
// Config is the common configuration that communicators allow within
|
||||
// a builder.
|
||||
type Config struct {
|
||||
Type string `mapstructure:"communicator"`
|
||||
Type string `mapstructure:"communicator"`
|
||||
|
||||
// SSH
|
||||
SSHHost string `mapstructure:"ssh_host"`
|
||||
SSHPort int `mapstructure:"ssh_port"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
|
@ -20,6 +22,13 @@ type Config struct {
|
|||
SSHPrivateKey string `mapstructure:"ssh_private_key_file"`
|
||||
SSHPty bool `mapstructure:"ssh_pty"`
|
||||
SSHTimeout time.Duration `mapstructure:"ssh_timeout"`
|
||||
|
||||
// WinRM
|
||||
WinRMUser string `mapstructure:"winrm_username"`
|
||||
WinRMPassword string `mapstructure:"winrm_password"`
|
||||
WinRMHost string `mapstructure:"winrm_host"`
|
||||
WinRMPort int `mapstructure:"winrm_port"`
|
||||
WinRMTimeout time.Duration `mapstructure:"winrm_timeout"`
|
||||
}
|
||||
|
||||
func (c *Config) Prepare(ctx *interpolate.Context) []error {
|
||||
|
@ -27,6 +36,22 @@ func (c *Config) Prepare(ctx *interpolate.Context) []error {
|
|||
c.Type = "ssh"
|
||||
}
|
||||
|
||||
var errs []error
|
||||
switch c.Type {
|
||||
case "ssh":
|
||||
if es := c.prepareSSH(ctx); len(es) > 0 {
|
||||
errs = append(errs, es...)
|
||||
}
|
||||
case "winrm":
|
||||
if es := c.prepareWinRM(ctx); len(es) > 0 {
|
||||
errs = append(errs, es...)
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *Config) prepareSSH(ctx *interpolate.Context) []error {
|
||||
if c.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
}
|
||||
|
@ -37,21 +62,36 @@ func (c *Config) Prepare(ctx *interpolate.Context) []error {
|
|||
|
||||
// Validation
|
||||
var errs []error
|
||||
if c.Type == "ssh" {
|
||||
if c.SSHUsername == "" {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified"))
|
||||
}
|
||||
if c.SSHUsername == "" {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified"))
|
||||
}
|
||||
|
||||
if c.SSHPrivateKey != "" {
|
||||
if _, err := os.Stat(c.SSHPrivateKey); err != nil {
|
||||
errs = append(errs, fmt.Errorf(
|
||||
"ssh_private_key_file is invalid: %s", err))
|
||||
} else if _, err := SSHFileSigner(c.SSHPrivateKey); err != nil {
|
||||
errs = append(errs, fmt.Errorf(
|
||||
"ssh_private_key_file is invalid: %s", err))
|
||||
}
|
||||
if c.SSHPrivateKey != "" {
|
||||
if _, err := os.Stat(c.SSHPrivateKey); err != nil {
|
||||
errs = append(errs, fmt.Errorf(
|
||||
"ssh_private_key_file is invalid: %s", err))
|
||||
} else if _, err := SSHFileSigner(c.SSHPrivateKey); err != nil {
|
||||
errs = append(errs, fmt.Errorf(
|
||||
"ssh_private_key_file is invalid: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *Config) prepareWinRM(ctx *interpolate.Context) []error {
|
||||
if c.WinRMPort == 0 {
|
||||
c.WinRMPort = 5985
|
||||
}
|
||||
|
||||
if c.WinRMTimeout == 0 {
|
||||
c.WinRMTimeout = 30 * time.Minute
|
||||
}
|
||||
|
||||
var errs []error
|
||||
if c.WinRMUser == "" {
|
||||
errs = append(errs, errors.New("winrm_username must be specified."))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
|
|
@ -26,6 +26,12 @@ type StepConnect struct {
|
|||
SSHConfig func(multistep.StateBag) (*gossh.ClientConfig, error)
|
||||
SSHPort func(multistep.StateBag) (int, error)
|
||||
|
||||
// The fields below are callbacks to assist with connecting to WinRM.
|
||||
//
|
||||
// WinRMConfig should return the default configuration for
|
||||
// connecting via WinRM.
|
||||
WinRMConfig func(multistep.StateBag) (*WinRMConfig, error)
|
||||
|
||||
substep multistep.Step
|
||||
}
|
||||
|
||||
|
@ -38,6 +44,11 @@ func (s *StepConnect) Run(state multistep.StateBag) multistep.StepAction {
|
|||
SSHConfig: s.SSHConfig,
|
||||
SSHPort: s.SSHPort,
|
||||
},
|
||||
"winrm": &StepConnectWinRM{
|
||||
Config: s.Config,
|
||||
Host: s.Host,
|
||||
WinRMConfig: s.WinRMConfig,
|
||||
},
|
||||
}
|
||||
|
||||
step, ok := typeMap[s.Config.Type]
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
package communicator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/communicator/winrm"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepConnectWinRM is a multistep Step implementation that waits for WinRM
|
||||
// to become available. It gets the connection information from a single
|
||||
// configuration when creating the step.
|
||||
//
|
||||
// Uses:
|
||||
// ui packer.Ui
|
||||
//
|
||||
// Produces:
|
||||
// communicator packer.Communicator
|
||||
type StepConnectWinRM struct {
|
||||
// All the fields below are documented on StepConnect
|
||||
Config *Config
|
||||
Host func(multistep.StateBag) (string, error)
|
||||
WinRMConfig func(multistep.StateBag) (*WinRMConfig, error)
|
||||
}
|
||||
|
||||
func (s *StepConnectWinRM) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
var comm packer.Communicator
|
||||
var err error
|
||||
|
||||
cancel := make(chan struct{})
|
||||
waitDone := make(chan bool, 1)
|
||||
go func() {
|
||||
ui.Say("Waiting for WinRM to become available...")
|
||||
comm, err = s.waitForWinRM(state, cancel)
|
||||
waitDone <- true
|
||||
}()
|
||||
|
||||
log.Printf("Waiting for WinRM, up to timeout: %s", s.Config.WinRMTimeout)
|
||||
timeout := time.After(s.Config.WinRMTimeout)
|
||||
WaitLoop:
|
||||
for {
|
||||
// Wait for either WinRM to become available, a timeout to occur,
|
||||
// or an interrupt to come through.
|
||||
select {
|
||||
case <-waitDone:
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error waiting for WinRM: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Connected to WinRM!")
|
||||
state.Put("communicator", comm)
|
||||
break WaitLoop
|
||||
case <-timeout:
|
||||
err := fmt.Errorf("Timeout waiting for WinRM.")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
close(cancel)
|
||||
return multistep.ActionHalt
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||
// The step sequence was cancelled, so cancel waiting for WinRM
|
||||
// and just start the halting process.
|
||||
close(cancel)
|
||||
log.Println("Interrupt detected, quitting waiting for WinRM.")
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepConnectWinRM) Cleanup(multistep.StateBag) {
|
||||
}
|
||||
|
||||
func (s *StepConnectWinRM) waitForWinRM(state multistep.StateBag, cancel <-chan struct{}) (packer.Communicator, error) {
|
||||
var comm packer.Communicator
|
||||
for {
|
||||
select {
|
||||
case <-cancel:
|
||||
log.Println("[INFO] WinRM wait cancelled. Exiting loop.")
|
||||
return nil, errors.New("WinRM wait cancelled")
|
||||
case <-time.After(5 * time.Second):
|
||||
}
|
||||
|
||||
host, err := s.Host(state)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Error getting WinRM host: %s", err)
|
||||
continue
|
||||
}
|
||||
port := s.Config.WinRMPort
|
||||
|
||||
user := s.Config.WinRMUser
|
||||
password := s.Config.WinRMPassword
|
||||
if s.WinRMConfig != nil {
|
||||
config, err := s.WinRMConfig(state)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Error getting WinRM config: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if config.Username != "" {
|
||||
user = config.Username
|
||||
}
|
||||
if config.Password != "" {
|
||||
password = config.Password
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[INFO] Attempting WinRM connection...")
|
||||
comm, err = winrm.New(&winrm.Config{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Username: user,
|
||||
Password: password,
|
||||
Timeout: s.Config.WinRMTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] WinRM connection err: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return comm, nil
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package communicator
|
||||
|
||||
// WinRMConfig is configuration that can be returned at runtime to
|
||||
// dynamically configure WinRM.
|
||||
type WinRMConfig struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
Loading…
Reference in New Issue