Skip to content

Commit

Permalink
Addressing issue #34 where powershell exit codes are not properly han…
Browse files Browse the repository at this point in the history
…dled for non-elevated provisioner. Added feature to allow user to specify valid exit codes in cases non-zero codes are in fact valid
  • Loading branch information
mefellows committed Mar 30, 2015
1 parent c3fcd2b commit e3ba9ee
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 9 deletions.
22 changes: 19 additions & 3 deletions provisioner/powershell/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ type config struct {
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"`

startRetryTimeout time.Duration
tpl *packer.ConfigTemplate
}
Expand Down Expand Up @@ -115,7 +120,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
}

if p.config.ExecuteCommand == "" {
p.config.ExecuteCommand = `powershell "& { {{.Vars}}{{.Path}} }"`
p.config.ExecuteCommand = `powershell "& { {{.Vars}}{{.Path}}; exit $LastExitCode}"`
}

if p.config.ElevatedExecuteCommand == "" {
Expand Down Expand Up @@ -157,6 +162,10 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
errors.New("Must supply an 'elevated_user' if 'elevated_password' provided"))
}

if p.config.ValidExitCodes == nil {
p.config.ValidExitCodes = []int{0}
}

if p.config.Script != "" {
p.config.Scripts = []string{p.config.Script}
}
Expand Down Expand Up @@ -319,8 +328,15 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
// 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)
// 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)
}
}

Expand Down
70 changes: 64 additions & 6 deletions provisioner/powershell/provisioner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,26 @@ func TestProvisionerPrepare_Defaults(t *testing.T) {
t.Error("expected elevated_password to be empty")
}

if p.config.ExecuteCommand != "powershell \"& { {{.Vars}}{{.Path}} }\"" {
if p.config.ExecuteCommand != "powershell \"& { {{.Vars}}{{.Path}}; exit $LastExitCode}\"" {
t.Fatalf("Default command should be powershell \"& { {{.Vars}}{{.Path}} }\", 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)
}
Expand Down Expand Up @@ -311,6 +323,52 @@ func testObjects() (packer.Ui, packer.Communicator) {
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")
Expand All @@ -331,7 +389,7 @@ func TestProvisionerProvision_Inline(t *testing.T) {
t.Fatal("should not have error")
}

expectedCommand := `powershell "& { $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; c:/Windows/Temp/inlineScript.bat }"`
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 {
Expand All @@ -350,7 +408,7 @@ func TestProvisionerProvision_Inline(t *testing.T) {
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 }"`
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 {
Expand All @@ -377,7 +435,7 @@ func TestProvisionerProvision_Scripts(t *testing.T) {
}

//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 }"`
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 {
Expand Down Expand Up @@ -410,7 +468,7 @@ func TestProvisionerProvision_ScriptsWithEnvVars(t *testing.T) {
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 }"`
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 {
Expand Down Expand Up @@ -524,7 +582,7 @@ func TestProvision_createCommandText(t *testing.T) {

// Non-elevated
cmd, _ := p.createCommandText()
if cmd != "powershell \"& { $env:PACKER_BUILDER_TYPE=\\\"\\\"; $env:PACKER_BUILD_NAME=\\\"\\\"; c:/Windows/Temp/script.ps1 }\"" {
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)
}

Expand Down

0 comments on commit e3ba9ee

Please sign in to comment.