communicator/winrm
This commit is contained in:
parent
acf31c31a1
commit
0c0f876654
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue