summaryrefslogtreecommitdiff
path: root/lib/chef/formatters/error_inspectors
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/formatters/error_inspectors')
-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
8 files changed, 931 insertions, 0 deletions
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
+