summaryrefslogtreecommitdiff
path: root/lib/chef/mixin/why_run.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/mixin/why_run.rb')
-rw-r--r--lib/chef/mixin/why_run.rb339
1 files changed, 339 insertions, 0 deletions
diff --git a/lib/chef/mixin/why_run.rb b/lib/chef/mixin/why_run.rb
new file mode 100644
index 0000000000..22c58c1e54
--- /dev/null
+++ b/lib/chef/mixin/why_run.rb
@@ -0,0 +1,339 @@
+#
+# Author:: Dan DeLeo ( <dan@opscode.com> )
+# Author:: Marc Paradise ( <marc@opscode.com> )
+# Copyright:: Copyright (c) 2012 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.
+#
+
+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]
+ end
+
+ # True if there are no actions to execute.
+ def empty?
+ @actions.empty?
+ end
+
+ # Iterate over the actions, and either print the action's message, or
+ # run its code block, depending on whether why_run mode is active.
+ def converge!
+ @actions.each do |descriptions, block|
+ if !Chef::Config[:why_run]
+ block.call
+ end
+ events.resource_update_applied(@resource, @action, descriptions)
+ end
+ 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 an dnon-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 soruce 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? and a.block_action?
+ @blocked_actions << action
+ return
+ end
+ end
+ end
+ end
+ end
+ end
+end