summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel DeLeo <dan@opscode.com>2012-06-22 11:03:31 -0700
committerDaniel DeLeo <dan@opscode.com>2012-06-22 15:51:56 -0700
commit57e1e47c90da569f4c205c8b6375301b1644e36e (patch)
treeb8be834fe965d7475a3e45a0166cb741758c5935
parentf964b47a80cb3a6d615f09175cc06a5e0ed09ae8 (diff)
downloadchef-57e1e47c90da569f4c205c8b6375301b1644e36e.tar.gz
add error formatter for cookbook resolve failure
-rw-r--r--chef/lib/chef/formatters/error_inspectors.rb1
-rw-r--r--chef/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb147
-rw-r--r--chef/spec/unit/formatters/error_inspectors/cookbook_resolve_error_inspector_spec.rb75
3 files changed, 223 insertions, 0 deletions
diff --git a/chef/lib/chef/formatters/error_inspectors.rb b/chef/lib/chef/formatters/error_inspectors.rb
index 2c007f43c9..304b95c28f 100644
--- a/chef/lib/chef/formatters/error_inspectors.rb
+++ b/chef/lib/chef/formatters/error_inspectors.rb
@@ -3,6 +3,7 @@ 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'
class Chef
module Formatters
diff --git a/chef/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb b/chef/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb
new file mode 100644
index 0000000000..6fc09f7c12
--- /dev/null
+++ b/chef/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb
@@ -0,0 +1,147 @@
+#--
+# 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)
+E
+ 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/chef/spec/unit/formatters/error_inspectors/cookbook_resolve_error_inspector_spec.rb b/chef/spec/unit/formatters/error_inspectors/cookbook_resolve_error_inspector_spec.rb
new file mode 100644
index 0000000000..8280c73476
--- /dev/null
+++ b/chef/spec/unit/formatters/error_inspectors/cookbook_resolve_error_inspector_spec.rb
@@ -0,0 +1,75 @@
+#--
+# 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 'spec_helper'
+
+describe Chef::Formatters::ErrorInspectors::CookbookResolveErrorInspector do
+
+ before do
+ @expanded_run_list = Chef::RunList.new("recipe[annoyances]", "recipe[apache2]", "recipe[users]", "recipe[chef::client]")
+
+ @description = Chef::Formatters::ErrorDescription.new("Error Resolving Cookbooks for Run List:")
+ @outputter = Chef::Formatters::Outputter.new(StringIO.new, STDERR)
+ #@outputter = Chef::Formatters::Outputter.new(STDOUT, STDERR)
+ end
+
+ describe "when explaining a PreconditionFailed (412) error with current error message style" do
+ # Chef currently returns error messages with some fields as JSON strings,
+ # which must be re-parsed to get the actual data.
+
+ before do
+
+ @response_body = "{\"error\":[\"{\\\"non_existent_cookbooks\\\":[\\\"apache2\\\"],\\\"cookbooks_with_no_versions\\\":[\\\"users\\\"],\\\"message\\\":\\\"Run list contains invalid items: no such cookbook nope.\\\"}\"]}"
+ @response = Net::HTTPPreconditionFailed.new("1.1", "412", "(response) unauthorized")
+ @response.stub!(:body).and_return(@response_body)
+ @exception = Net::HTTPServerException.new("(exception) precondition failed", @response)
+
+ @inspector = Chef::Formatters::ErrorInspectors::CookbookResolveErrorInspector.new(@expanded_run_list, @exception)
+ @inspector.add_explanation(@description)
+ end
+
+ it "prints a pretty message" do
+ @description.display(@outputter)
+ end
+
+ end
+
+ describe "when explaining a PreconditionFailed (412) error with single encoded JSON" do
+ # Chef currently returns error messages with some fields as JSON strings,
+ # which must be re-parsed to get the actual data.
+
+ before do
+
+ @response_body = "{\"error\":[{\"non_existent_cookbooks\":[\"apache2\"],\"cookbooks_with_no_versions\":[\"users\"],\"message\":\"Run list contains invalid items: no such cookbook nope.\"}]}"
+ @response = Net::HTTPPreconditionFailed.new("1.1", "412", "(response) unauthorized")
+ @response.stub!(:body).and_return(@response_body)
+ @exception = Net::HTTPServerException.new("(exception) precondition failed", @response)
+
+ @inspector = Chef::Formatters::ErrorInspectors::CookbookResolveErrorInspector.new(@expanded_run_list, @exception)
+ @inspector.add_explanation(@description)
+ end
+
+ it "prints a pretty message" do
+ @description.display(@outputter)
+ end
+
+ end
+end
+
+
+