diff options
author | Seth Chisamore <schisamo@opscode.com> | 2012-10-30 10:39:35 -0400 |
---|---|---|
committer | Seth Chisamore <schisamo@opscode.com> | 2012-10-30 10:39:35 -0400 |
commit | 24dc69a9a97e82a6e4207de68d6dcc664178249b (patch) | |
tree | 19bb289c9f88b4bbab066bc56b95d6d222fd5c35 /lib/chef/formatters | |
parent | 9348c1c9c80ee757354d624b7dc1b78ebc7605c4 (diff) | |
download | chef-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.rb | 247 | ||||
-rw-r--r-- | lib/chef/formatters/doc.rb | 236 | ||||
-rw-r--r-- | lib/chef/formatters/error_descriptor.rb | 66 | ||||
-rw-r--r-- | lib/chef/formatters/error_inspectors.rb | 19 | ||||
-rw-r--r-- | lib/chef/formatters/error_inspectors/api_error_formatting.rb | 111 | ||||
-rw-r--r-- | lib/chef/formatters/error_inspectors/compile_error_inspector.rb | 106 | ||||
-rw-r--r-- | lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb | 146 | ||||
-rw-r--r-- | lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb | 80 | ||||
-rw-r--r-- | lib/chef/formatters/error_inspectors/node_load_error_inspector.rb | 125 | ||||
-rw-r--r-- | lib/chef/formatters/error_inspectors/registration_error_inspector.rb | 137 | ||||
-rw-r--r-- | lib/chef/formatters/error_inspectors/resource_failure_inspector.rb | 108 | ||||
-rw-r--r-- | lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb | 118 | ||||
-rw-r--r-- | lib/chef/formatters/error_mapper.rb | 85 | ||||
-rw-r--r-- | lib/chef/formatters/minimal.rb | 235 |
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 + |