From 040ff0706d6b514d5a5b927982efe857bfc528c1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 11:01:28 -0700 Subject: [PATCH] provisioner/powershell --- plugin/provisioner-powershell/main.go | 15 + provisioner/powershell/elevated.go | 87 +++ provisioner/powershell/powershell.go | 17 + provisioner/powershell/provisioner.go | 459 ++++++++++++++ provisioner/powershell/provisioner_test.go | 656 +++++++++++++++++++++ 5 files changed, 1234 insertions(+) create mode 100644 plugin/provisioner-powershell/main.go create mode 100644 provisioner/powershell/elevated.go create mode 100644 provisioner/powershell/powershell.go create mode 100644 provisioner/powershell/provisioner.go create mode 100644 provisioner/powershell/provisioner_test.go diff --git a/plugin/provisioner-powershell/main.go b/plugin/provisioner-powershell/main.go new file mode 100644 index 000000000..672bdb43f --- /dev/null +++ b/plugin/provisioner-powershell/main.go @@ -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() +} diff --git a/provisioner/powershell/elevated.go b/provisioner/powershell/elevated.go new file mode 100644 index 000000000..00bc72e4a --- /dev/null +++ b/provisioner/powershell/elevated.go @@ -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 = @' + + + + {{.TaskDescription}} + + + + {{.User}} + Password + HighestAvailable + + + + IgnoreNew + false + false + true + false + false + + false + false + + true + true + false + false + false + PT24H + 4 + + + + cmd + /c powershell.exe -EncodedCommand {{.EncodedCommand}} > %TEMP%\{{.TaskName}}.out 2>&1 + + + +'@ +$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`)) diff --git a/provisioner/powershell/powershell.go b/provisioner/powershell/powershell.go new file mode 100644 index 000000000..1f5a7ffad --- /dev/null +++ b/provisioner/powershell/powershell.go @@ -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) +} diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go new file mode 100644 index 000000000..0c2454d0d --- /dev/null +++ b/provisioner/powershell/provisioner.go @@ -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 +} diff --git a/provisioner/powershell/provisioner_test.go b/provisioner/powershell/provisioner_test.go new file mode 100644 index 000000000..78484c184 --- /dev/null +++ b/provisioner/powershell/provisioner_test.go @@ -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 +}