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)
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
on a machine.
|
||||
|
||||
-> **Building Windows images?** You probably want to use the
|
||||
[PowerShell](/docs/provisioners/powershell.html) or
|
||||
[Windows Shell](/docs/provisioners/windows-shell.html) provisioners.
|
||||
|
||||
## Basic Example
|
||||
|
||||
The example below is fully functional.
|
||||
|
|
|
@ -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><a href="/docs/provisioners/shell.html">Shell Scripts</a></li>
|
||||
<li><a href="/docs/provisioners/file.html">File Uploads</a></li>
|
||||
<li><a href="/docs/provisioners/powershell.html">PowerShell</a></li>
|
||||
<li><a href="/docs/provisioners/windows-shell.html">Windows Shell</a></li>
|
||||
<li><a href="/docs/provisioners/ansible-local.html">Ansible</a></li>
|
||||
<li><a href="/docs/provisioners/chef-client.html">Chef Client</a></li>
|
||||
<li><a href="/docs/provisioners/chef-solo.html">Chef Solo</a></li>
|
||||
<li><a href="/docs/provisioners/puppet-masterless.html">Puppet Masterless</a></li>
|
||||
<li><a href="/docs/provisioners/puppet-server.html">Puppet Server</a></li>
|
||||
<li><a href="/docs/provisioners/salt-masterless.html">Salt</a></li>
|
||||
<li><a href="/docs/provisioners/windows-restart.html">Windows Restart</a></li>
|
||||
<li><a href="/docs/provisioners/custom.html">Custom</a></li>
|
||||
</ul>
|
||||
|
||||
|
|
Loading…
Reference in New Issue