Commit daa0f2af authored by Adam Edwards's avatar Adam Edwards
Browse files

Merge pull request #1316 from opscode/adamed/guard-interpreter

CHEF-4553: Guard interpreter and powershell boolean awareness
parents 852cadd9 d8d8ae0e
......@@ -65,6 +65,7 @@
* Increase bootstrap log_level when knife -V -V is set (CHEF-3610)
* Knife cookbook test should honor chefignore (CHEF-4203)
* Fix ImmutableMash and ImmutableArray to_hash and to_a methods (CHEF-5132)
* guard_interpreter attribute: use powershell\_script, other script resources in guards (CHEF-4553)
## Last Release: 11.10.0 (02/06/2014)
......
......@@ -177,3 +177,111 @@ end
* installer_type - The type of package being installed. Can be auto-detected. Currently only :msi is supported.
* timeout - The time in seconds allowed for the package to successfully be installed. Defaults to 600 seconds.
* returns - Return codes that signal a successful installation. Defaults to 0.
### New resource attribute: `guard_interpreter`
All resources have a new attribute, `guard_interpreter`, which specifies a
Chef script resource that should be used to evaluate a string command
passed to a guard. Any attributes of the evaluating resource may be specified in
the options that normally follow the guard's command string argument. For example:
# Tell Chef to use bash to interpret the guard string.
# Then we can use a guard command that is valid for bash
# but not for csh for instance
bash 'backupsettings' do
guard_interpreter :bash
code 'cp ~/appsettings.json ~/backup/appsettings.json'
not_if '[[ -e ./appsettings.json ]]', :cwd => '~/backup'
end
The argument for `guard_interpreter` may be set to any of the following values:
* The symbol name for a Chef Resource derived from the Chef `script` resource
such as `:bash`, `:powershell_script`, etc.
* The symbol `:default` which means that a resource is not used to evaluate
the guard command argument, it is simply executed by the default shell as in
previous releases of Chef.
By default, `guard_interpreter` is set to `:default` in this release.
#### Attribute inheritance with `guard_interpreter`
When `guard_interpreter` is not set to `:default`, the resource that evaluates the command will
also inherit certain attribute values from the resource that contains the
guard.
Inherited attributes for all `script` resources are:
* `:cwd`
* `:environment`
* `:group`
* `:path`
* `:user`
* `:umask`
For the `powershell_script` resource, the following attribute is inherited:
* `:architecture`
Inherited attributes may be overridden by specifying the same attribute as an
argument to the guard itself.
#### Guard inheritance example
In the following example, the `:environment` hash only needs to be set once
since the `bash` resource that execute the guard will inherit the same value:
script "javatooling" do
environment {"JAVA_HOME" => '/usr/lib/java/jdk1.7/home'}
code 'java-based-daemon-ctl.sh -start'
not_if 'java-based-daemon-ctl.sh -test-started' # No need to specify environment again
end
### New `powershell_script` resource attribute: `convert_boolean_return`
The `powershell_script` resource has a new attribute, `convert_boolean_return`
that causes the script interpreter to return 0 if the last line of the command
evaluted by PowerShell results in a boolean PowerShell data type that is true, or 1 if
it results in a boolean PowerShell data type that is false. For example, the
following two fragments will run successfully without error:
powershell_script 'false' do
code '$false'
end
powershell_script 'true' do
code '$true'
end
But when `convert_boolean_return` is set to `true`, the "true" case above will
still succeed, but the false case will raise an exception:
# Raises an exception
powershell_script 'false' do
convert_boolean_return true
code '$false'
end
When used at recipe scope, the default value of `convert_boolean_return` is
`false` in this release. However, if `guard_interpreter` is set to
`:powershell_script`, the guard expression will be evaluted with a
`powershell_script` resource that has the `convert_boolean_return` attribute
set to `true`.
#### Guard command example
The behavior of `convert_boolean_return` is similar to the "$?"
expression's value after use of the `test` command in Unix-flavored shells and
its translation to an exit code for the shell. Since this attribute is set to
`true` when `powershell_script` is used via the `guard_interpreter` to
evaluate the guard expression, the behavior of `powershell_script` is very
similar to guards executed with Unix shell interpreters as seen below:
bash 'make_safe_backup' do
code 'cp ~/data/nodes.json ~/data/nodes.bak'
not_if 'test -e ~/data/nodes.bak'
end
# convert_boolean_return is true by default in guards
powershell_script 'make_safe_backup' do
guard_interpreter :powershell_script
code 'cp ~/data/nodes.json ~/data/nodes.bak'
not_if 'test-path ~/data/nodes.bak'
end
......@@ -133,6 +133,32 @@ If you're an advanced user of attribute precedence, you may find some attributes
The weekday attribute now accepts the weekday as a symbol, e.g. :monday or :thursday.
There is a new attribute named ```time``` that takes special cron time values as a symbol, such as :reboot or :monthly.
#### `guard_interpreter` attribute
All Chef resources now support the `guard_interpreter` attribute, which
enables you to use a Chef `script` such as `bash`, `powershell_script`,
`perl`, etc., to evaluate the string command passed to a
guard (i.e. `not_if` or `only_if` attribute). This addresses the related ticket
[CHEF-4553](https://tickets.opscode.com/browse/CHEF-4453) which is concerned
with the usability of the `powershell_script` resource, but also benefits
users of resources like `python`, `bash`, etc:
# See CHEF-4553 -- let powershell_script execute the guard
powershell_script 'make_logshare' do
guard_interpreter :powershell_script
code 'new-smbshare logshare $env:systemdrive\\logs'
not_if 'get-smbshare logshare'
end
#### `convert_boolean_return` attribute for `powershell_script`
When set to `true`, the `convert_boolean_return` attribute will allow any script executed by
`powershell_script` that exits with a PowerShell boolean data type to convert
PowerShell boolean `$true` to exit status 0 and `$false` to exit status 1.
The new attribute defaults to `false` except when the `powershell_script` resource is executing script passed to a guard attribute
via the `guard_interpreter` attribute in which case it is `true` by default.
#### knife bootstrap log_level
Running ```knife bootstrap -V -V``` will run the initial chef-client with a log level of debug.
......
#
# Author:: Adam Edwards (<adamed@getchef.com>)
# Copyright:: Copyright (c) 2014 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
class Chef
class GuardInterpreter
class DefaultGuardInterpreter
include Chef::Mixin::ShellOut
protected
def initialize(command, opts)
@command = command
@command_opts = opts
end
public
def evaluate
shell_out(@command, @command_opts).status.success?
rescue Chef::Exceptions::CommandTimeout
Chef::Log.warn "Command '#{@command}' timed out"
false
end
end
end
end
#
# Author:: Adam Edwards (<adamed@getchef.com>)
# Copyright:: Copyright (c) 2014 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'chef/guard_interpreter/default_guard_interpreter'
class Chef
class GuardInterpreter
class ResourceGuardInterpreter < DefaultGuardInterpreter
def initialize(parent_resource, command, opts, &block)
super(command, opts)
@parent_resource = parent_resource
@resource = get_interpreter_resource(parent_resource)
end
def evaluate
# Add attributes inherited from the parent class
# to the resource
merge_inherited_attributes
# Script resources have a code attribute, which is
# what is used to execute the command, so include
# that with attributes specified by caller in opts
block_attributes = @command_opts.merge({:code => @command})
# Handles cases like powershell_script where default
# attributes are different when used in a guard vs. not. For
# powershell_script in particular, this will go away when
# the one attribue that causes this changes its default to be
# the same after some period to prepare for deprecation
if @resource.class.respond_to?(:get_default_attributes)
block_attributes = @resource.class.send(:get_default_attributes, @command_opts).merge(block_attributes)
end
resource_block = block_from_attributes(block_attributes)
evaluate_action(nil, &resource_block)
end
protected
def evaluate_action(action=nil, &block)
@resource.instance_eval(&block)
run_action = action || @resource.action
begin
@resource.run_action(run_action)
resource_updated = @resource.updated
rescue Mixlib::ShellOut::ShellCommandFailed
resource_updated = nil
end
resource_updated
end
def get_interpreter_resource(parent_resource)
if parent_resource.nil? || parent_resource.node.nil?
raise ArgumentError, "Node for guard resource parent must not be nil"
end
resource_class = Chef::Resource.resource_for_node(parent_resource.guard_interpreter, parent_resource.node)
if resource_class.nil?
raise ArgumentError, "Specified guard_interpreter resource #{parent_resource.guard_interpreter.to_s} unknown for this platform"
end
if ! resource_class.ancestors.include?(Chef::Resource::Script)
raise ArgumentError, "Specified guard interpreter class #{resource_class} must be a kind of Chef::Resource::Script resource"
end
empty_events = Chef::EventDispatch::Dispatcher.new
anonymous_run_context = Chef::RunContext.new(parent_resource.node, {}, empty_events)
interpreter_resource = resource_class.new('Guard resource', anonymous_run_context)
interpreter_resource
end
def block_from_attributes(attributes)
Proc.new do
attributes.keys.each do |attribute_name|
send(attribute_name, attributes[attribute_name]) if respond_to?(attribute_name)
end
end
end
def merge_inherited_attributes
inherited_attributes = []
if @parent_resource.class.respond_to?(:guard_inherited_attributes)
inherited_attributes = @parent_resource.class.send(:guard_inherited_attributes)
end
if inherited_attributes && !inherited_attributes.empty?
inherited_attributes.each do |attribute|
if @parent_resource.respond_to?(attribute) && @resource.respond_to?(attribute)
parent_value = @parent_resource.send(attribute)
child_value = @resource.send(attribute)
if parent_value || child_value
@resource.send(attribute, parent_value)
end
end
end
end
end
end
end
end
......@@ -241,7 +241,9 @@ class Chef
:service => Chef::Provider::Service::Windows,
:user => Chef::Provider::User::Windows,
:group => Chef::Provider::Group::Windows,
:mount => Chef::Provider::Mount::Windows
:mount => Chef::Provider::Mount::Windows,
:batch => Chef::Provider::Batch,
:powershell_script => Chef::Provider::PowershellScript
}
},
:mingw32 => {
......@@ -250,7 +252,9 @@ class Chef
:service => Chef::Provider::Service::Windows,
:user => Chef::Provider::User::Windows,
:group => Chef::Provider::Group::Windows,
:mount => Chef::Provider::Mount::Windows
:mount => Chef::Provider::Mount::Windows,
:batch => Chef::Provider::Batch,
:powershell_script => Chef::Provider::PowershellScript
}
},
:windows => {
......@@ -259,7 +263,9 @@ class Chef
:service => Chef::Provider::Service::Windows,
:user => Chef::Provider::User::Windows,
:group => Chef::Provider::Group::Windows,
:mount => Chef::Provider::Mount::Windows
:mount => Chef::Provider::Mount::Windows,
:batch => Chef::Provider::Batch,
:powershell_script => Chef::Provider::PowershellScript
}
},
:solaris => {},
......
......@@ -23,9 +23,9 @@ class Chef
class PowershellScript < Chef::Provider::WindowsScript
protected
EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -eq $true) {exit 0} elseif ( $LASTEXITCODE -ne 0) {exit $LASTEXITCODE} else { exit 1 }"
EXIT_STATUS_RESET_SCRIPT = "$LASTEXITCODE=0\n"
EXIT_STATUS_EXCEPTION_HANDLER = "\ntrap [Exception] {write-error -exception ($_.Exception.Message);exit 1}".freeze
EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -ne $true) { if ( $LASTEXITCODE -ne 0) {exit $LASTEXITCODE} else { exit 1 }}".freeze
EXIT_STATUS_RESET_SCRIPT = "\n$LASTEXITCODE=0".freeze
# Process exit codes are strange with PowerShell. Unless you
# explicitly call exit in Powershell, the powershell.exe
......@@ -36,15 +36,28 @@ class Chef
# 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 NormalizeScriptExitStatus( code )
@code = (! code.nil?) ? ( EXIT_STATUS_RESET_SCRIPT + code + EXIT_STATUS_NORMALIZATION_SCRIPT ) : nil
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
@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
end
public
def initialize (new_resource, run_context)
super(new_resource, run_context, '.ps1')
NormalizeScriptExitStatus(new_resource.code)
normalize_script_exit_status(new_resource.code)
end
def flags
......
......@@ -23,6 +23,7 @@ require 'chef/dsl/data_query'
require 'chef/dsl/registry_helper'
require 'chef/dsl/reboot_pending'
require 'chef/mixin/convert_to_class_name'
require 'chef//guard_interpreter/resource_guard_interpreter'
require 'chef/resource/conditional'
require 'chef/resource/conditional_action_not_nothing'
require 'chef/resource_collection'
......@@ -249,6 +250,7 @@ F
@not_if = []
@only_if = []
@source_line = nil
@guard_interpreter = :default
@elapsed_time = 0
@node = run_context ? deprecated_ivar(run_context.node, :node, :warn) : nil
......@@ -401,6 +403,14 @@ F
ignore_failure(arg)
end
def guard_interpreter(arg=nil)
set_or_return(
:guard_interpreter,
arg,
:kind_of => Symbol
)
end
# Sets up a notification from this resource to the resource specified by +resource_spec+.
def notifies(action, resource_spec, timing=:delayed)
# when using old-style resources(:template => "/foo.txt") style, you
......@@ -552,7 +562,7 @@ F
# * evaluates to false if the block is false, or if the command returns a non-zero exit code.
def only_if(command=nil, opts={}, &block)
if command || block_given?
@only_if << Conditional.only_if(command, opts, &block)
@only_if << Conditional.only_if(self, command, opts, &block)
end
@only_if
end
......@@ -573,7 +583,7 @@ F
# * evaluates to false if the block is true, or if the command returns a 0 exit status.
def not_if(command=nil, opts={}, &block)
if command || block_given?
@not_if << Conditional.not_if(command, opts, &block)
@not_if << Conditional.not_if(self, command, opts, &block)
end
@not_if
end
......@@ -819,6 +829,5 @@ F
end
end
end
end
end
......@@ -17,6 +17,7 @@
#
require 'chef/mixin/shell_out'
require 'chef/guard_interpreter/resource_guard_interpreter'
class Chef
class Resource
......@@ -29,12 +30,12 @@ class Chef
private :new
end
def self.not_if(command=nil, command_opts={}, &block)
new(:not_if, command, command_opts, &block)
def self.not_if(parent_resource, command=nil, command_opts={}, &block)
new(:not_if, parent_resource, command, command_opts, &block)
end
def self.only_if(command=nil, command_opts={}, &block)
new(:only_if, command, command_opts, &block)
def self.only_if(parent_resource, command=nil, command_opts={}, &block)
new(:only_if, parent_resource, command, command_opts, &block)
end
attr_reader :positivity
......@@ -42,14 +43,16 @@ class Chef
attr_reader :command_opts
attr_reader :block
def initialize(positivity, command=nil, command_opts={}, &block)
def initialize(positivity, parent_resource, command=nil, command_opts={}, &block)
@positivity = positivity
case command
when String
@guard_interpreter = new_guard_interpreter(parent_resource, command, command_opts, &block)
@command, @command_opts = command, command_opts
@block = nil
when nil
raise ArgumentError, "only_if/not_if requires either a command or a block" unless block_given?
@guard_interpreter = nil
@command, @command_opts = nil, nil
@block = block
else
......@@ -69,11 +72,11 @@ class Chef
end
def evaluate
@command ? evaluate_command : evaluate_block
@guard_interpreter ? evaluate_command : evaluate_block
end
def evaluate_command
shell_out(@command, @command_opts).status.success?
@guard_interpreter.evaluate
rescue Chef::Exceptions::CommandTimeout
Chef::Log.warn "Command '#{@command}' timed out"
false
......@@ -100,6 +103,16 @@ class Chef
end
end
private
def new_guard_interpreter(parent_resource, command, opts)
if parent_resource.guard_interpreter == :default
guard_interpreter = Chef::GuardInterpreter::DefaultGuardInterpreter.new(command, opts)
else
guard_interpreter = Chef::GuardInterpreter::ResourceGuardInterpreter.new(parent_resource, command, opts)
end
end
end
end
end
......@@ -125,8 +125,6 @@ class Chef
)
end
end
end
end
......@@ -15,17 +15,39 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'chef/resource/windows_script'
class Chef
class Resource
class PowershellScript < Chef::Resource::WindowsScript
set_guard_inherited_attributes(:architecture)
def initialize(name, run_context=nil)
super(name, run_context, :powershell_script, "powershell.exe")
@convert_boolean_return = false
end
def convert_boolean_return(arg=nil)
set_or_return(
:convert_boolean_return,
arg,
:kind_of => [ FalseClass, TrueClass ]
)
end
protected
# Allow callers evaluating guards to request default
# attribute values. This is needed to allow
# convert_boolean_return to be true in guard context by default,
# and false by default otherwise. When this mode becomes the
# default for this resource, this method can be removed since
# guard context and recipe resource context will have the
# same behavior.
def self.get_default_attributes(opts)
{:convert_boolean_return => true}
end
end
end
end
......@@ -58,6 +58,31 @@ class Chef
)
end
def self.set_guard_inherited_attributes(*inherited_attributes)
@class_inherited_attributes = inherited_attributes
end
def self.guard_inherited_attributes(*inherited_attributes)
# Similar to patterns elsewhere, return attributes from this
# class and superclasses as a form of inheritance
ancestor_attributes = []
if superclass.respond_to?(:guard_inherited_attributes)
ancestor_attributes = superclass.guard_inherited_attributes
end
ancestor_attributes.concat(@class_inherited_attributes ? @class_inherited_attributes : []).uniq
end
set_guard_inherited_attributes(
:cwd,
:environment,
:group,
:path,
:user,
:umask
)
end
end
end
......@@ -52,11 +52,6 @@ class Chef
"cannot execute script with requested architecture '#{desired_architecture.to_s}' on a system with architecture '#{node_windows_architecture(node)}'"
end
end
def node
run_context && run_context.node
end
end
end