Skip to content

Instantly share code, notes, and snippets.

@rasa
Last active August 29, 2015 14:01
Show Gist options
  • Save rasa/ae71926dcea1f38704ce to your computer and use it in GitHub Desktop.
Save rasa/ae71926dcea1f38704ce to your computer and use it in GitHub Desktop.
--- master..5248280.diff 2014-05-19 19:50:00.255845000 -0700
+++ master..112ed70.diff 2014-05-19 19:50:20.368995400 -0700
@@ -707,7 +707,7 @@
NoPty: b.config.SSHSkipRequestPty,
diff --git a/common/step_connect_winrm.go b/common/step_connect_winrm.go
new file mode 100644
-index 0000000..a91fa29
+index 0000000..40f4040
--- /dev/null
+++ b/common/step_connect_winrm.go
@@ -0,0 +1,130 @@
@@ -823,7 +823,7 @@
+ }
+
+ log.Println("Attempting WinRM connection...")
-+ comm, err = winrm.New(address, s.WinRMUser, s.WinRMPassword)
++ comm, err = winrm.New(address, s.WinRMUser, s.WinRMPassword, s.WinRMWaitTimeout)
+ if err != nil {
+ log.Printf("WinRM handshake err: %s", err)
+
@@ -841,20 +841,99 @@
+
+ return comm, nil
+}
+diff --git a/common/time/time.go b/common/time/time.go
+new file mode 100644
+index 0000000..4ab8894
+--- /dev/null
++++ b/common/time/time.go
+@@ -0,0 +1,30 @@
++package time
++
++import (
++ "fmt"
++ "time"
++)
++
++func ISO8601DurationString(d time.Duration) string {
++ // We're not supporting negative durations
++ if d.Seconds() <= 0 {
++ return "PT0S"
++ }
++
++ hours := int(d.Hours())
++ minutes := int(d.Minutes()) - (hours * 60)
++ seconds := int(d.Seconds()) - (hours*3600 + minutes*60)
++
++ s := "PT"
++ if hours > 0 {
++ s = fmt.Sprintf("%s%dH", s, hours)
++ }
++ if minutes > 0 {
++ s = fmt.Sprintf("%s%dM", s, minutes)
++ }
++ if seconds > 0 {
++ s = fmt.Sprintf("%s%dS", s, seconds)
++ }
++
++ return s
++}
+diff --git a/common/time/time_test.go b/common/time/time_test.go
+new file mode 100644
+index 0000000..8fb86f0
+--- /dev/null
++++ b/common/time/time_test.go
+@@ -0,0 +1,36 @@
++package time
++
++import (
++ "testing"
++ "time"
++)
++
++func TestISO8601DurationString(t *testing.T) {
++ // Test complex duration with hours, minutes, seconds
++ d := time.Duration(3701) * time.Second
++ s := ISO8601DurationString(d)
++ if s != "PT1H1M41S" {
++ t.Fatalf("bad ISO 8601 duration string: %s", s)
++ }
++
++ // Test only minutes duration
++ d = time.Duration(20) * time.Minute
++ s = ISO8601DurationString(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 = ISO8601DurationString(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 = ISO8601DurationString(d)
++ if s != "PT0S" {
++ t.Fatalf("bad ISO 8601 duration string for negative: %s", s)
++ }
++}
diff --git a/communicator/winrm/communicator.go b/communicator/winrm/communicator.go
new file mode 100644
-index 0000000..ad70d05
+index 0000000..e54262c
--- /dev/null
+++ b/communicator/winrm/communicator.go
-@@ -0,0 +1,106 @@
+@@ -0,0 +1,243 @@
+package winrm
+
+import (
-+ "errors"
++ isotime "github.com/mitchellh/packer/common/time"
+ "github.com/mitchellh/packer/packer"
+ "github.com/sneal/go-winrm"
+ "io"
+ "log"
++ "time"
+)
+
+type comm struct {
@@ -862,48 +941,114 @@
+ address string
+ user string
+ password string
++ timeout time.Duration
++}
++
++type elevatedShellOptions struct {
++ Command string
++ User string
++ Password string
+}
+
+// Creates a new packer.Communicator implementation over WinRM.
-+func New(address string, user string, password string) (result *comm, err error) {
-+ result = &comm{
++// Called when Packer tries to connect to WinRM
++func New(address string, user string, password string, timeout time.Duration) (*comm, error) {
++
++ // Create the WinRM client we use internally
++ params := winrm.DefaultParameters()
++ params.Timeout = isotime.ISO8601DurationString(timeout)
++ client := winrm.NewClientWithParameters(address, user, password, params)
++
++ // Attempt to connect to the WinRM service
++ shell, err := client.CreateShell()
++ if err != nil {
++ return nil, err
++ }
++
++ err = shell.Close()
++ if err != nil {
++ return nil, err
++ }
++
++ return &comm{
+ address: address,
+ user: user,
+ password: password,
++ timeout: timeout,
++ client: client,
++ }, nil
+ }
+
-+ if err = result.reconnect(); err != nil {
-+ result = nil
-+ return
++func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
++ return c.StartElevated(cmd)
+ }
+
-+ return
++func (c *comm) StartElevated(cmd *packer.RemoteCmd) (err error) {
++ // Wrap the command in scheduled task
++ tpl, err := packer.NewConfigTemplate()
++ if err != nil {
++ return err
+}
+
-+func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
-+ session, err := c.newSession()
++ elevatedCmd, err := tpl.Process(ElevatedShellTemplate, &elevatedShellOptions{
++ Command: cmd.Command,
++ User: c.user,
++ Password: c.password,
++ })
+ if err != nil {
-+ return
++ return err
++ }
++
++ return c.runCommand(winrm.Powershell(elevatedCmd), cmd)
+ }
+
++func (c *comm) StartUnelevated(cmd *packer.RemoteCmd) (err error) {
++ return c.runCommand(cmd.Command, cmd)
++}
++
++func (c *comm) runCommand(commandText string, cmd *packer.RemoteCmd) (err error) {
+ log.Printf("starting remote command: %s", cmd.Command)
-+ var sessionCmd *winrm.Command
-+ sessionCmd, err = session.Execute(cmd.Command, cmd.Stdout, cmd.Stderr)
++
++ // Create a new shell process on the guest
++ shell, err := c.client.CreateShell()
++ if err != nil {
++ log.Printf("error creating shell, retrying once more: %s", err)
++ shell, err = c.client.CreateShell()
++ if err != nil {
++ log.Printf("error creating shell, giving up: %s", err)
++ return err
++ }
++ }
++
++ // Execute the command
++ var winrmCmd *winrm.Command
++ winrmCmd, err = shell.Execute(commandText, cmd.Stdout, cmd.Stderr)
+ if err != nil {
++ log.Printf("error executing command: %s", err)
+ return err
+ }
+
-+ // Start a goroutine to wait for the session to end and set the
++ // Start a goroutine to wait for the shell to end and set the
+ // exit boolean and status.
+ go func() {
-+ defer session.Close()
++
++ defer func() {
++ err = winrmCmd.Close()
++ if err != nil {
++ log.Printf("winrm command failed to close: %s", err)
++ }
++ err = shell.Close()
++ if err != nil {
++ log.Printf("shell failed to close: %s", err)
++ }
++ }()
+
+ // Block until done
-+ sessionCmd.Wait()
++ winrmCmd.Wait()
+
+ // Report exit status and trigger done
-+ exitStatus := sessionCmd.ExitCode()
-+ log.Printf("remote command exited with '%d': %s", exitStatus, cmd.Command)
++ exitStatus := winrmCmd.ExitCode()
++ log.Printf("remote command exited with '%d'", exitStatus)
+ cmd.SetExited(exitStatus)
+ }()
+
@@ -928,37 +1073,107 @@
+ panic("Download not implemented yet")
+}
+
-+func (c *comm) reconnect() (err error) {
-+ c.client = winrm.NewClient(c.address, c.user, c.password)
-+ _, err = c.client.CreateShell()
-+ return err
-+}
++const ElevatedShellTemplate = `
++$command = "{{.Command}}"
++$user = '{{.User}}'
++$password = '{{.Password}}'
++
++$task_name = "WinRM_Elevated_Shell"
++$out_file = "$env:SystemRoot\Temp\WinRM_Elevated_Shell.log"
++
++if (Test-Path $out_file) {
++ del $out_file
++}
++
++$task_xml = @'
++<?xml version="1.0" encoding="UTF-16"?>
++<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
++ <Principals>
++ <Principal id="Author">
++ <UserId>{user}</UserId>
++ <LogonType>Password</LogonType>
++ <RunLevel>HighestAvailable</RunLevel>
++ </Principal>
++ </Principals>
++ <Settings>
++ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
++ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
++ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
++ <AllowHardTerminate>true</AllowHardTerminate>
++ <StartWhenAvailable>false</StartWhenAvailable>
++ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
++ <IdleSettings>
++ <StopOnIdleEnd>true</StopOnIdleEnd>
++ <RestartOnIdle>false</RestartOnIdle>
++ </IdleSettings>
++ <AllowStartOnDemand>true</AllowStartOnDemand>
++ <Enabled>true</Enabled>
++ <Hidden>false</Hidden>
++ <RunOnlyIfIdle>false</RunOnlyIfIdle>
++ <WakeToRun>false</WakeToRun>
++ <ExecutionTimeLimit>PT2H</ExecutionTimeLimit>
++ <Priority>4</Priority>
++ </Settings>
++ <Actions Context="Author">
++ <Exec>
++ <Command>cmd</Command>
++ <Arguments>{arguments}</Arguments>
++ </Exec>
++ </Actions>
++</Task>
++'@
++
++$bytes = [System.Text.Encoding]::Unicode.GetBytes($command)
++$encoded_command = [Convert]::ToBase64String($bytes)
++$arguments = "/c powershell.exe -EncodedCommand $encoded_command &gt; $out_file 2&gt;&amp;1"
++
++$task_xml = $task_xml.Replace("{arguments}", $arguments)
++$task_xml = $task_xml.Replace("{user}", $user)
++
++$schedule = New-Object -ComObject "Schedule.Service"
++$schedule.Connect()
++$task = $schedule.NewTask($null)
++$task.XmlText = $task_xml
++$folder = $schedule.GetFolder("\")
++$folder.RegisterTaskDefinition($task_name, $task, 6, $user, $password, 1, $null) | Out-Null
++
++$registered_task = $folder.GetTask("\$task_name")
++$registered_task.Run($null) | Out-Null
++
++$timeout = 10
++$sec = 0
++while ( (!($registered_task.state -eq 4)) -and ($sec -lt $timeout) ) {
++ Start-Sleep -s 1
++ $sec++
++}
++
++function SlurpOutput($out_file, $cur_line) {
++ if (Test-Path $out_file) {
++ get-content $out_file | select -skip $cur_line | ForEach {
++ $cur_line += 1
++ Write-Host "$_"
++ }
++ }
++ return $cur_line
++}
++
++$cur_line = 0
++do {
++ Start-Sleep -m 100
++ $cur_line = SlurpOutput $out_file $cur_line
++} while (!($registered_task.state -eq 3))
+
-+func (c *comm) newSession() (session *winrm.Shell, err error) {
-+ log.Println("opening new WinRM session")
-+ if c.client == nil {
-+ err = errors.New("client not available")
-+ } else {
-+ session, err = c.client.CreateShell()
-+ }
++$exit_code = $registered_task.LastTaskResult
++[System.Runtime.Interopservices.Marshal]::ReleaseComObject($schedule) | Out-Null
+
-+ if err != nil {
-+ log.Printf("WinRM session open error: '%s', attempting reconnect", err)
-+ if err := c.reconnect(); err != nil {
-+ return nil, err
-+ }
-+
-+ return c.client.CreateShell()
-+ }
-+
-+ return session, nil
-+}
++exit $exit_code
++`
diff --git a/communicator/winrm/communicator_test.go b/communicator/winrm/communicator_test.go
new file mode 100644
-index 0000000..961c002
+index 0000000..622d322
--- /dev/null
+++ b/communicator/winrm/communicator_test.go
-@@ -0,0 +1,77 @@
+@@ -0,0 +1,120 @@
+// +build !race
+
+package winrm
@@ -969,6 +1184,7 @@
+ "github.com/mitchellh/packer/packer"
+ "os"
+ "testing"
++ "time"
+)
+
+func TestStart(t *testing.T) {
@@ -976,7 +1192,7 @@
+ // You can comment this line out temporarily during development
+ t.Skip()
+
-+ comm, err := New("localhost:5985", "vagrant", "vagrant")
++ comm, err := New("localhost:5985", "vagrant", "vagrant", time.Duration(1) * time.Minute)
+ if err != nil {
+ t.Fatalf("error connecting to WinRM: %s", err)
+ }
@@ -996,7 +1212,49 @@
+
+ fmt.Println(outWriter.String())
+ fmt.Println(errWriter.String())
-+ fmt.Println(err)
++
++ if err != nil {
++ t.Fatalf("error running cmd: %s", err)
++ }
++
++ if cmd.ExitStatus != 0 {
++ t.Fatalf("exit status was non-zero: %d", cmd.ExitStatus)
++ }
++}
++
++func TestStartElevated(t *testing.T) {
++ // This test hits an already running Windows VM
++ // You can comment this line out temporarily during development
++ t.Skip()
++
++ comm, err := New("localhost:5985", "vagrant", "vagrant", time.Duration(1) * time.Minute)
++ if err != nil {
++ t.Fatalf("error connecting to WinRM: %s", err)
++ }
++
++ var cmd packer.RemoteCmd
++ var outWriter, errWriter bytes.Buffer
++
++ cmd.Command = "(New-Object System.Net.WebClient).DownloadFile('http://www.getchef.com/chef/install.msi', 'C:/Windows/Temp/chef.msi');Start-Process 'msiexec' -ArgumentList '/qb /i C:\\Windows\\Temp\\chef.msi' -NoNewWindow -Wait"
++ cmd.Stdout = &outWriter
++ cmd.Stderr = &errWriter
++
++ err = comm.StartElevated(&cmd)
++ if err != nil {
++ t.Fatalf("error starting cmd: %s", err)
++ }
++ cmd.Wait()
++
++ fmt.Println(outWriter.String())
++ fmt.Println(errWriter.String())
++
++ if err != nil {
++ t.Fatalf("error running cmd: %s", err)
++ }
++
++ if cmd.ExitStatus != 0 {
++ t.Fatalf("exit status was non-zero: %d", cmd.ExitStatus)
++ }
+}
+
+func TestUpload(t *testing.T) {
@@ -1004,12 +1262,12 @@
+ // You can comment this line out temporarily during development
+ t.Skip()
+
-+ comm, err := New("localhost:5985", "vagrant", "vagrant")
++ comm, err := New("localhost:5985", "vagrant", "vagrant", time.Duration(1) * time.Minute)
+ if err != nil {
+ t.Fatalf("error connecting to WinRM: %s", err)
+ }
+
-+ f, err := os.Open("/Users/sneal/Downloads/photo1.jpg")
++ f, err := os.Open("~/Downloads/photo1.jpg")
+ if err != nil {
+ t.Fatalf("error opening file: %s", err)
+ }
@@ -1026,22 +1284,22 @@
+ // You can comment this line out temporarily during development
+ t.Skip()
+
-+ comm, err := New("localhost:5985", "vagrant", "vagrant")
++ comm, err := New("localhost:5985", "vagrant", "vagrant", time.Duration(1) * time.Minute)
+ if err != nil {
+ t.Fatalf("error connecting to WinRM: %s", err)
+ }
+
-+ err = comm.UploadDir("c:\\winrm-upload", "/Users/sneal/src/vagrant-windows/lib", nil)
++ err = comm.UploadDir("c:\\src\\chef-repo", "~/src/chef-repo", nil)
+ if err != nil {
+ t.Fatalf("error uploading dir: %s", err)
+ }
+}
diff --git a/communicator/winrm/file_manager.go b/communicator/winrm/file_manager.go
new file mode 100644
-index 0000000..bd79181
+index 0000000..32f4896
--- /dev/null
+++ b/communicator/winrm/file_manager.go
-@@ -0,0 +1,140 @@
+@@ -0,0 +1,141 @@
+package winrm
+
+import (
@@ -1103,17 +1361,18 @@
+ }
+ else {
+ $dest_dir = ([System.IO.Path]::GetDirectoryName($dest_file_path))
-+ if ($dest_dir.length -gt 3) {
-+ New-Item -ItemType directory -Force -Path $dest_dir
-+ }
++ New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path $dest_dir
+ }
+
++ if (Test-Path $tmp_file_path) {
+ $base64_lines = Get-Content $tmp_file_path
+ $base64_string = [string]::join("",$base64_lines)
+ $bytes = [System.Convert]::FromBase64String($base64_string)
+ [System.IO.File]::WriteAllBytes($dest_file_path, $bytes)
-+
+ Remove-Item $tmp_file_path -Force -ErrorAction SilentlyContinue
++ } else {
++ echo $null > $dest_file_path
++ }
+ `, guestFilePath, dst)))
+
+ return err
@@ -1142,7 +1401,7 @@
+ Command: cmd,
+ }
+
-+ err := f.comm.Start(remoteCmd)
++ err := f.comm.StartUnelevated(remoteCmd)
+ if err != nil {
+ return err
+ }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment