summaryrefslogtreecommitdiff
path: root/lib/chef/formatters
diff options
context:
space:
mode:
authorSeth Chisamore <schisamo@opscode.com>2012-10-30 10:39:35 -0400
committerSeth Chisamore <schisamo@opscode.com>2012-10-30 10:39:35 -0400
commit24dc69a9a97e82a6e4207de68d6dcc664178249b (patch)
tree19bb289c9f88b4bbab066bc56b95d6d222fd5c35 /lib/chef/formatters
parent9348c1c9c80ee757354d624b7dc1b78ebc7605c4 (diff)
downloadchef-24dc69a9a97e82a6e4207de68d6dcc664178249b.tar.gz
[OC-3564] move core Chef to the repo root \o/ \m/
The opscode/chef repository now only contains the core Chef library code used by chef-client, knife and chef-solo!
Diffstat (limited to 'lib/chef/formatters')
-rw-r--r--lib/chef/formatters/base.rb247
-rw-r--r--lib/chef/formatters/doc.rb236
-rw-r--r--lib/chef/formatters/error_descriptor.rb66
-rw-r--r--lib/chef/formatters/error_inspectors.rb19
-rw-r--r--lib/chef/formatters/error_inspectors/api_error_formatting.rb111
-rw-r--r--lib/chef/formatters/error_inspectors/compile_error_inspector.rb106
-rw-r--r--lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb146
-rw-r--r--lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb80
-rw-r--r--lib/chef/formatters/error_inspectors/node_load_error_inspector.rb125
-rw-r--r--lib/chef/formatters/error_inspectors/registration_error_inspector.rb137
-rw-r--r--lib/chef/formatters/error_inspectors/resource_failure_inspector.rb108
-rw-r--r--lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb118
-rw-r--r--lib/chef/formatters/error_mapper.rb85
-rw-r--r--lib/chef/formatters/minimal.rb235
14 files changed, 1819 insertions, 0 deletions
diff --git a/lib/chef/formatters/base.rb b/lib/chef/formatters/base.rb
new file mode 100644
index 0000000000..d8b2e49d8e
--- /dev/null
+++ b/lib/chef/formatters/base.rb
@@ -0,0 +1,247 @@
+#
+# Author:: Tyler Cloke (<tyler@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.
+#
+
+require 'chef/event_dispatch/base'
+require 'chef/formatters/error_inspectors'
+require 'chef/formatters/error_descriptor'
+require 'chef/formatters/error_mapper'
+
+class Chef
+
+ # == Chef::Formatters
+ # Formatters handle printing output about the progress/status of a chef
+ # client run to the user's screen.
+ module Formatters
+
+ class UnknownFormatter < StandardError; end
+
+ def self.formatters_by_name
+ @formatters_by_name ||= {}
+ end
+
+ def self.register(name, formatter)
+ formatters_by_name[name.to_s] = formatter
+ end
+
+ def self.by_name(name)
+ formatters_by_name[name]
+ end
+
+ def self.available_formatters
+ formatters_by_name.keys
+ end
+
+ #--
+ # TODO: is it too clever to be defining new() on a module like this?
+ def self.new(name, out, err)
+ formatter_class = by_name(name) or
+ raise UnknownFormatter, "No output formatter found for #{name} (available: #{available_formatters.join(', ')})"
+
+ formatter_class.new(out, err)
+ end
+
+ # == Outputter
+ # Handles basic printing tasks like colorizing.
+ # --
+ # TODO: Duplicates functionality from knife, upfactor.
+ class Outputter
+
+ def initialize(out, err)
+ @out, @err = out, err
+ end
+
+ def highline
+ @highline ||= begin
+ require 'highline'
+ HighLine.new
+ end
+ end
+
+ def color(string, *colors)
+ if Chef::Config[:color]
+ @out.print highline.color(string, *colors)
+ else
+ @out.print string
+ end
+ end
+
+ alias :print :color
+
+ def puts(string, *colors)
+ if Chef::Config[:color]
+ @out.puts highline.color(string, *colors)
+ else
+ @out.puts string
+ end
+ end
+
+ end
+
+
+ # == Formatters::Base
+ # Base class that all formatters should inherit from.
+ class Base < EventDispatch::Base
+
+ include ErrorMapper
+
+ def self.cli_name(name)
+ Chef::Formatters.register(name, self)
+ end
+
+ attr_reader :out
+ attr_reader :err
+ attr_reader :output
+
+ def initialize(out, err)
+ @output = Outputter.new(out, err)
+ end
+
+ def puts(*args)
+ @output.puts(*args)
+ end
+
+ def print(*args)
+ @output.print(*args)
+ end
+
+ # Input: a Formatters::ErrorDescription object.
+ # Outputs error to SDOUT.
+ def display_error(description)
+ puts("")
+ description.display(output)
+ end
+
+ def registration_failed(node_name, exception, config)
+ #A Formatters::ErrorDescription object
+ description = ErrorMapper.registration_failed(node_name, exception, config)
+ display_error(description)
+ end
+
+ def node_load_failed(node_name, exception, config)
+ description = ErrorMapper.node_load_failed(node_name, exception, config)
+ display_error(description)
+ end
+
+ def run_list_expand_failed(node, exception)
+ description = ErrorMapper.run_list_expand_failed(node, exception)
+ display_error(description)
+ end
+
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ description = ErrorMapper.cookbook_resolution_failed(expanded_run_list, exception)
+ display_error(description)
+ end
+
+ def cookbook_sync_failed(cookbooks, exception)
+ description = ErrorMapper.cookbook_sync_failed(cookbooks, exception)
+ display_error(description)
+ end
+
+ def resource_failed(resource, action, exception)
+ description = ErrorMapper.resource_failed(resource, action, exception)
+ display_error(description)
+ end
+
+ # Generic callback for any attribute/library/lwrp/recipe file in a
+ # cookbook getting loaded. The per-filetype callbacks for file load are
+ # overriden so that they call this instead. This means that a subclass of
+ # Formatters::Base can implement #file_loaded to do the same thing for
+ # every kind of file that Chef loads from a recipe instead of
+ # implementing all the per-filetype callbacks.
+ def file_loaded(path)
+ end
+
+ # Generic callback for any attribute/library/lwrp/recipe file throwing an
+ # exception when loaded. Default behavior is to use CompileErrorInspector
+ # to print contextual info about the failure.
+ def file_load_failed(path, exception)
+ description = ErrorMapper.file_load_failed(path, exception)
+ display_error(description)
+ end
+
+ def recipe_not_found(exception)
+ description = ErrorMapper.file_load_failed(nil, exception)
+ display_error(description)
+ end
+
+ # Delegates to #file_loaded
+ def library_file_loaded(path)
+ file_loaded(path)
+ end
+
+ # Delegates to #file_load_failed
+ def library_file_load_failed(path, exception)
+ file_load_failed(path, exception)
+ end
+
+ # Delegates to #file_loaded
+ def lwrp_file_loaded(path)
+ file_loaded(path)
+ end
+
+ # Delegates to #file_load_failed
+ def lwrp_file_load_failed(path, exception)
+ file_load_failed(path, exception)
+ end
+
+ # Delegates to #file_loaded
+ def attribute_file_loaded(path)
+ file_loaded(path)
+ end
+
+ # Delegates to #file_load_failed
+ def attribute_file_load_failed(path, exception)
+ file_load_failed(path, exception)
+ end
+
+ # Delegates to #file_loaded
+ def definition_file_loaded(path)
+ file_loaded(path)
+ end
+
+ # Delegates to #file_load_failed
+ def definition_file_load_failed(path, exception)
+ file_load_failed(path, exception)
+ end
+
+ # Delegates to #file_loaded
+ def recipe_file_loaded(path)
+ file_loaded(path)
+ end
+
+ # Delegates to #file_load_failed
+ def recipe_file_load_failed(path, exception)
+ file_load_failed(path, exception)
+ end
+
+ end
+
+
+ # == NullFormatter
+ # Formatter that doesn't actually produce any ouput. You can use this to
+ # disable the use of output formatters.
+ class NullFormatter < Base
+
+ cli_name(:null)
+
+ end
+
+ end
+end
+
diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb
new file mode 100644
index 0000000000..e5b2ab17d7
--- /dev/null
+++ b/lib/chef/formatters/doc.rb
@@ -0,0 +1,236 @@
+require 'chef/formatters/base'
+require 'chef/config'
+
+class Chef
+ module Formatters
+ #--
+ # TODO: not sold on the name, but the output is similar to what rspec calls
+ # "specdoc"
+ class Doc < Formatters::Base
+
+ cli_name(:doc)
+
+ def initialize(out, err)
+ super
+
+ @updated_resources = 0
+ end
+
+ def run_start(version)
+ puts "Starting Chef Client, version #{version}"
+ end
+
+ def run_completed(node)
+ if Chef::Config[:whyrun]
+ puts "Chef Client finished, #{@updated_resources} resources would have been updated"
+ else
+ puts "Chef Client finished, #{@updated_resources} resources updated"
+ end
+ end
+
+ def run_failed(exception)
+ if Chef::Config[:whyrun]
+ puts "Chef Client failed. #{@updated_resources} resources would have been updated"
+ else
+ puts "Chef Client failed. #{@updated_resources} resources updated"
+ end
+ end
+
+ # Called right after ohai runs.
+ def ohai_completed(node)
+ end
+
+ # Already have a client key, assuming this node has registered.
+ def skipping_registration(node_name, config)
+ end
+
+ # About to attempt to register as +node_name+
+ def registration_start(node_name, config)
+ puts "Creating a new client identity for #{node_name} using the validator key."
+ end
+
+ def registration_completed
+ end
+
+ def node_load_start(node_name, config)
+ end
+
+ # Failed to load node data from the server
+ def node_load_failed(node_name, exception, config)
+ super
+ end
+
+ # Default and override attrs from roles have been computed, but not yet applied.
+ # Normal attrs from JSON have been added to the node.
+ def node_load_completed(node, expanded_run_list, config)
+ end
+
+ # Called before the cookbook collection is fetched from the server.
+ def cookbook_resolution_start(expanded_run_list)
+ puts "resolving cookbooks for run list: #{expanded_run_list.inspect}"
+ end
+
+ # Called when there is an error getting the cookbook collection from the
+ # server.
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ super
+ end
+
+ # Called when the cookbook collection is returned from the server.
+ def cookbook_resolution_complete(cookbook_collection)
+ end
+
+ # Called before unneeded cookbooks are removed
+ def cookbook_clean_start
+ end
+
+ # Called after the file at +path+ is removed. It may be removed if the
+ # cookbook containing it was removed from the run list, or if the file was
+ # removed from the cookbook.
+ def removed_cookbook_file(path)
+ end
+
+ # Called when cookbook cleaning is finished.
+ def cookbook_clean_complete
+ end
+
+ # Called before cookbook sync starts
+ def cookbook_sync_start(cookbook_count)
+ puts "Synchronizing Cookbooks:"
+ end
+
+ # Called when cookbook +cookbook_name+ has been sync'd
+ def synchronized_cookbook(cookbook_name)
+ puts " - #{cookbook_name}"
+ end
+
+ # Called when an individual file in a cookbook has been updated
+ def updated_cookbook_file(cookbook_name, path)
+ end
+
+ # Called after all cookbooks have been sync'd.
+ def cookbook_sync_complete
+ end
+
+ # Called when cookbook loading starts.
+ def library_load_start(file_count)
+ puts "Compiling Cookbooks..."
+ end
+
+ # Called after a file in a cookbook is loaded.
+ def file_loaded(path)
+ end
+
+ # Called when recipes have been loaded.
+ def recipe_load_complete
+ end
+
+ # Called before convergence starts
+ def converge_start(run_context)
+ puts "Converging #{run_context.resource_collection.all_resources.size} resources"
+ end
+
+ # Called when the converge phase is finished.
+ def converge_complete
+ end
+
+ # Called before action is executed on a resource.
+ def resource_action_start(resource, action, notification_type=nil, notifier=nil)
+ if resource.cookbook_name && resource.recipe_name
+ resource_recipe = "#{resource.cookbook_name}::#{resource.recipe_name}"
+ else
+ resource_recipe = "<Dynamically Defined Resource>"
+ end
+
+ if resource_recipe != @current_recipe
+ puts "Recipe: #{resource_recipe}"
+ @current_recipe = resource_recipe
+ end
+ # TODO: info about notifies
+ print " * #{resource} action #{action}"
+ end
+
+ # Called when a resource fails, but will retry.
+ def resource_failed_retriable(resource, action, retry_count, exception)
+ end
+
+ # Called when a resource fails and will not be retried.
+ def resource_failed(resource, action, exception)
+ super
+ end
+
+ # Called when a resource action has been skipped b/c of a conditional
+ def resource_skipped(resource, action, conditional)
+ # TODO: more info about conditional
+ puts " (skipped due to #{conditional.positivity})"
+ end
+
+ # Called after #load_current_resource has run.
+ def resource_current_state_loaded(resource, action, current_resource)
+ end
+
+ # Called when a resource has no converge actions, e.g., it was already correct.
+ def resource_up_to_date(resource, action)
+ puts " (up to date)"
+ end
+
+ def resource_bypassed(resource, action, provider)
+ puts " (Skipped: whyrun not supported by provider #{provider.class.name})"
+ end
+
+ def output_record(line)
+
+ end
+
+ # Called when a change has been made to a resource. May be called multiple
+ # times per resource, e.g., a file may have its content updated, and then
+ # its permissions updated.
+ def resource_update_applied(resource, action, update)
+ prefix = Chef::Config[:why_run] ? "Would " : ""
+ Array(update).each do |line|
+ next if line.nil?
+ output_record line
+ if line.kind_of? String
+ @output.color "\n - #{prefix}#{line}", :green
+ elsif line.kind_of? Array
+ # Expanded output - delta
+ # @todo should we have a resource_update_delta callback?
+ line.each do |detail|
+ @output.color "\n #{detail}", :white
+ end
+ end
+ end
+ end
+
+ # Called after a resource has been completely converged.
+ def resource_updated(resource, action)
+ @updated_resources += 1
+ puts "\n"
+ end
+
+ # Called when resource current state load is skipped due to the provider
+ # not supporting whyrun mode.
+ def resource_current_state_load_bypassed(resource, action, current_resource)
+ @output.color("\n * Whyrun not supported for #{resource}, bypassing load.", :yellow)
+ end
+
+ # Called when a provider makes an assumption after a failed assertion
+ # in whyrun mode, in order to allow execution to continue
+ def whyrun_assumption(action, resource, message)
+ return unless message
+ [ message ].flatten.each do |line|
+ @output.color("\n * #{line}", :yellow)
+ end
+ end
+
+ # Called when an assertion declared by a provider fails
+ def provider_requirement_failed(action, resource, exception, message)
+ return unless message
+ color = Chef::Config[:why_run] ? :yellow : :red
+ [ message ].flatten.each do |line|
+ @output.color("\n * #{line}", color)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_descriptor.rb b/lib/chef/formatters/error_descriptor.rb
new file mode 100644
index 0000000000..abf10076be
--- /dev/null
+++ b/lib/chef/formatters/error_descriptor.rb
@@ -0,0 +1,66 @@
+#
+# Author:: Tyler Cloke (<tyler@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 Formatters
+ # == Formatters::ErrorDescription
+ # Class for displaying errors on STDOUT.
+ class ErrorDescription
+
+ attr_reader :sections
+
+ def initialize(title)
+ @title = title
+ @sections = []
+ end
+
+ def section(heading, text)
+ @sections << [heading, text]
+ end
+
+ def display(out)
+ out.puts "=" * 80
+ out.puts @title, :red
+ out.puts "=" * 80
+ out.puts "\n"
+ sections.each do |section|
+ display_section(section, out)
+ end
+ end
+
+ def for_json()
+ {
+ 'title' => @title,
+ 'sections' => @sections
+ }
+ end
+
+ private
+
+ def display_section(section, out)
+ heading, text = section
+ out.puts heading
+ out.puts "-" * heading.size
+ out.puts text
+ out.puts "\n"
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors.rb b/lib/chef/formatters/error_inspectors.rb
new file mode 100644
index 0000000000..418457322d
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors.rb
@@ -0,0 +1,19 @@
+require 'chef/formatters/error_inspectors/node_load_error_inspector'
+require "chef/formatters/error_inspectors/registration_error_inspector"
+require 'chef/formatters/error_inspectors/compile_error_inspector'
+require 'chef/formatters/error_inspectors/resource_failure_inspector'
+require 'chef/formatters/error_inspectors/run_list_expansion_error_inspector'
+require 'chef/formatters/error_inspectors/cookbook_resolve_error_inspector'
+require "chef/formatters/error_inspectors/cookbook_sync_error_inspector"
+
+class Chef
+ module Formatters
+
+ # == ErrorInspectors
+ # Error inspectors wrap exceptions and contextual information. They
+ # generate diagnostic messages about possible causes of the error for user
+ # consumption.
+ module ErrorInspectors
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/api_error_formatting.rb b/lib/chef/formatters/error_inspectors/api_error_formatting.rb
new file mode 100644
index 0000000000..bb5379ed3f
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/api_error_formatting.rb
@@ -0,0 +1,111 @@
+#--
+# Author:: Daniel DeLeo (<dan@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 Formatters
+
+ module APIErrorFormatting
+
+ NETWORK_ERROR_CLASSES = [Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, SocketError]
+
+ def describe_network_errors(error_description)
+ error_description.section("Networking Error:",<<-E)
+#{exception.message}
+
+Your chef_server_url may be misconfigured, or the network could be down.
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+E
+ end
+
+ def describe_401_error(error_description)
+ if clock_skew?
+ error_description.section("Authentication Error:",<<-E)
+Failed to authenticate to the chef server (http 401).
+The request failed because your clock has drifted by more than 15 minutes.
+Syncing your clock to an NTP Time source should resolve the issue.
+E
+ else
+ error_description.section("Authentication Error:",<<-E)
+Failed to authenticate to the chef server (http 401).
+E
+
+ error_description.section("Server Response:", format_rest_error)
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+node_name "#{username}"
+client_key "#{api_key}"
+
+If these settings are correct, your client_key may be invalid.
+E
+ end
+ end
+
+ def describe_400_error(error_description)
+ error_description.section("Invalid Request Data:",<<-E)
+The data in your request was invalid (HTTP 400).
+E
+ error_description.section("Server Response:",format_rest_error)
+ end
+
+ def describe_500_error(error_description)
+ error_description.section("Unknown Server Error:",<<-E)
+The server had a fatal error attempting to load the node data.
+E
+ error_description.section("Server Response:", format_rest_error)
+ end
+
+ def describe_503_error(error_description)
+ error_description.section("Server Unavailable","The Chef Server is temporarily unavailable")
+ error_description.section("Server Response:", format_rest_error)
+ end
+
+
+ # Fallback for unexpected/uncommon http errors
+ def describe_http_error(error_description)
+ error_description.section("Unexpected API Request Failure:", format_rest_error)
+ end
+
+ # Parses JSON from the error response sent by Chef Server and returns the
+ # error message
+ def format_rest_error
+ Array(Chef::JSONCompat.from_json(exception.response.body)["error"]).join('; ')
+ rescue Exception
+ exception.response.body
+ end
+
+ def username
+ config[:node_name]
+ end
+
+ def api_key
+ config[:client_key]
+ end
+
+ def server_url
+ config[:chef_server_url]
+ end
+
+ def clock_skew?
+ exception.response.body =~ /synchronize the clock/i
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/compile_error_inspector.rb b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb
new file mode 100644
index 0000000000..1fa8a70b52
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb
@@ -0,0 +1,106 @@
+#--
+# Author:: Daniel DeLeo (<dan@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 Formatters
+ module ErrorInspectors
+
+ # == CompileErrorInspector
+ # Wraps exceptions that occur during the compile phase of a Chef run and
+ # tries to find the code responsible for the error.
+ class CompileErrorInspector
+
+ attr_reader :path
+ attr_reader :exception
+
+ def initialize(path, exception)
+ @path, @exception = path, exception
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when Chef::Exceptions::RecipeNotFound
+ error_description.section(exception.class.name, exception.message)
+ else
+ error_description.section(exception.class.name, exception.message)
+
+ traceback = filtered_bt.map {|line| " #{line}"}.join("\n")
+ error_description.section("Cookbook Trace:", traceback)
+ error_description.section("Relevant File Content:", context)
+ end
+ end
+
+ def context
+ context_lines = []
+ context_lines << "#{culprit_file}:\n\n"
+ Range.new(display_lower_bound, display_upper_bound).each do |i|
+ line_nr = (i + 1).to_s.rjust(3)
+ indicator = (i + 1) == culprit_line ? ">> " : ": "
+ context_lines << "#{line_nr}#{indicator}#{file_lines[i]}"
+ end
+ context_lines.join("")
+ end
+
+ def display_lower_bound
+ lower = (culprit_line - 8)
+ lower = 0 if lower < 0
+ lower
+ end
+
+ def display_upper_bound
+ upper = (culprit_line + 8)
+ upper = file_lines.size if upper > file_lines.size
+ upper
+ end
+
+ def file_lines
+ @file_lines ||= IO.readlines(culprit_file)
+ end
+
+ def culprit_backtrace_entry
+ @culprit_backtrace_entry ||= begin
+ bt_entry = filtered_bt.first
+ Chef::Log.debug("backtrace entry for compile error: '#{bt_entry}'")
+ bt_entry
+ end
+ end
+
+ def culprit_line
+ @culprit_line ||= begin
+ line_number = culprit_backtrace_entry[/^(?:.\:)?[^:]+:([\d]+)/,1].to_i
+ Chef::Log.debug("Line number of compile error: '#{line_number}'")
+ line_number
+ end
+ end
+
+ def culprit_file
+ @culprit_file ||= culprit_backtrace_entry[/^((?:.\:)?[^:]+):([\d]+)/,1]
+ end
+
+ def filtered_bt
+ filters = Array(Chef::Config.cookbook_path).map {|p| /^#{Regexp.escape(p)}/ }
+ r = exception.backtrace.select {|line| filters.any? {|filter| line =~ filter }}
+ Chef::Log.debug("filtered backtrace of compile error: #{r.join(",")}")
+ return r.count > 0 ? r : exception.backtrace
+ end
+
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb b/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb
new file mode 100644
index 0000000000..5642070336
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb
@@ -0,0 +1,146 @@
+#--
+# Author:: Daniel DeLeo (<dan@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.
+#
+
+require 'chef/formatters/error_inspectors/api_error_formatting'
+
+class Chef
+ module Formatters
+ module ErrorInspectors
+ class CookbookResolveErrorInspector
+
+ attr_reader :exception
+ attr_reader :expanded_run_list
+
+ include APIErrorFormatting
+
+ def initialize(expanded_run_list, exception)
+ @expanded_run_list = expanded_run_list
+ @exception = exception
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when Net::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(error_description)
+ when *NETWORK_ERROR_CLASSES
+ describe_network_errors(error_description)
+ else
+ error_description.section("Unexpected Error:","#{exception.class.name}: #{exception.message}")
+ end
+ end
+
+ def humanize_http_exception(error_description)
+ response = exception.response
+ case response
+ when Net::HTTPUnauthorized
+ # TODO: this is where you'd see conflicts b/c of username/clientname stuff
+ describe_401_error(error_description)
+ when Net::HTTPForbidden
+ # TODO: we're rescuing errors from Node.find_or_create
+ # * could be no write on nodes container
+ # * could be no read on the node
+ error_description.section("Authorization Error",<<-E)
+This client is not authorized to read some of the information required to
+access its coobooks (HTTP 403).
+
+To access its cookbooks, a client needs to be able to read its environment and
+all of the cookbooks in its expanded run list.
+E
+ error_description.section("Expanded Run List:", expanded_run_list_ul)
+ error_description.section("Server Response:", format_rest_error)
+ when Net::HTTPPreconditionFailed
+ describe_412_error(error_description)
+ when Net::HTTPBadRequest
+ describe_400_error(error_description)
+ when Net::HTTPNotFound
+ when Net::HTTPInternalServerError
+ describe_500_error(error_description)
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable
+ describe_503_error(error_description)
+ else
+ describe_http_error(error_description)
+ end
+ end
+
+ def describe_412_error(error_description)
+ explanation = ""
+ error_reasons = extract_412_error_message
+ if !error_reasons.respond_to?(:key?)
+ explanation << error_reasons.to_s
+ else
+ if error_reasons.key?("non_existent_cookbooks") && !Array(error_reasons["non_existent_cookbooks"]).empty?
+ explanation << "The following cookbooks are required by the client but don't exist on the server:\n"
+ Array(error_reasons["non_existent_cookbooks"]).each do |cookbook|
+ explanation << "* #{cookbook}\n"
+ end
+ explanation << "\n"
+ end
+ if error_reasons.key?("cookbooks_with_no_versions") && !Array(error_reasons["cookbooks_with_no_versions"]).empty?
+ explanation << "The following cookbooks exist on the server, but there is no version that meets\nthe version constraints in this environment:\n"
+ Array(error_reasons["cookbooks_with_no_versions"]).each do |cookbook|
+ explanation << "* #{cookbook}\n"
+ end
+ explanation << "\n"
+ end
+ end
+
+ error_description.section("Missing Cookbooks:", explanation)
+ error_description.section("Expanded Run List:", expanded_run_list_ul)
+ end
+
+ def expanded_run_list_ul
+ @expanded_run_list.map {|i| "* #{i}"}.join("\n")
+ end
+
+ # In my tests, the error from the server is double JSON encoded, but we
+ # should not rely on this not getting fixed.
+ #
+ # Return *should* be a Hash like this:
+ # { "non_existent_cookbooks" => ["nope"],
+ # "cookbooks_with_no_versions" => [],
+ # "message" => "Run list contains invalid items: no such cookbook nope."}
+ def extract_412_error_message
+ # Example:
+ # "{\"error\":[\"{\\\"non_existent_cookbooks\\\":[\\\"nope\\\"],\\\"cookbooks_with_no_versions\\\":[],\\\"message\\\":\\\"Run list contains invalid items: no such cookbook nope.\\\"}\"]}"
+
+ wrapped_error_message = attempt_json_parse(exception.response.body)
+ unless wrapped_error_message.kind_of?(Hash) && wrapped_error_message.key?("error")
+ return wrapped_error_message.to_s
+ end
+
+ error_description = Array(wrapped_error_message["error"]).first
+ if error_description.kind_of?(Hash)
+ return error_description
+ end
+ attempt_json_parse(error_description)
+ end
+
+ private
+
+ def attempt_json_parse(maybe_json_string)
+ Chef::JSONCompat.from_json(maybe_json_string)
+ rescue Exception
+ maybe_json_string
+ end
+
+
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb b/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb
new file mode 100644
index 0000000000..054984a50e
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb
@@ -0,0 +1,80 @@
+#--
+# Author:: Daniel DeLeo (<dan@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.
+#
+
+require 'chef/formatters/error_inspectors/api_error_formatting'
+
+class Chef
+ module Formatters
+ module ErrorInspectors
+
+ # == CookbookSyncErrorInspector
+ # Generates human-friendly explanations for errors encountered during
+ # cookbook sync.
+ #--
+ # TODO: Not sure what errors are commonly seen during cookbook sync, so
+ # the messaging is kinda generic.
+ class CookbookSyncErrorInspector
+
+ include APIErrorFormatting
+
+ attr_reader :exception
+ attr_reader :cookbooks
+
+ def initialize(cookbooks, exception)
+ @cookbooks, @exception = cookbooks, exception
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when *NETWORK_ERROR_CLASSES
+ describe_network_errors(error_description)
+ when Net::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(error_description)
+ else
+ error_description.section("Unexpected Error:","#{exception.class.name}: #{exception.message}")
+ end
+ end
+
+ def config
+ Chef::Config
+ end
+
+ def humanize_http_exception(error_description)
+ response = exception.response
+ case response
+ when Net::HTTPUnauthorized
+ # TODO: this is where you'd see conflicts b/c of username/clientname stuff
+ describe_401_error(error_description)
+ when Net::HTTPBadRequest
+ describe_400_error(error_description)
+ when Net::HTTPNotFound
+ when Net::HTTPInternalServerError
+ describe_500_error(error_description)
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable
+ describe_503_error(error_description)
+ else
+ describe_http_error(error_description)
+ end
+ end
+
+ end
+ end
+ end
+end
+
+
diff --git a/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb b/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb
new file mode 100644
index 0000000000..7168ac0edb
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb
@@ -0,0 +1,125 @@
+#--
+# Author:: Daniel DeLeo (<dan@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.
+#
+
+require 'chef/formatters/error_inspectors/api_error_formatting'
+
+class Chef
+ module Formatters
+ module ErrorInspectors
+
+
+ # == APIErrorInspector
+ # Wraps exceptions caused by API calls to the server.
+ class NodeLoadErrorInspector
+
+ include APIErrorFormatting
+
+ attr_reader :exception
+ attr_reader :node_name
+ attr_reader :config
+
+ def initialize(node_name, exception, config)
+ @node_name = node_name
+ @exception = exception
+ @config = config
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when Net::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(error_description)
+ when *NETWORK_ERROR_CLASSES
+ describe_network_errors(error_description)
+ when Chef::Exceptions::PrivateKeyMissing
+ error_description.section("Private Key Not Found:",<<-E)
+Your private key could not be loaded. If the key file exists, ensure that it is
+readable by chef-client.
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+client_key "#{api_key}"
+E
+ else
+ error_description.section("Unexpected Error:","#{exception.class.name}: #{exception.message}")
+ end
+ end
+
+ def humanize_http_exception(error_description)
+ response = exception.response
+ case response
+ when Net::HTTPUnauthorized
+ # TODO: this is where you'd see conflicts b/c of username/clientname stuff
+ describe_401_error(error_description)
+ when Net::HTTPForbidden
+ # TODO: we're rescuing errors from Node.find_or_create
+ # * could be no write on nodes container
+ # * could be no read on the node
+ error_description.section("Authorization Error",<<-E)
+Your client is not authorized to load the node data (HTTP 403).
+E
+ error_description.section("Server Response:", format_rest_error)
+
+ error_description.section("Possible Causes:",<<-E)
+* Your client (#{username}) may have misconfigured authorization permissions.
+E
+ when Net::HTTPBadRequest
+ describe_400_error(error_description)
+ when Net::HTTPNotFound
+ describe_404_error(error_description)
+ when Net::HTTPInternalServerError
+ describe_500_error(error_description)
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable
+ describe_503_error(error_description)
+ else
+ describe_http_error(error_description)
+ end
+ end
+
+ # Custom 404 error messaging. Users sometimes see 404s when they have
+ # misconfigured server URLs, and the wrong one redirects to the new
+ # one, e.g., PUT http://wrong.url/nodes/node-name becomes a GET after a
+ # redirect.
+ def describe_404_error(error_description)
+ error_description.section("Resource Not Found:",<<-E)
+The server returned a HTTP 404. This usually indicates that your chef_server_url is incorrect.
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+E
+ end
+
+ def username
+ config[:node_name]
+ end
+
+ def api_key
+ config[:client_key]
+ end
+
+ def server_url
+ config[:chef_server_url]
+ end
+
+ def clock_skew?
+ exception.response.body =~ /synchronize the clock/i
+ end
+
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/registration_error_inspector.rb b/lib/chef/formatters/error_inspectors/registration_error_inspector.rb
new file mode 100644
index 0000000000..5389f9f7d0
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/registration_error_inspector.rb
@@ -0,0 +1,137 @@
+class Chef
+ module Formatters
+ module ErrorInspectors
+
+ # == RegistrationErrorInspector
+ # Wraps exceptions that occur during the client registration process and
+ # suggests possible causes.
+ #--
+ # TODO: Lots of duplication with the node_load_error_inspector, just
+ # slightly tweaked to talk about validation keys instead of other keys.
+ class RegistrationErrorInspector
+ attr_reader :exception
+ attr_reader :node_name
+ attr_reader :config
+
+ def initialize(node_name, exception, config)
+ @node_name = node_name
+ @exception = exception
+ @config = config
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when Net::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(error_description)
+ when Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, SocketError
+ error_description.section("Network Error:",<<-E)
+There was a network error connecting to the Chef Server:
+#{exception.message}
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+
+If your chef_server_url is correct, your network could be down.
+E
+ when Chef::Exceptions::PrivateKeyMissing
+ error_description.section("Private Key Not Found:",<<-E)
+Your private key could not be loaded. If the key file exists, ensure that it is
+readable by chef-client.
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+validation_key "#{api_key}"
+E
+ else
+ "#{exception.class.name}: #{exception.message}"
+ end
+ end
+
+ def humanize_http_exception(error_description)
+ response = exception.response
+ case response
+ when Net::HTTPUnauthorized
+ if clock_skew?
+ error_description.section("Authentication Error:",<<-E)
+Failed to authenticate to the chef server (http 401).
+The request failed because your clock has drifted by more than 15 minutes.
+Syncing your clock to an NTP Time source should resolve the issue.
+E
+ else
+ error_description.section("Authentication Error:",<<-E)
+Failed to authenticate to the chef server (http 401).
+E
+
+ error_description.section("Server Response:", format_rest_error)
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+validation_client_name "#{username}"
+validation_key "#{api_key}"
+
+If these settings are correct, your validation_key may be invalid.
+E
+ end
+ when Net::HTTPForbidden
+ error_description.section("Authorization Error:",<<-E)
+Your validation client is not authorized to create the client for this node (HTTP 403).
+E
+ error_description.section("Possible Causes:",<<-E)
+* There may already be a client named "#{config[:node_name]}"
+* Your validation client (#{username}) may have misconfigured authorization permissions.
+E
+ when Net::HTTPBadRequest
+ error_description.section("Invalid Request Data:",<<-E)
+The data in your request was invalid (HTTP 400).
+E
+ error_description.section("Server Response:",format_rest_error)
+ when Net::HTTPNotFound
+ error_description.section("Resource Not Found:",<<-E)
+The server returned a HTTP 404. This usually indicates that your chef_server_url is incorrect.
+E
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+E
+ when Net::HTTPInternalServerError
+ error_description.section("Unknown Server Error:",<<-E)
+The server had a fatal error attempting to load the node data.
+E
+ error_description.section("Server Response:", format_rest_error)
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable
+ error_description.section("Server Unavailable","The Chef Server is temporarily unavailable")
+ error_description.section("Server Response:", format_rest_error)
+ else
+ error_description.section("Unexpected API Request Failure:", format_rest_error)
+ end
+ end
+
+ def username
+ #config[:node_name]
+ config[:validation_client_name]
+ end
+
+ def api_key
+ config[:validation_key]
+ #config[:client_key]
+ end
+
+ def server_url
+ config[:chef_server_url]
+ end
+
+ def clock_skew?
+ exception.response.body =~ /synchronize the clock/i
+ end
+
+ # Parses JSON from the error response sent by Chef Server and returns the
+ # error message
+ #--
+ # TODO: this code belongs in Chef::REST
+ def format_rest_error
+ Array(Chef::JSONCompat.from_json(exception.response.body)["error"]).join('; ')
+ rescue Exception
+ exception.response.body
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb b/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb
new file mode 100644
index 0000000000..57d8de0ef9
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb
@@ -0,0 +1,108 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Tyler Cloke (<tyler@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 Formatters
+ module ErrorInspectors
+ class ResourceFailureInspector
+
+ attr_reader :resource
+ attr_reader :action
+ attr_reader :exception
+
+ def initialize(resource, action, exception)
+ @resource = resource
+ @action = action
+ @exception = exception
+ end
+
+ def add_explanation(error_description)
+ error_description.section(exception.class.name, exception.message)
+
+ unless filtered_bt.empty?
+ error_description.section("Cookbook Trace:", filtered_bt.join("\n"))
+ end
+
+ unless dynamic_resource?
+ error_description.section("Resource Declaration:", recipe_snippet)
+ end
+
+ error_description.section("Compiled Resource:", "#{resource.to_text}")
+
+ # Template errors get wrapped in an exception class that can show the relevant template code,
+ # so add them to the error output.
+ if exception.respond_to?(:source_listing)
+ error_description.section("Template Context:", "#{exception.source_location}\n#{exception.source_listing}")
+ end
+ end
+
+ def recipe_snippet
+ return nil if dynamic_resource?
+ @snippet ||= begin
+ if file = resource.source_line[/^(([\w]:)?[^:]+):([\d]+)/,1] and line = resource.source_line[/^#{file}:([\d]+)/,1].to_i
+ lines = IO.readlines(file)
+
+ relevant_lines = ["# In #{file}\n\n"]
+
+
+ current_line = line - 1
+ current_line = 0 if current_line < 0
+ nesting = 0
+
+ loop do
+
+ # low rent parser. try to gracefully handle nested blocks in resources
+ nesting += 1 if lines[current_line] =~ /[\s]+do[\s]*/
+ nesting -= 1 if lines[current_line] =~ /end[\s]*$/
+
+ relevant_lines << format_line(current_line, lines[current_line])
+
+ break if lines[current_line + 1].nil?
+ break if current_line >= (line + 50)
+ break if nesting <= 0
+
+ current_line += 1
+ end
+ relevant_lines << format_line(current_line + 1, lines[current_line + 1]) if lines[current_line + 1]
+ relevant_lines.join("")
+ end
+ end
+ end
+
+ def dynamic_resource?
+ !resource.source_line
+ end
+
+ def filtered_bt
+ filters = Array(Chef::Config.cookbook_path).map {|p| /^#{Regexp.escape(p)}/ }
+ exception.backtrace.select {|line| filters.any? {|filter| line =~ filter }}
+ end
+
+ private
+
+ def format_line(line_nr, line)
+ # Print line number as 1-indexed not zero
+ line_nr_string = (line_nr + 1).to_s.rjust(3) + ": "
+ line_nr_string + line
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb b/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb
new file mode 100644
index 0000000000..ac19a983af
--- /dev/null
+++ b/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb
@@ -0,0 +1,118 @@
+#--
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Tyler Cloke (<tyler@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.
+#
+
+require 'chef/formatters/error_inspectors/api_error_formatting'
+
+class Chef
+ module Formatters
+ module ErrorInspectors
+ class RunListExpansionErrorInspector
+
+ include APIErrorFormatting
+
+ attr_reader :exception
+ attr_reader :node
+
+ def initialize(node, exception)
+ @node, @exception = node, exception
+ end
+
+ def add_explanation(error_description)
+ case exception
+ when Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, SocketError
+ error_description.section("Networking Error:",<<-E)
+#{exception.message}
+
+Your chef_server_url may be misconfigured, or the network could be down.
+E
+ when Net::HTTPServerException, Net::HTTPFatalError
+ humanize_http_exception(error_description)
+ when Chef::Exceptions::MissingRole
+ describe_missing_role(error_description)
+ else
+ error_description.section("Unexpected Error:","#{exception.class.name}: #{exception.message}")
+ end
+ end
+
+ def describe_missing_role(error_description)
+ error_description.section("Missing Role(s) in Run List:", missing_roles_explained)
+ original_run_list = node.run_list.map {|item| "* #{item}"}.join("\n")
+ error_description.section("Original Run List", original_run_list)
+ end
+
+ def missing_roles_explained
+ run_list_expansion.missing_roles_with_including_role.map do |role, includer|
+ "* #{role} included by '#{includer}'"
+ end.join("\n")
+ end
+
+ def run_list_expansion
+ exception.expansion
+ end
+
+ def config
+ Chef::Config
+ end
+
+ def humanize_http_exception(error_description)
+ response = exception.response
+ case response
+ when Net::HTTPUnauthorized
+ error_description.section("Authentication Error:",<<-E)
+Failed to authenticate to the chef server (http 401).
+E
+
+ error_description.section("Server Response:", format_rest_error)
+ error_description.section("Relevant Config Settings:",<<-E)
+chef_server_url "#{server_url}"
+node_name "#{username}"
+client_key "#{api_key}"
+
+If these settings are correct, your client_key may be invalid.
+E
+ when Net::HTTPForbidden
+ # TODO: we're rescuing errors from Node.find_or_create
+ # * could be no write on nodes container
+ # * could be no read on the node
+ error_description.section("Authorization Error",<<-E)
+Your client is not authorized to load one or more of your roles (HTTP 403).
+E
+ error_description.section("Server Response:", format_rest_error)
+
+ error_description.section("Possible Causes:",<<-E)
+* Your client (#{username}) may have misconfigured authorization permissions.
+E
+ when Net::HTTPInternalServerError
+ error_description.section("Unknown Server Error:",<<-E)
+The server had a fatal error attempting to load a role.
+E
+ error_description.section("Server Response:", format_rest_error)
+ when Net::HTTPBadGateway, Net::HTTPServiceUnavailable
+ error_description.section("Server Unavailable","The Chef Server is temporarily unavailable")
+ error_description.section("Server Response:", format_rest_error)
+ else
+ error_description.section("Unexpected API Request Failure:", format_rest_error)
+ end
+ end
+
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/formatters/error_mapper.rb b/lib/chef/formatters/error_mapper.rb
new file mode 100644
index 0000000000..2140c638bc
--- /dev/null
+++ b/lib/chef/formatters/error_mapper.rb
@@ -0,0 +1,85 @@
+#--
+# Author:: Tyler Cloke (<tyler@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 Formatters
+ # == Formatters::ErrorMapper
+ # Collection of methods for creating and returning
+ # Formatters::ErrorDescription objects based on node,
+ # exception, and configuration information.
+ module ErrorMapper
+
+ # Failed to register this client with the server.
+ def self.registration_failed(node_name, exception, config)
+ error_inspector = ErrorInspectors::RegistrationErrorInspector.new(node_name, exception, config)
+ headline = "Chef encountered an error attempting to create the client \"#{node_name}\""
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.node_load_failed(node_name, exception, config)
+ error_inspector = ErrorInspectors::NodeLoadErrorInspector.new(node_name, exception, config)
+ headline = "Chef encountered an error attempting to load the node data for \"#{node_name}\""
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.run_list_expand_failed(node, exception)
+ error_inspector = ErrorInspectors::RunListExpansionErrorInspector.new(node, exception)
+ headline = "Error expanding the run_list:"
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.cookbook_resolution_failed(expanded_run_list, exception)
+ error_inspector = ErrorInspectors::CookbookResolveErrorInspector.new(expanded_run_list, exception)
+ headline = "Error Resolving Cookbooks for Run List:"
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.cookbook_sync_failed(cookbooks, exception)
+ error_inspector = ErrorInspectors::CookbookSyncErrorInspector.new(cookbooks, exception)
+ headline = "Error Syncing Cookbooks:"
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.resource_failed(resource, action, exception)
+ error_inspector = ErrorInspectors::ResourceFailureInspector.new(resource, action, exception)
+ headline = "Error executing action `#{action}` on resource '#{resource}'"
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ return description
+ end
+
+ def self.file_load_failed(path, exception)
+ error_inspector = ErrorInspectors::CompileErrorInspector.new(path, exception)
+ headline = "Recipe Compile Error" + ( path ? " in #{path}" : "" )
+ description = ErrorDescription.new(headline)
+ error_inspector.add_explanation(description)
+ description
+ end
+ end
+ end
+end
diff --git a/lib/chef/formatters/minimal.rb b/lib/chef/formatters/minimal.rb
new file mode 100644
index 0000000000..a189cc67eb
--- /dev/null
+++ b/lib/chef/formatters/minimal.rb
@@ -0,0 +1,235 @@
+require 'chef/formatters/base'
+
+class Chef
+
+ module Formatters
+
+
+ # == Formatters::Minimal
+ # Shows the progress of the chef run by printing single characters, and
+ # displays a summary of updates at the conclusion of the run. For events
+ # that don't have meaningful status information (loading a file, syncing a
+ # cookbook) a dot is printed. For resources, a dot, 'S' or 'U' is printed
+ # if the resource is up to date, skipped by not_if/only_if, or updated,
+ # respectively.
+ class Minimal < Formatters::Base
+
+ cli_name(:minimal)
+ cli_name(:min)
+
+ attr_reader :updated_resources
+ attr_reader :updates_by_resource
+
+
+ def initialize(out, err)
+ super
+ @updated_resources = []
+ @updates_by_resource = Hash.new {|h, k| h[k] = []}
+ end
+
+ # Called at the very start of a Chef Run
+ def run_start(version)
+ puts "Starting Chef Client, version #{version}"
+ end
+
+ # Called at the end of the Chef run.
+ def run_completed(node)
+ puts "chef client finished, #{@updated_resources.size} resources updated"
+ end
+
+ # called at the end of a failed run
+ def run_failed(exception)
+ puts "chef client failed. #{@updated_resources.size} resources updated"
+ end
+
+ # Called right after ohai runs.
+ def ohai_completed(node)
+ end
+
+ # Already have a client key, assuming this node has registered.
+ def skipping_registration(node_name, config)
+ end
+
+ # About to attempt to register as +node_name+
+ def registration_start(node_name, config)
+ end
+
+ def registration_completed
+ end
+
+ # Failed to register this client with the server.
+ def registration_failed(node_name, exception, config)
+ super
+ end
+
+ def node_load_start(node_name, config)
+ end
+
+ # Failed to load node data from the server
+ def node_load_failed(node_name, exception, config)
+ end
+
+ # Default and override attrs from roles have been computed, but not yet applied.
+ # Normal attrs from JSON have been added to the node.
+ def node_load_completed(node, expanded_run_list, config)
+ end
+
+ # Called before the cookbook collection is fetched from the server.
+ def cookbook_resolution_start(expanded_run_list)
+ puts "resolving cookbooks for run list: #{expanded_run_list.inspect}"
+ end
+
+ # Called when there is an error getting the cookbook collection from the
+ # server.
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ end
+
+ # Called when the cookbook collection is returned from the server.
+ def cookbook_resolution_complete(cookbook_collection)
+ end
+
+ # Called before unneeded cookbooks are removed
+ #--
+ # TODO: Should be called in CookbookVersion.sync_cookbooks
+ def cookbook_clean_start
+ end
+
+ # Called after the file at +path+ is removed. It may be removed if the
+ # cookbook containing it was removed from the run list, or if the file was
+ # removed from the cookbook.
+ def removed_cookbook_file(path)
+ end
+
+ # Called when cookbook cleaning is finished.
+ def cookbook_clean_complete
+ end
+
+ # Called before cookbook sync starts
+ def cookbook_sync_start(cookbook_count)
+ puts "Synchronizing cookbooks"
+ end
+
+ # Called when cookbook +cookbook_name+ has been sync'd
+ def synchronized_cookbook(cookbook_name)
+ print "."
+ end
+
+ # Called when an individual file in a cookbook has been updated
+ def updated_cookbook_file(cookbook_name, path)
+ end
+
+ # Called after all cookbooks have been sync'd.
+ def cookbook_sync_complete
+ puts "done."
+ end
+
+ # Called when cookbook loading starts.
+ def library_load_start(file_count)
+ puts "Compiling cookbooks"
+ end
+
+ # Called after a file in a cookbook is loaded.
+ def file_loaded(path)
+ print '.'
+ end
+
+ def file_load_failed(path, exception)
+ super
+ end
+
+ # Called when recipes have been loaded.
+ def recipe_load_complete
+ puts "done."
+ end
+
+ # Called before convergence starts
+ def converge_start(run_context)
+ puts "Converging #{run_context.resource_collection.all_resources.size} resources"
+ end
+
+ # Called when the converge phase is finished.
+ def converge_complete
+ puts "\n"
+ puts "System converged."
+ if updated_resources.empty?
+ puts "no resources updated"
+ else
+ puts "\n"
+ puts "resources updated this run:"
+ updated_resources.each do |resource|
+ puts "* #{resource.to_s}"
+ updates_by_resource[resource.name].flatten.each do |update|
+ puts " - #{update}"
+ end
+ puts "\n"
+ end
+ end
+ end
+
+ # Called before action is executed on a resource.
+ def resource_action_start(resource, action, notification_type=nil, notifier=nil)
+ end
+
+ # Called when a resource fails, but will retry.
+ def resource_failed_retriable(resource, action, retry_count, exception)
+ end
+
+ # Called when a resource fails and will not be retried.
+ def resource_failed(resource, action, exception)
+ end
+
+ # Called when a resource action has been skipped b/c of a conditional
+ def resource_skipped(resource, action, conditional)
+ print "S"
+ end
+
+ # Called after #load_current_resource has run.
+ def resource_current_state_loaded(resource, action, current_resource)
+ end
+
+ # Called when a resource has no converge actions, e.g., it was already correct.
+ def resource_up_to_date(resource, action)
+ print "."
+ end
+
+ ## TODO: callback for assertion failures
+
+ ## TODO: callback for assertion fallback in why run
+
+ # Called when a change has been made to a resource. May be called multiple
+ # times per resource, e.g., a file may have its content updated, and then
+ # its permissions updated.
+ def resource_update_applied(resource, action, update)
+ @updates_by_resource[resource.name] << Array(update)[0]
+ end
+
+ # Called after a resource has been completely converged.
+ def resource_updated(resource, action)
+ updated_resources << resource
+ print "U"
+ end
+
+ # Called before handlers run
+ def handlers_start(handler_count)
+ end
+
+ # Called after an individual handler has run
+ def handler_executed(handler)
+ end
+
+ # Called after all handlers have executed
+ def handlers_completed
+ end
+
+ # An uncategorized message. This supports the case that a user needs to
+ # pass output that doesn't fit into one of the callbacks above. Note that
+ # there's no semantic information about the content or importance of the
+ # message. That means that if you're using this too often, you should add a
+ # callback for it.
+ def msg(message)
+ end
+
+ end
+ end
+end
+