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