Merge branch 'master' into f-vtolstov-compress

This commit is contained in:
Chris Bednarski 2015-06-18 05:19:30 -07:00
commit f06847ff10
46 changed files with 3194 additions and 67 deletions

View File

@ -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

View File

@ -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(

View File

@ -38,8 +38,8 @@ func TestBlockDevice(t *testing.T) {
},
{
Config: &BlockDevice{
DeviceName: "/dev/sdb",
VolumeSize: 8,
DeviceName: "/dev/sdb",
VolumeSize: 8,
},
Result: &ec2.BlockDeviceMapping{

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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))

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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,
}

View File

@ -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{

View File

@ -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))

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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()
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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 = @'
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>{{.TaskDescription}}</Description>
</RegistrationInfo>
<Principals>
<Principal id="Author">
<UserId>{{.User}}</UserId>
<LogonType>Password</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT24H</ExecutionTimeLimit>
<Priority>4</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>cmd</Command>
<Arguments>/c powershell.exe -EncodedCommand {{.EncodedCommand}} &gt; %TEMP%\{{.TaskName}}.out 2&gt;&amp;1</Arguments>
</Exec>
</Actions>
</Task>
'@
$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`))

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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"

View File

@ -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.

View File

@ -49,12 +49,15 @@
<li><h4>Provisioners</h4></li>
<li><a href="/docs/provisioners/shell.html">Shell Scripts</a></li>
<li><a href="/docs/provisioners/file.html">File Uploads</a></li>
<li><a href="/docs/provisioners/powershell.html">PowerShell</a></li>
<li><a href="/docs/provisioners/windows-shell.html">Windows Shell</a></li>
<li><a href="/docs/provisioners/ansible-local.html">Ansible</a></li>
<li><a href="/docs/provisioners/chef-client.html">Chef Client</a></li>
<li><a href="/docs/provisioners/chef-solo.html">Chef Solo</a></li>
<li><a href="/docs/provisioners/puppet-masterless.html">Puppet Masterless</a></li>
<li><a href="/docs/provisioners/puppet-server.html">Puppet Server</a></li>
<li><a href="/docs/provisioners/salt-masterless.html">Salt</a></li>
<li><a href="/docs/provisioners/windows-restart.html">Windows Restart</a></li>
<li><a href="/docs/provisioners/custom.html">Custom</a></li>
</ul>