summaryrefslogtreecommitdiff
path: root/lib/chef/resource_reporter.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/resource_reporter.rb')
-rw-r--r--lib/chef/resource_reporter.rb272
1 files changed, 272 insertions, 0 deletions
diff --git a/lib/chef/resource_reporter.rb b/lib/chef/resource_reporter.rb
new file mode 100644
index 0000000000..3d10c1e961
--- /dev/null
+++ b/lib/chef/resource_reporter.rb
@@ -0,0 +1,272 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Author:: Prajakta Purohit (prajakta@opscode.com>)
+# Auther:: 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 'uri'
+require 'chef/event_dispatch/base'
+
+class Chef
+ class ResourceReporter < EventDispatch::Base
+
+
+
+ class ResourceReport < Struct.new(:new_resource,
+ :current_resource,
+ :action,
+ :exception,
+ :elapsed_time)
+
+ def self.new_with_current_state(new_resource, action, current_resource)
+ report = new
+ report.new_resource = new_resource
+ report.action = action
+ report.current_resource = current_resource
+ report
+ end
+
+ def self.new_for_exception(new_resource, action)
+ report = new
+ report.new_resource = new_resource
+ report.action = action
+ report
+ end
+
+ def for_json
+ as_hash = {}
+ as_hash["type"] = new_resource.class.dsl_name
+ as_hash["name"] = new_resource.name
+ as_hash["id"] = new_resource.identity
+ as_hash["after"] = new_resource.state
+ as_hash["before"] = current_resource ? current_resource.state : {}
+ as_hash["duration"] = (elapsed_time * 1000).to_i.to_s
+ as_hash["delta"] = new_resource.diff if new_resource.respond_to?("diff")
+ as_hash["delta"] = "" if as_hash["delta"].nil?
+
+ # TODO: rename as "action"
+ as_hash["result"] = action.to_s
+ if success?
+ else
+ #as_hash["result"] = "failed"
+ end
+ as_hash["cookbook_name"] = new_resource.cookbook_name
+ as_hash["cookbook_version"] = new_resource.cookbook_version.version
+ as_hash
+
+ end
+
+ def finish
+ self.elapsed_time = new_resource.elapsed_time
+ end
+
+ def success?
+ !self.exception
+ end
+ end
+
+ attr_reader :updated_resources
+ attr_reader :status
+ attr_reader :exception
+ attr_reader :run_id
+ attr_reader :error_descriptions
+ attr_reader :summary_only
+
+ def initialize(rest_client)
+ if Chef::Config[:enable_reporting] && !Chef::Config[:why_run]
+ @reporting_enabled = true
+ else
+ @reporting_enabled = false
+ end
+ @updated_resources = []
+ @total_res_count = 0
+ @pending_update = nil
+ @status = "success"
+ @exception = nil
+ @run_id = nil
+ @rest_client = rest_client
+ @node = nil
+ @error_descriptions = {}
+ @summary_only = true
+ end
+
+ def node_load_completed(node, expanded_run_list_with_versions, config)
+ @node = node
+ if reporting_enabled?
+ begin
+ resource_history_url = "reports/nodes/#{node.name}/runs"
+ server_response = @rest_client.post_rest(resource_history_url, {:action => :begin})
+ run_uri = URI.parse(server_response["uri"])
+ @run_id = ::File.basename(run_uri.path)
+ Chef::Log.info("Chef server generated run history id: #{@run_id}")
+ @summary_only = server_response["summary_only"]
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
+ if !e.response || e.response.code.to_s != 404
+ if Chef::Config[:enable_reporting_url_fatals]
+ Chef::Log.error("Received exception attempting to generate run history id (URL Path: #{resource_history_url}), and enable_reporting_url_fatals is set, aborting run.")
+ raise
+ else
+ Chef::Log.info("Received exception attempting to generate run history id (URL Path: #{resource_history_url}), disabling reporting for this run.")
+ end
+ else
+ Chef::Log.debug("Received 404 attempting to generate run history id (URL Path: #{resource_history_url}), assuming feature is not supported.")
+ end
+ @reporting_enabled = false
+ end
+ end
+ end
+
+ def resource_current_state_loaded(new_resource, action, current_resource)
+ unless nested_resource?(new_resource)
+ @pending_update = ResourceReport.new_with_current_state(new_resource, action, current_resource)
+ end
+ end
+
+ def resource_up_to_date(new_resource, action)
+ @total_res_count += 1
+ @pending_update = nil unless nested_resource?(new_resource)
+ end
+
+ def resource_skipped(resource, action, conditional)
+ @total_res_count += 1
+ @pending_update = nil unless nested_resource?(resource)
+ end
+
+ def resource_updated(new_resource, action)
+ @total_res_count += 1
+ end
+
+ def resource_failed(new_resource, action, exception)
+ @total_res_count += 1
+ unless nested_resource?(new_resource)
+ @pending_update ||= ResourceReport.new_for_exception(new_resource, action)
+ @pending_update.exception = exception
+ end
+ description = Formatters::ErrorMapper.resource_failed(new_resource, action, exception)
+ @error_descriptions = description.for_json
+ end
+
+ def resource_completed(new_resource)
+ if @pending_update && !nested_resource?(new_resource)
+ @pending_update.finish
+ @updated_resources << @pending_update
+ @pending_update = nil
+ end
+ end
+
+ def run_completed(node)
+ @status = "success"
+ post_reporting_data
+ end
+
+ def run_failed(exception)
+ @exception = exception
+ @status = "failure"
+ post_reporting_data
+ end
+
+ def post_reporting_data
+ if reporting_enabled?
+ run_data = prepare_run_data
+ resource_history_url = "reports/nodes/#{@node.name}/runs/#{@run_id}"
+ Chef::Log.info("Sending resource update report (run-id: #{@run_id})")
+ Chef::Log.debug run_data.inspect
+ compressed_data = encode_gzip(run_data.to_json)
+ #if summary only is enabled send the uncompressed run_data excluding the run_data["resources"] and some additional metrics.
+ if @summary_only
+ run_data = report_summary(run_data, compressed_data)
+ Chef::Log.info("run_data_summary: #{run_data}")
+ @rest_client.post_rest(resource_history_url, run_data)
+ else
+ Chef::Log.debug("Sending Compressed Run Data...")
+ # Since we're posting compressed data we can not directly call
+ # post_rest which expects JSON
+ reporting_url = @rest_client.create_url(resource_history_url)
+ @rest_client.raw_http_request(:POST, reporting_url, {'Content-Encoding' => 'gzip'}, compressed_data)
+ end
+ else
+ Chef::Log.debug("Server doesn't support resource history, skipping resource report.")
+ end
+ end
+
+ def prepare_run_data
+ run_data = {}
+ run_data["action"] = "end"
+ run_data["resources"] = updated_resources.map do |resource_record|
+ resource_record.for_json
+ end
+ run_data["status"] = @status
+ run_data["run_list"] = @node.run_list.to_json
+ run_data["total_res_count"] = @total_res_count.to_s
+ run_data["data"] = {}
+
+ if exception
+ exception_data = {}
+ exception_data["class"] = exception.inspect
+ exception_data["message"] = exception.message
+ exception_data["backtrace"] = exception.backtrace.to_json
+ exception_data["description"] = @error_descriptions
+ run_data["data"]["exception"] = exception_data
+ end
+ run_data
+ end
+
+ def report_summary(run_data, compressed_data)
+ run_data["updated_res_count"] = updated_resources.count.to_s
+ run_data["post_size"] = compressed_data.bytesize.to_s
+ run_data["resources"] = []
+ run_data
+ end
+
+ def run_list_expand_failed(node, exception)
+ description = Formatters::ErrorMapper.run_list_expand_failed(node, exception)
+ @error_descriptions = description.for_json
+ end
+
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ description = Formatters::ErrorMapper.cookbook_resolution_failed(expanded_run_list, exception)
+ @error_descriptions = description.for_json
+ end
+
+ def cookbook_sync_failed(cookbooks, exception)
+ description = Formatters::ErrorMapper.cookbook_sync_failed(cookbooks, exception)
+ @error_descriptions = description.for_json
+ end
+
+ def reporting_enabled?
+ @reporting_enabled
+ end
+
+ private
+
+ # If we are getting messages about a resource while we are in the middle of
+ # another resource's update, we assume that the nested resource is just the
+ # implementation of a provider, and we want to hide it from the reporting
+ # output.
+ def nested_resource?(new_resource)
+ @pending_update && @pending_update.new_resource != new_resource
+ end
+
+ def encode_gzip(data)
+ "".tap do |out|
+ Zlib::GzipWriter.wrap(StringIO.new(out)){|gz| gz << data }
+ end
+ end
+
+ end
+end