diff --git a/CHANGELOG.md b/CHANGELOG.md
index 38718e928..985c4d4fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,10 @@ 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
with SSH, just set `communicator` to "ssh" [GH-2244]
* **File provisioner can download**: The file provisioner can now download
@@ -32,6 +36,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:
@@ -44,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
@@ -65,6 +76,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]
@@ -88,6 +101,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,
@@ -104,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
@@ -112,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/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(
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/googlecompute/config.go b/builder/googlecompute/config.go
index 7f59aa183..762976385 100644
--- a/builder/googlecompute/config.go
+++ b/builder/googlecompute/config.go
@@ -35,6 +35,7 @@ type Config struct {
SourceImageProjectId string `mapstructure:"source_image_project_id"`
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/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)
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
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
diff --git a/builder/qemu/step_configure_vnc.go b/builder/qemu/step_configure_vnc.go
index be452620d..e24c4f9ca 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 {
diff --git a/builder/qemu/step_copy_disk.go b/builder/qemu/step_copy_disk.go
index 6afb70cb0..69383068d 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 a1b5623a4..3af48cd4b 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_forward_ssh.go b/builder/qemu/step_forward_ssh.go
index c88d5623d..feda414d5 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/qemu/step_resize_disk.go b/builder/qemu/step_resize_disk.go
index 58e405747..22c56dc37 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 21472f179..356481085 100644
--- a/builder/qemu/step_run.go
+++ b/builder/qemu/step_run.go
@@ -65,8 +65,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)
diff --git a/builder/virtualbox/common/step_forward_ssh.go b/builder/virtualbox/common/step_forward_ssh.go
index 11bbcff9f..86376c834 100644
--- a/builder/virtualbox/common/step_forward_ssh.go
+++ b/builder/virtualbox/common/step_forward_ssh.go
@@ -47,12 +47,17 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
for {
sshHostPort = offset + int(s.HostPortMin)
+ if sshHostPort >= int(s.HostPortMax) {
+ offset = 0
+ sshHostPort = int(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
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
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/communicator/winrm/communicator.go b/communicator/winrm/communicator.go
index 2b53ac62c..d90cd8450 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
diff --git a/helper/communicator/config.go b/helper/communicator/config.go
index 4c316ff69..e3da09618 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,16 @@ func (c *Config) prepareSSH(ctx *interpolate.Context) []error {
c.SSHHandshakeAttempts = 10
}
+ if c.SSHBastionHost != "" {
+ if c.SSHBastionPort == 0 {
+ c.SSHBastionPort = 22
+ }
+
+ if c.SSHBastionPrivateKey == "" && c.SSHPrivateKey != "" {
+ c.SSHBastionPrivateKey = c.SSHPrivateKey
+ }
+ }
+
// Validation
var errs []error
if c.SSHUsername == "" {
@@ -93,6 +108,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
+}
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/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/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/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..b31781d88
--- /dev/null
+++ b/provisioner/powershell/provisioner.go
@@ -0,0 +1,461 @@
+// 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: %v",
+ 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
+}
diff --git a/provisioner/puppet-masterless/provisioner.go b/provisioner/puppet-masterless/provisioner.go
index 2002cf359..8534aab32 100644
--- a/provisioner/puppet-masterless/provisioner.go
+++ b/provisioner/puppet-masterless/provisioner.go
@@ -276,7 +276,15 @@ 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)
+ } else {
+ ui.Say("WARNING: manifest_file should be a file. Use manifest_dir for directories")
+ }
+
remoteManifestFile := fmt.Sprintf("%s/%s", remoteManifestsPath, manifestFilename)
if err := comm.Upload(remoteManifestFile, f, nil); err != nil {
return "", err
diff --git a/provisioner/windows-restart/provisioner.go b/provisioner/windows-restart/provisioner.go
new file mode 100644
index 000000000..7c1c5ada9
--- /dev/null
+++ b/provisioner/windows-restart/provisioner.go
@@ -0,0 +1,204 @@
+package restart
+
+import (
+ "fmt"
+ "log"
+ "sync"
+ "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 = "powershell \"& {Restart-Computer -force }\""
+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{}
+ cancelLock sync.Mutex
+}
+
+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 {
+ p.cancelLock.Lock()
+ p.cancel = make(chan struct{})
+ p.cancelLock.Unlock()
+
+ ui.Say("Restarting Machine")
+ p.comm = comm
+ p.ui = ui
+
+ 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()")
+
+ 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
+// 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..bbb89e116
--- /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 != "powershell \"& {Restart-Computer -force }\"" {
+ 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.Fatalf("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")
+ }
+}
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
+}
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
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.
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/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-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/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 84cae0b05..8099b461a 100644
--- a/website/source/layouts/docs.erb
+++ b/website/source/layouts/docs.erb
@@ -49,12 +49,15 @@
Provisioners
Shell Scripts
File Uploads
+ PowerShell
+ Windows Shell
Ansible
Chef Client
Chef Solo
Puppet Masterless
Puppet Server
Salt
+ Windows Restart
Custom