diff --git a/communicator/winrm/communicator.go b/communicator/winrm/communicator.go new file mode 100644 index 000000000..d0b7eb76c --- /dev/null +++ b/communicator/winrm/communicator.go @@ -0,0 +1,129 @@ +package winrm + +import ( + "fmt" + "io" + "log" + + "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) error { + wcp, err := c.newCopyClient() + if err != nil { + return err + } + log.Printf("Uploading file to '%s'", path) + 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("Uploading 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.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..73ac6d7b2 --- /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"))) + 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) + } +}