diff options
author | Daniel DeLeo <dan@opscode.com> | 2012-06-22 11:03:31 -0700 |
---|---|---|
committer | Daniel DeLeo <dan@opscode.com> | 2012-06-22 15:51:56 -0700 |
commit | 57e1e47c90da569f4c205c8b6375301b1644e36e (patch) | |
tree | b8be834fe965d7475a3e45a0166cb741758c5935 | |
parent | f964b47a80cb3a6d615f09175cc06a5e0ed09ae8 (diff) | |
download | chef-57e1e47c90da569f4c205c8b6375301b1644e36e.tar.gz |
add error formatter for cookbook resolve failure
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 + + + |