# # Author:: Dan DeLeo ( ) # Author:: Marc Paradise ( ) # Copyright:: Copyright 2012-2016, 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 module Mixin module WhyRun # ==ConvergeActions # ConvergeActions implements the logic for why run. A ConvergeActions # object wraps a collection of actions, which consist of a descriptive # string and a block/Proc. Actions are executed by calling #converge! # When why_run mode is enabled, each action's description will be # printed, but the block will not be called. Conversely, in normal mode, # the block is called, but the message is not printed. # # In general, this class should be accessed through the API provided by # Chef::Provider. class ConvergeActions attr_reader :actions def initialize(resource, run_context, action) @resource, @run_context = resource, run_context @actions = [] end def events @run_context.events end # Adds an action to the list. +descriptions+ can either be an Array of # Strings, or a single String describing the action; +block+ is a # block/proc that implements the action. def add_action(descriptions, &block) @actions << [descriptions, block] if (@resource.respond_to?(:is_guard_interpreter) && @resource.is_guard_interpreter) || !Chef::Config[:why_run] yield end events.resource_update_applied(@resource, @action, descriptions) end # True if there are no actions to execute. def empty? @actions.empty? end end # == ResourceRequirements # ResourceRequirements provides a framework for making assertions about # the host system's state. It also provides a mechanism for making # assumptions about what the system's state might have been when running # in why run mode. # # For example, consider a recipe that consists of a package resource and # a service resource. If the service's init script is installed by the # package, and Chef is running in why run mode, then the service resource # would fail when attempting to run `/etc/init.d/software-name status`. # In order to provide a more useful approximation of what would happen in # a real chef run, we want to instead assume that the service was created # but isn't running. The logic would look like this: # # # Hypothetical service provider demonstrating why run assumption logic. # # This isn't the actual API, it just shows the logic. # class HypotheticalServiceProvider < Chef::Provider # # def load_current_resource # # Make sure we have the init script available: # if ::File.exist?("/etc/init.d/some-service" # # If the init script exists, proceed as normal: # status_cmd = shell_out("/etc/init.d/some-service status") # if status_cmd.success? # @current_resource.status(:running) # else # @current_resource.status(:stopped) # end # else # if whyrun_mode? # # If the init script is not available, and we're in why run mode, # # assume that some previous action would've created it: # log("warning: init script '/etc/init.d/some-service' is not available") # log("warning: assuming that the init script would have been created, assuming the state of 'some-service' is 'stopped'") # @current_resource.status(:stopped) # else # raise "expected init script /etc/init.d/some-service doesn't exist" # end # end # end # # end # # In short, the code above does the following: # * runs a test to determine if a requirement is met: # `::File.exist?("/etc/init.d/some-service"` # * raises an error if the requirement is not met, and we're not in why # run mode. # * if we *are* in why run mode, print a message explaining the # situation, and run some code that makes an assumption about what the # state of the system would be. In this case, we also skip the normal # `load_current_resource` logic # * when the requirement *is* met, we run the normal `load_current_resource` # logic # # ResourceRequirements encapsulates the above logic in a more declarative API. # # === Examples # Assertions and assumptions should be created through the WhyRun#assert # method, which gets mixed in to providers. See that method's # documentation for examples. class ResourceRequirements # Implements the logic for a single assertion/assumption. See the # documentation for ResourceRequirements for full discussion. class Assertion class AssertionFailure < RuntimeError end def initialize @block_action = false @assertion_proc = nil @failure_message = nil @whyrun_message = nil @resource_modifier = nil @assertion_failed = false @exception_type = AssertionFailure end # Defines the code block that determines if a requirement is met. The # block should return a truthy value to indicate that the requirement # is met, and a falsey value if the requirement is not met. # # in a provider: # assert(:some_action) do |a| # # This provider requires the file /tmp/foo to exist: # a.assertion { ::File.exist?("/tmp/foo") } # end def assertion(&assertion_proc) @assertion_proc = assertion_proc end # Defines the failure message, and optionally the Exception class to # use when a requirement is not met. It works like `raise`: # # in a provider: # assert(:some_action) do |a| # # This example shows usage with 1 or 2 args by calling #failure_message twice. # # In practice you should only call this once per Assertion. # # # Set the Exception class explicitly # a.failure_message(Chef::Exceptions::MissingRequiredFile, "File /tmp/foo doesn't exist") # # Fallback to the default error class (AssertionFailure) # a.failure_message("File /tmp/foo" doesn't exist") # end def failure_message(*args) case args.size when 1 @failure_message = args[0] when 2 @exception_type, @failure_message = args[0], args[1] else raise ArgumentError, "#{self.class}#failure_message takes 1 or 2 arguments, you gave #{args.inspect}" end end # Defines a message and optionally provides a code block to execute # when the requirement is not met and Chef is executing in why run # mode # # If no failure_message is provided (above), then execution # will be allowed to continue in both whyrun and non-whyrun # mode # # With a service resource that requires /etc/init.d/service-name to exist: # # in a provider # assert(:start, :restart) do |a| # a.assertion { ::File.exist?("/etc/init.d/service-name") } # a.whyrun("Init script '/etc/init.d/service-name' doesn't exist, assuming a prior action would have created it.") do # # blindly assume that the service exists but is stopped in why run mode: # @new_resource.status(:stopped) # end # end def whyrun(message, &resource_modifier) @whyrun_message = message @resource_modifier = resource_modifier end # Prevents associated actions from being invoked in whyrun mode. # This will also stop further processing of assertions for a given action. # # An example from the template provider: if the source template doesn't exist # we can't parse it in the action_create block of template - something that we do # even in whyrun mode. Because the source template may have been created in an earlier # step, we still want to keep going in whyrun mode. # # assert(:create, :create_if_missing) do |a| # a.assertion { File::exists?(@new_resource.source) } # a.whyrun "Template source file does not exist, assuming it would have been created." # a.block_action! # end # def block_action! @block_action = true end def block_action? @block_action end def assertion_failed? @assertion_failed end # Runs the assertion/assumption logic. Will raise an Exception of the # type specified in #failure_message (or AssertionFailure by default) # if the requirement is not met and Chef is not running in why run # mode. An exception will also be raised if running in why run mode # and no why run message or block has been declared. def run(action, events, resource) if !@assertion_proc || !@assertion_proc.call @assertion_failed = true if Chef::Config[:why_run] && @whyrun_message events.provider_requirement_failed(action, resource, @exception_type, @failure_message) events.whyrun_assumption(action, resource, @whyrun_message) if @whyrun_message @resource_modifier.call if @resource_modifier else if @failure_message events.provider_requirement_failed(action, resource, @exception_type, @failure_message) raise @exception_type, @failure_message end end end end end def initialize(resource, run_context) @resource, @run_context = resource, run_context @assertions = Hash.new { |h, k| h[k] = [] } @blocked_actions = [] end def events @run_context.events end # Check to see if a given action is blocked by a failed assertion # # Takes the action name to be verified. def action_blocked?(action) @blocked_actions.include?(action) end # Define a new Assertion. # # Takes a list of action names for which the assertion should be made. # ==== Examples: # A File provider that requires the parent directory to exist: # # assert(:create, :create_if_missing) do |a| # parent_dir = File.basename(@new_resource.path) # a.assertion { ::File.directory?(parent_dir) } # a.failure_message(Exceptions::ParentDirectoryDoesNotExist, # "Can't create file #{@new_resource.path}: parent directory #{parent_dir} doesn't exist") # a.why_run("assuming parent directory #{parent_dir} would have been previously created" # end # # A service provider that requires the init script to exist: # # assert(:start, :restart) do |a| # a.assertion { ::File.exist?(@new_resource.init_script) } # a.failure_message(Exceptions::MissingInitScript, # "Can't check status of #{@new_resource}: init script #{@new_resource.init_script} is missing") # a.why_run("Assuming init script would have been created and service is stopped") do # @current_resource.status(:stopped) # end # end # # A File provider that will error out if you don't have permissions do # delete the file, *even in why run mode*: # # assert(:delete) do |a| # a.assertion { ::File.writable?(@new_resource.path) } # a.failure_message(Exceptions::InsufficientPrivileges, # "You don't have sufficient privileges to delete #{@new_resource.path}") # end # # A Template provider that will prevent action execution but continue the run in # whyrun mode if the template source is not available. # assert(:create, :create_if_missing) do |a| # a.assertion { File::exist?(@new_resource.source) } # a.failure_message Chef::Exceptions::TemplateError, "Template #{@new_resource.source} could not be found exist." # a.whyrun "Template source #{@new_resource.source} does not exist. Assuming it would have been created." # a.block_action! # end # # assert(:delete) do |a| # a.assertion { ::File.writable?(@new_resource.path) } # a.failure_message(Exceptions::InsufficientPrivileges, # "You don't have sufficient privileges to delete #{@new_resource.path}") # end def assert(*actions) assertion = Assertion.new yield assertion actions.each { |action| @assertions[action] << assertion } end # Run the assertion and assumption logic. def run(action) @assertions[action.to_sym].each do |a| a.run(action, events, @resource) if a.assertion_failed? && a.block_action? @blocked_actions << action break end end end end end end end