Merge pull request #2243 from mitchellh/f-powershell
Windows provisioners: powershell, windows-shell, windows-restart
This commit is contained in:
commit
49769a17c2
|
@ -89,7 +89,10 @@ func runCommand(shell *winrm.Shell, cmd *winrm.Command, rc *packer.RemoteCmd) {
|
||||||
go io.Copy(rc.Stderr, cmd.Stderr)
|
go io.Copy(rc.Stderr, cmd.Stderr)
|
||||||
|
|
||||||
cmd.Wait()
|
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
|
// Upload implementation of communicator.Communicator interface
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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}} > %TEMP%\{{.TaskName}}.out 2>&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`))
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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.
|
|
@ -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
|
Shell provisioning is the easiest way to get software installed and configured
|
||||||
on a machine.
|
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
|
## Basic Example
|
||||||
|
|
||||||
The example below is fully functional.
|
The example below is fully functional.
|
||||||
|
|
|
@ -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"
|
|
@ -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.
|
|
@ -49,12 +49,15 @@
|
||||||
<li><h4>Provisioners</h4></li>
|
<li><h4>Provisioners</h4></li>
|
||||||
<li><a href="/docs/provisioners/shell.html">Shell Scripts</a></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/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/ansible-local.html">Ansible</a></li>
|
||||||
<li><a href="/docs/provisioners/chef-client.html">Chef Client</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/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-masterless.html">Puppet Masterless</a></li>
|
||||||
<li><a href="/docs/provisioners/puppet-server.html">Puppet Server</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/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>
|
<li><a href="/docs/provisioners/custom.html">Custom</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue