Merge pull request #2243 from mitchellh/f-powershell

Windows provisioners: powershell, windows-shell, windows-restart
This commit is contained in:
Mitchell Hashimoto 2015-06-17 17:51:42 +02:00
commit 49769a17c2
17 changed files with 2791 additions and 1 deletions

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

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

@ -0,0 +1,194 @@
package restart
import (
"fmt"
"log"
"time"
"github.com/masterzen/winrm/winrm"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
)
var DefaultRestartCommand = "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{}
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
err := config.Decode(&p.config, &config.DecodeOpts{
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"execute_command",
},
},
}, raws...)
if err != nil {
return err
}
if p.config.RestartCommand == "" {
p.config.RestartCommand = DefaultRestartCommand
}
if p.config.RestartCheckCommand == "" {
p.config.RestartCheckCommand = DefaultRestartCheckCommand
}
if p.config.RestartTimeout == 0 {
p.config.RestartTimeout = 5 * time.Minute
}
return nil
}
func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
ui.Say("Restarting Machine")
p.comm = comm
p.ui = ui
p.cancel = make(chan struct{})
var cmd *packer.RemoteCmd
command := p.config.RestartCommand
err := p.retryable(func() error {
cmd = &packer.RemoteCmd{Command: command}
return cmd.StartWithUi(comm, ui)
})
if err != nil {
return err
}
if cmd.ExitStatus != 0 {
return fmt.Errorf("Restart script exited with non-zero exit status: %d", cmd.ExitStatus)
}
return waitForRestart(p)
}
var waitForRestart = func(p *Provisioner) error {
ui := p.ui
ui.Say("Waiting for machine to restart...")
waitDone := make(chan bool, 1)
timeout := time.After(p.config.RestartTimeout)
var err error
go func() {
log.Printf("Waiting for machine to become available...")
err = waitForCommunicator(p)
waitDone <- true
}()
log.Printf("Waiting for machine to reboot with timeout: %s", p.config.RestartTimeout)
WaitLoop:
for {
// Wait for either WinRM to become available, a timeout to occur,
// or an interrupt to come through.
select {
case <-waitDone:
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for WinRM: %s", err))
return err
}
ui.Say("Machine successfully restarted, moving on")
close(p.cancel)
break WaitLoop
case <-timeout:
err := fmt.Errorf("Timeout waiting for WinRM.")
ui.Error(err.Error())
close(p.cancel)
return err
case <-p.cancel:
close(waitDone)
return fmt.Errorf("Interrupt detected, quitting waiting for machine to restart")
break WaitLoop
}
}
return nil
}
var waitForCommunicator = func(p *Provisioner) error {
cmd := &packer.RemoteCmd{Command: p.config.RestartCheckCommand}
for {
select {
case <-p.cancel:
log.Println("Communicator wait cancelled, exiting loop")
return fmt.Errorf("Communicator wait cancelled")
case <-time.After(retryableSleep):
}
log.Printf("Attempting to communicator to machine with: '%s'", cmd.Command)
err := cmd.StartWithUi(p.comm, p.ui)
if err != nil {
log.Printf("Communication connection err: %s", err)
continue
}
log.Printf("Connected to machine")
break
}
return nil
}
func (p *Provisioner) Cancel() {
log.Printf("Received interrupt Cancel()")
close(p.cancel)
}
// retryable will retry the given function over and over until a
// non-error is returned.
func (p *Provisioner) retryable(f func() error) error {
startTimeout := time.After(p.config.RestartTimeout)
for {
var err error
if err = f(); err == nil {
return nil
}
// Create an error and log it
err = fmt.Errorf("Retryable error: %s", err)
log.Printf(err.Error())
// Check if we timed out, otherwise we retry. It is safe to
// retry since the only error case above is if the command
// failed to START.
select {
case <-startTimeout:
return err
default:
time.Sleep(retryableSleep)
}
}
}

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 != "shutdown /r /c \"packer restart\" /t 5 && net stop winrm" {
t.Errorf("unexpected remote path: %s", p.config.RestartCommand)
}
}
func TestProvisionerPrepare_ConfigRetryTimeout(t *testing.T) {
var p Provisioner
config := testConfig()
config["restart_timeout"] = "1m"
err := p.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if p.config.RestartTimeout != 1*time.Minute {
t.Errorf("unexpected remote path: %s", p.config.RestartTimeout)
}
}
func TestProvisionerPrepare_ConfigErrors(t *testing.T) {
var p Provisioner
config := testConfig()
config["restart_timeout"] = "m"
err := p.Prepare(config)
if err == nil {
t.Fatal("Expected error parsing restart_timeout but did not receive one.")
}
}
func TestProvisionerPrepare_InvalidKey(t *testing.T) {
var p Provisioner
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
err := p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func testUi() *packer.BasicUi {
return &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
ErrorWriter: new(bytes.Buffer),
}
}
func TestProvisionerProvision_Success(t *testing.T) {
config := testConfig()
// Defaults provided by Packer
ui := testUi()
p := new(Provisioner)
// Defaults provided by Packer
comm := new(packer.MockCommunicator)
p.Prepare(config)
waitForCommunicatorOld := waitForCommunicator
waitForCommunicator = func(p *Provisioner) error {
return nil
}
err := p.Provision(ui, comm)
if err != nil {
t.Fatal("should not have error")
}
expectedCommand := DefaultRestartCommand
// Should run the command without alteration
if comm.StartCmd.Command != expectedCommand {
t.Fatalf("Expect command to be: %s, got %s", expectedCommand, comm.StartCmd.Command)
}
// Set this back!
waitForCommunicator = waitForCommunicatorOld
}
func TestProvisionerProvision_CustomCommand(t *testing.T) {
config := testConfig()
// Defaults provided by Packer
ui := testUi()
p := new(Provisioner)
expectedCommand := "specialrestart.exe -NOW"
config["restart_command"] = expectedCommand
// Defaults provided by Packer
comm := new(packer.MockCommunicator)
p.Prepare(config)
waitForCommunicatorOld := waitForCommunicator
waitForCommunicator = func(p *Provisioner) error {
return nil
}
err := p.Provision(ui, comm)
if err != nil {
t.Fatal("should not have error")
}
// Should run the command without alteration
if comm.StartCmd.Command != expectedCommand {
t.Fatalf("Expect command to be: %s, got %s", expectedCommand, comm.StartCmd.Command)
}
// Set this back!
waitForCommunicator = waitForCommunicatorOld
}
func TestProvisionerProvision_RestartCommandFail(t *testing.T) {
config := testConfig()
ui := testUi()
p := new(Provisioner)
comm := new(packer.MockCommunicator)
comm.StartStderr = "WinRM terminated"
comm.StartExitStatus = 1
p.Prepare(config)
err := p.Provision(ui, comm)
if err == nil {
t.Fatal("should have error")
}
}
func TestProvisionerProvision_WaitForRestartFail(t *testing.T) {
config := testConfig()
// Defaults provided by Packer
ui := testUi()
p := new(Provisioner)
// Defaults provided by Packer
comm := new(packer.MockCommunicator)
p.Prepare(config)
waitForCommunicatorOld := waitForCommunicator
waitForCommunicator = func(p *Provisioner) error {
return fmt.Errorf("Machine did not restart properly")
}
err := p.Provision(ui, comm)
if err == nil {
t.Fatal("should have error")
}
// Set this back!
waitForCommunicator = waitForCommunicatorOld
}
func TestProvision_waitForRestartTimeout(t *testing.T) {
retryableSleep = 10 * time.Millisecond
config := testConfig()
config["restart_timeout"] = "1ms"
ui := testUi()
p := new(Provisioner)
comm := new(packer.MockCommunicator)
var err error
p.Prepare(config)
waitForCommunicatorOld := waitForCommunicator
waitDone := make(chan bool)
// Block until cancel comes through
waitForCommunicator = func(p *Provisioner) error {
for {
select {
case <-waitDone:
}
}
}
go func() {
err = p.Provision(ui, comm)
waitDone <- true
}()
<-waitDone
if err == nil {
t.Fatal("should not have error")
}
// Set this back!
waitForCommunicator = waitForCommunicatorOld
}
func TestProvision_waitForCommunicator(t *testing.T) {
config := testConfig()
// Defaults provided by Packer
ui := testUi()
p := new(Provisioner)
// Defaults provided by Packer
comm := new(packer.MockCommunicator)
p.comm = comm
p.ui = ui
comm.StartStderr = "WinRM terminated"
comm.StartExitStatus = 1
p.Prepare(config)
err := waitForCommunicator(p)
if err != nil {
t.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

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