diff --git a/communicator/winrm/communicator.go b/communicator/winrm/communicator.go new file mode 100644 index 000000000..82686e2a7 --- /dev/null +++ b/communicator/winrm/communicator.go @@ -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 + }) +} diff --git a/communicator/winrm/communicator_test.go b/communicator/winrm/communicator_test.go new file mode 100644 index 000000000..5c29a7403 --- /dev/null +++ b/communicator/winrm/communicator_test.go @@ -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) + } +} diff --git a/communicator/winrm/config.go b/communicator/winrm/config.go new file mode 100644 index 000000000..32c082987 --- /dev/null +++ b/communicator/winrm/config.go @@ -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 +} diff --git a/communicator/winrm/time.go b/communicator/winrm/time.go new file mode 100644 index 000000000..f8fb6fe8d --- /dev/null +++ b/communicator/winrm/time.go @@ -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 +} diff --git a/communicator/winrm/time_test.go b/communicator/winrm/time_test.go new file mode 100644 index 000000000..4daf4cedf --- /dev/null +++ b/communicator/winrm/time_test.go @@ -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) + } +} diff --git a/helper/communicator/config.go b/helper/communicator/config.go index a2d93a480..f0cb78df7 100644 --- a/helper/communicator/config.go +++ b/helper/communicator/config.go @@ -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 +} diff --git a/helper/communicator/step_connect.go b/helper/communicator/step_connect.go index a31dd4eb8..ce77333e1 100644 --- a/helper/communicator/step_connect.go +++ b/helper/communicator/step_connect.go @@ -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] diff --git a/helper/communicator/step_connect_winrm.go b/helper/communicator/step_connect_winrm.go new file mode 100644 index 000000000..bdd0c1499 --- /dev/null +++ b/helper/communicator/step_connect_winrm.go @@ -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 +} diff --git a/helper/communicator/winrm.go b/helper/communicator/winrm.go new file mode 100644 index 000000000..afdf2569d --- /dev/null +++ b/helper/communicator/winrm.go @@ -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 +}