provisioner/powershell

This commit is contained in:
Mitchell Hashimoto 2015-06-14 11:01:28 -07:00
parent cf570a71dc
commit 040ff0706d
5 changed files with 1234 additions and 0 deletions

View File

@ -0,0 +1,15 @@
package main
import (
"github.com/mitchellh/packer/packer/plugin"
"github.com/mitchellh/packer/provisioner/powershell"
)
func main() {
server, err := plugin.Server()
if err != nil {
panic(err)
}
server.RegisterProvisioner(new(powershell.Provisioner))
server.Serve()
}

View File

@ -0,0 +1,87 @@
package powershell
import (
"text/template"
)
type elevatedOptions struct {
User string
Password string
TaskName string
TaskDescription string
EncodedCommand string
}
var elevatedTemplate = template.Must(template.New("ElevatedCommand").Parse(`
$name = "{{.TaskName}}"
$log = "$env:TEMP\$name.out"
$s = New-Object -ComObject "Schedule.Service"
$s.Connect()
$t = $s.NewTask($null)
$t.XmlText = @'
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>{{.TaskDescription}}</Description>
</RegistrationInfo>
<Principals>
<Principal id="Author">
<UserId>{{.User}}</UserId>
<LogonType>Password</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT24H</ExecutionTimeLimit>
<Priority>4</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>cmd</Command>
<Arguments>/c powershell.exe -EncodedCommand {{.EncodedCommand}} &gt; %TEMP%\{{.TaskName}}.out 2&gt;&amp;1</Arguments>
</Exec>
</Actions>
</Task>
'@
$f = $s.GetFolder("\")
$f.RegisterTaskDefinition($name, $t, 6, "{{.User}}", "{{.Password}}", 1, $null) | Out-Null
$t = $f.GetTask("\$name")
$t.Run($null) | Out-Null
$timeout = 10
$sec = 0
while ((!($t.state -eq 4)) -and ($sec -lt $timeout)) {
Start-Sleep -s 1
$sec++
}
function SlurpOutput($l) {
if (Test-Path $log) {
Get-Content $log | select -skip $l | ForEach {
$l += 1
Write-Host "$_"
}
}
return $l
}
$line = 0
do {
Start-Sleep -m 100
$line = SlurpOutput $line
} while (!($t.state -eq 3))
$result = $t.LastTaskResult
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($s) | Out-Null
exit $result`))

View File

@ -0,0 +1,17 @@
package powershell
import (
"encoding/base64"
)
func powershellEncode(buffer []byte) string {
// 2 byte chars to make PowerShell happy
wideCmd := ""
for _, b := range buffer {
wideCmd += string(b) + "\x00"
}
// Base64 encode the command
input := []uint8(wideCmd)
return base64.StdEncoding.EncodeToString(input)
}

View File

@ -0,0 +1,459 @@
// 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: %s", cmd.ExitStatus, p.config.ValidExitCodes)
}
}
return nil
}
func (p *Provisioner) Cancel() {
// Just hard quit. It isn't a big deal if what we're doing keeps
// running on the other side.
os.Exit(0)
}
// retryable will retry the given function over and over until a
// non-error is returned.
func (p *Provisioner) retryable(f func() error) error {
startTimeout := time.After(p.config.StartRetryTimeout)
for {
var err error
if err = f(); err == nil {
return nil
}
// Create an error and log it
err = fmt.Errorf("Retryable error: %s", err)
log.Printf(err.Error())
// Check if we timed out, otherwise we retry. It is safe to
// retry since the only error case above is if the command
// failed to START.
select {
case <-startTimeout:
return err
default:
time.Sleep(retryableSleep)
}
}
}
func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string, err error) {
flattened = ""
envVars := make(map[string]string)
// Always available Packer provided env vars
envVars["PACKER_BUILD_NAME"] = p.config.PackerBuildName
envVars["PACKER_BUILDER_TYPE"] = p.config.PackerBuilderType
// Split vars into key/value components
for _, envVar := range p.config.Vars {
keyValue := strings.Split(envVar, "=")
if len(keyValue) != 2 {
err = errors.New("Shell provisioner environment variables must be in key=value format")
return
}
envVars[keyValue[0]] = keyValue[1]
}
// Create a list of env var keys in sorted order
var keys []string
for k := range envVars {
keys = append(keys, k)
}
sort.Strings(keys)
format := p.config.EnvVarFormat
if elevated {
format = p.config.ElevatedEnvVarFormat
}
// Re-assemble vars using OS specific format pattern and flatten
for _, key := range keys {
flattened += fmt.Sprintf(format, key, envVars[key])
}
return
}
func (p *Provisioner) createCommandText() (command string, err error) {
// Create environment variables to set before executing the command
flattenedEnvVars, err := p.createFlattenedEnvVars(false)
if err != nil {
return "", err
}
p.config.ctx.Data = &ExecuteCommandTemplate{
Vars: flattenedEnvVars,
Path: p.config.RemotePath,
}
command, err = interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
if err != nil {
return "", fmt.Errorf("Error processing command: %s", err)
}
// Return the interpolated command
if p.config.ElevatedUser == "" {
return command, nil
}
// Can't double escape the env vars, lets create shiny new ones
flattenedEnvVars, err = p.createFlattenedEnvVars(true)
p.config.ctx.Data = &ExecuteCommandTemplate{
Vars: flattenedEnvVars,
Path: p.config.RemotePath,
}
command, err = interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
if err != nil {
return "", fmt.Errorf("Error processing command: %s", err)
}
// OK so we need an elevated shell runner to wrap our command, this is going to have its own path
// generate the script and update the command runner in the process
path, err := p.generateElevatedRunner(command)
// Return the path to the elevated shell wrapper
command = fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path)
return
}
func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath string, err error) {
log.Printf("Building elevated command wrapper for: %s", command)
// generate command
var buffer bytes.Buffer
err = elevatedTemplate.Execute(&buffer, elevatedOptions{
User: p.config.ElevatedUser,
Password: p.config.ElevatedPassword,
TaskDescription: "Packer elevated task",
TaskName: fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()),
EncodedCommand: powershellEncode([]byte(command + "; exit $LASTEXITCODE")),
})
if err != nil {
fmt.Printf("Error creating elevated template: %s", err)
return "", err
}
tmpFile, err := ioutil.TempFile(os.TempDir(), "packer-elevated-shell.ps1")
writer := bufio.NewWriter(tmpFile)
if _, err := writer.WriteString(string(buffer.Bytes())); err != nil {
return "", fmt.Errorf("Error preparing elevated shell script: %s", err)
}
if err := writer.Flush(); err != nil {
return "", fmt.Errorf("Error preparing elevated shell script: %s", err)
}
tmpFile.Close()
f, err := os.Open(tmpFile.Name())
if err != nil {
return "", fmt.Errorf("Error opening temporary elevated shell script: %s", err)
}
defer f.Close()
uuid := uuid.TimeOrderedUUID()
path := fmt.Sprintf(`${env:TEMP}\packer-elevated-shell-%s.ps1`, uuid)
log.Printf("Uploading elevated shell wrapper for command [%s] to [%s] from [%s]", command, path, tmpFile.Name())
err = p.communicator.Upload(path, f, nil)
if err != nil {
return "", fmt.Errorf("Error preparing elevated shell script: %s", err)
}
// CMD formatted Path required for this op
path = fmt.Sprintf("%s-%s.ps1", "%TEMP%\\packer-elevated-shell", uuid)
return path, err
}

View File

@ -0,0 +1,656 @@
package powershell
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
//"log"
"os"
"regexp"
"strings"
"testing"
"time"
"github.com/mitchellh/packer/packer"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"inline": []interface{}{"foo", "bar"},
}
}
func init() {
//log.SetOutput(ioutil.Discard)
}
func TestProvisionerPrepare_extractScript(t *testing.T) {
config := testConfig()
p := new(Provisioner)
_ = p.Prepare(config)
file, err := extractScript(p)
if err != nil {
t.Fatalf("Should not be error: %s", err)
}
t.Logf("File: %s", file)
if strings.Index(file, os.TempDir()) != 0 {
t.Fatalf("Temp file should reside in %s. File location: %s", os.TempDir(), file)
}
// File contents should contain 2 lines concatenated by newlines: foo\nbar
readFile, err := ioutil.ReadFile(file)
expectedContents := "foo\nbar\n"
s := string(readFile[:])
if s != expectedContents {
t.Fatalf("Expected generated inlineScript to equal '%s', got '%s'", expectedContents, s)
}
}
func TestProvisioner_Impl(t *testing.T) {
var raw interface{}
raw = &Provisioner{}
if _, ok := raw.(packer.Provisioner); !ok {
t.Fatalf("must be a Provisioner")
}
}
func TestProvisionerPrepare_Defaults(t *testing.T) {
var p Provisioner
config := testConfig()
err := p.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if p.config.RemotePath != DefaultRemotePath {
t.Errorf("unexpected remote path: %s", p.config.RemotePath)
}
if p.config.ElevatedUser != "" {
t.Error("expected elevated_user to be empty")
}
if p.config.ElevatedPassword != "" {
t.Error("expected elevated_password to be empty")
}
if p.config.ExecuteCommand != "powershell \"& { {{.Vars}}{{.Path}}; exit $LastExitCode}\"" {
t.Fatalf("Default command should be powershell \"& { {{.Vars}}{{.Path}}; exit $LastExitCode}\", but got %s", p.config.ExecuteCommand)
}
if p.config.ElevatedExecuteCommand != "{{.Vars}}{{.Path}}" {
t.Fatalf("Default command should be powershell {{.Vars}}{{.Path}}, but got %s", p.config.ElevatedExecuteCommand)
}
if p.config.ValidExitCodes == nil {
t.Fatalf("ValidExitCodes should not be nil")
}
if p.config.ValidExitCodes != nil {
expCodes := []int{0}
for i, v := range p.config.ValidExitCodes {
if v != expCodes[i] {
t.Fatalf("Expected ValidExitCodes don't match actual")
}
}
}
if p.config.ElevatedEnvVarFormat != `$env:%s="%s"; ` {
t.Fatalf("Default command should be powershell \"{{.Vars}}{{.Path}}\", but got %s", p.config.ElevatedEnvVarFormat)
}
}
func TestProvisionerPrepare_Config(t *testing.T) {
config := testConfig()
config["elevated_user"] = "{{user `user`}}"
config["elevated_password"] = "{{user `password`}}"
config[packer.UserVariablesConfigKey] = map[string]string{
"user": "myusername",
"password": "mypassword",
}
var p Provisioner
err := p.Prepare(config)
if err != nil {
t.Fatalf("err: %s", err)
}
if p.config.ElevatedUser != "myusername" {
t.Fatalf("Expected 'myusername' for key `elevated_user`: %s", p.config.ElevatedUser)
}
if p.config.ElevatedPassword != "mypassword" {
t.Fatalf("Expected 'mypassword' for key `elevated_password`: %s", p.config.ElevatedPassword)
}
}
func TestProvisionerPrepare_InvalidKey(t *testing.T) {
var p Provisioner
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
err := p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestProvisionerPrepare_Elevated(t *testing.T) {
var p Provisioner
config := testConfig()
// Add a random key
config["elevated_user"] = "vagrant"
err := p.Prepare(config)
if err == nil {
t.Fatal("should have error (only provided elevated_user)")
}
config["elevated_password"] = "vagrant"
err = p.Prepare(config)
if err != nil {
t.Fatal("should not have error")
}
}
func TestProvisionerPrepare_Script(t *testing.T) {
config := testConfig()
delete(config, "inline")
config["script"] = "/this/should/not/exist"
p := new(Provisioner)
err := p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["script"] = tf.Name()
p = new(Provisioner)
err = p.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestProvisionerPrepare_ScriptAndInline(t *testing.T) {
var p Provisioner
config := testConfig()
delete(config, "inline")
delete(config, "script")
err := p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with both
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["inline"] = []interface{}{"foo"}
config["script"] = tf.Name()
err = p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestProvisionerPrepare_ScriptAndScripts(t *testing.T) {
var p Provisioner
config := testConfig()
// Test with both
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["inline"] = []interface{}{"foo"}
config["scripts"] = []string{tf.Name()}
err = p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestProvisionerPrepare_Scripts(t *testing.T) {
config := testConfig()
delete(config, "inline")
config["scripts"] = []string{}
p := new(Provisioner)
err := p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["scripts"] = []string{tf.Name()}
p = new(Provisioner)
err = p.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestProvisionerPrepare_EnvironmentVars(t *testing.T) {
config := testConfig()
// Test with a bad case
config["environment_vars"] = []string{"badvar", "good=var"}
p := new(Provisioner)
err := p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with a trickier case
config["environment_vars"] = []string{"=bad"}
p = new(Provisioner)
err = p.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test with a good case
// Note: baz= is a real env variable, just empty
config["environment_vars"] = []string{"FOO=bar", "baz="}
p = new(Provisioner)
err = p.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestProvisionerQuote_EnvironmentVars(t *testing.T) {
config := testConfig()
config["environment_vars"] = []string{"keyone=valueone", "keytwo=value\ntwo", "keythree='valuethree'", "keyfour='value\nfour'"}
p := new(Provisioner)
p.Prepare(config)
expectedValue := "keyone=valueone"
if p.config.Vars[0] != expectedValue {
t.Fatalf("%s should be equal to %s", p.config.Vars[0], expectedValue)
}
expectedValue = "keytwo=value\ntwo"
if p.config.Vars[1] != expectedValue {
t.Fatalf("%s should be equal to %s", p.config.Vars[1], expectedValue)
}
expectedValue = "keythree='valuethree'"
if p.config.Vars[2] != expectedValue {
t.Fatalf("%s should be equal to %s", p.config.Vars[2], expectedValue)
}
expectedValue = "keyfour='value\nfour'"
if p.config.Vars[3] != expectedValue {
t.Fatalf("%s should be equal to %s", p.config.Vars[3], expectedValue)
}
}
func testUi() *packer.BasicUi {
return &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
ErrorWriter: new(bytes.Buffer),
}
}
func testObjects() (packer.Ui, packer.Communicator) {
ui := testUi()
return ui, new(packer.MockCommunicator)
}
func TestProvisionerProvision_ValidExitCodes(t *testing.T) {
config := testConfig()
delete(config, "inline")
// Defaults provided by Packer
config["remote_path"] = "c:/Windows/Temp/inlineScript.bat"
config["inline"] = []string{"whoami"}
ui := testUi()
p := new(Provisioner)
// Defaults provided by Packer
p.config.PackerBuildName = "vmware"
p.config.PackerBuilderType = "iso"
p.config.ValidExitCodes = []int{0, 200}
comm := new(packer.MockCommunicator)
comm.StartExitStatus = 200
p.Prepare(config)
err := p.Provision(ui, comm)
if err != nil {
t.Fatal("should not have error")
}
}
func TestProvisionerProvision_InvalidExitCodes(t *testing.T) {
config := testConfig()
delete(config, "inline")
// Defaults provided by Packer
config["remote_path"] = "c:/Windows/Temp/inlineScript.bat"
config["inline"] = []string{"whoami"}
ui := testUi()
p := new(Provisioner)
// Defaults provided by Packer
p.config.PackerBuildName = "vmware"
p.config.PackerBuilderType = "iso"
p.config.ValidExitCodes = []int{0, 200}
comm := new(packer.MockCommunicator)
comm.StartExitStatus = 201 // Invalid!
p.Prepare(config)
err := p.Provision(ui, comm)
if err == nil {
t.Fatal("should have error")
}
}
func TestProvisionerProvision_Inline(t *testing.T) {
config := testConfig()
delete(config, "inline")
// Defaults provided by Packer
config["remote_path"] = "c:/Windows/Temp/inlineScript.bat"
config["inline"] = []string{"whoami"}
ui := testUi()
p := new(Provisioner)
// Defaults provided by Packer
p.config.PackerBuildName = "vmware"
p.config.PackerBuilderType = "iso"
comm := new(packer.MockCommunicator)
p.Prepare(config)
err := p.Provision(ui, comm)
if err != nil {
t.Fatal("should not have error")
}
expectedCommand := `powershell "& { $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; c:/Windows/Temp/inlineScript.bat; exit $LastExitCode}"`
// Should run the command without alteration
if comm.StartCmd.Command != expectedCommand {
t.Fatalf("Expect command to be: %s, got %s", expectedCommand, comm.StartCmd.Command)
}
envVars := make([]string, 2)
envVars[0] = "FOO=BAR"
envVars[1] = "BAR=BAZ"
config["environment_vars"] = envVars
config["remote_path"] = "c:/Windows/Temp/inlineScript.bat"
p.Prepare(config)
err = p.Provision(ui, comm)
if err != nil {
t.Fatal("should not have error")
}
expectedCommand = `powershell "& { $env:BAR=\"BAZ\"; $env:FOO=\"BAR\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; c:/Windows/Temp/inlineScript.bat; exit $LastExitCode}"`
// Should run the command without alteration
if comm.StartCmd.Command != expectedCommand {
t.Fatalf("Expect command to be: %s, got: %s", expectedCommand, comm.StartCmd.Command)
}
}
func TestProvisionerProvision_Scripts(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "packer")
defer os.Remove(tempFile.Name())
config := testConfig()
delete(config, "inline")
config["scripts"] = []string{tempFile.Name()}
config["packer_build_name"] = "foobuild"
config["packer_builder_type"] = "footype"
ui := testUi()
p := new(Provisioner)
comm := new(packer.MockCommunicator)
p.Prepare(config)
err := p.Provision(ui, comm)
if err != nil {
t.Fatal("should not have error")
}
//powershell -Command "$env:PACKER_BUILDER_TYPE=''"; powershell -Command "$env:PACKER_BUILD_NAME='foobuild'"; powershell -Command c:/Windows/Temp/script.ps1
expectedCommand := `powershell "& { $env:PACKER_BUILDER_TYPE=\"footype\"; $env:PACKER_BUILD_NAME=\"foobuild\"; c:/Windows/Temp/script.ps1; exit $LastExitCode}"`
// Should run the command without alteration
if comm.StartCmd.Command != expectedCommand {
t.Fatalf("Expect command to be %s NOT %s", expectedCommand, comm.StartCmd.Command)
}
}
func TestProvisionerProvision_ScriptsWithEnvVars(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "packer")
config := testConfig()
ui := testUi()
defer os.Remove(tempFile.Name())
delete(config, "inline")
config["scripts"] = []string{tempFile.Name()}
config["packer_build_name"] = "foobuild"
config["packer_builder_type"] = "footype"
// Env vars - currently should not effect them
envVars := make([]string, 2)
envVars[0] = "FOO=BAR"
envVars[1] = "BAR=BAZ"
config["environment_vars"] = envVars
p := new(Provisioner)
comm := new(packer.MockCommunicator)
p.Prepare(config)
err := p.Provision(ui, comm)
if err != nil {
t.Fatal("should not have error")
}
expectedCommand := `powershell "& { $env:BAR=\"BAZ\"; $env:FOO=\"BAR\"; $env:PACKER_BUILDER_TYPE=\"footype\"; $env:PACKER_BUILD_NAME=\"foobuild\"; c:/Windows/Temp/script.ps1; exit $LastExitCode}"`
// Should run the command without alteration
if comm.StartCmd.Command != expectedCommand {
t.Fatalf("Expect command to be %s NOT %s", expectedCommand, comm.StartCmd.Command)
}
}
func TestProvisionerProvision_UISlurp(t *testing.T) {
// UI should be called n times
// UI should receive following messages / output
}
func TestProvisioner_createFlattenedElevatedEnvVars_windows(t *testing.T) {
config := testConfig()
p := new(Provisioner)
err := p.Prepare(config)
if err != nil {
t.Fatalf("should not have error preparing config: %s", err)
}
// Defaults provided by Packer
p.config.PackerBuildName = "vmware"
p.config.PackerBuilderType = "iso"
// no user env var
flattenedEnvVars, err := p.createFlattenedEnvVars(true)
if err != nil {
t.Fatalf("should not have error creating flattened env vars: %s", err)
}
if flattenedEnvVars != "$env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; " {
t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars)
}
// single user env var
p.config.Vars = []string{"FOO=bar"}
flattenedEnvVars, err = p.createFlattenedEnvVars(true)
if err != nil {
t.Fatalf("should not have error creating flattened env vars: %s", err)
}
if flattenedEnvVars != "$env:FOO=\"bar\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; " {
t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars)
}
// multiple user env vars
p.config.Vars = []string{"FOO=bar", "BAZ=qux"}
flattenedEnvVars, err = p.createFlattenedEnvVars(true)
if err != nil {
t.Fatalf("should not have error creating flattened env vars: %s", err)
}
if flattenedEnvVars != "$env:BAZ=\"qux\"; $env:FOO=\"bar\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; " {
t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars)
}
}
func TestProvisioner_createFlattenedEnvVars_windows(t *testing.T) {
config := testConfig()
p := new(Provisioner)
err := p.Prepare(config)
if err != nil {
t.Fatalf("should not have error preparing config: %s", err)
}
// Defaults provided by Packer
p.config.PackerBuildName = "vmware"
p.config.PackerBuilderType = "iso"
// no user env var
flattenedEnvVars, err := p.createFlattenedEnvVars(false)
if err != nil {
t.Fatalf("should not have error creating flattened env vars: %s", err)
}
if flattenedEnvVars != "$env:PACKER_BUILDER_TYPE=\\\"iso\\\"; $env:PACKER_BUILD_NAME=\\\"vmware\\\"; " {
t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars)
}
// single user env var
p.config.Vars = []string{"FOO=bar"}
flattenedEnvVars, err = p.createFlattenedEnvVars(false)
if err != nil {
t.Fatalf("should not have error creating flattened env vars: %s", err)
}
if flattenedEnvVars != "$env:FOO=\\\"bar\\\"; $env:PACKER_BUILDER_TYPE=\\\"iso\\\"; $env:PACKER_BUILD_NAME=\\\"vmware\\\"; " {
t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars)
}
// multiple user env vars
p.config.Vars = []string{"FOO=bar", "BAZ=qux"}
flattenedEnvVars, err = p.createFlattenedEnvVars(false)
if err != nil {
t.Fatalf("should not have error creating flattened env vars: %s", err)
}
if flattenedEnvVars != "$env:BAZ=\\\"qux\\\"; $env:FOO=\\\"bar\\\"; $env:PACKER_BUILDER_TYPE=\\\"iso\\\"; $env:PACKER_BUILD_NAME=\\\"vmware\\\"; " {
t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars)
}
}
func TestProvision_createCommandText(t *testing.T) {
config := testConfig()
p := new(Provisioner)
comm := new(packer.MockCommunicator)
p.communicator = comm
_ = p.Prepare(config)
// Non-elevated
cmd, _ := p.createCommandText()
if cmd != "powershell \"& { $env:PACKER_BUILDER_TYPE=\\\"\\\"; $env:PACKER_BUILD_NAME=\\\"\\\"; c:/Windows/Temp/script.ps1; exit $LastExitCode}\"" {
t.Fatalf("Got unexpected non-elevated command: %s", cmd)
}
// Elevated
p.config.ElevatedUser = "vagrant"
p.config.ElevatedPassword = "vagrant"
cmd, _ = p.createCommandText()
matched, _ := regexp.MatchString("powershell -executionpolicy bypass -file \"%TEMP%(.{1})packer-elevated-shell.*", cmd)
if !matched {
t.Fatalf("Got unexpected elevated command: %s", cmd)
}
}
func TestProvision_generateElevatedShellRunner(t *testing.T) {
// Non-elevated
config := testConfig()
p := new(Provisioner)
p.Prepare(config)
comm := new(packer.MockCommunicator)
p.communicator = comm
path, err := p.generateElevatedRunner("whoami")
if err != nil {
t.Fatalf("Did not expect error: %s", err.Error())
}
if comm.UploadCalled != true {
t.Fatalf("Should have uploaded file")
}
matched, _ := regexp.MatchString("%TEMP%(.{1})packer-elevated-shell.*", path)
if !matched {
t.Fatalf("Got unexpected file: %s", path)
}
}
func TestRetryable(t *testing.T) {
config := testConfig()
count := 0
retryMe := func() error {
t.Logf("RetryMe, attempt number %d", count)
if count == 2 {
return nil
}
count++
return errors.New(fmt.Sprintf("Still waiting %d more times...", 2-count))
}
retryableSleep = 50 * time.Millisecond
p := new(Provisioner)
p.config.StartRetryTimeout = 155 * time.Millisecond
err := p.Prepare(config)
err = p.retryable(retryMe)
if err != nil {
t.Fatalf("should not have error retrying funuction")
}
count = 0
p.config.StartRetryTimeout = 10 * time.Millisecond
err = p.Prepare(config)
err = p.retryable(retryMe)
if err == nil {
t.Fatalf("should have error retrying funuction")
}
}
func TestCancel(t *testing.T) {
// Don't actually call Cancel() as it performs an os.Exit(0)
// which kills the 'go test' tool
}