File Manager
Viewing File: why_run.rb
#
# Author:: Dan DeLeo ( <dan@chef.io> )
# Author:: Marc Paradise ( <marc@chef.io> )
# 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