diff options
Diffstat (limited to 'lib/chef/provider/powershell_script.rb')
-rw-r--r-- | lib/chef/provider/powershell_script.rb | 176 |
1 files changed, 129 insertions, 47 deletions
diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index f9dcd6d80c..ed44dee6ae 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -24,71 +24,153 @@ class Chef provides :powershell_script, os: "windows" + def initialize (new_resource, run_context) + super(new_resource, run_context, '.ps1') + add_exit_status_wrapper + end + + def action_run + valid_syntax = validate_script_syntax! + super if valid_syntax + end + + def flags + # Must use -File rather than -Command to launch the script + # file created by the base class that contains the script + # code -- otherwise, powershell.exe does not propagate the + # error status of a failed Windows process that ran at the + # end of the script, it gets changed to '1'. + interpreter_flags = [default_interpreter_flags, '-File'].join(' ') + + if ! (@new_resource.flags.nil?) + interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') + end + + interpreter_flags + end + protected - EXIT_STATUS_EXCEPTION_HANDLER = "\ntrap [Exception] {write-error -exception ($_.Exception.Message);exit 1}".freeze - EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -ne $true) { if ( $LASTEXITCODE ) {exit $LASTEXITCODE} else { exit 1 }}".freeze - EXIT_STATUS_RESET_SCRIPT = "\n$global:LASTEXITCODE=$null".freeze - # Process exit codes are strange with PowerShell. Unless you - # explicitly call exit in Powershell, the powershell.exe - # interpreter returns only 0 for success or 1 for failure. Since - # we'd like to get specific exit codes from executable tools run - # with Powershell, we do some work using the automatic variables - # $? and $LASTEXITCODE to return the process exit code of the - # last process run in the script if it is the last command - # executed, otherwise 0 or 1 based on whether $? is set to true - # (success, where we return 0) or false (where we return 1). - def normalize_script_exit_status( code ) - target_code = ( EXIT_STATUS_EXCEPTION_HANDLER + - EXIT_STATUS_RESET_SCRIPT + - "\n" + - code.to_s + - EXIT_STATUS_NORMALIZATION_SCRIPT ) - convert_boolean_return = @new_resource.convert_boolean_return - self.code = <<EOH -new-variable -name interpolatedexitcode -visibility private -value $#{convert_boolean_return} -new-variable -name chefscriptresult -visibility private -$chefscriptresult = { -#{target_code} -}.invokereturnasis() -if ($interpolatedexitcode -and $chefscriptresult.gettype().name -eq 'boolean') { exit [int32](!$chefscriptresult) } else { exit 0 } -EOH - Chef::Log.debug("powershell_script provider called with script code:\n\n#{code}\n") + # Process exit codes are strange with PowerShell and require + # special handling to cover common use cases. + def add_exit_status_wrapper + self.code = wrapper_script + Chef::Log.debug("powershell_script provider called with script code:\n\n#{@new_resource.code}\n") Chef::Log.debug("powershell_script provider will execute transformed code:\n\n#{self.code}\n") end - public + def validate_script_syntax! + interpreter_arguments = default_interpreter_flags.join(' ') + Tempfile.open(['chef_powershell_script-user-code', '.ps1']) do | user_script_file | + user_script_file.puts("{#{@new_resource.code}}") + user_script_file.close - def initialize (new_resource, run_context) - super(new_resource, run_context, '.ps1') - normalize_script_exit_status(new_resource.code) + validation_command = "\"#{interpreter}\" #{interpreter_arguments} -Command #{user_script_file.path}" + + # For consistency with other script resources, allow even syntax errors + # to be suppressed if the returns attribute would have suppressed it + # at converge. + valid_returns = [0] + specified_returns = @new_resource.returns.is_a?(Integer) ? + [@new_resource.returns] : + @new_resource.returns + valid_returns.concat([1]) if specified_returns.include?(1) + + result = shell_out!(validation_command, {returns: valid_returns}) + result.exitstatus == 0 + end end - def flags - default_flags = [ + def default_interpreter_flags + # 'Bypass' is preferable since it doesn't require user input confirmation + # for files such as PowerShell modules downloaded from the + # Internet. However, 'Bypass' is not supported prior to + # PowerShell 3.0, so the fallback is 'Unrestricted' + execution_policy = Chef::Platform.supports_powershell_execution_bypass?(run_context.node) ? 'Bypass' : 'Unrestricted' + + [ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Unrestricted", + "-ExecutionPolicy #{execution_policy}", # Powershell will hang if STDIN is redirected # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected - "-InputFormat None", - # Must use -File rather than -Command to launch the script - # file created by the base class that contains the script - # code -- otherwise, powershell.exe does not propagate the - # error status of a failed Windows process that ran at the - # end of the script, it gets changed to '1'. - "-File" + "-InputFormat None" ] + end - interpreter_flags = default_flags.join(' ') + # A wrapper script is used to launch user-supplied script while + # still obtaining useful process exit codes. Unless you + # explicitly call exit in Powershell, the powershell.exe + # interpreter returns only 0 for success or 1 for failure. Since + # we'd like to get specific exit codes from executable tools run + # with Powershell, we do some work using the automatic variables + # $? and $LASTEXITCODE to return the process exit code of the + # last process run in the script if it is the last command + # executed, otherwise 0 or 1 based on whether $? is set to true + # (success, where we return 0) or false (where we return 1). + def wrapper_script +<<-EOH +# Chef Client wrapper for powershell_script resources - if ! (@new_resource.flags.nil?) - interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') - end +# LASTEXITCODE can be uninitialized -- make it explictly 0 +# to avoid incorrect detection of failure (non-zero) codes +$global:LASTEXITCODE = 0 - interpreter_flags +# Catch any exceptions -- without this, exceptions will result +# In a zero return code instead of the desired non-zero code +# that indicates a failure +trap [Exception] {write-error ($_.Exception.Message);exit 1} + +# Variable state that should not be accessible to the user code +new-variable -name interpolatedexitcode -visibility private -value $#{@new_resource.convert_boolean_return} +new-variable -name chefscriptresult -visibility private + +# Initialize a variable we use to capture $? inside a block +$global:lastcmdlet = $null + +# Execute the user's code in a script block -- +$chefscriptresult = +{ + #{@new_resource.code} + + # This assignment doesn't affect the block's return value + $global:lastcmdlet = $? +}.invokereturnasis() + +# Assume failure status of 1 -- success cases +# will have to override this +$exitstatus = 1 + +# If convert_boolean_return is enabled, the block's return value +# gets precedence in determining our exit status +if ($interpolatedexitcode -and $chefscriptresult -ne $null -and $chefscriptresult.gettype().name -eq 'boolean') +{ + $exitstatus = [int32](!$chefscriptresult) +} +elseif ($lastcmdlet) +{ + # Otherwise, a successful cmdlet execution defines the status + $exitstatus = 0 +} +elseif ( $LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0 ) +{ + # If the cmdlet status is failed, allow the Win32 status + # in $LASTEXITCODE to define exit status. This handles the case + # where no cmdlets, only Win32 processes have run since $? + # will be set to $false whenever a Win32 process returns a non-zero + # status. + $exitstatus = $LASTEXITCODE +} + +# If this script is launched with -File, the process exit +# status of PowerShell.exe will be $exitstatus. If it was +# launched with -Command, it will be 0 if $exitstatus was 0, +# 1 (i.e. failed) otherwise. +exit $exitstatus +EOH end + end end end |