From 956b9ded0a673ef04589e6c0eda8eefb10571bf4 Mon Sep 17 00:00:00 2001 From: Vilmos Nebehaj Date: Fri, 10 Apr 2015 14:57:32 -0700 Subject: [PATCH 01/31] Try another ssh port if the current one is taken. --- builder/qemu/step_forward_ssh.go | 5 +++++ builder/virtualbox/common/step_forward_ssh.go | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/builder/qemu/step_forward_ssh.go b/builder/qemu/step_forward_ssh.go index 3b84d26c1..ebf54b093 100644 --- a/builder/qemu/step_forward_ssh.go +++ b/builder/qemu/step_forward_ssh.go @@ -34,12 +34,17 @@ func (s *stepForwardSSH) Run(state multistep.StateBag) multistep.StepAction { for { sshHostPort = offset + config.SSHHostPortMin + if sshHostPort >= config.SSHHostPortMax { + offset = 0 + sshHostPort = config.SSHHostPortMin + } log.Printf("Trying port: %d", sshHostPort) l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort)) if err == nil { defer l.Close() break } + offset++ } ui.Say(fmt.Sprintf("Found port for SSH: %d.", sshHostPort)) diff --git a/builder/virtualbox/common/step_forward_ssh.go b/builder/virtualbox/common/step_forward_ssh.go index 862432952..4772f8d37 100644 --- a/builder/virtualbox/common/step_forward_ssh.go +++ b/builder/virtualbox/common/step_forward_ssh.go @@ -42,12 +42,17 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction { for { sshHostPort = offset + s.HostPortMin + if sshHostPort >= s.HostPortMax { + offset = 0 + sshHostPort = s.HostPortMin + } log.Printf("Trying port: %d", sshHostPort) l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort)) if err == nil { defer l.Close() break } + offset++ } // Create a forwarded port mapping to the VM From fd5f4c61ae419f3abe324876a532f011370f1e69 Mon Sep 17 00:00:00 2001 From: Mitch Garnaat Date: Fri, 17 Apr 2015 12:35:26 -0700 Subject: [PATCH 02/31] Added a call to grep for path in /proc/mounts before attempting to umount. If path is not there, it is already unmounted. --- builder/amazon/chroot/step_mount_extra.go | 30 +++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/builder/amazon/chroot/step_mount_extra.go b/builder/amazon/chroot/step_mount_extra.go index d589d6c74..aa63b4b61 100644 --- a/builder/amazon/chroot/step_mount_extra.go +++ b/builder/amazon/chroot/step_mount_extra.go @@ -6,6 +6,8 @@ import ( "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" "os" + "os/exec" + "syscall" ) // StepMountExtra mounts the attached device. @@ -90,13 +92,37 @@ func (s *StepMountExtra) CleanupFunc(state multistep.StateBag) error { var path string lastIndex := len(s.mounts) - 1 path, s.mounts = s.mounts[lastIndex], s.mounts[:lastIndex] + + grepCommand, err := wrappedCommand(fmt.Sprintf("grep %s /proc/mounts", path)) + if err != nil { + return fmt.Errorf("Error creating grep command: %s", err) + } + + // Before attempting to unmount, + // check to see if path is already unmounted + stderr := new(bytes.Buffer) + cmd := ShellCommand(grepCommand) + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + if status, ok := exitError.Sys().(syscall.WaitStatus); ok { + exitStatus := status.ExitStatus() + if exitStatus == 1 { + // path has already been unmounted + // just skip this path + continue + } + } + } + } + unmountCommand, err := wrappedCommand(fmt.Sprintf("umount %s", path)) if err != nil { return fmt.Errorf("Error creating unmount command: %s", err) } - stderr := new(bytes.Buffer) - cmd := ShellCommand(unmountCommand) + stderr = new(bytes.Buffer) + cmd = ShellCommand(unmountCommand) cmd.Stderr = stderr if err := cmd.Run(); err != nil { return fmt.Errorf( From ebdd0d991a1d952971e1544f89561758e9b503d9 Mon Sep 17 00:00:00 2001 From: Andrew Bayer Date: Fri, 29 May 2015 14:50:11 -0700 Subject: [PATCH 03/31] Adds support for using the internal IP rather than NAT IP in GCE --- builder/googlecompute/config.go | 1 + builder/googlecompute/config_test.go | 15 ++++++ builder/googlecompute/driver.go | 3 ++ builder/googlecompute/driver_gce.go | 17 ++++++- builder/googlecompute/driver_mock.go | 11 +++++ builder/googlecompute/ssh.go | 2 +- builder/googlecompute/step_instance_info.go | 48 +++++++++++++------ .../googlecompute/step_instance_info_test.go | 40 ++++++++++++++++ 8 files changed, 120 insertions(+), 17 deletions(-) diff --git a/builder/googlecompute/config.go b/builder/googlecompute/config.go index 743e4745c..e0e6624fb 100644 --- a/builder/googlecompute/config.go +++ b/builder/googlecompute/config.go @@ -36,6 +36,7 @@ type Config struct { RawSSHTimeout string `mapstructure:"ssh_timeout"` RawStateTimeout string `mapstructure:"state_timeout"` Tags []string `mapstructure:"tags"` + UseInternalIP bool `mapstructure:"use_internal_ip"` Zone string `mapstructure:"zone"` account accountFile diff --git a/builder/googlecompute/config_test.go b/builder/googlecompute/config_test.go index 4a7a1ed67..c28c35a0f 100644 --- a/builder/googlecompute/config_test.go +++ b/builder/googlecompute/config_test.go @@ -116,6 +116,21 @@ func TestConfigPrepare(t *testing.T) { "5s", false, }, + { + "use_internal_ip", + nil, + false, + }, + { + "use_internal_ip", + false, + false, + }, + { + "use_internal_ip", + "SO VERY BAD", + true, + }, } for _, tc := range cases { diff --git a/builder/googlecompute/driver.go b/builder/googlecompute/driver.go index 6f035c562..be697fe6b 100644 --- a/builder/googlecompute/driver.go +++ b/builder/googlecompute/driver.go @@ -24,6 +24,9 @@ type Driver interface { // GetNatIP gets the NAT IP address for the instance. GetNatIP(zone, name string) (string, error) + // GetInternalIP gets the GCE-internal IP address for the instance. + GetInternalIP(zone, name string) (string, error) + // RunInstance takes the given config and launches an instance. RunInstance(*InstanceConfig) (<-chan error, error) diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index b9ed4693e..f52ee6321 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -157,7 +157,6 @@ func (d *driverGCE) GetNatIP(zone, name string) (string, error) { if ni.AccessConfigs == nil { continue } - for _, ac := range ni.AccessConfigs { if ac.NatIP != "" { return ac.NatIP, nil @@ -168,6 +167,22 @@ func (d *driverGCE) GetNatIP(zone, name string) (string, error) { return "", nil } +func (d *driverGCE) GetInternalIP(zone, name string) (string, error) { + instance, err := d.service.Instances.Get(d.projectId, zone, name).Do() + if err != nil { + return "", err + } + + for _, ni := range instance.NetworkInterfaces { + if ni.NetworkIP == "" { + continue + } + return ni.NetworkIP, nil + } + + return "", nil +} + func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { // Get the zone d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone)) diff --git a/builder/googlecompute/driver_mock.go b/builder/googlecompute/driver_mock.go index 1a9c03ae9..196aa1d81 100644 --- a/builder/googlecompute/driver_mock.go +++ b/builder/googlecompute/driver_mock.go @@ -30,6 +30,11 @@ type DriverMock struct { GetNatIPResult string GetNatIPErr error + GetInternalIPZone string + GetInternalIPName string + GetInternalIPResult string + GetInternalIPErr error + RunInstanceConfig *InstanceConfig RunInstanceErrCh <-chan error RunInstanceErr error @@ -108,6 +113,12 @@ func (d *DriverMock) GetNatIP(zone, name string) (string, error) { return d.GetNatIPResult, d.GetNatIPErr } +func (d *DriverMock) GetInternalIP(zone, name string) (string, error) { + d.GetInternalIPZone = zone + d.GetInternalIPName = name + return d.GetInternalIPResult, d.GetInternalIPErr +} + func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) { d.RunInstanceConfig = c diff --git a/builder/googlecompute/ssh.go b/builder/googlecompute/ssh.go index a4e0151f4..e04029e44 100644 --- a/builder/googlecompute/ssh.go +++ b/builder/googlecompute/ssh.go @@ -1,9 +1,9 @@ package googlecompute import ( - "golang.org/x/crypto/ssh" "fmt" "github.com/mitchellh/multistep" + "golang.org/x/crypto/ssh" ) // sshAddress returns the ssh address. diff --git a/builder/googlecompute/step_instance_info.go b/builder/googlecompute/step_instance_info.go index b79e7c042..92f382f06 100644 --- a/builder/googlecompute/step_instance_info.go +++ b/builder/googlecompute/step_instance_info.go @@ -40,23 +40,41 @@ func (s *StepInstanceInfo) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } - ip, err := driver.GetNatIP(config.Zone, instanceName) - if err != nil { - err := fmt.Errorf("Error retrieving instance nat ip address: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - if s.Debug { - if ip != "" { - ui.Message(fmt.Sprintf("Public IP: %s", ip)) + if config.UseInternalIP { + ip, err := driver.GetInternalIP(config.Zone, instanceName) + if err != nil { + err := fmt.Errorf("Error retrieving instance internal ip address: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } - } - ui.Message(fmt.Sprintf("IP: %s", ip)) - state.Put("instance_ip", ip) - return multistep.ActionContinue + if s.Debug { + if ip != "" { + ui.Message(fmt.Sprintf("Internal IP: %s", ip)) + } + } + ui.Message(fmt.Sprintf("IP: %s", ip)) + state.Put("instance_ip", ip) + return multistep.ActionContinue + } else { + ip, err := driver.GetNatIP(config.Zone, instanceName) + if err != nil { + err := fmt.Errorf("Error retrieving instance nat ip address: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if s.Debug { + if ip != "" { + ui.Message(fmt.Sprintf("Public IP: %s", ip)) + } + } + ui.Message(fmt.Sprintf("IP: %s", ip)) + state.Put("instance_ip", ip) + return multistep.ActionContinue + } } // Cleanup. diff --git a/builder/googlecompute/step_instance_info_test.go b/builder/googlecompute/step_instance_info_test.go index 8566ce722..5b6c01d0a 100644 --- a/builder/googlecompute/step_instance_info_test.go +++ b/builder/googlecompute/step_instance_info_test.go @@ -49,6 +49,46 @@ func TestStepInstanceInfo(t *testing.T) { } } +func TestStepInstanceInfo_InternalIP(t *testing.T) { + state := testState(t) + step := new(StepInstanceInfo) + defer step.Cleanup(state) + + state.Put("instance_name", "foo") + + config := state.Get("config").(*Config) + config.UseInternalIP = true + driver := state.Get("driver").(*DriverMock) + driver.GetNatIPResult = "1.2.3.4" + driver.GetInternalIPResult = "5.6.7.8" + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + // Verify state + if driver.WaitForInstanceState != "RUNNING" { + t.Fatalf("bad: %#v", driver.WaitForInstanceState) + } + if driver.WaitForInstanceZone != config.Zone { + t.Fatalf("bad: %#v", driver.WaitForInstanceZone) + } + if driver.WaitForInstanceName != "foo" { + t.Fatalf("bad: %#v", driver.WaitForInstanceName) + } + + ipRaw, ok := state.GetOk("instance_ip") + if !ok { + t.Fatal("should have ip") + } + if ip, ok := ipRaw.(string); !ok { + t.Fatal("ip is not a string") + } else if ip != "5.6.7.8" { + t.Fatalf("bad ip: %s", ip) + } +} + func TestStepInstanceInfo_getNatIPError(t *testing.T) { state := testState(t) step := new(StepInstanceInfo) From 3d94462e37a2dace1600f64689142f0eaab90aeb Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Sun, 17 May 2015 17:35:39 +0300 Subject: [PATCH 04/31] remove image format from image name Signed-off-by: Vasiliy Tolstov --- builder/qemu/step_copy_disk.go | 7 +++---- builder/qemu/step_create_disk.go | 6 +++--- builder/qemu/step_resize_disk.go | 7 +++---- builder/qemu/step_run.go | 3 +-- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/builder/qemu/step_copy_disk.go b/builder/qemu/step_copy_disk.go index 54c3084ac..dd5b6d8d7 100644 --- a/builder/qemu/step_copy_disk.go +++ b/builder/qemu/step_copy_disk.go @@ -3,7 +3,6 @@ package qemu import ( "fmt" "path/filepath" - "strings" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" @@ -18,13 +17,13 @@ func (s *stepCopyDisk) Run(state multistep.StateBag) multistep.StepAction { driver := state.Get("driver").(Driver) isoPath := state.Get("iso_path").(string) ui := state.Get("ui").(packer.Ui) - path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", config.VMName, - strings.ToLower(config.Format))) - name := config.VMName + "." + strings.ToLower(config.Format) + path := filepath.Join(config.OutputDir, fmt.Sprintf("%s", config.VMName)) + name := config.VMName command := []string{ "convert", "-f", config.Format, + "-O", config.Format, isoPath, path, } diff --git a/builder/qemu/step_create_disk.go b/builder/qemu/step_create_disk.go index 986df1d2b..ed757ca8c 100644 --- a/builder/qemu/step_create_disk.go +++ b/builder/qemu/step_create_disk.go @@ -2,10 +2,10 @@ package qemu import ( "fmt" + "path/filepath" + "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" - "path/filepath" - "strings" ) // This step creates the virtual disk that will be used as the @@ -16,7 +16,7 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*config) driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) - name := config.VMName + "." + strings.ToLower(config.Format) + name := config.VMName path := filepath.Join(config.OutputDir, name) command := []string{ diff --git a/builder/qemu/step_resize_disk.go b/builder/qemu/step_resize_disk.go index 4e8536c32..6f27e6843 100644 --- a/builder/qemu/step_resize_disk.go +++ b/builder/qemu/step_resize_disk.go @@ -2,10 +2,10 @@ package qemu import ( "fmt" + "path/filepath" + "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" - "path/filepath" - "strings" ) // This step resizes the virtual disk that will be used as the @@ -16,8 +16,7 @@ func (s *stepResizeDisk) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*config) driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) - path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", config.VMName, - strings.ToLower(config.Format))) + path := filepath.Join(config.OutputDir, config.VMName) command := []string{ "resize", diff --git a/builder/qemu/step_run.go b/builder/qemu/step_run.go index 3f900d651..8524d9f9e 100644 --- a/builder/qemu/step_run.go +++ b/builder/qemu/step_run.go @@ -64,8 +64,7 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error vnc := fmt.Sprintf("0.0.0.0:%d", vncPort-5900) vmName := config.VMName - imgPath := filepath.Join(config.OutputDir, - fmt.Sprintf("%s.%s", vmName, strings.ToLower(config.Format))) + imgPath := filepath.Join(config.OutputDir, vmName) defaultArgs := make(map[string]string) From 040ff0706d6b514d5a5b927982efe857bfc528c1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 11:01:28 -0700 Subject: [PATCH 05/31] provisioner/powershell --- plugin/provisioner-powershell/main.go | 15 + provisioner/powershell/elevated.go | 87 +++ provisioner/powershell/powershell.go | 17 + provisioner/powershell/provisioner.go | 459 ++++++++++++++ provisioner/powershell/provisioner_test.go | 656 +++++++++++++++++++++ 5 files changed, 1234 insertions(+) create mode 100644 plugin/provisioner-powershell/main.go create mode 100644 provisioner/powershell/elevated.go create mode 100644 provisioner/powershell/powershell.go create mode 100644 provisioner/powershell/provisioner.go create mode 100644 provisioner/powershell/provisioner_test.go diff --git a/plugin/provisioner-powershell/main.go b/plugin/provisioner-powershell/main.go new file mode 100644 index 000000000..672bdb43f --- /dev/null +++ b/plugin/provisioner-powershell/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/provisioner/powershell" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterProvisioner(new(powershell.Provisioner)) + server.Serve() +} diff --git a/provisioner/powershell/elevated.go b/provisioner/powershell/elevated.go new file mode 100644 index 000000000..00bc72e4a --- /dev/null +++ b/provisioner/powershell/elevated.go @@ -0,0 +1,87 @@ +package powershell + +import ( + "text/template" +) + +type elevatedOptions struct { + User string + Password string + TaskName string + TaskDescription string + EncodedCommand string +} + +var elevatedTemplate = template.Must(template.New("ElevatedCommand").Parse(` +$name = "{{.TaskName}}" +$log = "$env:TEMP\$name.out" +$s = New-Object -ComObject "Schedule.Service" +$s.Connect() +$t = $s.NewTask($null) +$t.XmlText = @' + + + + {{.TaskDescription}} + + + + {{.User}} + Password + HighestAvailable + + + + IgnoreNew + false + false + true + false + false + + false + false + + true + true + false + false + false + PT24H + 4 + + + + cmd + /c powershell.exe -EncodedCommand {{.EncodedCommand}} > %TEMP%\{{.TaskName}}.out 2>&1 + + + +'@ +$f = $s.GetFolder("\") +$f.RegisterTaskDefinition($name, $t, 6, "{{.User}}", "{{.Password}}", 1, $null) | Out-Null +$t = $f.GetTask("\$name") +$t.Run($null) | Out-Null +$timeout = 10 +$sec = 0 +while ((!($t.state -eq 4)) -and ($sec -lt $timeout)) { + Start-Sleep -s 1 + $sec++ +} +function SlurpOutput($l) { + if (Test-Path $log) { + Get-Content $log | select -skip $l | ForEach { + $l += 1 + Write-Host "$_" + } + } + return $l +} +$line = 0 +do { + Start-Sleep -m 100 + $line = SlurpOutput $line +} while (!($t.state -eq 3)) +$result = $t.LastTaskResult +[System.Runtime.Interopservices.Marshal]::ReleaseComObject($s) | Out-Null +exit $result`)) diff --git a/provisioner/powershell/powershell.go b/provisioner/powershell/powershell.go new file mode 100644 index 000000000..1f5a7ffad --- /dev/null +++ b/provisioner/powershell/powershell.go @@ -0,0 +1,17 @@ +package powershell + +import ( + "encoding/base64" +) + +func powershellEncode(buffer []byte) string { + // 2 byte chars to make PowerShell happy + wideCmd := "" + for _, b := range buffer { + wideCmd += string(b) + "\x00" + } + + // Base64 encode the command + input := []uint8(wideCmd) + return base64.StdEncoding.EncodeToString(input) +} diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go new file mode 100644 index 000000000..0c2454d0d --- /dev/null +++ b/provisioner/powershell/provisioner.go @@ -0,0 +1,459 @@ +// This package implements a provisioner for Packer that executes +// shell scripts within the remote machine. +package powershell + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "sort" + "strings" + "time" + + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/common/uuid" + "github.com/mitchellh/packer/helper/config" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template/interpolate" +) + +const DefaultRemotePath = "c:/Windows/Temp/script.ps1" + +var retryableSleep = 2 * time.Second + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + // If true, the script contains binary and line endings will not be + // converted from Windows to Unix-style. + Binary bool + + // An inline script to execute. Multiple strings are all executed + // in the context of a single shell. + Inline []string + + // The local path of the shell script to upload and execute. + Script string + + // An array of multiple scripts to run. + Scripts []string + + // An array of environment variables that will be injected before + // your command(s) are executed. + Vars []string `mapstructure:"environment_vars"` + + // The remote path where the local shell script will be uploaded to. + // This should be set to a writable file that is in a pre-existing directory. + RemotePath string `mapstructure:"remote_path"` + + // The command used to execute the script. The '{{ .Path }}' variable + // should be used to specify where the script goes, {{ .Vars }} + // can be used to inject the environment_vars into the environment. + ExecuteCommand string `mapstructure:"execute_command"` + + // The command used to execute the elevated script. The '{{ .Path }}' variable + // should be used to specify where the script goes, {{ .Vars }} + // can be used to inject the environment_vars into the environment. + ElevatedExecuteCommand string `mapstructure:"elevated_execute_command"` + + // The timeout for retrying to start the process. Until this timeout + // is reached, if the provisioner can't start a process, it retries. + // This can be set high to allow for reboots. + StartRetryTimeout time.Duration `mapstructure:"start_retry_timeout"` + + // This is used in the template generation to format environment variables + // inside the `ExecuteCommand` template. + EnvVarFormat string + + // This is used in the template generation to format environment variables + // inside the `ElevatedExecuteCommand` template. + ElevatedEnvVarFormat string `mapstructure:"elevated_env_var_format"` + + // Instructs the communicator to run the remote script as a + // Windows scheduled task, effectively elevating the remote + // user by impersonating a logged-in user + ElevatedUser string `mapstructure:"elevated_user"` + ElevatedPassword string `mapstructure:"elevated_password"` + + // Valid Exit Codes - 0 is not always the only valid error code! + // See http://www.symantec.com/connect/articles/windows-system-error-codes-exit-codes-description for examples + // such as 3010 - "The requested operation is successful. Changes will not be effective until the system is rebooted." + ValidExitCodes []int `mapstructure:"valid_exit_codes"` + + ctx interpolate.Context +} + +type Provisioner struct { + config Config + communicator packer.Communicator +} + +type ExecuteCommandTemplate struct { + Vars string + Path string +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + err := config.Decode(&p.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "execute_command", + }, + }, + }, raws...) + if err != nil { + return err + } + + if p.config.EnvVarFormat == "" { + p.config.EnvVarFormat = `$env:%s=\"%s\"; ` + } + + if p.config.ElevatedEnvVarFormat == "" { + p.config.ElevatedEnvVarFormat = `$env:%s="%s"; ` + } + + if p.config.ExecuteCommand == "" { + p.config.ExecuteCommand = `powershell "& { {{.Vars}}{{.Path}}; exit $LastExitCode}"` + } + + if p.config.ElevatedExecuteCommand == "" { + p.config.ElevatedExecuteCommand = `{{.Vars}}{{.Path}}` + } + + if p.config.Inline != nil && len(p.config.Inline) == 0 { + p.config.Inline = nil + } + + if p.config.StartRetryTimeout == 0 { + p.config.StartRetryTimeout = 5 * time.Minute + } + + if p.config.RemotePath == "" { + p.config.RemotePath = DefaultRemotePath + } + + if p.config.Scripts == nil { + p.config.Scripts = make([]string, 0) + } + + if p.config.Vars == nil { + p.config.Vars = make([]string, 0) + } + + if p.config.ValidExitCodes == nil { + p.config.ValidExitCodes = []int{0} + } + + var errs error + if p.config.Script != "" && len(p.config.Scripts) > 0 { + errs = packer.MultiErrorAppend(errs, + errors.New("Only one of script or scripts can be specified.")) + } + + if p.config.ElevatedUser != "" && p.config.ElevatedPassword == "" { + errs = packer.MultiErrorAppend(errs, + errors.New("Must supply an 'elevated_password' if 'elevated_user' provided")) + } + + if p.config.ElevatedUser == "" && p.config.ElevatedPassword != "" { + errs = packer.MultiErrorAppend(errs, + errors.New("Must supply an 'elevated_user' if 'elevated_password' provided")) + } + + if p.config.Script != "" { + p.config.Scripts = []string{p.config.Script} + } + + if len(p.config.Scripts) == 0 && p.config.Inline == nil { + errs = packer.MultiErrorAppend(errs, + errors.New("Either a script file or inline script must be specified.")) + } else if len(p.config.Scripts) > 0 && p.config.Inline != nil { + errs = packer.MultiErrorAppend(errs, + errors.New("Only a script file or an inline script can be specified, not both.")) + } + + for _, path := range p.config.Scripts { + if _, err := os.Stat(path); err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Bad script '%s': %s", path, err)) + } + } + + // Do a check for bad environment variables, such as '=foo', 'foobar' + for _, kv := range p.config.Vars { + vs := strings.SplitN(kv, "=", 2) + if len(vs) != 2 || vs[0] == "" { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Environment variable not in format 'key=value': %s", kv)) + } + } + + if errs != nil { + return errs + } + + return nil +} + +// Takes the inline scripts, concatenates them +// into a temporary file and returns a string containing the location +// of said file. +func extractScript(p *Provisioner) (string, error) { + temp, err := ioutil.TempFile(os.TempDir(), "packer-powershell-provisioner") + if err != nil { + return "", err + } + defer temp.Close() + writer := bufio.NewWriter(temp) + for _, command := range p.config.Inline { + log.Printf("Found command: %s", command) + if _, err := writer.WriteString(command + "\n"); err != nil { + return "", fmt.Errorf("Error preparing shell script: %s", err) + } + } + + if err := writer.Flush(); err != nil { + return "", fmt.Errorf("Error preparing shell script: %s", err) + } + + return temp.Name(), nil +} + +func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { + ui.Say(fmt.Sprintf("Provisioning with Powershell...")) + p.communicator = comm + + scripts := make([]string, len(p.config.Scripts)) + copy(scripts, p.config.Scripts) + + // Build our variables up by adding in the build name and builder type + envVars := make([]string, len(p.config.Vars)+2) + envVars[0] = "PACKER_BUILD_NAME=" + p.config.PackerBuildName + envVars[1] = "PACKER_BUILDER_TYPE=" + p.config.PackerBuilderType + copy(envVars, p.config.Vars) + + if p.config.Inline != nil { + temp, err := extractScript(p) + if err != nil { + ui.Error(fmt.Sprintf("Unable to extract inline scripts into a file: %s", err)) + } + scripts = append(scripts, temp) + } + + for _, path := range scripts { + ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path)) + + log.Printf("Opening %s for reading", path) + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("Error opening shell script: %s", err) + } + defer f.Close() + + command, err := p.createCommandText() + if err != nil { + return fmt.Errorf("Error processing command: %s", err) + } + + // Upload the file and run the command. Do this in the context of + // a single retryable function so that we don't end up with + // the case that the upload succeeded, a restart is initiated, + // and then the command is executed but the file doesn't exist + // any longer. + var cmd *packer.RemoteCmd + err = p.retryable(func() error { + if _, err := f.Seek(0, 0); err != nil { + return err + } + + if err := comm.Upload(p.config.RemotePath, f, nil); err != nil { + return fmt.Errorf("Error uploading script: %s", err) + } + + cmd = &packer.RemoteCmd{Command: command} + return cmd.StartWithUi(comm, ui) + }) + if err != nil { + return err + } + + // Close the original file since we copied it + f.Close() + + // Check exit code against allowed codes (likely just 0) + validExitCode := false + for _, v := range p.config.ValidExitCodes { + if cmd.ExitStatus == v { + validExitCode = true + } + } + if !validExitCode { + return fmt.Errorf("Script exited with non-zero exit status: %d. Allowed exit codes are: %s", cmd.ExitStatus, p.config.ValidExitCodes) + } + } + + return nil +} + +func (p *Provisioner) Cancel() { + // Just hard quit. It isn't a big deal if what we're doing keeps + // running on the other side. + os.Exit(0) +} + +// retryable will retry the given function over and over until a +// non-error is returned. +func (p *Provisioner) retryable(f func() error) error { + startTimeout := time.After(p.config.StartRetryTimeout) + for { + var err error + if err = f(); err == nil { + return nil + } + + // Create an error and log it + err = fmt.Errorf("Retryable error: %s", err) + log.Printf(err.Error()) + + // Check if we timed out, otherwise we retry. It is safe to + // retry since the only error case above is if the command + // failed to START. + select { + case <-startTimeout: + return err + default: + time.Sleep(retryableSleep) + } + } +} + +func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string, err error) { + flattened = "" + envVars := make(map[string]string) + + // Always available Packer provided env vars + envVars["PACKER_BUILD_NAME"] = p.config.PackerBuildName + envVars["PACKER_BUILDER_TYPE"] = p.config.PackerBuilderType + + // Split vars into key/value components + for _, envVar := range p.config.Vars { + keyValue := strings.Split(envVar, "=") + if len(keyValue) != 2 { + err = errors.New("Shell provisioner environment variables must be in key=value format") + return + } + envVars[keyValue[0]] = keyValue[1] + } + + // Create a list of env var keys in sorted order + var keys []string + for k := range envVars { + keys = append(keys, k) + } + sort.Strings(keys) + format := p.config.EnvVarFormat + if elevated { + format = p.config.ElevatedEnvVarFormat + } + + // Re-assemble vars using OS specific format pattern and flatten + for _, key := range keys { + flattened += fmt.Sprintf(format, key, envVars[key]) + } + return +} + +func (p *Provisioner) createCommandText() (command string, err error) { + // Create environment variables to set before executing the command + flattenedEnvVars, err := p.createFlattenedEnvVars(false) + if err != nil { + return "", err + } + + p.config.ctx.Data = &ExecuteCommandTemplate{ + Vars: flattenedEnvVars, + Path: p.config.RemotePath, + } + command, err = interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) + if err != nil { + return "", fmt.Errorf("Error processing command: %s", err) + } + + // Return the interpolated command + if p.config.ElevatedUser == "" { + return command, nil + } + + // Can't double escape the env vars, lets create shiny new ones + flattenedEnvVars, err = p.createFlattenedEnvVars(true) + p.config.ctx.Data = &ExecuteCommandTemplate{ + Vars: flattenedEnvVars, + Path: p.config.RemotePath, + } + command, err = interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) + if err != nil { + return "", fmt.Errorf("Error processing command: %s", err) + } + + // OK so we need an elevated shell runner to wrap our command, this is going to have its own path + // generate the script and update the command runner in the process + path, err := p.generateElevatedRunner(command) + + // Return the path to the elevated shell wrapper + command = fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path) + + return +} + +func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath string, err error) { + log.Printf("Building elevated command wrapper for: %s", command) + + // generate command + var buffer bytes.Buffer + err = elevatedTemplate.Execute(&buffer, elevatedOptions{ + User: p.config.ElevatedUser, + Password: p.config.ElevatedPassword, + TaskDescription: "Packer elevated task", + TaskName: fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()), + EncodedCommand: powershellEncode([]byte(command + "; exit $LASTEXITCODE")), + }) + + if err != nil { + fmt.Printf("Error creating elevated template: %s", err) + return "", err + } + + tmpFile, err := ioutil.TempFile(os.TempDir(), "packer-elevated-shell.ps1") + writer := bufio.NewWriter(tmpFile) + if _, err := writer.WriteString(string(buffer.Bytes())); err != nil { + return "", fmt.Errorf("Error preparing elevated shell script: %s", err) + } + + if err := writer.Flush(); err != nil { + return "", fmt.Errorf("Error preparing elevated shell script: %s", err) + } + tmpFile.Close() + f, err := os.Open(tmpFile.Name()) + if err != nil { + return "", fmt.Errorf("Error opening temporary elevated shell script: %s", err) + } + defer f.Close() + + uuid := uuid.TimeOrderedUUID() + path := fmt.Sprintf(`${env:TEMP}\packer-elevated-shell-%s.ps1`, uuid) + log.Printf("Uploading elevated shell wrapper for command [%s] to [%s] from [%s]", command, path, tmpFile.Name()) + err = p.communicator.Upload(path, f, nil) + if err != nil { + return "", fmt.Errorf("Error preparing elevated shell script: %s", err) + } + + // CMD formatted Path required for this op + path = fmt.Sprintf("%s-%s.ps1", "%TEMP%\\packer-elevated-shell", uuid) + return path, err +} diff --git a/provisioner/powershell/provisioner_test.go b/provisioner/powershell/provisioner_test.go new file mode 100644 index 000000000..78484c184 --- /dev/null +++ b/provisioner/powershell/provisioner_test.go @@ -0,0 +1,656 @@ +package powershell + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + //"log" + "os" + "regexp" + "strings" + "testing" + "time" + + "github.com/mitchellh/packer/packer" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "inline": []interface{}{"foo", "bar"}, + } +} + +func init() { + //log.SetOutput(ioutil.Discard) +} + +func TestProvisionerPrepare_extractScript(t *testing.T) { + config := testConfig() + p := new(Provisioner) + _ = p.Prepare(config) + file, err := extractScript(p) + if err != nil { + t.Fatalf("Should not be error: %s", err) + } + t.Logf("File: %s", file) + if strings.Index(file, os.TempDir()) != 0 { + t.Fatalf("Temp file should reside in %s. File location: %s", os.TempDir(), file) + } + + // File contents should contain 2 lines concatenated by newlines: foo\nbar + readFile, err := ioutil.ReadFile(file) + expectedContents := "foo\nbar\n" + s := string(readFile[:]) + if s != expectedContents { + t.Fatalf("Expected generated inlineScript to equal '%s', got '%s'", expectedContents, s) + } +} + +func TestProvisioner_Impl(t *testing.T) { + var raw interface{} + raw = &Provisioner{} + if _, ok := raw.(packer.Provisioner); !ok { + t.Fatalf("must be a Provisioner") + } +} + +func TestProvisionerPrepare_Defaults(t *testing.T) { + var p Provisioner + config := testConfig() + + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.RemotePath != DefaultRemotePath { + t.Errorf("unexpected remote path: %s", p.config.RemotePath) + } + + if p.config.ElevatedUser != "" { + t.Error("expected elevated_user to be empty") + } + if p.config.ElevatedPassword != "" { + t.Error("expected elevated_password to be empty") + } + + if p.config.ExecuteCommand != "powershell \"& { {{.Vars}}{{.Path}}; exit $LastExitCode}\"" { + t.Fatalf("Default command should be powershell \"& { {{.Vars}}{{.Path}}; exit $LastExitCode}\", but got %s", p.config.ExecuteCommand) + } + + if p.config.ElevatedExecuteCommand != "{{.Vars}}{{.Path}}" { + t.Fatalf("Default command should be powershell {{.Vars}}{{.Path}}, but got %s", p.config.ElevatedExecuteCommand) + } + + if p.config.ValidExitCodes == nil { + t.Fatalf("ValidExitCodes should not be nil") + } + if p.config.ValidExitCodes != nil { + expCodes := []int{0} + for i, v := range p.config.ValidExitCodes { + if v != expCodes[i] { + t.Fatalf("Expected ValidExitCodes don't match actual") + } + } + } + + if p.config.ElevatedEnvVarFormat != `$env:%s="%s"; ` { + t.Fatalf("Default command should be powershell \"{{.Vars}}{{.Path}}\", but got %s", p.config.ElevatedEnvVarFormat) + } +} + +func TestProvisionerPrepare_Config(t *testing.T) { + config := testConfig() + config["elevated_user"] = "{{user `user`}}" + config["elevated_password"] = "{{user `password`}}" + config[packer.UserVariablesConfigKey] = map[string]string{ + "user": "myusername", + "password": "mypassword", + } + + var p Provisioner + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.ElevatedUser != "myusername" { + t.Fatalf("Expected 'myusername' for key `elevated_user`: %s", p.config.ElevatedUser) + } + if p.config.ElevatedPassword != "mypassword" { + t.Fatalf("Expected 'mypassword' for key `elevated_password`: %s", p.config.ElevatedPassword) + } + +} + +func TestProvisionerPrepare_InvalidKey(t *testing.T) { + var p Provisioner + config := testConfig() + + // Add a random key + config["i_should_not_be_valid"] = true + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestProvisionerPrepare_Elevated(t *testing.T) { + var p Provisioner + config := testConfig() + + // Add a random key + config["elevated_user"] = "vagrant" + err := p.Prepare(config) + + if err == nil { + t.Fatal("should have error (only provided elevated_user)") + } + + config["elevated_password"] = "vagrant" + err = p.Prepare(config) + + if err != nil { + t.Fatal("should not have error") + } +} + +func TestProvisionerPrepare_Script(t *testing.T) { + config := testConfig() + delete(config, "inline") + + config["script"] = "/this/should/not/exist" + p := new(Provisioner) + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + + config["script"] = tf.Name() + p = new(Provisioner) + err = p.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestProvisionerPrepare_ScriptAndInline(t *testing.T) { + var p Provisioner + config := testConfig() + + delete(config, "inline") + delete(config, "script") + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with both + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + + config["inline"] = []interface{}{"foo"} + config["script"] = tf.Name() + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestProvisionerPrepare_ScriptAndScripts(t *testing.T) { + var p Provisioner + config := testConfig() + + // Test with both + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + + config["inline"] = []interface{}{"foo"} + config["scripts"] = []string{tf.Name()} + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestProvisionerPrepare_Scripts(t *testing.T) { + config := testConfig() + delete(config, "inline") + + config["scripts"] = []string{} + p := new(Provisioner) + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + + config["scripts"] = []string{tf.Name()} + p = new(Provisioner) + err = p.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestProvisionerPrepare_EnvironmentVars(t *testing.T) { + config := testConfig() + + // Test with a bad case + config["environment_vars"] = []string{"badvar", "good=var"} + p := new(Provisioner) + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a trickier case + config["environment_vars"] = []string{"=bad"} + p = new(Provisioner) + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good case + // Note: baz= is a real env variable, just empty + config["environment_vars"] = []string{"FOO=bar", "baz="} + p = new(Provisioner) + err = p.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestProvisionerQuote_EnvironmentVars(t *testing.T) { + config := testConfig() + + config["environment_vars"] = []string{"keyone=valueone", "keytwo=value\ntwo", "keythree='valuethree'", "keyfour='value\nfour'"} + p := new(Provisioner) + p.Prepare(config) + + expectedValue := "keyone=valueone" + if p.config.Vars[0] != expectedValue { + t.Fatalf("%s should be equal to %s", p.config.Vars[0], expectedValue) + } + + expectedValue = "keytwo=value\ntwo" + if p.config.Vars[1] != expectedValue { + t.Fatalf("%s should be equal to %s", p.config.Vars[1], expectedValue) + } + + expectedValue = "keythree='valuethree'" + if p.config.Vars[2] != expectedValue { + t.Fatalf("%s should be equal to %s", p.config.Vars[2], expectedValue) + } + + expectedValue = "keyfour='value\nfour'" + if p.config.Vars[3] != expectedValue { + t.Fatalf("%s should be equal to %s", p.config.Vars[3], expectedValue) + } +} + +func testUi() *packer.BasicUi { + return &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + ErrorWriter: new(bytes.Buffer), + } +} + +func testObjects() (packer.Ui, packer.Communicator) { + ui := testUi() + return ui, new(packer.MockCommunicator) +} + +func TestProvisionerProvision_ValidExitCodes(t *testing.T) { + config := testConfig() + delete(config, "inline") + + // Defaults provided by Packer + config["remote_path"] = "c:/Windows/Temp/inlineScript.bat" + config["inline"] = []string{"whoami"} + ui := testUi() + p := new(Provisioner) + + // Defaults provided by Packer + p.config.PackerBuildName = "vmware" + p.config.PackerBuilderType = "iso" + p.config.ValidExitCodes = []int{0, 200} + comm := new(packer.MockCommunicator) + comm.StartExitStatus = 200 + p.Prepare(config) + err := p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } +} + +func TestProvisionerProvision_InvalidExitCodes(t *testing.T) { + config := testConfig() + delete(config, "inline") + + // Defaults provided by Packer + config["remote_path"] = "c:/Windows/Temp/inlineScript.bat" + config["inline"] = []string{"whoami"} + ui := testUi() + p := new(Provisioner) + + // Defaults provided by Packer + p.config.PackerBuildName = "vmware" + p.config.PackerBuilderType = "iso" + p.config.ValidExitCodes = []int{0, 200} + comm := new(packer.MockCommunicator) + comm.StartExitStatus = 201 // Invalid! + p.Prepare(config) + err := p.Provision(ui, comm) + if err == nil { + t.Fatal("should have error") + } +} + +func TestProvisionerProvision_Inline(t *testing.T) { + config := testConfig() + delete(config, "inline") + + // Defaults provided by Packer + config["remote_path"] = "c:/Windows/Temp/inlineScript.bat" + config["inline"] = []string{"whoami"} + ui := testUi() + p := new(Provisioner) + + // Defaults provided by Packer + p.config.PackerBuildName = "vmware" + p.config.PackerBuilderType = "iso" + comm := new(packer.MockCommunicator) + p.Prepare(config) + err := p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } + + expectedCommand := `powershell "& { $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; c:/Windows/Temp/inlineScript.bat; exit $LastExitCode}"` + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be: %s, got %s", expectedCommand, comm.StartCmd.Command) + } + + envVars := make([]string, 2) + envVars[0] = "FOO=BAR" + envVars[1] = "BAR=BAZ" + config["environment_vars"] = envVars + config["remote_path"] = "c:/Windows/Temp/inlineScript.bat" + + p.Prepare(config) + err = p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } + + expectedCommand = `powershell "& { $env:BAR=\"BAZ\"; $env:FOO=\"BAR\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; c:/Windows/Temp/inlineScript.bat; exit $LastExitCode}"` + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be: %s, got: %s", expectedCommand, comm.StartCmd.Command) + } +} + +func TestProvisionerProvision_Scripts(t *testing.T) { + tempFile, _ := ioutil.TempFile("", "packer") + defer os.Remove(tempFile.Name()) + config := testConfig() + delete(config, "inline") + config["scripts"] = []string{tempFile.Name()} + config["packer_build_name"] = "foobuild" + config["packer_builder_type"] = "footype" + ui := testUi() + + p := new(Provisioner) + comm := new(packer.MockCommunicator) + p.Prepare(config) + err := p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } + + //powershell -Command "$env:PACKER_BUILDER_TYPE=''"; powershell -Command "$env:PACKER_BUILD_NAME='foobuild'"; powershell -Command c:/Windows/Temp/script.ps1 + expectedCommand := `powershell "& { $env:PACKER_BUILDER_TYPE=\"footype\"; $env:PACKER_BUILD_NAME=\"foobuild\"; c:/Windows/Temp/script.ps1; exit $LastExitCode}"` + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be %s NOT %s", expectedCommand, comm.StartCmd.Command) + } +} + +func TestProvisionerProvision_ScriptsWithEnvVars(t *testing.T) { + tempFile, _ := ioutil.TempFile("", "packer") + config := testConfig() + ui := testUi() + defer os.Remove(tempFile.Name()) + delete(config, "inline") + + config["scripts"] = []string{tempFile.Name()} + config["packer_build_name"] = "foobuild" + config["packer_builder_type"] = "footype" + + // Env vars - currently should not effect them + envVars := make([]string, 2) + envVars[0] = "FOO=BAR" + envVars[1] = "BAR=BAZ" + config["environment_vars"] = envVars + + p := new(Provisioner) + comm := new(packer.MockCommunicator) + p.Prepare(config) + err := p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } + + expectedCommand := `powershell "& { $env:BAR=\"BAZ\"; $env:FOO=\"BAR\"; $env:PACKER_BUILDER_TYPE=\"footype\"; $env:PACKER_BUILD_NAME=\"foobuild\"; c:/Windows/Temp/script.ps1; exit $LastExitCode}"` + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be %s NOT %s", expectedCommand, comm.StartCmd.Command) + } +} + +func TestProvisionerProvision_UISlurp(t *testing.T) { + // UI should be called n times + + // UI should receive following messages / output +} + +func TestProvisioner_createFlattenedElevatedEnvVars_windows(t *testing.T) { + config := testConfig() + + p := new(Provisioner) + err := p.Prepare(config) + if err != nil { + t.Fatalf("should not have error preparing config: %s", err) + } + + // Defaults provided by Packer + p.config.PackerBuildName = "vmware" + p.config.PackerBuilderType = "iso" + + // no user env var + flattenedEnvVars, err := p.createFlattenedEnvVars(true) + if err != nil { + t.Fatalf("should not have error creating flattened env vars: %s", err) + } + if flattenedEnvVars != "$env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; " { + t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) + } + + // single user env var + p.config.Vars = []string{"FOO=bar"} + + flattenedEnvVars, err = p.createFlattenedEnvVars(true) + if err != nil { + t.Fatalf("should not have error creating flattened env vars: %s", err) + } + if flattenedEnvVars != "$env:FOO=\"bar\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; " { + t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) + } + + // multiple user env vars + p.config.Vars = []string{"FOO=bar", "BAZ=qux"} + + flattenedEnvVars, err = p.createFlattenedEnvVars(true) + if err != nil { + t.Fatalf("should not have error creating flattened env vars: %s", err) + } + if flattenedEnvVars != "$env:BAZ=\"qux\"; $env:FOO=\"bar\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; " { + t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) + } +} + +func TestProvisioner_createFlattenedEnvVars_windows(t *testing.T) { + config := testConfig() + + p := new(Provisioner) + err := p.Prepare(config) + if err != nil { + t.Fatalf("should not have error preparing config: %s", err) + } + + // Defaults provided by Packer + p.config.PackerBuildName = "vmware" + p.config.PackerBuilderType = "iso" + + // no user env var + flattenedEnvVars, err := p.createFlattenedEnvVars(false) + if err != nil { + t.Fatalf("should not have error creating flattened env vars: %s", err) + } + if flattenedEnvVars != "$env:PACKER_BUILDER_TYPE=\\\"iso\\\"; $env:PACKER_BUILD_NAME=\\\"vmware\\\"; " { + t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) + } + + // single user env var + p.config.Vars = []string{"FOO=bar"} + + flattenedEnvVars, err = p.createFlattenedEnvVars(false) + if err != nil { + t.Fatalf("should not have error creating flattened env vars: %s", err) + } + if flattenedEnvVars != "$env:FOO=\\\"bar\\\"; $env:PACKER_BUILDER_TYPE=\\\"iso\\\"; $env:PACKER_BUILD_NAME=\\\"vmware\\\"; " { + t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) + } + + // multiple user env vars + p.config.Vars = []string{"FOO=bar", "BAZ=qux"} + + flattenedEnvVars, err = p.createFlattenedEnvVars(false) + if err != nil { + t.Fatalf("should not have error creating flattened env vars: %s", err) + } + if flattenedEnvVars != "$env:BAZ=\\\"qux\\\"; $env:FOO=\\\"bar\\\"; $env:PACKER_BUILDER_TYPE=\\\"iso\\\"; $env:PACKER_BUILD_NAME=\\\"vmware\\\"; " { + t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) + } +} + +func TestProvision_createCommandText(t *testing.T) { + + config := testConfig() + p := new(Provisioner) + comm := new(packer.MockCommunicator) + p.communicator = comm + _ = p.Prepare(config) + + // Non-elevated + cmd, _ := p.createCommandText() + if cmd != "powershell \"& { $env:PACKER_BUILDER_TYPE=\\\"\\\"; $env:PACKER_BUILD_NAME=\\\"\\\"; c:/Windows/Temp/script.ps1; exit $LastExitCode}\"" { + t.Fatalf("Got unexpected non-elevated command: %s", cmd) + } + + // Elevated + p.config.ElevatedUser = "vagrant" + p.config.ElevatedPassword = "vagrant" + cmd, _ = p.createCommandText() + matched, _ := regexp.MatchString("powershell -executionpolicy bypass -file \"%TEMP%(.{1})packer-elevated-shell.*", cmd) + if !matched { + t.Fatalf("Got unexpected elevated command: %s", cmd) + } +} + +func TestProvision_generateElevatedShellRunner(t *testing.T) { + + // Non-elevated + config := testConfig() + p := new(Provisioner) + p.Prepare(config) + comm := new(packer.MockCommunicator) + p.communicator = comm + path, err := p.generateElevatedRunner("whoami") + + if err != nil { + t.Fatalf("Did not expect error: %s", err.Error()) + } + + if comm.UploadCalled != true { + t.Fatalf("Should have uploaded file") + } + + matched, _ := regexp.MatchString("%TEMP%(.{1})packer-elevated-shell.*", path) + if !matched { + t.Fatalf("Got unexpected file: %s", path) + } +} + +func TestRetryable(t *testing.T) { + config := testConfig() + + count := 0 + retryMe := func() error { + t.Logf("RetryMe, attempt number %d", count) + if count == 2 { + return nil + } + count++ + return errors.New(fmt.Sprintf("Still waiting %d more times...", 2-count)) + } + retryableSleep = 50 * time.Millisecond + p := new(Provisioner) + p.config.StartRetryTimeout = 155 * time.Millisecond + err := p.Prepare(config) + err = p.retryable(retryMe) + if err != nil { + t.Fatalf("should not have error retrying funuction") + } + + count = 0 + p.config.StartRetryTimeout = 10 * time.Millisecond + err = p.Prepare(config) + err = p.retryable(retryMe) + if err == nil { + t.Fatalf("should have error retrying funuction") + } +} + +func TestCancel(t *testing.T) { + // Don't actually call Cancel() as it performs an os.Exit(0) + // which kills the 'go test' tool +} From 9364809d01bc62a5eba910f7aaa0b466ebaf2e49 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 11:08:32 -0700 Subject: [PATCH 06/31] website: document powershell --- .../provisioners/powershell.html.markdown | 82 +++++++++++++++++++ website/source/layouts/docs.erb | 1 + 2 files changed, 83 insertions(+) create mode 100644 website/source/docs/provisioners/powershell.html.markdown diff --git a/website/source/docs/provisioners/powershell.html.markdown b/website/source/docs/provisioners/powershell.html.markdown new file mode 100644 index 000000000..69cb90b9a --- /dev/null +++ b/website/source/docs/provisioners/powershell.html.markdown @@ -0,0 +1,82 @@ +--- +layout: "docs" +page_title: "PowerShell Provisioner" +description: |- + The shell Packer provisioner provisions machines built by Packer using shell scripts. Shell provisioning is the easiest way to get software installed and configured on a machine. +--- + +# PowerShell Provisioner + +Type: `powershell` + +The PowerShell Packer provisioner runs PowerShell scripts on Windows machines. +It assumes that the communicator in use is WinRM. + +## Basic Example + +The example below is fully functional. + +```javascript +{ + "type": "powershell", + "inline": ["dir c:\\"] +} +``` + +## Configuration Reference + +The reference of available configuration options is listed below. The only +required element is either "inline" or "script". Every other option is optional. + +Exactly _one_ of the following is required: + +* `inline` (array of strings) - This is an array of commands to execute. + The commands are concatenated by newlines and turned into a single file, + so they are all executed within the same context. This allows you to + change directories in one command and use something in the directory in + the next and so on. Inline scripts are the easiest way to pull off simple + tasks within the machine. + +* `script` (string) - The path to a script to upload and execute in the machine. + This path can be absolute or relative. If it is relative, it is relative + to the working directory when Packer is executed. + +* `scripts` (array of strings) - An array of scripts to execute. The scripts + will be uploaded and executed in the order specified. Each script is executed + in isolation, so state such as variables from one script won't carry on to + the next. + +Optional parameters: + +* `binary` (boolean) - If true, specifies that the script(s) are binary + files, and Packer should therefore not convert Windows line endings to + Unix line endings (if there are any). By default this is false. + +* `environment_vars` (array of strings) - An array of key/value pairs + to inject prior to the execute_command. The format should be + `key=value`. Packer injects some environmental variables by default + into the environment, as well, which are covered in the section below. + +* `execute_command` (string) - The command to use to execute the script. + By default this is `powershell "& { {{.Vars}}{{.Path}}; exit $LastExitCode}"`. + The value of this is treated as [configuration template](/docs/templates/configuration-templates.html). + There are two available variables: `Path`, which is + the path to the script to run, and `Vars`, which is the list of + `environment_vars`, if configured. + +* `elevated_user` and `elevated_password` (string) - If specified, + the PowerShell script will be run with elevated privileges using + the given Windows user. + +* `remote_path` (string) - The path where the script will be uploaded to + in the machine. This defaults to "/tmp/script.sh". This value must be + a writable location and any parent directories must already exist. + +* `start_retry_timeout` (string) - The amount of time to attempt to + _start_ the remote process. By default this is "5m" or 5 minutes. This + setting exists in order to deal with times when SSH may restart, such as + a system reboot. Set this to a higher value if reboots take a longer + amount of time. + +* `valid_exit_codes` (list of ints) - Valid exit codes for the script. + By default this is just 0. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 84cae0b05..2d83962bf 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -49,6 +49,7 @@
  • Provisioners

  • Shell Scripts
  • File Uploads
  • +
  • PowerShell
  • Ansible
  • Chef Client
  • Chef Solo
  • From 840ddb4f20d2203445c26c90e9192bd20d6fc0dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 11:14:47 -0700 Subject: [PATCH 07/31] provisioner/windows-restart --- plugin/provisioner-windows-restart/main.go | 15 + provisioner/windows-restart/provisioner.go | 194 ++++++++++ .../windows-restart/provisioner_test.go | 355 ++++++++++++++++++ 3 files changed, 564 insertions(+) create mode 100644 plugin/provisioner-windows-restart/main.go create mode 100644 provisioner/windows-restart/provisioner.go create mode 100644 provisioner/windows-restart/provisioner_test.go diff --git a/plugin/provisioner-windows-restart/main.go b/plugin/provisioner-windows-restart/main.go new file mode 100644 index 000000000..0adf82216 --- /dev/null +++ b/plugin/provisioner-windows-restart/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/provisioner/windows-restart" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterProvisioner(new(restart.Provisioner)) + server.Serve() +} diff --git a/provisioner/windows-restart/provisioner.go b/provisioner/windows-restart/provisioner.go new file mode 100644 index 000000000..234980183 --- /dev/null +++ b/provisioner/windows-restart/provisioner.go @@ -0,0 +1,194 @@ +package restart + +import ( + "fmt" + "log" + "time" + + "github.com/masterzen/winrm/winrm" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/helper/config" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template/interpolate" +) + +var DefaultRestartCommand = "shutdown /r /c \"packer restart\" /t 5 && net stop winrm" +var DefaultRestartCheckCommand = winrm.Powershell(`echo "${env:COMPUTERNAME} restarted."`) +var retryableSleep = 5 * time.Second + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + // The command used to restart the guest machine + RestartCommand string `mapstructure:"restart_command"` + + // The command used to check if the guest machine has restarted + // The output of this command will be displayed to the user + RestartCheckCommand string `mapstructure:"restart_check_command"` + + // The timeout for waiting for the machine to restart + RestartTimeout time.Duration `mapstructure:"restart_timeout"` + + ctx interpolate.Context +} + +type Provisioner struct { + config Config + comm packer.Communicator + ui packer.Ui + cancel chan struct{} +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + err := config.Decode(&p.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "execute_command", + }, + }, + }, raws...) + if err != nil { + return err + } + + if p.config.RestartCommand == "" { + p.config.RestartCommand = DefaultRestartCommand + } + + if p.config.RestartCheckCommand == "" { + p.config.RestartCheckCommand = DefaultRestartCheckCommand + } + + if p.config.RestartTimeout == 0 { + p.config.RestartTimeout = 5 * time.Minute + } + + return nil +} + +func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { + ui.Say("Restarting Machine") + p.comm = comm + p.ui = ui + p.cancel = make(chan struct{}) + + var cmd *packer.RemoteCmd + command := p.config.RestartCommand + err := p.retryable(func() error { + cmd = &packer.RemoteCmd{Command: command} + return cmd.StartWithUi(comm, ui) + }) + + if err != nil { + return err + } + + if cmd.ExitStatus != 0 { + return fmt.Errorf("Restart script exited with non-zero exit status: %d", cmd.ExitStatus) + } + + return waitForRestart(p) +} + +var waitForRestart = func(p *Provisioner) error { + ui := p.ui + ui.Say("Waiting for machine to restart...") + waitDone := make(chan bool, 1) + timeout := time.After(p.config.RestartTimeout) + var err error + + go func() { + log.Printf("Waiting for machine to become available...") + err = waitForCommunicator(p) + waitDone <- true + }() + + log.Printf("Waiting for machine to reboot with timeout: %s", p.config.RestartTimeout) + +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 err + } + + ui.Say("Machine successfully restarted, moving on") + close(p.cancel) + break WaitLoop + case <-timeout: + err := fmt.Errorf("Timeout waiting for WinRM.") + ui.Error(err.Error()) + close(p.cancel) + return err + case <-p.cancel: + close(waitDone) + return fmt.Errorf("Interrupt detected, quitting waiting for machine to restart") + break WaitLoop + } + } + + return nil + +} + +var waitForCommunicator = func(p *Provisioner) error { + cmd := &packer.RemoteCmd{Command: p.config.RestartCheckCommand} + + for { + select { + case <-p.cancel: + log.Println("Communicator wait cancelled, exiting loop") + return fmt.Errorf("Communicator wait cancelled") + case <-time.After(retryableSleep): + } + + log.Printf("Attempting to communicator to machine with: '%s'", cmd.Command) + + err := cmd.StartWithUi(p.comm, p.ui) + if err != nil { + log.Printf("Communication connection err: %s", err) + continue + } + + log.Printf("Connected to machine") + break + } + + return nil +} + +func (p *Provisioner) Cancel() { + log.Printf("Received interrupt Cancel()") + close(p.cancel) +} + +// retryable will retry the given function over and over until a +// non-error is returned. +func (p *Provisioner) retryable(f func() error) error { + startTimeout := time.After(p.config.RestartTimeout) + for { + var err error + if err = f(); err == nil { + return nil + } + + // Create an error and log it + err = fmt.Errorf("Retryable error: %s", err) + log.Printf(err.Error()) + + // Check if we timed out, otherwise we retry. It is safe to + // retry since the only error case above is if the command + // failed to START. + select { + case <-startTimeout: + return err + default: + time.Sleep(retryableSleep) + } + } +} diff --git a/provisioner/windows-restart/provisioner_test.go b/provisioner/windows-restart/provisioner_test.go new file mode 100644 index 000000000..f0f2766e3 --- /dev/null +++ b/provisioner/windows-restart/provisioner_test.go @@ -0,0 +1,355 @@ +package restart + +import ( + "bytes" + "errors" + "fmt" + "github.com/mitchellh/packer/packer" + "testing" + "time" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{} +} + +func TestProvisioner_Impl(t *testing.T) { + var raw interface{} + raw = &Provisioner{} + if _, ok := raw.(packer.Provisioner); !ok { + t.Fatalf("must be a Provisioner") + } +} + +func TestProvisionerPrepare_Defaults(t *testing.T) { + var p Provisioner + config := testConfig() + + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.RestartTimeout != 5*time.Minute { + t.Errorf("unexpected remote path: %s", p.config.RestartTimeout) + } + + if p.config.RestartCommand != "shutdown /r /c \"packer restart\" /t 5 && net stop winrm" { + t.Errorf("unexpected remote path: %s", p.config.RestartCommand) + } +} + +func TestProvisionerPrepare_ConfigRetryTimeout(t *testing.T) { + var p Provisioner + config := testConfig() + config["restart_timeout"] = "1m" + + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.RestartTimeout != 1*time.Minute { + t.Errorf("unexpected remote path: %s", p.config.RestartTimeout) + } +} + +func TestProvisionerPrepare_ConfigErrors(t *testing.T) { + var p Provisioner + config := testConfig() + config["restart_timeout"] = "m" + + err := p.Prepare(config) + if err == nil { + t.Fatal("Expected error parsing restart_timeout but did not receive one.") + } +} + +func TestProvisionerPrepare_InvalidKey(t *testing.T) { + var p Provisioner + config := testConfig() + + // Add a random key + config["i_should_not_be_valid"] = true + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func testUi() *packer.BasicUi { + return &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + ErrorWriter: new(bytes.Buffer), + } +} + +func TestProvisionerProvision_Success(t *testing.T) { + config := testConfig() + + // Defaults provided by Packer + ui := testUi() + p := new(Provisioner) + + // Defaults provided by Packer + comm := new(packer.MockCommunicator) + p.Prepare(config) + waitForCommunicatorOld := waitForCommunicator + waitForCommunicator = func(p *Provisioner) error { + return nil + } + err := p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } + + expectedCommand := DefaultRestartCommand + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be: %s, got %s", expectedCommand, comm.StartCmd.Command) + } + // Set this back! + waitForCommunicator = waitForCommunicatorOld +} + +func TestProvisionerProvision_CustomCommand(t *testing.T) { + config := testConfig() + + // Defaults provided by Packer + ui := testUi() + p := new(Provisioner) + expectedCommand := "specialrestart.exe -NOW" + config["restart_command"] = expectedCommand + + // Defaults provided by Packer + comm := new(packer.MockCommunicator) + p.Prepare(config) + waitForCommunicatorOld := waitForCommunicator + waitForCommunicator = func(p *Provisioner) error { + return nil + } + err := p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be: %s, got %s", expectedCommand, comm.StartCmd.Command) + } + // Set this back! + waitForCommunicator = waitForCommunicatorOld +} + +func TestProvisionerProvision_RestartCommandFail(t *testing.T) { + config := testConfig() + ui := testUi() + p := new(Provisioner) + comm := new(packer.MockCommunicator) + comm.StartStderr = "WinRM terminated" + comm.StartExitStatus = 1 + + p.Prepare(config) + err := p.Provision(ui, comm) + if err == nil { + t.Fatal("should have error") + } +} +func TestProvisionerProvision_WaitForRestartFail(t *testing.T) { + config := testConfig() + + // Defaults provided by Packer + ui := testUi() + p := new(Provisioner) + + // Defaults provided by Packer + comm := new(packer.MockCommunicator) + p.Prepare(config) + waitForCommunicatorOld := waitForCommunicator + waitForCommunicator = func(p *Provisioner) error { + return fmt.Errorf("Machine did not restart properly") + } + err := p.Provision(ui, comm) + if err == nil { + t.Fatal("should have error") + } + + // Set this back! + waitForCommunicator = waitForCommunicatorOld +} + +func TestProvision_waitForRestartTimeout(t *testing.T) { + retryableSleep = 10 * time.Millisecond + config := testConfig() + config["restart_timeout"] = "1ms" + ui := testUi() + p := new(Provisioner) + comm := new(packer.MockCommunicator) + var err error + + p.Prepare(config) + waitForCommunicatorOld := waitForCommunicator + waitDone := make(chan bool) + + // Block until cancel comes through + waitForCommunicator = func(p *Provisioner) error { + for { + select { + case <-waitDone: + } + } + } + + go func() { + err = p.Provision(ui, comm) + waitDone <- true + }() + <-waitDone + + if err == nil { + t.Fatal("should not have error") + } + + // Set this back! + waitForCommunicator = waitForCommunicatorOld + +} + +func TestProvision_waitForCommunicator(t *testing.T) { + config := testConfig() + + // Defaults provided by Packer + ui := testUi() + p := new(Provisioner) + + // Defaults provided by Packer + comm := new(packer.MockCommunicator) + p.comm = comm + p.ui = ui + comm.StartStderr = "WinRM terminated" + comm.StartExitStatus = 1 + p.Prepare(config) + err := waitForCommunicator(p) + + if err != nil { + t.Fatal("should not have error, got: %s", err.Error()) + } + + expectedCommand := DefaultRestartCheckCommand + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be: %s, got %s", expectedCommand, comm.StartCmd.Command) + } +} + +func TestProvision_waitForCommunicatorWithCancel(t *testing.T) { + config := testConfig() + + // Defaults provided by Packer + ui := testUi() + p := new(Provisioner) + + // Defaults provided by Packer + comm := new(packer.MockCommunicator) + p.comm = comm + p.ui = ui + retryableSleep = 10 * time.Millisecond + p.cancel = make(chan struct{}) + var err error + + comm.StartStderr = "WinRM terminated" + comm.StartExitStatus = 1 // Always fail + p.Prepare(config) + + // Run 2 goroutines; + // 1st to call waitForCommunicator (that will always fail) + // 2nd to cancel the operation + waitDone := make(chan bool) + go func() { + err = waitForCommunicator(p) + }() + + go func() { + p.Cancel() + waitDone <- true + }() + <-waitDone + + // Expect a Cancel error + if err == nil { + t.Fatalf("Should have err") + } +} + +func TestRetryable(t *testing.T) { + config := testConfig() + + count := 0 + retryMe := func() error { + t.Logf("RetryMe, attempt number %d", count) + if count == 2 { + return nil + } + count++ + return errors.New(fmt.Sprintf("Still waiting %d more times...", 2-count)) + } + retryableSleep = 50 * time.Millisecond + p := new(Provisioner) + p.config.RestartTimeout = 155 * time.Millisecond + err := p.Prepare(config) + err = p.retryable(retryMe) + if err != nil { + t.Fatalf("should not have error retrying funuction") + } + + count = 0 + p.config.RestartTimeout = 10 * time.Millisecond + err = p.Prepare(config) + err = p.retryable(retryMe) + if err == nil { + t.Fatalf("should have error retrying funuction") + } +} + +func TestProvision_Cancel(t *testing.T) { + config := testConfig() + + // Defaults provided by Packer + ui := testUi() + p := new(Provisioner) + + var err error + + comm := new(packer.MockCommunicator) + p.Prepare(config) + waitDone := make(chan bool) + + // Block until cancel comes through + waitForCommunicator = func(p *Provisioner) error { + for { + select { + case <-waitDone: + } + } + } + + // Create two go routines to provision and cancel in parallel + // Provision will block until cancel happens + go func() { + err = p.Provision(ui, comm) + waitDone <- true + }() + + go func() { + p.Cancel() + }() + <-waitDone + + // Expect interupt error + if err == nil { + t.Fatal("should have error") + } +} From 506a657775088e57e69769c9e6c66b493937e249 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 11:17:50 -0700 Subject: [PATCH 08/31] website: doc windows-restart --- .../docs/provisioners/windows-restart.html.md | 43 +++++++++++++++++++ website/source/layouts/docs.erb | 1 + 2 files changed, 44 insertions(+) create mode 100644 website/source/docs/provisioners/windows-restart.html.md diff --git a/website/source/docs/provisioners/windows-restart.html.md b/website/source/docs/provisioners/windows-restart.html.md new file mode 100644 index 000000000..a1b65cae1 --- /dev/null +++ b/website/source/docs/provisioners/windows-restart.html.md @@ -0,0 +1,43 @@ +--- +layout: "docs" +page_title: "Windows Restart Provisioner" +description: |- + The Windows restart provisioner restarts a Windows machine and waits for it to come back up. +--- + +# Windows Restart Provisioner + +Type: `windows-restart` + +The Windows restart provisioner initiates a reboot on a Windows machine +and waits for the machine to come back online. + +The Windows provisioning process often requires multiple reboots, and this +provisioner helps to ease that process. + +## Basic Example + +The example below is fully functional. + +```javascript +{ + "type": "windows-restart" +} +``` + +## Configuration Reference + +The reference of available configuration options is listed below. + +Optional parameters: + +* `restart_command` (string) - The command to execute to initiate the + restart. By default this is `shutdown /r /c "packer restart" /t 5 && net stop winrm`. + A key action of this is to stop WinRM so that Packer can detect it + is rebooting. + +* `restart_check_command` (string) - A command to execute to check if the + restart succeeded. This will be done in a loop. + +* `restart_timeout` (string) - The timeout to wait for the restart. + By default this is 5 minutes. Example value: "5m" diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 2d83962bf..8b524b087 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -56,6 +56,7 @@
  • Puppet Masterless
  • Puppet Server
  • Salt
  • +
  • Windows Restart
  • Custom
  • From b25b7d1fb22bf187c3740dfab04f1d11ad6ccece Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 11:19:26 -0700 Subject: [PATCH 09/31] communicator/winrm: log exit code of processes --- communicator/winrm/communicator.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/communicator/winrm/communicator.go b/communicator/winrm/communicator.go index 82686e2a7..804580843 100644 --- a/communicator/winrm/communicator.go +++ b/communicator/winrm/communicator.go @@ -89,7 +89,10 @@ func runCommand(shell *winrm.Shell, cmd *winrm.Command, rc *packer.RemoteCmd) { go io.Copy(rc.Stderr, cmd.Stderr) cmd.Wait() - rc.SetExited(cmd.ExitCode()) + + code := cmd.ExitCode() + log.Printf("[INFO] command '%s' exited with code: %d", rc.Command, code) + rc.SetExited(code) } // Upload implementation of communicator.Communicator interface From 339a4ccdab01525b1ca36bd8529d9feac71b2103 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 11:23:29 -0700 Subject: [PATCH 10/31] provisioner/windows-shell --- plugin/provisioner-windows-shell/main.go | 15 + provisioner/windows-shell/provisioner.go | 324 +++++++++++++ provisioner/windows-shell/provisioner_test.go | 441 ++++++++++++++++++ 3 files changed, 780 insertions(+) create mode 100644 plugin/provisioner-windows-shell/main.go create mode 100644 provisioner/windows-shell/provisioner.go create mode 100644 provisioner/windows-shell/provisioner_test.go diff --git a/plugin/provisioner-windows-shell/main.go b/plugin/provisioner-windows-shell/main.go new file mode 100644 index 000000000..342a8ed9b --- /dev/null +++ b/plugin/provisioner-windows-shell/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/provisioner/windows-shell" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterProvisioner(new(shell.Provisioner)) + server.Serve() +} diff --git a/provisioner/windows-shell/provisioner.go b/provisioner/windows-shell/provisioner.go new file mode 100644 index 000000000..50c0aaeb1 --- /dev/null +++ b/provisioner/windows-shell/provisioner.go @@ -0,0 +1,324 @@ +// This package implements a provisioner for Packer that executes +// shell scripts within the remote machine. +package shell + +import ( + "bufio" + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "sort" + "strings" + "time" + + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/helper/config" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template/interpolate" +) + +const DefaultRemotePath = "c:/Windows/Temp/script.bat" + +var retryableSleep = 2 * time.Second + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + // If true, the script contains binary and line endings will not be + // converted from Windows to Unix-style. + Binary bool + + // An inline script to execute. Multiple strings are all executed + // in the context of a single shell. + Inline []string + + // The local path of the shell script to upload and execute. + Script string + + // An array of multiple scripts to run. + Scripts []string + + // An array of environment variables that will be injected before + // your command(s) are executed. + Vars []string `mapstructure:"environment_vars"` + + // The remote path where the local shell script will be uploaded to. + // This should be set to a writable file that is in a pre-existing directory. + RemotePath string `mapstructure:"remote_path"` + + // The command used to execute the script. The '{{ .Path }}' variable + // should be used to specify where the script goes, {{ .Vars }} + // can be used to inject the environment_vars into the environment. + ExecuteCommand string `mapstructure:"execute_command"` + + // The timeout for retrying to start the process. Until this timeout + // is reached, if the provisioner can't start a process, it retries. + // This can be set high to allow for reboots. + StartRetryTimeout time.Duration `mapstructure:"start_retry_timeout"` + + // This is used in the template generation to format environment variables + // inside the `ExecuteCommand` template. + EnvVarFormat string + + ctx interpolate.Context +} + +type Provisioner struct { + config Config +} + +type ExecuteCommandTemplate struct { + Vars string + Path string +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + err := config.Decode(&p.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "execute_command", + }, + }, + }, raws...) + if err != nil { + return err + } + + if p.config.EnvVarFormat == "" { + p.config.EnvVarFormat = `set "%s=%s" && ` + } + + if p.config.ExecuteCommand == "" { + p.config.ExecuteCommand = `{{.Vars}}"{{.Path}}"` + } + + if p.config.Inline != nil && len(p.config.Inline) == 0 { + p.config.Inline = nil + } + + if p.config.StartRetryTimeout == 0 { + p.config.StartRetryTimeout = 5 * time.Minute + } + + if p.config.RemotePath == "" { + p.config.RemotePath = DefaultRemotePath + } + + if p.config.Scripts == nil { + p.config.Scripts = make([]string, 0) + } + + if p.config.Vars == nil { + p.config.Vars = make([]string, 0) + } + + var errs error + if p.config.Script != "" && len(p.config.Scripts) > 0 { + errs = packer.MultiErrorAppend(errs, + errors.New("Only one of script or scripts can be specified.")) + } + + if p.config.Script != "" { + p.config.Scripts = []string{p.config.Script} + } + + if len(p.config.Scripts) == 0 && p.config.Inline == nil { + errs = packer.MultiErrorAppend(errs, + errors.New("Either a script file or inline script must be specified.")) + } else if len(p.config.Scripts) > 0 && p.config.Inline != nil { + errs = packer.MultiErrorAppend(errs, + errors.New("Only a script file or an inline script can be specified, not both.")) + } + + for _, path := range p.config.Scripts { + if _, err := os.Stat(path); err != nil { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Bad script '%s': %s", path, err)) + } + } + + // Do a check for bad environment variables, such as '=foo', 'foobar' + for _, kv := range p.config.Vars { + vs := strings.SplitN(kv, "=", 2) + if len(vs) != 2 || vs[0] == "" { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("Environment variable not in format 'key=value': %s", kv)) + } + } + + if errs != nil { + return errs + } + + return nil +} + +// This function takes the inline scripts, concatenates them +// into a temporary file and returns a string containing the location +// of said file. +func extractScript(p *Provisioner) (string, error) { + temp, err := ioutil.TempFile(os.TempDir(), "packer-windows-shell-provisioner") + if err != nil { + log.Printf("Unable to create temporary file for inline scripts: %s", err) + return "", err + } + writer := bufio.NewWriter(temp) + for _, command := range p.config.Inline { + log.Printf("Found command: %s", command) + if _, err := writer.WriteString(command + "\n"); err != nil { + return "", fmt.Errorf("Error preparing shell script: %s", err) + } + } + + if err := writer.Flush(); err != nil { + return "", fmt.Errorf("Error preparing shell script: %s", err) + } + + temp.Close() + + return temp.Name(), nil +} + +func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { + ui.Say(fmt.Sprintf("Provisioning with windows-shell...")) + scripts := make([]string, len(p.config.Scripts)) + copy(scripts, p.config.Scripts) + + // Build our variables up by adding in the build name and builder type + envVars := make([]string, len(p.config.Vars)+2) + envVars[0] = "PACKER_BUILD_NAME=" + p.config.PackerBuildName + envVars[1] = "PACKER_BUILDER_TYPE=" + p.config.PackerBuilderType + + copy(envVars, p.config.Vars) + + if p.config.Inline != nil { + temp, err := extractScript(p) + if err != nil { + ui.Error(fmt.Sprintf("Unable to extract inline scripts into a file: %s", err)) + } + scripts = append(scripts, temp) + } + + for _, path := range scripts { + ui.Say(fmt.Sprintf("Provisioning with shell script: %s", path)) + + log.Printf("Opening %s for reading", path) + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("Error opening shell script: %s", err) + } + defer f.Close() + + // Create environment variables to set before executing the command + flattendVars, err := p.createFlattenedEnvVars() + if err != nil { + return err + } + + // Compile the command + p.config.ctx.Data = &ExecuteCommandTemplate{ + Vars: flattendVars, + Path: p.config.RemotePath, + } + command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) + if err != nil { + return fmt.Errorf("Error processing command: %s", err) + } + + // Upload the file and run the command. Do this in the context of + // a single retryable function so that we don't end up with + // the case that the upload succeeded, a restart is initiated, + // and then the command is executed but the file doesn't exist + // any longer. + var cmd *packer.RemoteCmd + err = p.retryable(func() error { + if _, err := f.Seek(0, 0); err != nil { + return err + } + + if err := comm.Upload(p.config.RemotePath, f, nil); err != nil { + return fmt.Errorf("Error uploading script: %s", err) + } + + cmd = &packer.RemoteCmd{Command: command} + return cmd.StartWithUi(comm, ui) + }) + if err != nil { + return err + } + + // Close the original file since we copied it + f.Close() + + if cmd.ExitStatus != 0 { + return fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) + } + } + + return nil +} + +func (p *Provisioner) Cancel() { + // Just hard quit. It isn't a big deal if what we're doing keeps + // running on the other side. + os.Exit(0) +} + +// retryable will retry the given function over and over until a +// non-error is returned. +func (p *Provisioner) retryable(f func() error) error { + startTimeout := time.After(p.config.StartRetryTimeout) + for { + var err error + if err = f(); err == nil { + return nil + } + + // Create an error and log it + err = fmt.Errorf("Retryable error: %s", err) + log.Printf(err.Error()) + + // Check if we timed out, otherwise we retry. It is safe to + // retry since the only error case above is if the command + // failed to START. + select { + case <-startTimeout: + return err + default: + time.Sleep(retryableSleep) + } + } +} + +func (p *Provisioner) createFlattenedEnvVars() (flattened string, err error) { + flattened = "" + envVars := make(map[string]string) + + // Always available Packer provided env vars + envVars["PACKER_BUILD_NAME"] = p.config.PackerBuildName + envVars["PACKER_BUILDER_TYPE"] = p.config.PackerBuilderType + + // Split vars into key/value components + for _, envVar := range p.config.Vars { + keyValue := strings.Split(envVar, "=") + if len(keyValue) != 2 { + err = errors.New("Shell provisioner environment variables must be in key=value format") + return + } + envVars[keyValue[0]] = keyValue[1] + } + // Create a list of env var keys in sorted order + var keys []string + for k := range envVars { + keys = append(keys, k) + } + sort.Strings(keys) + // Re-assemble vars using OS specific format pattern and flatten + for _, key := range keys { + flattened += fmt.Sprintf(p.config.EnvVarFormat, key, envVars[key]) + } + return +} diff --git a/provisioner/windows-shell/provisioner_test.go b/provisioner/windows-shell/provisioner_test.go new file mode 100644 index 000000000..5c4dddd90 --- /dev/null +++ b/provisioner/windows-shell/provisioner_test.go @@ -0,0 +1,441 @@ +package shell + +import ( + "bytes" + "errors" + "fmt" + "github.com/mitchellh/packer/packer" + "io/ioutil" + "log" + "os" + "strings" + "testing" + "time" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "inline": []interface{}{"foo", "bar"}, + } +} + +func TestProvisionerPrepare_extractScript(t *testing.T) { + config := testConfig() + p := new(Provisioner) + _ = p.Prepare(config) + file, err := extractScript(p) + if err != nil { + t.Fatalf("Should not be error: %s", err) + } + log.Printf("File: %s", file) + if strings.Index(file, os.TempDir()) != 0 { + t.Fatalf("Temp file should reside in %s. File location: %s", os.TempDir(), file) + } + + // File contents should contain 2 lines concatenated by newlines: foo\nbar + readFile, err := ioutil.ReadFile(file) + expectedContents := "foo\nbar\n" + s := string(readFile[:]) + if s != expectedContents { + t.Fatalf("Expected generated inlineScript to equal '%s', got '%s'", expectedContents, s) + } +} + +func TestProvisioner_Impl(t *testing.T) { + var raw interface{} + raw = &Provisioner{} + if _, ok := raw.(packer.Provisioner); !ok { + t.Fatalf("must be a Provisioner") + } +} + +func TestProvisionerPrepare_Defaults(t *testing.T) { + var p Provisioner + config := testConfig() + + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.RemotePath != DefaultRemotePath { + t.Errorf("unexpected remote path: %s", p.config.RemotePath) + } + + if p.config.ExecuteCommand != "{{.Vars}}\"{{.Path}}\"" { + t.Fatalf("Default command should be powershell {{.Vars}}\"{{.Path}}\", but got %s", p.config.ExecuteCommand) + } +} + +func TestProvisionerPrepare_Config(t *testing.T) { + +} + +func TestProvisionerPrepare_InvalidKey(t *testing.T) { + var p Provisioner + config := testConfig() + + // Add a random key + config["i_should_not_be_valid"] = true + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestProvisionerPrepare_Script(t *testing.T) { + config := testConfig() + delete(config, "inline") + + config["script"] = "/this/should/not/exist" + p := new(Provisioner) + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + + config["script"] = tf.Name() + p = new(Provisioner) + err = p.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestProvisionerPrepare_ScriptAndInline(t *testing.T) { + var p Provisioner + config := testConfig() + + delete(config, "inline") + delete(config, "script") + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with both + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + + config["inline"] = []interface{}{"foo"} + config["script"] = tf.Name() + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestProvisionerPrepare_ScriptAndScripts(t *testing.T) { + var p Provisioner + config := testConfig() + + // Test with both + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + + config["inline"] = []interface{}{"foo"} + config["scripts"] = []string{tf.Name()} + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestProvisionerPrepare_Scripts(t *testing.T) { + config := testConfig() + delete(config, "inline") + + config["scripts"] = []string{} + p := new(Provisioner) + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good one + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("error tempfile: %s", err) + } + defer os.Remove(tf.Name()) + + config["scripts"] = []string{tf.Name()} + p = new(Provisioner) + err = p.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestProvisionerPrepare_EnvironmentVars(t *testing.T) { + config := testConfig() + + // Test with a bad case + config["environment_vars"] = []string{"badvar", "good=var"} + p := new(Provisioner) + err := p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a trickier case + config["environment_vars"] = []string{"=bad"} + p = new(Provisioner) + err = p.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test with a good case + // Note: baz= is a real env variable, just empty + config["environment_vars"] = []string{"FOO=bar", "baz="} + p = new(Provisioner) + err = p.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } +} + +func TestProvisionerQuote_EnvironmentVars(t *testing.T) { + config := testConfig() + + config["environment_vars"] = []string{"keyone=valueone", "keytwo=value\ntwo", "keythree='valuethree'", "keyfour='value\nfour'"} + p := new(Provisioner) + p.Prepare(config) + + expectedValue := "keyone=valueone" + if p.config.Vars[0] != expectedValue { + t.Fatalf("%s should be equal to %s", p.config.Vars[0], expectedValue) + } + + expectedValue = "keytwo=value\ntwo" + if p.config.Vars[1] != expectedValue { + t.Fatalf("%s should be equal to %s", p.config.Vars[1], expectedValue) + } + + expectedValue = "keythree='valuethree'" + if p.config.Vars[2] != expectedValue { + t.Fatalf("%s should be equal to %s", p.config.Vars[2], expectedValue) + } + + expectedValue = "keyfour='value\nfour'" + if p.config.Vars[3] != expectedValue { + t.Fatalf("%s should be equal to %s", p.config.Vars[3], expectedValue) + } +} + +func testUi() *packer.BasicUi { + return &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + ErrorWriter: new(bytes.Buffer), + } +} + +func testObjects() (packer.Ui, packer.Communicator) { + ui := testUi() + return ui, new(packer.MockCommunicator) +} + +func TestProvisionerProvision_Inline(t *testing.T) { + config := testConfig() + delete(config, "inline") + + // Defaults provided by Packer + config["remote_path"] = "c:/Windows/Temp/inlineScript.bat" + config["inline"] = []string{"whoami"} + ui := testUi() + p := new(Provisioner) + + // Defaults provided by Packer + p.config.PackerBuildName = "vmware" + p.config.PackerBuilderType = "iso" + comm := new(packer.MockCommunicator) + p.Prepare(config) + err := p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } + + expectedCommand := `set "PACKER_BUILDER_TYPE=iso" && set "PACKER_BUILD_NAME=vmware" && "c:/Windows/Temp/inlineScript.bat"` + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be: %s, got %s", expectedCommand, comm.StartCmd.Command) + } + + envVars := make([]string, 2) + envVars[0] = "FOO=BAR" + envVars[1] = "BAR=BAZ" + config["environment_vars"] = envVars + config["remote_path"] = "c:/Windows/Temp/inlineScript.bat" + + p.Prepare(config) + err = p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } + + expectedCommand = `set "BAR=BAZ" && set "FOO=BAR" && set "PACKER_BUILDER_TYPE=iso" && set "PACKER_BUILD_NAME=vmware" && "c:/Windows/Temp/inlineScript.bat"` + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be: %s, got: %s", expectedCommand, comm.StartCmd.Command) + } +} + +func TestProvisionerProvision_Scripts(t *testing.T) { + tempFile, _ := ioutil.TempFile("", "packer") + defer os.Remove(tempFile.Name()) + config := testConfig() + delete(config, "inline") + config["scripts"] = []string{tempFile.Name()} + config["packer_build_name"] = "foobuild" + config["packer_builder_type"] = "footype" + ui := testUi() + + p := new(Provisioner) + comm := new(packer.MockCommunicator) + p.Prepare(config) + err := p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } + + //powershell -Command "$env:PACKER_BUILDER_TYPE=''"; powershell -Command "$env:PACKER_BUILD_NAME='foobuild'"; powershell -Command c:/Windows/Temp/script.ps1 + expectedCommand := `set "PACKER_BUILDER_TYPE=footype" && set "PACKER_BUILD_NAME=foobuild" && "c:/Windows/Temp/script.bat"` + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be %s NOT %s", expectedCommand, comm.StartCmd.Command) + } +} + +func TestProvisionerProvision_ScriptsWithEnvVars(t *testing.T) { + tempFile, _ := ioutil.TempFile("", "packer") + config := testConfig() + ui := testUi() + defer os.Remove(tempFile.Name()) + delete(config, "inline") + + config["scripts"] = []string{tempFile.Name()} + config["packer_build_name"] = "foobuild" + config["packer_builder_type"] = "footype" + + // Env vars - currently should not effect them + envVars := make([]string, 2) + envVars[0] = "FOO=BAR" + envVars[1] = "BAR=BAZ" + config["environment_vars"] = envVars + + p := new(Provisioner) + comm := new(packer.MockCommunicator) + p.Prepare(config) + err := p.Provision(ui, comm) + if err != nil { + t.Fatal("should not have error") + } + + expectedCommand := `set "BAR=BAZ" && set "FOO=BAR" && set "PACKER_BUILDER_TYPE=footype" && set "PACKER_BUILD_NAME=foobuild" && "c:/Windows/Temp/script.bat"` + + // Should run the command without alteration + if comm.StartCmd.Command != expectedCommand { + t.Fatalf("Expect command to be %s NOT %s", expectedCommand, comm.StartCmd.Command) + } +} + +func TestProvisioner_createFlattenedEnvVars_windows(t *testing.T) { + config := testConfig() + + p := new(Provisioner) + err := p.Prepare(config) + if err != nil { + t.Fatalf("should not have error preparing config: %s", err) + } + + // Defaults provided by Packer + p.config.PackerBuildName = "vmware" + p.config.PackerBuilderType = "iso" + + // no user env var + flattenedEnvVars, err := p.createFlattenedEnvVars() + if err != nil { + t.Fatalf("should not have error creating flattened env vars: %s", err) + } + expectedEnvVars := `set "PACKER_BUILDER_TYPE=iso" && set "PACKER_BUILD_NAME=vmware" && ` + if flattenedEnvVars != expectedEnvVars { + t.Fatalf("expected flattened env vars to be: %s, got: %s", expectedEnvVars, flattenedEnvVars) + } + + // single user env var + p.config.Vars = []string{"FOO=bar"} + + flattenedEnvVars, err = p.createFlattenedEnvVars() + if err != nil { + t.Fatalf("should not have error creating flattened env vars: %s", err) + } + expectedEnvVars = `set "FOO=bar" && set "PACKER_BUILDER_TYPE=iso" && set "PACKER_BUILD_NAME=vmware" && ` + if flattenedEnvVars != expectedEnvVars { + t.Fatalf("expected flattened env vars to be: %s, got: %s", expectedEnvVars, flattenedEnvVars) + } + + // multiple user env vars + p.config.Vars = []string{"FOO=bar", "BAZ=qux"} + + flattenedEnvVars, err = p.createFlattenedEnvVars() + if err != nil { + t.Fatalf("should not have error creating flattened env vars: %s", err) + } + expectedEnvVars = `set "BAZ=qux" && set "FOO=bar" && set "PACKER_BUILDER_TYPE=iso" && set "PACKER_BUILD_NAME=vmware" && ` + if flattenedEnvVars != expectedEnvVars { + t.Fatalf("expected flattened env vars to be: %s, got: %s", expectedEnvVars, flattenedEnvVars) + } +} + +func TestRetryable(t *testing.T) { + config := testConfig() + + count := 0 + retryMe := func() error { + log.Printf("RetryMe, attempt number %d", count) + if count == 2 { + return nil + } + count++ + return errors.New(fmt.Sprintf("Still waiting %d more times...", 2-count)) + } + retryableSleep = 50 * time.Millisecond + p := new(Provisioner) + p.config.StartRetryTimeout = 155 * time.Millisecond + err := p.Prepare(config) + err = p.retryable(retryMe) + if err != nil { + t.Fatalf("should not have error retrying funuction") + } + + count = 0 + p.config.StartRetryTimeout = 10 * time.Millisecond + err = p.Prepare(config) + err = p.retryable(retryMe) + if err == nil { + t.Fatalf("should have error retrying funuction") + } +} + +func TestCancel(t *testing.T) { + // Don't actually call Cancel() as it performs an os.Exit(0) + // which kills the 'go test' tool +} From e1530c39dc120211612e19c50e541bfaea117350 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 11:27:48 -0700 Subject: [PATCH 11/31] website: windows-shell --- .../docs/provisioners/shell.html.markdown | 4 + .../docs/provisioners/windows-shell.html.md | 75 +++++++++++++++++++ website/source/layouts/docs.erb | 1 + 3 files changed, 80 insertions(+) create mode 100644 website/source/docs/provisioners/windows-shell.html.md diff --git a/website/source/docs/provisioners/shell.html.markdown b/website/source/docs/provisioners/shell.html.markdown index 89a442a83..75bf5e7d1 100644 --- a/website/source/docs/provisioners/shell.html.markdown +++ b/website/source/docs/provisioners/shell.html.markdown @@ -13,6 +13,10 @@ The shell Packer provisioner provisions machines built by Packer using shell scr Shell provisioning is the easiest way to get software installed and configured on a machine. +-> **Building Windows images?** You probably want to use the +[PowerShell](/docs/provisioners/powershell.html) or +[Windows Shell](/docs/provisioners/windows-shell.html) provisioners. + ## Basic Example The example below is fully functional. diff --git a/website/source/docs/provisioners/windows-shell.html.md b/website/source/docs/provisioners/windows-shell.html.md new file mode 100644 index 000000000..c758a5ebd --- /dev/null +++ b/website/source/docs/provisioners/windows-shell.html.md @@ -0,0 +1,75 @@ +--- +layout: "docs" +page_title: "Windows Shell Provisioner" +description: |- + The windows-shell Packer provisioner runs commands on Windows using the cmd shell. +--- + +# Windows Shell Provisioner + +Type: `windows-shell` + +The windows-shell Packer provisioner runs commands on a Windows machine +using `cmd`. It assumes it is running over WinRM. + +## Basic Example + +The example below is fully functional. + +```javascript +{ + "type": "windows-shell", + "inline": ["dir c:\\"] +} +``` + +## Configuration Reference + +The reference of available configuration options is listed below. The only +required element is either "inline" or "script". Every other option is optional. + +Exactly _one_ of the following is required: + +* `inline` (array of strings) - This is an array of commands to execute. + The commands are concatenated by newlines and turned into a single file, + so they are all executed within the same context. This allows you to + change directories in one command and use something in the directory in + the next and so on. Inline scripts are the easiest way to pull off simple + tasks within the machine. + +* `script` (string) - The path to a script to upload and execute in the machine. + This path can be absolute or relative. If it is relative, it is relative + to the working directory when Packer is executed. + +* `scripts` (array of strings) - An array of scripts to execute. The scripts + will be uploaded and executed in the order specified. Each script is executed + in isolation, so state such as variables from one script won't carry on to + the next. + +Optional parameters: + +* `binary` (boolean) - If true, specifies that the script(s) are binary + files, and Packer should therefore not convert Windows line endings to + Unix line endings (if there are any). By default this is false. + +* `environment_vars` (array of strings) - An array of key/value pairs + to inject prior to the execute_command. The format should be + `key=value`. Packer injects some environmental variables by default + into the environment, as well, which are covered in the section below. + +* `execute_command` (string) - The command to use to execute the script. + By default this is `{{ .Vars }}"{{ .Path }}"`. The value of this is + treated as [configuration template](/docs/templates/configuration-templates.html). + There are two available variables: `Path`, which is + the path to the script to run, and `Vars`, which is the list of + `environment_vars`, if configured. + +* `remote_path` (string) - The path where the script will be uploaded to + in the machine. This defaults to "/tmp/script.sh". This value must be + a writable location and any parent directories must already exist. + +* `start_retry_timeout` (string) - The amount of time to attempt to + _start_ the remote process. By default this is "5m" or 5 minutes. This + setting exists in order to deal with times when SSH may restart, such as + a system reboot. Set this to a higher value if reboots take a longer + amount of time. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 8b524b087..8099b461a 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -50,6 +50,7 @@
  • Shell Scripts
  • File Uploads
  • PowerShell
  • +
  • Windows Shell
  • Ansible
  • Chef Client
  • Chef Solo
  • From ab6a330d86cbe2f0785c8d7eaf7b8529141bde63 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 12:39:39 -0700 Subject: [PATCH 12/31] provisioner/*: fix go vet --- provisioner/powershell/provisioner.go | 4 +++- provisioner/windows-restart/provisioner_test.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index 0c2454d0d..b31781d88 100644 --- a/provisioner/powershell/provisioner.go +++ b/provisioner/powershell/provisioner.go @@ -294,7 +294,9 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { } } if !validExitCode { - return fmt.Errorf("Script exited with non-zero exit status: %d. Allowed exit codes are: %s", cmd.ExitStatus, p.config.ValidExitCodes) + return fmt.Errorf( + "Script exited with non-zero exit status: %d. Allowed exit codes are: %v", + cmd.ExitStatus, p.config.ValidExitCodes) } } diff --git a/provisioner/windows-restart/provisioner_test.go b/provisioner/windows-restart/provisioner_test.go index f0f2766e3..d2a54d274 100644 --- a/provisioner/windows-restart/provisioner_test.go +++ b/provisioner/windows-restart/provisioner_test.go @@ -234,7 +234,7 @@ func TestProvision_waitForCommunicator(t *testing.T) { err := waitForCommunicator(p) if err != nil { - t.Fatal("should not have error, got: %s", err.Error()) + t.Fatalf("should not have error, got: %s", err.Error()) } expectedCommand := DefaultRestartCheckCommand From 742e5568363eecef3c00f602611ce6ad2e9c10a6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Jun 2015 14:44:54 -0700 Subject: [PATCH 13/31] provisioner/puppet-masterless: only base if manifest is a file [GH-1933] --- provisioner/puppet-masterless/provisioner.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/provisioner/puppet-masterless/provisioner.go b/provisioner/puppet-masterless/provisioner.go index eb364da58..a92a22094 100644 --- a/provisioner/puppet-masterless/provisioner.go +++ b/provisioner/puppet-masterless/provisioner.go @@ -259,7 +259,13 @@ func (p *Provisioner) uploadManifests(ui packer.Ui, comm packer.Communicator) (s } defer f.Close() - manifestFilename := filepath.Base(p.config.ManifestFile) + manifestFilename := p.config.ManifestFile + if fi, err := os.Stat(p.config.ManifestFile); err != nil { + return "", fmt.Errorf("Error inspecting manifest file: %s", err) + } else if !fi.IsDir() { + manifestFilename = filepath.Base(manifestFilename) + } + remoteManifestFile := fmt.Sprintf("%s/%s", remoteManifestsPath, manifestFilename) if err := comm.Upload(remoteManifestFile, f, nil); err != nil { return "", err From 8990c38d5e53f4148fbee669a5ca1703d701abe2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Jun 2015 09:38:24 -0700 Subject: [PATCH 14/31] provisioner/puppet-masterless: deprecation warning --- provisioner/puppet-masterless/provisioner.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/provisioner/puppet-masterless/provisioner.go b/provisioner/puppet-masterless/provisioner.go index a92a22094..643a555d0 100644 --- a/provisioner/puppet-masterless/provisioner.go +++ b/provisioner/puppet-masterless/provisioner.go @@ -264,6 +264,8 @@ func (p *Provisioner) uploadManifests(ui packer.Ui, comm packer.Communicator) (s return "", fmt.Errorf("Error inspecting manifest file: %s", err) } else if !fi.IsDir() { manifestFilename = filepath.Base(manifestFilename) + } else { + ui.Say("WARNING: manifest_file should be a file. Use manifest_dir for directories") } remoteManifestFile := fmt.Sprintf("%s/%s", remoteManifestsPath, manifestFilename) From af4d8b99b41a4d5b9329ba75f61444a8fcda71bf Mon Sep 17 00:00:00 2001 From: Gonzalo Peci Date: Wed, 17 Jun 2015 12:29:10 +1200 Subject: [PATCH 15/31] Add quickfix to restart provisioner as existing one was not working. For more information read https://github.com/mitchellh/packer/pull/2243 --- provisioner/windows-restart/provisioner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioner/windows-restart/provisioner.go b/provisioner/windows-restart/provisioner.go index 234980183..e88aa0b3b 100644 --- a/provisioner/windows-restart/provisioner.go +++ b/provisioner/windows-restart/provisioner.go @@ -12,7 +12,7 @@ import ( "github.com/mitchellh/packer/template/interpolate" ) -var DefaultRestartCommand = "shutdown /r /c \"packer restart\" /t 5 && net stop winrm" +var DefaultRestartCommand = "powershell \"& {Restart-Computer -force }\"" var DefaultRestartCheckCommand = winrm.Powershell(`echo "${env:COMPUTERNAME} restarted."`) var retryableSleep = 5 * time.Second From a53cc8b07045599c2d20c022629c040b6f859ee3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 17 Jun 2015 17:53:40 +0200 Subject: [PATCH 16/31] update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38718e928..a495a3b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ FEATURES: connections. Note that provisioners won't work if this is done. [GH-1591] * **SSH Agent Forwarding:** SSH Agent Forwarding will now be enabled to allow access to remote servers such as private git repos. [GH-1066] + * **OpenStack v3 Identity:** The OpenStack builder now supports the + v3 identity API. * **Docker builder supports SSH**: The Docker builder now supports containers with SSH, just set `communicator` to "ssh" [GH-2244] * **File provisioner can download**: The file provisioner can now download @@ -32,6 +34,12 @@ FEATURES: builder. This is useful for provisioners. [GH-2232] * **New config function: `template_dir`**: The directory to the template being built. This should be used for template-relative paths. [GH-54] + * **New provisioner: powershell**: Provision Windows machines + with PowerShell scripts. [GH-2243] + * **New provisioner: windows-shell**: Provision Windows machines with + batch files. [GH-2243] + * **New provisioner: windows-restart**: Restart a Windows machines and + wait for it to come back online. [GH-2243] IMPROVEMENTS: From 7711e07f053ccccd491f811c9cf33334a0895120 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 17 Jun 2015 17:56:31 +0200 Subject: [PATCH 17/31] provisioner/windows-restart: test fix --- provisioner/windows-restart/provisioner_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioner/windows-restart/provisioner_test.go b/provisioner/windows-restart/provisioner_test.go index d2a54d274..bbb89e116 100644 --- a/provisioner/windows-restart/provisioner_test.go +++ b/provisioner/windows-restart/provisioner_test.go @@ -34,7 +34,7 @@ func TestProvisionerPrepare_Defaults(t *testing.T) { t.Errorf("unexpected remote path: %s", p.config.RestartTimeout) } - if p.config.RestartCommand != "shutdown /r /c \"packer restart\" /t 5 && net stop winrm" { + if p.config.RestartCommand != "powershell \"& {Restart-Computer -force }\"" { t.Errorf("unexpected remote path: %s", p.config.RestartCommand) } } From b2609db395fb4d7520f6d391319c2c77bbd07101 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 17 Jun 2015 18:22:27 +0200 Subject: [PATCH 18/31] provisioner/windows-restart: fix potential panic case --- provisioner/windows-restart/provisioner.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/provisioner/windows-restart/provisioner.go b/provisioner/windows-restart/provisioner.go index e88aa0b3b..7c1c5ada9 100644 --- a/provisioner/windows-restart/provisioner.go +++ b/provisioner/windows-restart/provisioner.go @@ -3,6 +3,7 @@ package restart import ( "fmt" "log" + "sync" "time" "github.com/masterzen/winrm/winrm" @@ -33,10 +34,11 @@ type Config struct { } type Provisioner struct { - config Config - comm packer.Communicator - ui packer.Ui - cancel chan struct{} + config Config + comm packer.Communicator + ui packer.Ui + cancel chan struct{} + cancelLock sync.Mutex } func (p *Provisioner) Prepare(raws ...interface{}) error { @@ -68,10 +70,13 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { + p.cancelLock.Lock() + p.cancel = make(chan struct{}) + p.cancelLock.Unlock() + ui.Say("Restarting Machine") p.comm = comm p.ui = ui - p.cancel = make(chan struct{}) var cmd *packer.RemoteCmd command := p.config.RestartCommand @@ -164,7 +169,12 @@ var waitForCommunicator = func(p *Provisioner) error { func (p *Provisioner) Cancel() { log.Printf("Received interrupt Cancel()") - close(p.cancel) + + p.cancelLock.Lock() + defer p.cancelLock.Unlock() + if p.cancel != nil { + close(p.cancel) + } } // retryable will retry the given function over and over until a From cbaaf0da52cd0860e4efe43eaa0d675240ee6362 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 17 Jun 2015 22:10:42 +0200 Subject: [PATCH 19/31] communicator/ssh: support for bastion SSH --- communicator/ssh/connect.go | 43 ++++++++++++++++++ helper/communicator/config.go | 18 ++++++++ helper/communicator/step_connect_ssh.go | 58 +++++++++++++++++++++++-- 3 files changed, 116 insertions(+), 3 deletions(-) diff --git a/communicator/ssh/connect.go b/communicator/ssh/connect.go index b280f3ead..43277595c 100644 --- a/communicator/ssh/connect.go +++ b/communicator/ssh/connect.go @@ -1,8 +1,11 @@ package ssh import ( + "fmt" "net" "time" + + "golang.org/x/crypto/ssh" ) // ConnectFunc is a convenience method for returning a function @@ -23,3 +26,43 @@ func ConnectFunc(network, addr string) func() (net.Conn, error) { return c, nil } } + +// BastionConnectFunc is a convenience method for returning a function +// that connects to a host over a bastion connection. +func BastionConnectFunc( + bProto string, + bAddr string, + bConf *ssh.ClientConfig, + proto string, + addr string) func() (net.Conn, error) { + return func() (net.Conn, error) { + // Connect to the bastion + bastion, err := ssh.Dial(bProto, bAddr, bConf) + if err != nil { + return nil, fmt.Errorf("Error connecting to bastion: %s", err) + } + + // Connect through to the end host + conn, err := bastion.Dial(proto, addr) + if err != nil { + bastion.Close() + return nil, err + } + + // Wrap it up so we close both things properly + return &bastionConn{ + Conn: conn, + Bastion: bastion, + }, nil + } +} + +type bastionConn struct { + net.Conn + Bastion *ssh.Client +} + +func (c *bastionConn) Close() error { + c.Conn.Close() + return c.Bastion.Close() +} diff --git a/helper/communicator/config.go b/helper/communicator/config.go index 4c316ff69..4d10bbbb6 100644 --- a/helper/communicator/config.go +++ b/helper/communicator/config.go @@ -23,6 +23,11 @@ type Config struct { SSHPty bool `mapstructure:"ssh_pty"` SSHTimeout time.Duration `mapstructure:"ssh_timeout"` SSHHandshakeAttempts int `mapstructure:"ssh_handshake_attempts"` + SSHBastionHost string `mapstructure:"ssh_bastion_host"` + SSHBastionPort int `mapstructure:"ssh_bastion_port"` + SSHBastionUsername string `mapstructure:"ssh_bastion_username"` + SSHBastionPassword string `mapstructure:"ssh_bastion_password"` + SSHBastionPrivateKey string `mapstructure:"ssh_bastion_private_key_file"` // WinRM WinRMUser string `mapstructure:"winrm_username"` @@ -77,6 +82,12 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error { c.SSHHandshakeAttempts = 10 } + if c.SSHBastionHost != "" { + if c.SSHBastionPort == 0 { + c.SSHBastionPort = 22 + } + } + // Validation var errs []error if c.SSHUsername == "" { @@ -93,6 +104,13 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error { } } + if c.SSHBastionHost != "" { + if c.SSHBastionPassword == "" && c.SSHBastionPrivateKey == "" { + errs = append(errs, errors.New( + "ssh_bastion_password or ssh_bastion_private_key_file must be specified")) + } + } + return errs } diff --git a/helper/communicator/step_connect_ssh.go b/helper/communicator/step_connect_ssh.go index 4b664fe4c..fd6b585f8 100644 --- a/helper/communicator/step_connect_ssh.go +++ b/helper/communicator/step_connect_ssh.go @@ -4,10 +4,12 @@ import ( "errors" "fmt" "log" + "net" "strings" "time" "github.com/mitchellh/multistep" + commonssh "github.com/mitchellh/packer/common/ssh" "github.com/mitchellh/packer/communicator/ssh" "github.com/mitchellh/packer/packer" gossh "golang.org/x/crypto/ssh" @@ -79,6 +81,24 @@ func (s *StepConnectSSH) Cleanup(multistep.StateBag) { } func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan struct{}) (packer.Communicator, error) { + // Determine if we're using a bastion host, and if so, retrieve + // that configuration. This configuration doesn't change so we + // do this one before entering the retry loop. + var bProto, bAddr string + var bConf *gossh.ClientConfig + if s.Config.SSHBastionHost != "" { + // The protocol is hardcoded for now, but may be configurable one day + bProto = "tcp" + bAddr = fmt.Sprintf( + "%s:%d", s.Config.SSHBastionHost, s.Config.SSHBastionPort) + + conf, err := sshBastionConfig(s.Config) + if err != nil { + return nil, fmt.Errorf("Error configuring bastion: %s", err) + } + bConf = conf + } + handshakeAttempts := 0 var comm packer.Communicator @@ -117,10 +137,18 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru continue } - address := fmt.Sprintf("%s:%d", host, port) - // Attempt to connect to SSH port - connFunc := ssh.ConnectFunc("tcp", address) + var connFunc func() (net.Conn, error) + address := fmt.Sprintf("%s:%d", host, port) + if bAddr != "" { + // We're using a bastion host, so use the bastion connfunc + connFunc = ssh.BastionConnectFunc( + bProto, bAddr, bConf, "tcp", address) + } else { + // No bastion host, connect directly + connFunc = ssh.ConnectFunc("tcp", address) + } + nc, err := connFunc() if err != nil { log.Printf("[DEBUG] TCP connection to SSH ip/port failed: %s", err) @@ -164,3 +192,27 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru return comm, nil } + +func sshBastionConfig(config *Config) (*gossh.ClientConfig, error) { + auth := make([]gossh.AuthMethod, 0, 2) + if config.SSHBastionPassword != "" { + auth = append(auth, + gossh.Password(config.SSHBastionPassword), + gossh.KeyboardInteractive( + ssh.PasswordKeyboardInteractive(config.SSHBastionPassword))) + } + + if config.SSHBastionPrivateKey != "" { + signer, err := commonssh.FileSigner(config.SSHBastionPrivateKey) + if err != nil { + return nil, err + } + + auth = append(auth, gossh.PublicKeys(signer)) + } + + return &gossh.ClientConfig{ + User: config.SSHBastionUsername, + Auth: auth, + }, nil +} From 889b5b610595a6ca984f8140bb413fda8749263e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 17 Jun 2015 22:21:04 +0200 Subject: [PATCH 20/31] website: update vsphere --- .../source/docs/post-processors/vsphere.html.markdown | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/source/docs/post-processors/vsphere.html.markdown b/website/source/docs/post-processors/vsphere.html.markdown index d49683a43..a6f790bc1 100644 --- a/website/source/docs/post-processors/vsphere.html.markdown +++ b/website/source/docs/post-processors/vsphere.html.markdown @@ -25,14 +25,17 @@ Required: * `datacenter` (string) - The name of the datacenter within vSphere to add the VM to. +* `datastore` (string) - The name of the datastore to store this VM. + This is _not required_ if `resource_pool` is specified. + * `host` (string) - The vSphere host that will be contacted to perform the VM upload. * `password` (string) - Password to use to authenticate to the vSphere endpoint. -* `resource_pool` (string) - The resource pool to upload the VM to. This can be - " " if you do not have resource pools configured +* `resource_pool` (string) - The resource pool to upload the VM to. + This is _not required_ if `datastore` is specified. * `username` (string) - The username to use to authenticate to the vSphere endpoint. @@ -41,8 +44,6 @@ Required: Optional: -* `datastore` (string) - The name of the datastore to store this VM. - * `disk_mode` (string) - Target disk format. See `ovftool` manual for available options. By default, "thick" will be used. From 8401177363df6181c440273d5e9bedbd015b6bdf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 17 Jun 2015 22:21:30 +0200 Subject: [PATCH 21/31] update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a495a3b4d..244006486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,8 @@ IMPROVEMENTS: * post-processor/docker-save: Can be chained [GH-2179] * post-processor/docker-tag: Support `force` option [GH-2055] * post-processor/docker-tag: Can be chained [GH-2179] + * post-processor/vsphere: Make more fields optional, support empty + resource pools. [GH-1868] * provisioner/puppet-masterless: `working_directory` option [GH-1831] * provisioner/puppet-masterless: `packer_build_name` and `packer_build_type` are default facts. [GH-1878] From b20e26be17b56e893d9b29945441041824d0e110 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 17 Jun 2015 22:30:57 +0200 Subject: [PATCH 22/31] website: update docs for ami_groups claritifaction [GH-1068] --- website/source/docs/builders/amazon-ebs.html.markdown | 1 + website/source/docs/builders/amazon-instance.html.markdown | 1 + 2 files changed, 2 insertions(+) diff --git a/website/source/docs/builders/amazon-ebs.html.markdown b/website/source/docs/builders/amazon-ebs.html.markdown index 6c7840575..5420c10fd 100644 --- a/website/source/docs/builders/amazon-ebs.html.markdown +++ b/website/source/docs/builders/amazon-ebs.html.markdown @@ -88,6 +88,7 @@ each category, the available configuration keys are alphabetized. * `ami_groups` (array of strings) - A list of groups that have access to launch the resulting AMI(s). By default no groups have permission to launch the AMI. `all` will make the AMI publicly accessible. + AWS currently doesn't accept any value other than "all". * `ami_product_codes` (array of strings) - A list of product codes to associate with the AMI. By default no product codes are associated with diff --git a/website/source/docs/builders/amazon-instance.html.markdown b/website/source/docs/builders/amazon-instance.html.markdown index ff9e7c9a2..bacb5ee58 100644 --- a/website/source/docs/builders/amazon-instance.html.markdown +++ b/website/source/docs/builders/amazon-instance.html.markdown @@ -107,6 +107,7 @@ each category, the available configuration keys are alphabetized. * `ami_groups` (array of strings) - A list of groups that have access to launch the resulting AMI(s). By default no groups have permission to launch the AMI. `all` will make the AMI publicly accessible. + AWS currently doesn't accept any value other than "all". * `ami_product_codes` (array of strings) - A list of product codes to associate with the AMI. By default no product codes are associated with From 6cdc17dda4ada1a7c9e85ed27d60502d4b2e2a57 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 17 Jun 2015 22:33:59 +0200 Subject: [PATCH 23/31] helper/communicator: default bastion PK to normal PK --- helper/communicator/config.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helper/communicator/config.go b/helper/communicator/config.go index 4d10bbbb6..e3da09618 100644 --- a/helper/communicator/config.go +++ b/helper/communicator/config.go @@ -86,6 +86,10 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error { if c.SSHBastionPort == 0 { c.SSHBastionPort = 22 } + + if c.SSHBastionPrivateKey == "" && c.SSHPrivateKey != "" { + c.SSHBastionPrivateKey = c.SSHPrivateKey + } } // Validation From ac510cd84581418cdeedd33c49c2c9eb53b0b1d2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 17 Jun 2015 22:36:44 +0200 Subject: [PATCH 24/31] update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 244006486..56cbb1eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ FEATURES: connections. Note that provisioners won't work if this is done. [GH-1591] * **SSH Agent Forwarding:** SSH Agent Forwarding will now be enabled to allow access to remote servers such as private git repos. [GH-1066] + * **SSH Bastion Hosts:** You can now specify a bastion host for + SSH access (works with all builders). [GH-387] * **OpenStack v3 Identity:** The OpenStack builder now supports the v3 identity API. * **Docker builder supports SSH**: The Docker builder now supports containers From b77fcd90f31e7e31a2c2f3f1295f8a8c05f58cbb Mon Sep 17 00:00:00 2001 From: Bob Kuo Date: Wed, 17 Jun 2015 14:31:55 -0500 Subject: [PATCH 25/31] Force qemu to use a VNC port by setting vnc_min_port == vnc_max_port Similar to Issue #1288, this prevents a crash when we set the VNC minimum port equivalent to the VNC maximum port --- builder/qemu/step_configure_vnc.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/builder/qemu/step_configure_vnc.go b/builder/qemu/step_configure_vnc.go index be452620d..913175dd6 100644 --- a/builder/qemu/step_configure_vnc.go +++ b/builder/qemu/step_configure_vnc.go @@ -32,7 +32,12 @@ func (stepConfigureVNC) Run(state multistep.StateBag) multistep.StepAction { var vncPort uint portRange := int(config.VNCPortMax - config.VNCPortMin) for { - vncPort = uint(rand.Intn(portRange)) + config.VNCPortMin + if portRange > 0 { + vncPort = uint(rand.Intn(portRange)) + config.VNCPortMin + } else { + vncPort = config.VNCPortMin + } + log.Printf("Trying port: %d", vncPort) l, err := net.Listen("tcp", fmt.Sprintf(":%d", vncPort)) if err == nil { From b7dab2689a8fa5e3510e9d459905a2ce8277ab74 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 18 Jun 2015 05:23:04 +0200 Subject: [PATCH 26/31] fmt --- builder/amazon/common/block_device_test.go | 4 ++-- builder/qemu/step_configure_vnc.go | 4 ++-- builder/vmware/common/step_compact_disk.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/builder/amazon/common/block_device_test.go b/builder/amazon/common/block_device_test.go index 12d1530bf..b99a22747 100644 --- a/builder/amazon/common/block_device_test.go +++ b/builder/amazon/common/block_device_test.go @@ -38,8 +38,8 @@ func TestBlockDevice(t *testing.T) { }, { Config: &BlockDevice{ - DeviceName: "/dev/sdb", - VolumeSize: 8, + DeviceName: "/dev/sdb", + VolumeSize: 8, }, Result: &ec2.BlockDeviceMapping{ diff --git a/builder/qemu/step_configure_vnc.go b/builder/qemu/step_configure_vnc.go index 913175dd6..e24c4f9ca 100644 --- a/builder/qemu/step_configure_vnc.go +++ b/builder/qemu/step_configure_vnc.go @@ -33,11 +33,11 @@ func (stepConfigureVNC) Run(state multistep.StateBag) multistep.StepAction { portRange := int(config.VNCPortMax - config.VNCPortMin) for { if portRange > 0 { - vncPort = uint(rand.Intn(portRange)) + config.VNCPortMin + vncPort = uint(rand.Intn(portRange)) + config.VNCPortMin } else { vncPort = config.VNCPortMin } - + log.Printf("Trying port: %d", vncPort) l, err := net.Listen("tcp", fmt.Sprintf(":%d", vncPort)) if err == nil { diff --git a/builder/vmware/common/step_compact_disk.go b/builder/vmware/common/step_compact_disk.go index 4319b7fe9..9a81c98c8 100644 --- a/builder/vmware/common/step_compact_disk.go +++ b/builder/vmware/common/step_compact_disk.go @@ -36,11 +36,11 @@ func (s StepCompactDisk) Run(state multistep.StateBag) multistep.StepAction { state.Put("error", fmt.Errorf("Error compacting disk: %s", err)) return multistep.ActionHalt } - + if state.Get("additional_disk_paths") != nil { if moreDisks := state.Get("additional_disk_paths").([]string); len(moreDisks) > 0 { for i, path := range moreDisks { - ui.Say(fmt.Sprintf("Compacting additional disk image %d",i+1)) + ui.Say(fmt.Sprintf("Compacting additional disk image %d", i+1)) if err := driver.CompactDisk(path); err != nil { state.Put("error", fmt.Errorf("Error compacting additional disk %d: %s", i+1, err)) return multistep.ActionHalt From 6fbf4147cde3fbbd1196698515436b77ea184ce1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 18 Jun 2015 10:12:33 +0200 Subject: [PATCH 27/31] update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56cbb1eed..f3b9a0aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ BUG FIXES: * builder/amazon: Improved retry logic around waiting for instances. [GH-1764] * builder/amazon: Fix issues with creating Block Devices. [GH-2195] * builder/amazon/chroot: Retry waiting for disk attachments [GH-2046] + * builder/amazon/chroot: Only unmount path if it is mounted [GH-2054] * builder/amazon/instance: Use `-i` in sudo commands so PATH is inherited. [GH-1930] * builder/amazon/instance: Use `--region` flag for bundle upload command. [GH-1931] * builder/digitalocean: Wait for droplet to unlock before changing state, From 40cb558f2fa598f2e1fa6d094154169fc980a2a9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 18 Jun 2015 10:15:53 +0200 Subject: [PATCH 28/31] update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3b9a0aed..e100ad128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ IMPROVEMENTS: * builder/amazon: Support custom keypairs [GH-1837] * builder/digitalocean: Save SSH key to pwd if debug mode is on. [GH-1829] * builder/digitalocean: User data support [GH-2113] + * builder/googlecompute: Option to use internal IP for connections. [GH-2152] * builder/parallels: Support Parallels Desktop 11 [GH-2199] * builder/openstack: Add `rackconnect_wait` for Rackspace customers to wait for RackConnect data to appear From d9fceaf39d315fc81572da883dbee56591325eef Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 18 Jun 2015 10:19:46 +0200 Subject: [PATCH 29/31] update CHANGELOG --- CHANGELOG.md | 2 ++ builder/virtualbox/common/step_forward_ssh.go | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e100ad128..985c4d4fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ BUG FIXES: to retrieve the SSH IP from. [GH-2220] * builder/qemu: Add `disk_discard` option [GH-2120] * builder/qemu: Use proper SSH port, not hardcoded to 22. [GH-2236] + * builder/qemu: Find unused SSH port if SSH port is taken. [GH-2032] * builder/virtualbox: Bind HTTP server to IPv4, which is more compatible with OS installers. [GH-1709] * builder/virtualbox: Remove the floppy controller in addition to the @@ -126,6 +127,7 @@ BUG FIXES: ".iso" extension didn't work. [GH-1839] * builder/virtualbox: Output dir is verified at runtime, not template validation time. [GH-2233] + * builder/virtualbox: Find unused SSH port if SSH port is taken. [GH-2032] * builder/vmware: Add 100ms delay between keystrokes to avoid subtle timing issues in most cases. [GH-1663] * builder/vmware: Bind HTTP server to IPv4, which is more compatible with diff --git a/builder/virtualbox/common/step_forward_ssh.go b/builder/virtualbox/common/step_forward_ssh.go index a33efc977..86376c834 100644 --- a/builder/virtualbox/common/step_forward_ssh.go +++ b/builder/virtualbox/common/step_forward_ssh.go @@ -46,10 +46,10 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction { } for { - sshHostPort = offset + s.HostPortMin - if sshHostPort >= s.HostPortMax { + sshHostPort = offset + int(s.HostPortMin) + if sshHostPort >= int(s.HostPortMax) { offset = 0 - sshHostPort = s.HostPortMin + sshHostPort = int(s.HostPortMin) } log.Printf("Trying port: %d", sshHostPort) l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort)) From f7af571cd95718204c286b53588bffd5bd12e634 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Thu, 18 Jun 2015 12:04:02 +0300 Subject: [PATCH 30/31] builder/parallels: Add "SetDefaultConfiguration" function This functions applies the default configuration to the virtual machine. Also, it disables some integration features which should not present in the resulted VM image. Functions are different in PD 9 and 10 structs because some additional options appeared only in Parallels Desktop 10 release. --- builder/parallels/common/driver.go | 3 +++ builder/parallels/common/driver_10.go | 24 ++++++++++++++++++++++++ builder/parallels/common/driver_9.go | 19 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/builder/parallels/common/driver.go b/builder/parallels/common/driver.go index 5c3d93c09..03a4e0f09 100644 --- a/builder/parallels/common/driver.go +++ b/builder/parallels/common/driver.go @@ -44,6 +44,9 @@ type Driver interface { // Send scancodes to the vm using the prltype python script. SendKeyScanCodes(string, ...string) error + // Apply default сonfiguration settings to the virtual machine + SetDefaultConfiguration(string) error + // Finds the MAC address of the NIC nic0 Mac(string) (string, error) diff --git a/builder/parallels/common/driver_10.go b/builder/parallels/common/driver_10.go index 9ab0754de..c37d421dc 100644 --- a/builder/parallels/common/driver_10.go +++ b/builder/parallels/common/driver_10.go @@ -5,3 +5,27 @@ package common type Parallels10Driver struct { Parallels9Driver } + +func (d *Parallels10Driver) SetDefaultConfiguration(vmName string) error { + commands := make([][]string, 12) + commands[0] = []string{"set", vmName, "--cpus", "1"} + commands[1] = []string{"set", vmName, "--memsize", "512"} + commands[2] = []string{"set", vmName, "--startup-view", "same"} + commands[3] = []string{"set", vmName, "--on-shutdown", "close"} + commands[4] = []string{"set", vmName, "--on-window-close", "keep-running"} + commands[5] = []string{"set", vmName, "--auto-share-camera", "off"} + commands[6] = []string{"set", vmName, "--smart-guard", "off"} + commands[7] = []string{"set", vmName, "--shared-cloud", "off"} + commands[8] = []string{"set", vmName, "--shared-profile", "off"} + commands[9] = []string{"set", vmName, "--smart-mount", "off"} + commands[10] = []string{"set", vmName, "--sh-app-guest-to-host", "off"} + commands[11] = []string{"set", vmName, "--sh-app-host-to-guest", "off"} + + for _, command := range commands { + err := d.Prlctl(command...) + if err != nil { + return err + } + } + return nil +} diff --git a/builder/parallels/common/driver_9.go b/builder/parallels/common/driver_9.go index 98d36cc24..c577151dc 100644 --- a/builder/parallels/common/driver_9.go +++ b/builder/parallels/common/driver_9.go @@ -255,6 +255,25 @@ func prepend(head string, tail []string) []string { return tmp } +func (d *Parallels9Driver) SetDefaultConfiguration(vmName string) error { + commands := make([][]string, 7) + commands[0] = []string{"set", vmName, "--cpus", "1"} + commands[1] = []string{"set", vmName, "--memsize", "512"} + commands[2] = []string{"set", vmName, "--startup-view", "same"} + commands[3] = []string{"set", vmName, "--on-shutdown", "close"} + commands[4] = []string{"set", vmName, "--on-window-close", "keep-running"} + commands[5] = []string{"set", vmName, "--auto-share-camera", "off"} + commands[6] = []string{"set", vmName, "--smart-guard", "off"} + + for _, command := range commands { + err := d.Prlctl(command...) + if err != nil { + return err + } + } + return nil +} + func (d *Parallels9Driver) Mac(vmName string) (string, error) { var stdout bytes.Buffer From 2860bfdf82eb84549f7f43619abd998144774593 Mon Sep 17 00:00:00 2001 From: Mikhail Zholobov Date: Thu, 18 Jun 2015 12:06:49 +0300 Subject: [PATCH 31/31] builder/parallels: Apply default settings on the VM creation step. --- builder/parallels/iso/step_create_vm.go | 40 +++++++++++-------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/builder/parallels/iso/step_create_vm.go b/builder/parallels/iso/step_create_vm.go index ebe7effa8..ca8c7c44e 100644 --- a/builder/parallels/iso/step_create_vm.go +++ b/builder/parallels/iso/step_create_vm.go @@ -23,37 +23,33 @@ func (s *stepCreateVM) Run(state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) name := config.VMName - commands := make([][]string, 8) - commands[0] = []string{ + command := []string{ "create", name, "--distribution", config.GuestOSType, "--dst", config.OutputDir, "--vmtype", "vm", "--no-hdd", } - commands[1] = []string{"set", name, "--cpus", "1"} - commands[2] = []string{"set", name, "--memsize", "512"} - commands[3] = []string{"set", name, "--startup-view", "same"} - commands[4] = []string{"set", name, "--on-shutdown", "close"} - commands[5] = []string{"set", name, "--on-window-close", "keep-running"} - commands[6] = []string{"set", name, "--auto-share-camera", "off"} - commands[7] = []string{"set", name, "--smart-guard", "off"} ui.Say("Creating virtual machine...") - for _, command := range commands { - err := driver.Prlctl(command...) - ui.Say(fmt.Sprintf("Executing: prlctl %s", command)) - if err != nil { - err := fmt.Errorf("Error creating VM: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } + if err := driver.Prlctl(command...); err != nil { + err := fmt.Errorf("Error creating VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } - // Set the VM name property on the first command - if s.vmName == "" { - s.vmName = name - } + ui.Say("Applying default settings...") + if err := driver.SetDefaultConfiguration(name); err != nil { + err := fmt.Errorf("Error VM configuration: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set the VM name property on the first command + if s.vmName == "" { + s.vmName = name } // Set the final name in the state bag so others can use it