summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2019-03-11 11:49:31 -0700
committerLamont Granquist <lamont@scriptkiddie.org>2019-03-11 11:49:31 -0700
commit66015ba654469f4dacfd78d40b02aafee52bbf1b (patch)
treeb00d0de111d18980f446b006ac63ef599eea8108 /lib
parent4037976199b728d4bdc18fd428e8d40a84c97e2b (diff)
downloadchef-66015ba654469f4dacfd78d40b02aafee52bbf1b.tar.gz
Extract Action Collection from Data Collector
See the PR for details on this change. Signed-off-by: Lamont Granquist <lamont@scriptkiddie.org>
Diffstat (limited to 'lib')
-rw-r--r--lib/chef/action_collection.rb252
-rw-r--r--lib/chef/client.rb43
-rw-r--r--lib/chef/data_collector.rb660
-rw-r--r--lib/chef/data_collector/config_validation.rb88
-rw-r--r--lib/chef/data_collector/error_handlers.rb116
-rw-r--r--lib/chef/data_collector/message_helpers.rb50
-rw-r--r--lib/chef/data_collector/messages.rb100
-rw-r--r--lib/chef/data_collector/messages/helpers.rb159
-rw-r--r--lib/chef/data_collector/resource_report.rb123
-rw-r--r--lib/chef/data_collector/run_end_message.rb172
-rw-r--r--lib/chef/data_collector/run_start_message.rb60
-rw-r--r--lib/chef/event_dispatch/base.rb19
-rw-r--r--lib/chef/event_dispatch/dispatcher.rb54
-rw-r--r--lib/chef/event_loggers/windows_eventlog.rb4
-rw-r--r--lib/chef/formatters/doc.rb2
-rw-r--r--lib/chef/formatters/minimal.rb2
-rw-r--r--lib/chef/node.rb24
-rw-r--r--lib/chef/policy_builder/dynamic.rb3
-rw-r--r--lib/chef/policy_builder/expand_node_object.rb17
-rw-r--r--lib/chef/policy_builder/policyfile.rb4
-rw-r--r--lib/chef/resource.rb12
-rw-r--r--lib/chef/resource_collection/resource_list.rb2
-rw-r--r--lib/chef/resource_reporter.rb183
-rw-r--r--lib/chef/run_context.rb5
-rw-r--r--lib/chef/run_context/cookbook_compiler.rb2
-rw-r--r--lib/chef/run_status.rb3
-rw-r--r--lib/chef/runner.rb8
27 files changed, 1128 insertions, 1039 deletions
diff --git a/lib/chef/action_collection.rb b/lib/chef/action_collection.rb
new file mode 100644
index 0000000000..a74ae5c449
--- /dev/null
+++ b/lib/chef/action_collection.rb
@@ -0,0 +1,252 @@
+#
+# Copyright:: Copyright 2018-2019, Chef Software 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"
+
+class Chef
+ class ActionCollection < EventDispatch::Base
+ include Enumerable
+ extend Forwardable
+
+ class ActionRecord
+
+ # XXX: this is buggy since we (ab)use this resource for "after" state and it may be
+ # inaccurate and it may be mutated by the user. A third after_resource should be added
+ # to new_resource + current_resource to properly implement this.
+ #
+ # @return [Chef::Resource] The declared resource state.
+ #
+ attr_accessor :new_resource
+
+ # @return [Chef::Resource] The current_resource object (before-state). This can be nil
+ # for non-why-run-safe resources in why-run mode, or if load_current_resource itself
+ # threw an exception (which should be considered a bug in that load_current_resource
+ # implementation, but must be handled), or for unprocessed resources.
+ attr_accessor :current_resource
+
+ # @return [Symbol] # The action that was run (or scheduled to run in the case of "unprocessed" resources).
+ attr_accessor :action
+
+ # @return [Exception] The exception that was thrown
+ attr_accessor :exception
+
+ # @return [Numeric] The elapsed time in seconds with machine precision
+ attr_accessor :elapsed_time
+
+ # @return [Chef::Resource::Conditional] The conditional that caused the resource to be skipped
+ attr_accessor :conditional
+
+ # The status of the resource:
+ # - updated: ran and converged
+ # - up_to_date: skipped due to idempotency
+ # - skipped: skipped due to a conditional
+ # - failed: failed with an exception
+ # - unprocessed: resources that were not touched by a run that failed
+ #
+ # @return [Symbol] status
+ #
+ attr_accessor :status
+
+ # The "nesting" level. Outer resources in recipe context are 0 here, while for every
+ # sub-resource_collection inside of a custom resource this number is incremented by 1.
+ # Resources that are fired via build-resource or manually creating and firing
+ #
+ # @return [Integer]
+ #
+ attr_accessor :nesting_level
+
+ def initialize(new_resource, action, nesting_level)
+ @new_resource = new_resource
+ @action = action
+ @nesting_level = nesting_level
+ end
+
+ # @return [Boolean] true if there was no exception
+ def success?
+ !exception
+ end
+ end
+
+ attr_reader :action_records
+ attr_reader :pending_updates
+ attr_reader :run_context
+ attr_reader :consumers
+ attr_reader :events
+
+ def initialize(events, run_context = nil, action_records = [])
+ @action_records = action_records
+ @pending_updates = []
+ @consumers = []
+ @events = events
+ @run_context = run_context
+ end
+
+ def_delegators :@action_records, :each, :last
+
+ # Allows getting at the action_records collection filtered by nesting level and status.
+ #
+ # TODO: filtering by resource type+name
+ #
+ # @return [Chef::ActionCollection]
+ #
+ def filtered_collection(max_nesting: nil, up_to_date: true, skipped: true, updated: true, failed: true, unprocessed: true)
+ subrecords = action_records.select do |rec|
+ ( max_nesting.nil? || rec.nesting_level <= max_nesting ) &&
+ ( rec.status == :up_to_date && up_to_date ||
+ rec.status == :skipped && skipped ||
+ rec.status == :updated && updated ||
+ rec.status == :failed && failed ||
+ rec.status == :unprocessed && unprocessed )
+ end
+ self.class.new(events, run_context, subrecords)
+ end
+
+ # This hook gives us the run_context immediately after it is created so that we can wire up this object to it.
+ #
+ # This also causes the action_collection_registration event to fire, all consumers that have not yet registered with the
+ # action_collection must register via this callback. This is the latest point before resources actually start to get
+ # evaluated.
+ #
+ # (see EventDispatch::Base#)
+ #
+ def cookbook_compilation_start(run_context)
+ run_context.action_collection = self
+ # fire the action_colleciton_registration hook after cookbook_compilation_start -- last chance for consumers to register
+ run_context.events.enqueue(:action_collection_registration, self)
+ @run_context = run_context
+ end
+
+ # Consumers must call register -- either directly or through the action_collection_registration hook. If
+ # nobody has registered any interest, then no action tracking will be done.
+ #
+ # @params object [Object] callers should call with `self`
+ #
+ def register(object)
+ consumers << object
+ end
+
+ # End of an unsuccessful converge used to fire off detect_unprocessed_resources.
+ #
+ # (see EventDispatch::Base#)
+ #
+ def converge_failed(exception)
+ return if consumers.empty?
+ detect_unprocessed_resources
+ end
+
+ # Hook to start processing a resource. May be called within processing of an outer resource
+ # so the pending_updates array forms a stack that sub-resources are popped onto and off of.
+ # This is always called.
+ #
+ # (see EventDispatch::Base#)
+ #
+ def resource_action_start(new_resource, action, notification_type = nil, notifier = nil)
+ return if consumers.empty?
+ pending_updates << ActionRecord.new(new_resource, action, pending_updates.length)
+ end
+
+ # Hook called after a resource is loaded. If load_current_resource fails, this hook will
+ # not be called and current_resource will be nil, and the resource_failed hook will be called.
+ #
+ # (see EventDispatch::Base#)
+ #
+ def resource_current_state_loaded(new_resource, action, current_resource)
+ return if consumers.empty?
+ current_record.current_resource = current_resource
+ end
+
+ # Hook called after an action is determined to be up to date.
+ #
+ # (see EventDispatch::Base#)
+ #
+ def resource_up_to_date(new_resource, action)
+ return if consumers.empty?
+ current_record.status = :up_to_date
+ end
+
+ # Hook called after an action is determined to be skipped due to a conditional.
+ #
+ # (see EventDispatch::Base#)
+ #
+ def resource_skipped(resource, action, conditional)
+ return if consumers.empty?
+ current_record.status = :skipped
+ current_record.conditional = conditional
+ end
+
+ # Hook called after an action modifies the system and is marked updated.
+ #
+ # (see EventDispatch::Base#)
+ #
+ def resource_updated(new_resource, action)
+ return if consumers.empty?
+ current_record.status = :updated
+ end
+
+ # Hook called after an action fails.
+ #
+ # (see EventDispatch::Base#)
+ #
+ def resource_failed(new_resource, action, exception)
+ return if consumers.empty?
+ current_record.status = :failed
+ current_record.exception = exception
+ end
+
+ # Hook called after an action is completed. This is always called, even if the action fails.
+ #
+ # (see EventDispatch::Base#)
+ #
+ def resource_completed(new_resource)
+ return if consumers.empty?
+ current_record.elapsed_time = new_resource.elapsed_time
+
+ # Verify if the resource has sensitive data and create a new blank resource with only
+ # the name so we can report it back without sensitive data
+ # XXX?: what about sensitive data in the current_resource?
+ # FIXME: this needs to be display-logic
+ if current_record.new_resource.sensitive
+ klass = current_record.new_resource.class
+ resource_name = current_record.new_resource.name
+ current_record.new_resource = klass.new(resource_name)
+ end
+
+ action_records << pending_updates.pop
+ end
+
+ private
+
+ # @return [Chef::ActionCollection::ActionRecord] the current record we are working on at the top of the stack
+ def current_record
+ pending_updates[-1]
+ end
+
+ # If the chef-client run fails in the middle, we are left with a half-completed resource_collection, this
+ # method is responsible for adding all of the resources which have not yet been touched. They are marked
+ # as being "unprocessed".
+ #
+ def detect_unprocessed_resources
+ run_context.resource_collection.all_resources.select { |resource| resource.executed_by_runner == false }.each do |resource|
+ Array(resource.action).each do |action|
+ record = ActionRecord.new(resource, action, 0)
+ record.status = :unprocessed
+ action_records << record
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/client.rb b/lib/chef/client.rb
index 3347536c12..0dd6b2666f 100644
--- a/lib/chef/client.rb
+++ b/lib/chef/client.rb
@@ -3,7 +3,7 @@
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Christopher Brown (<cb@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
-# Copyright:: Copyright 2008-2018, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -45,6 +45,7 @@ require "chef/formatters/base"
require "chef/formatters/doc"
require "chef/formatters/minimal"
require "chef/version"
+require "chef/action_collection"
require "chef/resource_reporter"
require "chef/data_collector"
require "chef/audit/audit_reporter"
@@ -249,9 +250,14 @@ class Chef
begin
runlock.save_pid
- request_id = Chef::RequestID.instance.request_id
+ events.register(Chef::DataCollector::Reporter.new(events))
+ events.register(Chef::ActionCollection.new(events))
+
+ run_status.run_id = request_id = Chef::RequestID.instance.request_id
+
run_context = nil
- events.run_start(Chef::VERSION)
+ events.run_start(Chef::VERSION, run_status)
+
logger.info("*** Chef #{Chef::VERSION} ***")
logger.info("Platform: #{RUBY_PLATFORM}")
logger.info "Chef-client pid: #{Process.pid}"
@@ -259,16 +265,12 @@ class Chef
enforce_path_sanity
run_ohai
- generate_guid
-
register unless Chef::Config[:solo_legacy_mode]
- register_data_collector_reporter
load_node
build_node
- run_status.run_id = request_id
run_status.start_clock
logger.info("Starting Chef Run for #{node.name}")
run_started
@@ -998,33 +1000,6 @@ class Chef
Chef::ReservedNames::Win32::Security.has_admin_privileges?
end
-
- # Ensure that we have a GUID for this node
- # If we've got the proper configuration, we'll simply set that.
- # If we're registed with the data collector, we'll migrate that UUID into our configuration and use that
- # Otherwise, we'll create a new GUID and save it
- def generate_guid
- Chef::Config[:chef_guid] ||=
- if File.exists?(Chef::Config[:chef_guid_path])
- File.read(Chef::Config[:chef_guid_path])
- else
- uuid = UUIDFetcher.node_uuid
- File.open(Chef::Config[:chef_guid_path], "w+") do |fh|
- fh.write(uuid)
- end
- uuid
- end
- end
-
- class UUIDFetcher
- extend Chef::DataCollector::Messages::Helpers
- end
-
- # Register the data collector reporter to send event information to the
- # data collector server
- def register_data_collector_reporter
- events.register(Chef::DataCollector::Reporter.new) if Chef::DataCollector.register_reporter?
- end
end
end
diff --git a/lib/chef/data_collector.rb b/lib/chef/data_collector.rb
index a0b4eeb4cb..f445dc2ed9 100644
--- a/lib/chef/data_collector.rb
+++ b/lib/chef/data_collector.rb
@@ -2,7 +2,7 @@
# Author:: Adam Leff (<adamleff@chef.io>)
# Author:: Ryan Cragun (<ryan@chef.io>)
#
-# Copyright:: Copyright 2012-2018, Chef Software Inc.
+# Copyright:: Copyright 2012-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,321 +18,173 @@
# limitations under the License.
#
-require "uri"
require "chef/server_api"
require "chef/http/simple_json"
require "chef/event_dispatch/base"
-require "chef/data_collector/messages"
-require "chef/data_collector/resource_report"
-require "ostruct"
require "set"
+require "chef/data_collector/run_end_message"
+require "chef/data_collector/run_start_message"
+require "chef/data_collector/config_validation"
+require "chef/data_collector/error_handlers"
class Chef
-
- # == Chef::DataCollector
- # Provides methods for determinine whether a reporter should be registered.
class DataCollector
+ # The DataCollector is mode-agnostic reporting tool which can be used with
+ # server-based and solo-based clients. It can report to a file, to an
+ # authenticated Chef Automate reporting endpoint, or to a user-supplied
+ # webhook. It sends two messages: one at the start of the run and one
+ # at the end of the run. Most early failures in the actual Chef::Client itself
+ # are reported, but parsing of the client.rb must have succeeded and some code
+ # in Chef::Application could throw so early as to prevent reporting. If
+ # exceptions are thrown both run-start and run-end messages are still sent in
+ # pairs.
+ #
+ class Reporter < EventDispatch::Base
+ include Chef::DataCollector::ErrorHandlers
- # Whether or not to enable data collection:
- # * always disabled for why run mode
- # * disabled when the user sets `Chef::Config[:data_collector][:mode]` to a
- # value that excludes the mode (client or solo) that we are running as
- # * disabled in solo mode if the user did not configure the auth token
- # * disabled if `Chef::Config[:data_collector][:server_url]` is set to a
- # falsey value
- def self.register_reporter?
- if why_run?
- Chef::Log.trace("data collector is disabled for why run mode")
- return false
- end
- unless reporter_enabled_for_current_mode?
- Chef::Log.trace("data collector is configured to only run in " \
- "#{Chef::Config[:data_collector][:mode].inspect} modes, disabling it")
- return false
- end
- unless data_collector_url_configured? || data_collector_output_locations_configured?
- Chef::Log.trace("Neither data collector URL or output locations have been configured, disabling data collector")
- return false
- end
- if solo? && !token_auth_configured?
- Chef::Log.trace("Data collector token must be configured to use Chef Automate data collector with Chef Solo")
- end
- if !solo? && token_auth_configured?
- Chef::Log.warn("Data collector token authentication is not recommended for client-server mode" \
- "Please upgrade Chef Server to 12.11.0 and remove the token from your config file " \
- "to use key based authentication instead")
- end
- true
- end
-
- def self.data_collector_url_configured?
- !!Chef::Config[:data_collector][:server_url]
- end
-
- def self.data_collector_output_locations_configured?
- !!Chef::Config[:data_collector][:output_locations]
- end
+ # @return [Chef::RunList::RunListExpansion] the expanded run list
+ attr_reader :expanded_run_list
- def self.why_run?
- !!Chef::Config[:why_run]
- end
+ # @return [Chef::RunStatus] the run status
+ attr_reader :run_status
- def self.token_auth_configured?
- !!Chef::Config[:data_collector][:token]
- end
+ # @return [Chef::Node] the chef node
+ attr_reader :node
- def self.solo?
- !!Chef::Config[:solo] || !!Chef::Config[:local_mode]
- end
+ # @return [Set<Hash>] the acculumated list of deprecation warnings
+ attr_reader :deprecations
- def self.reporter_enabled_for_current_mode?
- if Chef::Config[:solo] || Chef::Config[:local_mode]
- acceptable_modes = [:solo, :both]
- else
- acceptable_modes = [:client, :both]
- end
+ # @return [Chef::ActionCollection] the action collection object
+ attr_reader :action_collection
- acceptable_modes.include?(Chef::Config[:data_collector][:mode])
- end
+ # @return [Chef::EventDispatch::Dispatcher] the event dispatcher
+ attr_reader :events
- # == Chef::DataCollector::Reporter
- # Provides an event handler that can be registered to report on Chef
- # run data. Unlike the existing Chef::ResourceReporter event handler,
- # the DataCollector handler is not tied to a Chef Server / Chef Reporting
- # and exports its data through a webhook-like mechanism to a configured
- # endpoint.
- class Reporter < EventDispatch::Base
- attr_reader :all_resource_reports, :status, :exception, :error_descriptions,
- :expanded_run_list, :run_context, :run_status, :http,
- :current_resource_report, :enabled, :deprecations
-
- def initialize
- validate_data_collector_server_url!
- validate_data_collector_output_locations! if data_collector_output_locations
- @all_resource_reports = []
- @current_resource_loaded = nil
- @error_descriptions = {}
+ # @param events [Chef::EventDispatch::Dispatcher] the event dispatcher
+ def initialize(events)
+ @events = events
@expanded_run_list = {}
@deprecations = Set.new
- @enabled = true
-
- @http = setup_http_client(data_collector_server_url)
- if data_collector_output_locations
- @http_output_locations = setup_http_output_locations if data_collector_output_locations[:urls]
- end
- end
-
- # see EventDispatch::Base#run_started
- # Upon receipt, we will send our run start message to the
- # configured DataCollector endpoint. Depending on whether
- # the user has configured raise_on_failure, if we cannot
- # send the message, we will either disable the DataCollector
- # Reporter for the duration of this run, or we'll raise an
- # exception.
- def run_started(current_run_status)
- update_run_status(current_run_status)
-
- message = Chef::DataCollector::Messages.run_start_message(current_run_status)
- disable_reporter_on_error do
- send_to_data_collector(message)
- end
- send_to_output_locations(message) if data_collector_output_locations
- end
-
- # see EventDispatch::Base#run_completed
- # Upon receipt, we will send our run completion message to the
- # configured DataCollector endpoint.
- def run_completed(node)
- send_run_completion(status: "success")
- end
-
- # see EventDispatch::Base#run_failed
- def run_failed(exception)
- send_run_completion(status: "failure")
end
- # see EventDispatch::Base#converge_start
- # Upon receipt, we stash the run_context for use at the
- # end of the run in order to determine what resource+action
- # combinations have not yet fired so we can report on
- # unprocessed resources.
- def converge_start(run_context)
- @run_context = run_context
- end
-
- # see EventDispatch::Base#converge_complete
- # At the end of the converge, we add any unprocessed resources
- # to our report list.
- def converge_complete
- detect_unprocessed_resources
- end
-
- # see EventDispatch::Base#converge_failed
- # At the end of the converge, we add any unprocessed resources
- # to our report list
- def converge_failed(exception)
- detect_unprocessed_resources
- end
-
- # see EventDispatch::Base#resource_current_state_loaded
- # Create a new ResourceReport instance that we'll use to track
- # the state of this resource during the run. Nested resources are
- # ignored as they are assumed to be an inline resource of a custom
- # resource, and we only care about tracking top-level resources.
- def resource_current_state_loaded(new_resource, action, current_resource)
- return if nested_resource?(new_resource)
- initialize_resource_report_if_needed(new_resource, action, current_resource)
- end
-
- # see EventDispatch::Base#resource_up_to_date
- # Mark our ResourceReport status accordingly
- def resource_up_to_date(new_resource, action)
- initialize_resource_report_if_needed(new_resource, action)
- current_resource_report.up_to_date unless nested_resource?(new_resource)
- end
-
- # see EventDispatch::Base#resource_skipped
- # If this is a top-level resource, we create a ResourceReport
- # instance (because a skipped resource does not trigger the
- # resource_current_state_loaded event), and flag it as skipped.
- def resource_skipped(new_resource, action, conditional)
- return if nested_resource?(new_resource)
-
- initialize_resource_report_if_needed(new_resource, action)
- current_resource_report.skipped(conditional)
- end
-
- # see EventDispatch::Base#resource_updated
- # Flag the current ResourceReport instance as updated (as long as it's
- # a top-level resource).
- def resource_updated(new_resource, action)
- initialize_resource_report_if_needed(new_resource, action)
- current_resource_report.updated unless nested_resource?(new_resource)
- end
-
- # see EventDispatch::Base#resource_failed
- # Flag the current ResourceReport as failed and supply the exception as
- # long as it's a top-level resource, and update the run error text
- # with the proper Formatter.
- def resource_failed(new_resource, action, exception)
- initialize_resource_report_if_needed(new_resource, action)
- current_resource_report.failed(exception) unless nested_resource?(new_resource)
- update_error_description(
- Formatters::ErrorMapper.resource_failed(
- new_resource,
- action,
- exception
- ).for_json
- )
+ # Hook to grab the run_status. We also make the decision to run or not run here (our
+ # config has been parsed so we should know if we need to run, we unregister if we do
+ # not want to run).
+ #
+ # (see EventDispatch::Base#run_start)
+ #
+ def run_start(chef_version, run_status)
+ events.unregister(self) unless should_be_enabled?
+ @run_status = run_status
end
- # see EventDispatch::Base#resource_completed
- # Mark the ResourceReport instance as finished (for timing details).
- # This marks the end of this resource during this run.
- def resource_completed(new_resource)
- if current_resource_report && !nested_resource?(new_resource)
- current_resource_report.finish
- add_resource_report(current_resource_report)
- clear_current_resource_report
- end
+ # Hook to grab the node object after it has been successfully loaded
+ #
+ # (see EventDispatch::Base#node_load_success)
+ #
+ def node_load_success(node)
+ @node = node
end
- # see EventDispatch::Base#run_list_expanded
# The expanded run list is stored for later use by the run_completed
# event and message.
+ #
+ # (see EventDispatch::Base#run_list_expanded)
+ #
def run_list_expanded(run_list_expansion)
@expanded_run_list = run_list_expansion
end
- # see EventDispatch::Base#run_list_expand_failed
- # The run error text is updated with the output of the appropriate
- # formatter.
- def run_list_expand_failed(node, exception)
- update_error_description(
- Formatters::ErrorMapper.run_list_expand_failed(
- node,
- exception
- ).for_json
- )
+ # Hook event to register with the action_collection if we are still enabled.
+ #
+ # This is also how we wire up to the action_collection since it passes itself as the argument.
+ #
+ # (see EventDispatch::Base#action_collection_registration)
+ #
+ def action_collection_registration(action_collection)
+ @action_collection = action_collection
+ action_collection.register(self)
end
- # see EventDispatch::Base#cookbook_resolution_failed
- # The run error text is updated with the output of the appropriate
- # formatter.
- def cookbook_resolution_failed(expanded_run_list, exception)
- update_error_description(
- Formatters::ErrorMapper.cookbook_resolution_failed(
- expanded_run_list,
- exception
- ).for_json
- )
- end
+ # - Creates and writes our NodeUUID back to the node object
+ # - Sanity checks the data collector
+ # - Sends the run start message
+ # - If the run_start message fails, this may disable the rest of data collection or fail hard
+ #
+ # (see EventDispatch::Base#run_started)
+ #
+ def run_started(run_status)
+ Chef::DataCollector::ConfigValidation.validate_server_url!
+ Chef::DataCollector::ConfigValidation.validate_output_locations!
- # see EventDispatch::Base#cookbook_sync_failed
- # The run error text is updated with the output of the appropriate
- # formatter.
- def cookbook_sync_failed(cookbooks, exception)
- update_error_description(
- Formatters::ErrorMapper.cookbook_sync_failed(
- cookbooks,
- exception
- ).for_json
- )
+ send_run_start
end
- # see EventDispatch::Base#deprecation
- # Append a received deprecation to the list of deprecations
+ # Hook event to accumulating deprecation messages
+ #
+ # (see EventDispatch::Base#deprecation)
+ #
def deprecation(message, location = caller(2..2)[0])
- add_deprecation(message.message, message.url, location)
+ @deprecations << { message: message.message, url: message.url, location: message.location }
+ end
+
+ # Hook to send the run completion message with a status of success
+ #
+ # (see EventDispatch::Base#run_completed)
+ #
+ def run_completed(node)
+ send_run_completion("success")
+ end
+
+ # Hook to send the run completion message with a status of failed
+ #
+ # (see EventDispatch::Base#run_failed)
+ #
+ def run_failed(exception)
+ send_run_completion("failure")
end
private
- # Selects the type of HTTP client to use based on whether we are using
- # token-based or signed header authentication. Token authentication is
- # intended to be used primarily for Chef Solo in which case no signing
- # key will be available (in which case `Chef::ServerAPI.new()` would
- # raise an exception.
+ # Construct a http client for either the main data collector or for the http output_locations.
+ #
+ # Note that based on the token setting either the main data collector and all the http output_locations
+ # are going to all require chef-server authentication or not. There is no facility to mix-and-match on
+ # a per-url basis.
+ #
+ # @param url [String] the string url to connect to
+ # @returns [Chef::HTTP] the appropriate Chef::HTTP subclass instance to use
+ #
def setup_http_client(url)
- if data_collector_token.nil?
+ if Chef::Config[:data_collector][:token].nil?
Chef::ServerAPI.new(url, validate_utf8: false)
else
Chef::HTTP::SimpleJSON.new(url, validate_utf8: false)
end
end
- def setup_http_output_locations
- Chef::Config[:data_collector][:output_locations][:urls].each_with_object({}) do |location_url, http_output_locations|
- http_output_locations[location_url] = setup_http_client(location_url)
- end
- end
-
+ # Handle POST'ing data to the data collector. Note that this is a totally separate concern
+ # from the array of URI's in the extra configured output_locations.
#
- # Yields to the passed-in block (which is expected to be some interaction
- # with the DataCollector endpoint). If some communication failure occurs,
- # either disable any future communications to the DataCollector endpoint, or
- # raise an exception (if the user has set
- # Chef::Config.data_collector.raise_on_failure to true.)
+ # On failure this will unregister the data collector (if there are no other configured output_locations)
+ # and optionally will either silently continue or fail hard depending on configuration.
#
- # @param block [Proc] A ruby block to run. Ignored if a command is given.
+ # @param message [Hash] message to send
#
- def disable_reporter_on_error
- yield
+ def send_to_data_collector(message)
+ return unless Chef::Config[:data_collector][:server_url]
+ @http ||= setup_http_client(Chef::Config[:data_collector][:server_url])
+ @http.post(nil, message, headers)
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
- Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse,
- Net::HTTPHeaderSyntaxError, Net::ProtocolError, OpenSSL::SSL::SSLError,
- Errno::EHOSTDOWN => e
+ Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse,
+ Net::HTTPHeaderSyntaxError, Net::ProtocolError, OpenSSL::SSL::SSLError,
+ Errno::EHOSTDOWN => e
# Do not disable data collector reporter if additional output_locations have been specified
- disable_data_collector_reporter unless data_collector_output_locations
- code = if e.respond_to?(:response) && e.response.code
- e.response.code.to_s
- else
- "Exception Code Empty"
- end
+ events.unregister(self) unless Chef::Config[:data_collector][:output_locations]
+
+ code = e&.response&.code&.to_s || "Exception Code Empty"
- msg = "Error while reporting run start to Data Collector. " \
- "URL: #{data_collector_server_url} " \
- "Exception: #{code} -- #{e.message} "
+ msg = "Error while reporting run start to Data Collector. URL: #{Chef::Config[:data_collector][:server_url]} Exception: #{code} -- #{e.message} "
if Chef::Config[:data_collector][:raise_on_failure]
Chef::Log.error(msg)
@@ -344,222 +196,128 @@ class Chef
end
end
- def send_to_data_collector(message)
- return unless data_collector_accessible?
- http.post(nil, message, headers) if data_collector_server_url
- end
-
+ # Process sending the configured message to all the extra output locations.
+ #
+ # @param message [Hash] message to send
+ #
def send_to_output_locations(message)
- data_collector_output_locations.each do |type, location_list|
- location_list.each do |l|
- handle_output_location(type, l, message)
+ return unless Chef::Config[:data_collector][:output_locations]
+
+ Chef::Config[:data_collector][:output_locations].each do |type, locations|
+ locations.each do |location|
+ send_to_file_location(location, message) if type == :files
+ send_to_http_location(location, message) if type == :urls
end
end
end
- def handle_output_location(type, loc, message)
- type == :urls ? send_to_http_location(loc, message) : send_to_file_location(loc, message)
- end
-
+ # Sends a single message to a file, rendered as JSON.
+ #
+ # @param file_name [String] the file to write to
+ # @param message [Hash] the message to render as JSON
+ #
def send_to_file_location(file_name, message)
- open(file_name, "a") { |f| f.puts message }
+ File.open(file_name, "a") do |fh|
+ fh.puts Chef::JSONCompat.to_json(message)
+ end
end
+ # Sends a single message to a http uri, rendered as JSON. Maintains a cache of Chef::HTTP
+ # objects to use on subsequent requests.
+ #
+ # @param http_url [String] the configured http uri string endpoint to send to
+ # @param message [Hash] the message to render as JSON
+ #
def send_to_http_location(http_url, message)
- @http_output_locations[http_url].post(nil, message, headers) if @http_output_locations[http_url]
+ @http_output_locations_clients[http_url] ||= setup_http_client(http_url)
+ @http_output_locations_clients[http_url].post(nil, message, headers)
rescue
- Chef::Log.trace("Data collector failed to send to URL location #{http_url}. Please check your configured data_collector.output_locations")
+ # FIXME: we do all kinds of complexity to deal with errors in send_to_data_collector and we just don't care here, which feels like
+ # like poor behavior on several different levels, at least its a warn now... (I don't quite understand why it was written this way)
+ Chef::Log.warn("Data collector failed to send to URL location #{http_url}. Please check your configured data_collector.output_locations")
end
+ # @return [Boolean] if we've sent a run_start message yet
+ def sent_run_start?
+ !!@sent_run_start
+ end
+
+ # Send the run start message to the configured server or output locations
#
- # Send any messages to the DataCollector endpoint that are necessary to
- # indicate the run has completed. Currently, two messages are sent:
- #
- # - An "action" message with the node object indicating it's been updated
- # - An "run_converge" (i.e. RunEnd) message with details about the run,
- # what resources were modified/up-to-date/skipped, etc.
+ def send_run_start
+ message = Chef::DataCollector::RunStartMessage.construct_message(self)
+ send_to_data_collector(message)
+ send_to_output_locations(message)
+ @sent_run_start = true
+ end
+
+ # Send the run completion message to the configured server or output locations
#
- # @param opts [Hash] Additional details about the run, such as its success/failure.
+ # @param status [String] Either "success" or "failed"
#
- def send_run_completion(opts)
- # If run_status is nil we probably failed before the client triggered
- # the run_started callback. In this case we'll skip updating because
- # we have nothing to report.
- return unless run_status
+ def send_run_completion(status)
+ # this is necessary to send a run_start message when we fail before the run_started chef event.
+ # we adhere to a contract that run_start + run_completion events happen in pairs.
+ send_run_start unless sent_run_start?
- message = Chef::DataCollector::Messages.run_end_message(
- run_status: run_status,
- expanded_run_list: expanded_run_list,
- resources: all_resource_reports,
- status: opts[:status],
- error_descriptions: error_descriptions,
- deprecations: deprecations.to_a
- )
- disable_reporter_on_error do
- send_to_data_collector(message)
- end
- send_to_output_locations(message) if data_collector_output_locations
+ message = Chef::DataCollector::RunEndMessage.construct_message(self, status)
+ send_to_data_collector(message)
+ send_to_output_locations(message)
end
+ # @return [Hash] HTTP headers for the data collector endpoint
def headers
headers = { "Content-Type" => "application/json" }
- unless data_collector_token.nil?
- headers["x-data-collector-token"] = data_collector_token
+ unless Chef::Config[:data_collector][:token].nil?
+ headers["x-data-collector-token"] = Chef::Config[:data_collector][:token]
headers["x-data-collector-auth"] = "version=1.0"
end
headers
end
- def data_collector_server_url
- Chef::Config[:data_collector][:server_url]
- end
-
- def data_collector_output_locations
- Chef::Config[:data_collector][:output_locations]
- end
-
- def data_collector_token
- Chef::Config[:data_collector][:token]
- end
-
- def add_resource_report(resource_report)
- @all_resource_reports << OpenStruct.new(
- resource: resource_report.new_resource,
- action: resource_report.action,
- report_data: resource_report.to_h
- )
- end
-
- def disable_data_collector_reporter
- @enabled = false
- end
-
- def data_collector_accessible?
- @enabled
- end
-
- def update_run_status(run_status)
- @run_status = run_status
- end
-
- def update_error_description(discription_hash)
- @error_descriptions = discription_hash
- end
-
- def add_deprecation(message, url, location)
- @deprecations << { message: message, url: url, location: location }
- end
-
- def initialize_resource_report_if_needed(new_resource, action, current_resource = nil)
- return unless current_resource_report.nil?
- @current_resource_report = create_resource_report(new_resource, action, current_resource)
- end
-
- def create_resource_report(new_resource, action, current_resource = nil)
- Chef::DataCollector::ResourceReport.new(
- new_resource,
- action,
- current_resource
- )
- end
-
- def clear_current_resource_report
- @current_resource_report = nil
- end
-
- def detect_unprocessed_resources
- # create a Hash (for performance reasons, rather than an Array) containing all
- # resource+action combinations from the Resource Collection
- #
- # We use the object ID instead of the resource itself in the Hash key because
- # we currently allow users to create a property called "hash" which creates
- # a #hash instance method on the resource. Ruby expects that to be a Fixnum,
- # so bad things happen when adding an object to an Array or a Hash if it's not.
- collection_resources = {}
- run_context.resource_collection.all_resources.each do |resource|
- Array(resource.action).each do |action|
- collection_resources[[resource.__id__, action]] = resource
- end
- end
-
- # Delete from the Hash any resource+action combination we have
- # already processed.
- all_resource_reports.each do |report|
- collection_resources.delete([report.resource.__id__, report.action])
- end
-
- # The items remaining in the Hash are unprocessed resource+actions,
- # so we'll create new resource reports for them which default to
- # a state of "unprocessed".
- collection_resources.each do |key, resource|
- # The Hash key is an array of the Resource's object ID and the action.
- # We need to pluck out the action.
- add_resource_report(create_resource_report(resource, key[1]))
- end
- end
-
- # 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)
- @current_resource_report && @current_resource_report.new_resource != new_resource
- end
-
- def validate_and_return_uri(uri)
- URI(uri)
- rescue URI::InvalidURIError
- nil
- end
-
- def validate_and_create_file(file)
- send_to_file_location(file, "")
- true
- # Rescue exceptions raised by the file path being non-existent or not writeable and re-raise them to the user
- # with clearer explanatory text.
- rescue Errno::ENOENT
- raise Chef::Exceptions::ConfigurationError,
- "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which is a non existent file path."
- rescue Errno::EACCES
- raise Chef::Exceptions::ConfigurationError,
- "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which cannnot be written to by Chef."
- end
-
- def validate_data_collector_server_url!
- unless !data_collector_server_url && data_collector_output_locations
- uri = validate_and_return_uri(data_collector_server_url)
- unless uri
- raise Chef::Exceptions::ConfigurationError, "Chef::Config[:data_collector][:server_url] (#{data_collector_server_url}) is not a valid URI."
- end
-
- if uri.host.nil?
- raise Chef::Exceptions::ConfigurationError,
- "Chef::Config[:data_collector][:server_url] (#{data_collector_server_url}) is a URI with no host. Please supply a valid URL."
- end
+ # Main logic controlling the data collector being enabled or disabled:
+ #
+ # * disabled in why-run mode
+ # * disabled when `Chef::Config[:data_collector][:mode]` excludes the solo-vs-client mode
+ # * disabled if there is no server_url or no output_locations to log to
+ # * enabled if there is a configured output_location even without a token
+ # * disabled in solo mode if the user did not configure the auth token
+ #
+ # @return [Boolean] true if the data collector should be enabled
+ #
+ def should_be_enabled?
+ running_mode = ( Chef::Config[:solo_legacy_mode] || Chef::Config[:local_mode] ) ? :solo : :client
+ want_mode = Chef::Config[:data_collector][:mode]
+
+ case
+ when Chef::Config[:why_run]
+ Chef::Log.trace("data collector is disabled for why run mode")
+ return false
+ when (want_mode != :both) && running_mode != want_mode
+ Chef::Log.trace("data collector is configured to only run in #{Chef::Config[:data_collector][:mode]} modes, disabling it")
+ return false
+ when !(Chef::Config[:data_collector][:server_url] || Chef::Config[:data_collector][:output_locations])
+ Chef::Log.trace("Neither data collector URL or output locations have been configured, disabling data collector")
+ return false
+ when running_mode == :client && Chef::Config[:data_collector][:token]
+ Chef::Log.warn("Data collector token authentication is not recommended for client-server mode. " \
+ "Please upgrade Chef Server to 12.11.0 and remove the token from your config file " \
+ "to use key based authentication instead")
+ return true
+ when Chef::Config[:data_collector][:output_locations] && Chef::Config[:data_collector][:output_locations][:files] && !Chef::Config[:data_collector][:output_locations][:files].empty?
+ # we can run fine to a file without a token, even in solo mode.
+ return true
+ when running_mode == :solo && !Chef::Config[:data_collector][:token]
+ # we are in solo mode and are not logging to a file, so must have a token
+ Chef::Log.trace("Data collector token must be configured to use Chef Automate data collector with Chef Solo")
+ return false
+ else
+ return true
end
end
- def handle_type(type, loc)
- type == :urls ? validate_and_return_uri(loc) : validate_and_create_file(loc)
- end
-
- def validate_data_collector_output_locations!
- if data_collector_output_locations.empty?
- raise Chef::Exceptions::ConfigurationError,
- "Chef::Config[:data_collector][:output_locations] is empty. Please supply an hash of valid URLs and / or local file paths."
- end
-
- data_collector_output_locations.each do |type, locations|
- locations.each do |l|
- unless handle_type(type, l)
- raise Chef::Exceptions::ConfigurationError,
- "Chef::Config[:data_collector][:output_locations] contains the location #{l} which is not valid."
- end
- end
- end
- end
end
end
end
diff --git a/lib/chef/data_collector/config_validation.rb b/lib/chef/data_collector/config_validation.rb
new file mode 100644
index 0000000000..670b59dd80
--- /dev/null
+++ b/lib/chef/data_collector/config_validation.rb
@@ -0,0 +1,88 @@
+#
+# Copyright:: Copyright 2012-2019, Chef Software 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"
+
+class Chef
+ class DataCollector
+ module ConfigValidation
+ class << self
+ def validate_server_url!
+ # if we have a server_url set we ALWAYS validate it, and we MUST have an output_location set to skip server_url validation
+ # (having output_locations set and no server_url is valid, but both of them unset blows up in here)
+ return if !Chef::Config[:data_collector][:server_url] && Chef::Config[:data_collector][:output_locations]
+
+ begin
+ uri = URI(Chef::Config[:data_collector][:server_url])
+ rescue
+ raise Chef::Exceptions::ConfigurationError, "Chef::Config[:data_collector][:server_url] (#{Chef::Config[:data_collector][:server_url]}) is not a valid URI."
+ end
+
+ if uri.host.nil?
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:server_url] (#{Chef::Config[:data_collector][:server_url]}) is a URI with no host. Please supply a valid URL."
+ end
+ end
+
+ def validate_output_locations!
+ # not having an output_location set at all is fine, we just skip it then
+ output_locations = Chef::Config[:data_collector][:output_locations]
+ return unless output_locations
+
+ # but deliberately setting an empty output_location we consider to be an error (XXX: but should we?)
+ if output_locations.empty?
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:output_locations] is empty. Please supply an hash of valid URLs and / or local file paths."
+ end
+
+ # loop through all the types and locations and validate each one-by-one
+ output_locations.each do |type, locations|
+ locations.each do |location|
+ validate_url!(location) if type == :urls
+ validate_file!(location) if type == :files
+ end
+ end
+ end
+
+ private
+
+ # validate an output_location file
+ def validate_file!(file)
+ open(file, "a") {}
+ rescue Errno::ENOENT
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which is a non existent file path."
+ rescue Errno::EACCES
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which cannnot be written to by Chef."
+ rescue Exception => e
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which is invalid: #{e.message}."
+ end
+
+ # validate an output_location url
+ def validate_url!(url)
+ URI(url)
+ rescue
+ raise Chef::Exceptions::ConfigurationError,
+ "Chef::Config[:data_collector][:output_locations][:urls] contains the url #{url} which is not valid."
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/data_collector/error_handlers.rb b/lib/chef/data_collector/error_handlers.rb
new file mode 100644
index 0000000000..bb892c457c
--- /dev/null
+++ b/lib/chef/data_collector/error_handlers.rb
@@ -0,0 +1,116 @@
+#
+# Copyright:: Copyright 2012-2019, Chef Software 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
+ class DataCollector
+
+ # This module isolates the handling of collecting error descriptions to insert into the data_colletor
+ # report output. For very early errors it is repsonsible for collecting the node_name for the report
+ # to use. For all failure conditions that have an ErrorMapper it collects the output.
+ #
+ # No external code should call anything in this module directly.
+ #
+ # @api private
+ #
+ module ErrorHandlers
+
+ # @return [String] the fallback node name if we do NOT have a node due to early failures
+ attr_reader :node_name
+
+ # @return [Hash] JSON-formatted error description from the Chef::Formatters::ErrorMapper
+ def error_description
+ @error_description ||= {}
+ end
+
+ # This is an exceptionally "early" failure that results in not having a valid Chef::Node object,
+ # so it must capture the node_name from the config.rb
+ #
+ # (see EventDispatch::Base#registration_failed)
+ #
+ def registration_failed(node_name, exception, config)
+ description = Formatters::ErrorMapper.registration_failed(node_name, exception, config)
+ @node_name = node_name
+ @error_description = description.for_json
+ end
+
+ # This is an exceptionally "early" failure that results in not having a valid Chef::Node object,
+ # so it must capture the node_name from the config.rb
+ #
+ # (see EventDispatch::Base#node_load_failed)
+ #
+ def node_load_failed(node_name, exception, config)
+ description = Formatters::ErrorMapper.node_load_failed(node_name, exception, config)
+ @node_name = node_name
+ @error_description = description.for_json
+ end
+
+ # This is an "early" failure during run_list expansion
+ #
+ # (see EventDispatch::Base#run_list_expand_failed)
+ #
+ def run_list_expand_failed(node, exception)
+ description = Formatters::ErrorMapper.run_list_expand_failed(node, exception)
+ @error_description = description.for_json
+ end
+
+ # This is an "early" failure during cookbook resolution / depsolving / talking to cookbook_version endpoint on a server
+ #
+ # (see EventDispatch::Base#cookbook_resolution_failed)
+ #
+ def cookbook_resolution_failed(expanded_run_list, exception)
+ description = Formatters::ErrorMapper.cookbook_resolution_failed(expanded_run_list, exception)
+ @error_description = description.for_json
+ end
+
+ # This is an "early" failure during cookbook synchronization
+ #
+ # (see EventDispatch::Base#cookbook_sync_failed)
+ #
+ def cookbook_sync_failed(cookbooks, exception)
+ description = Formatters::ErrorMapper.cookbook_sync_failed(cookbooks, exception)
+ @error_description = description.for_json
+ end
+
+ # This failure happens during library loading / attribute file parsing, etc.
+ #
+ # (see EventDispatch::Base#file_load_failed)
+ #
+ def file_load_failed(path, exception)
+ description = Formatters::ErrorMapper.file_load_failed(path, exception)
+ @error_description = description.for_json
+ end
+
+ # This failure happens at converge time during recipe parsing
+ #
+ # (see EventDispatch::Base#recipe_not_failed)
+ #
+ def recipe_not_found(exception)
+ description = Formatters::ErrorMapper.file_load_failed(nil, exception)
+ @error_description = description.for_json
+ end
+
+ # This is a normal resource failure event during compile/converge phases
+ #
+ # (see EventDispatch::Base#resource_failed)
+ #
+ def resource_failed(new_resource, action, exception)
+ description = Formatters::ErrorMapper.resource_failed(new_resource, action, exception)
+ @error_description = description.for_json
+ end
+ end
+ end
+end
diff --git a/lib/chef/data_collector/message_helpers.rb b/lib/chef/data_collector/message_helpers.rb
new file mode 100644
index 0000000000..75783d8fd0
--- /dev/null
+++ b/lib/chef/data_collector/message_helpers.rb
@@ -0,0 +1,50 @@
+#
+# Copyright:: Copyright 2012-2019, Chef Software 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
+ class DataCollector
+
+ # This is for shared code between the run_start_message and run_end_message modules.
+ #
+ # No external code should call this module directly
+ #
+ # @api private
+ #
+ module MessageHelpers
+ private
+
+ # The organization name the node is associated with. For Chef Solo runs the default
+ # is "chef_solo" which can be overridden by the user.
+ #
+ # @return [String] Chef organization associated with the node
+ #
+ def organization
+ if solo_run?
+ # configurable fake organization name for chef-solo users
+ Chef::Config[:data_collector][:organization]
+ else
+ Chef::Config[:chef_server_url].match(%r{/+organizations/+([^\s/]+)}).nil? ? "unknown_organization" : $1
+ end
+ end
+
+ # @return [Boolean] True if we're in a chef-solo/chef-zero or legacy chef-solo run
+ def solo_run?
+ Chef::Config[:solo_legacy_mode] || Chef::Config[:local_mode]
+ end
+ end
+ end
+end
diff --git a/lib/chef/data_collector/messages.rb b/lib/chef/data_collector/messages.rb
deleted file mode 100644
index c375475c72..0000000000
--- a/lib/chef/data_collector/messages.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-#
-# Author:: Adam Leff (<adamleff@chef.io)
-# Author:: Ryan Cragun (<ryan@chef.io>)
-#
-# Copyright:: Copyright 2012-2016, Chef Software 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 "securerandom"
-require_relative "messages/helpers"
-
-class Chef
- class DataCollector
- module Messages
- extend Helpers
-
- #
- # Message payload that is sent to the DataCollector server at the
- # start of a Chef run.
- #
- # @param run_status [Chef::RunStatus] The RunStatus instance for this node/run.
- #
- # @return [Hash] A hash containing the run start message data.
- #
- def self.run_start_message(run_status)
- {
- "chef_server_fqdn" => chef_server_fqdn,
- "entity_uuid" => node_uuid,
- "id" => run_status.run_id,
- "message_version" => "1.0.0",
- "message_type" => "run_start",
- "node_name" => run_status.node.name,
- "organization_name" => organization,
- "run_id" => run_status.run_id,
- "source" => collector_source,
- "start_time" => run_status.start_time.utc.iso8601,
- }
- end
-
- #
- # Message payload that is sent to the DataCollector server at the
- # end of a Chef run.
- #
- # @param reporter_data [Hash] Data supplied by the Reporter, such as run_status, resource counts, etc.
- #
- # @return [Hash] A hash containing the run end message data.
- #
- def self.run_end_message(reporter_data)
- run_status = reporter_data[:run_status]
-
- message = {
- "chef_server_fqdn" => chef_server_fqdn,
- "entity_uuid" => node_uuid,
- "expanded_run_list" => reporter_data[:expanded_run_list],
- "id" => run_status.run_id,
- "message_version" => "1.1.0",
- "message_type" => "run_converge",
- "node" => run_status.node,
- "node_name" => run_status.node.name,
- "organization_name" => organization,
- "resources" => reporter_data[:resources].map(&:report_data),
- "run_id" => run_status.run_id,
- "run_list" => run_status.node.run_list.for_json,
- "policy_name" => run_status.node.policy_name,
- "policy_group" => run_status.node.policy_group,
- "start_time" => run_status.start_time.utc.iso8601,
- "end_time" => run_status.end_time.utc.iso8601,
- "source" => collector_source,
- "status" => reporter_data[:status],
- "total_resource_count" => reporter_data[:resources].count,
- "updated_resource_count" => reporter_data[:resources].select { |r| r.report_data["status"] == "updated" }.count,
- "deprecations" => reporter_data[:deprecations],
- }
-
- if run_status.exception
- message["error"] = {
- "class" => run_status.exception.class,
- "message" => run_status.exception.message,
- "backtrace" => run_status.exception.backtrace,
- "description" => reporter_data[:error_descriptions],
- }
- end
-
- message
- end
- end
- end
-end
diff --git a/lib/chef/data_collector/messages/helpers.rb b/lib/chef/data_collector/messages/helpers.rb
deleted file mode 100644
index e4eda5ebb2..0000000000
--- a/lib/chef/data_collector/messages/helpers.rb
+++ /dev/null
@@ -1,159 +0,0 @@
-#
-# Author:: Adam Leff (<adamleff@chef.io)
-# Author:: Ryan Cragun (<ryan@chef.io>)
-#
-# Copyright:: Copyright 2012-2016, Chef Software 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
- class DataCollector
- module Messages
- module Helpers
- #
- # Fully-qualified domain name of the Chef Server configured in Chef::Config
- # If the chef_server_url cannot be parsed as a URI, the node["fqdn"] attribute
- # will be returned, or "localhost" if the run_status is unavailable to us.
- #
- # @return [String] FQDN of the configured Chef Server, or node/localhost if not found.
- #
- def chef_server_fqdn
- if !Chef::Config[:chef_server_url].nil?
- URI(Chef::Config[:chef_server_url]).host
- elsif !Chef::Config[:node_name].nil?
- Chef::Config[:node_name]
- else
- "localhost"
- end
- end
-
- #
- # The organization name the node is associated with. For Chef Solo runs, a
- # user-configured organization string is returned, or the string "chef_solo"
- # if such a string is not configured.
- #
- # @return [String] Organization to which the node is associated
- #
- def organization
- solo_run? ? data_collector_organization : chef_server_organization
- end
-
- #
- # Returns the user-configured organization, or "chef_solo" if none is configured.
- #
- # This is only used when Chef is run in Solo mode.
- #
- # @return [String] Data-collector-specific organization used when running in Chef Solo
- #
- def data_collector_organization
- Chef::Config[:data_collector][:organization] || "chef_solo"
- end
-
- #
- # Return the organization assumed by the configured chef_server_url.
- #
- # We must parse this from the Chef::Config[:chef_server_url] because a node
- # has no knowledge of an organization or to which organization is belongs.
- #
- # If we cannot determine the organization, we return "unknown_organization"
- #
- # @return [String] shortname of the Chef Server organization
- #
- def chef_server_organization
- return "unknown_organization" unless Chef::Config[:chef_server_url]
-
- Chef::Config[:chef_server_url].match(%r{/+organizations/+([a-z0-9][a-z0-9_-]{0,254})}).nil? ? "unknown_organization" : $1
- end
-
- #
- # The source of the data collecting during this run, used by the
- # DataCollector endpoint to determine if Chef was in Solo mode or not.
- #
- # @return [String] "chef_solo" if in Solo mode, "chef_client" if in Client mode
- #
- def collector_source
- solo_run? ? "chef_solo" : "chef_client"
- end
-
- #
- # If we're running in Solo (legacy) mode, or in Solo (formerly
- # "Chef Client Local Mode"), we're considered to be in a "solo run".
- #
- # @return [Boolean] Whether we're in a solo run or not
- #
- def solo_run?
- Chef::Config[:solo] || Chef::Config[:local_mode]
- end
-
- #
- # Returns a UUID that uniquely identifies this node for reporting reasons.
- #
- # The node is read in from disk if it exists, or it's generated if it does
- # does not exist.
- #
- # @return [String] UUID for the node
- #
- def node_uuid
- Chef::Config[:chef_guid] || read_node_uuid || generate_node_uuid
- end
-
- #
- # Generates a UUID for the node via SecureRandom.uuid and writes out
- # metadata file so the UUID persists between runs.
- #
- # @return [String] UUID for the node
- #
- def generate_node_uuid
- uuid = SecureRandom.uuid
- update_metadata("node_uuid", uuid)
-
- uuid
- end
-
- #
- # Reads in the node UUID from the node metadata file
- #
- # @return [String] UUID for the node
- #
- def read_node_uuid
- metadata["node_uuid"]
- end
-
- #
- # Returns the DataCollector metadata for this node
- #
- # If the metadata file does not exist in the file cache path,
- # an empty hash will be returned.
- #
- # @return [Hash] DataCollector metadata for this node
- #
- def metadata
- Chef::JSONCompat.parse(Chef::FileCache.load(metadata_filename))
- rescue Chef::Exceptions::FileNotFound
- {}
- end
-
- def update_metadata(key, value)
- updated_metadata = metadata.tap { |x| x[key] = value }
- Chef::FileCache.store(metadata_filename, Chef::JSONCompat.to_json(updated_metadata), 0644)
- end
-
- def metadata_filename
- "data_collector_metadata.json"
- end
- end
- end
- end
-end
diff --git a/lib/chef/data_collector/resource_report.rb b/lib/chef/data_collector/resource_report.rb
deleted file mode 100644
index 9a99747f7f..0000000000
--- a/lib/chef/data_collector/resource_report.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-#
-# Author:: Adam Leff (<adamleff@chef.io>)
-# Author:: Ryan Cragun (<ryan@chef.io>)
-#
-# Copyright:: Copyright 2012-2018, Chef Software 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/exceptions"
-
-class Chef
- class DataCollector
- class ResourceReport
-
- attr_reader :action, :elapsed_time, :new_resource, :status
- attr_accessor :conditional, :current_resource, :exception
-
- def initialize(new_resource, action, current_resource = nil)
- @new_resource = new_resource
- @action = action
- @current_resource = current_resource
- @status = "unprocessed"
- end
-
- def skipped(conditional)
- @status = "skipped"
- @conditional = conditional
- end
-
- def updated
- @status = "updated"
- end
-
- def failed(exception)
- @current_resource = nil
- @status = "failed"
- @exception = exception
- end
-
- def up_to_date
- @status = "up-to-date"
- end
-
- def finish
- @elapsed_time = new_resource.elapsed_time
- end
-
- def elapsed_time_in_milliseconds
- elapsed_time.nil? ? nil : (elapsed_time * 1000).to_i
- end
-
- def potentially_changed?
- %w{updated failed}.include?(status)
- end
-
- def to_h
- hash = {
- "type" => new_resource.resource_name.to_sym,
- "name" => new_resource.name.to_s,
- "id" => resource_identity,
- "after" => new_resource_state_reporter,
- "before" => current_resource_state_reporter,
- "duration" => elapsed_time_in_milliseconds.to_s,
- "delta" => new_resource.respond_to?(:diff) && potentially_changed? ? new_resource.diff : "",
- "ignore_failure" => new_resource.ignore_failure,
- "result" => action.to_s,
- "status" => status,
- }
-
- if new_resource.cookbook_name
- hash["cookbook_name"] = new_resource.cookbook_name
- hash["cookbook_version"] = new_resource.cookbook_version.version
- hash["recipe_name"] = new_resource.recipe_name
- end
-
- hash["conditional"] = conditional.to_text if status == "skipped"
- hash["error_message"] = exception.message unless exception.nil?
-
- hash
- end
- alias_method :to_hash, :to_h
- alias_method :for_json, :to_h
-
- # We should be able to call the identity of a resource safely, but there
- # is an edge case where resources that have a lazy property that is both
- # the name_property and the identity property, it will thow a validation
- # exception causing the chef-client run to fail. We are not fixing this
- # case since Chef is actually doing the right thing but we are making the
- # ResourceReporter smarter so that it detects the failure and sends a
- # message to the data collector containing a static resource identity
- # since we were unable to generate a proper one.
- def resource_identity
- new_resource.identity.to_s
- rescue => e
- "unknown identity (due to #{e.class})"
- end
-
- def new_resource_state_reporter
- new_resource.state_for_resource_reporter
- rescue
- {}
- end
-
- def current_resource_state_reporter
- current_resource ? current_resource.state_for_resource_reporter : {}
- rescue
- {}
- end
- end
- end
-end
diff --git a/lib/chef/data_collector/run_end_message.rb b/lib/chef/data_collector/run_end_message.rb
new file mode 100644
index 0000000000..1aba651d96
--- /dev/null
+++ b/lib/chef/data_collector/run_end_message.rb
@@ -0,0 +1,172 @@
+#
+# Copyright:: Copyright 2012-2019, Chef Software 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/data_collector/message_helpers"
+
+class Chef
+ class DataCollector
+ module RunEndMessage
+ extend Chef::DataCollector::MessageHelpers
+
+ # This module encapsulates rendering the run_end_message given the state gathered in the data_collector
+ # and the action_collection. It is deliberately a stateless module and is deliberately not mixed into
+ # the data_collector and only uses the public api methods of the data_collector and action_collection.
+ #
+ # No external code should call this module directly.
+ #
+ # @api private
+ class << self
+
+ # Construct the message payload that is sent to the DataCollector server at the
+ # end of a Chef run.
+ #
+ # @param data_collector [Chef::DataCollector::Reporter] the calling data_collector instance
+ # @param status [String] the overall status of the run, either "success" or "failure"
+ #
+ # @return [Hash] A hash containing the run end message data.
+ #
+ def construct_message(data_collector, status)
+ action_collection = data_collector.action_collection
+ run_status = data_collector.run_status
+ node = data_collector.node
+
+ message = {
+ "chef_server_fqdn" => URI(Chef::Config[:chef_server_url]).host,
+ "entity_uuid" => Chef::Config[:chef_guid],
+ "expanded_run_list" => data_collector.expanded_run_list,
+ "id" => run_status&.run_id,
+ "message_version" => "1.1.0",
+ "message_type" => "run_converge",
+ "node" => node || {},
+ "node_name" => node&.name || data_collector.node_name,
+ "organization_name" => organization,
+ "resources" => all_action_records(action_collection),
+ "run_id" => run_status&.run_id,
+ "run_list" => node&.run_list&.for_json || [],
+ "policy_name" => node&.policy_name,
+ "policy_group" => node&.policy_group,
+ "start_time" => run_status.start_time.utc.iso8601,
+ "end_time" => run_status.end_time.utc.iso8601,
+ "source" => solo_run? ? "chef_solo" : "chef_client",
+ "status" => status,
+ "total_resource_count" => all_action_records(action_collection).count,
+ "updated_resource_count" => updated_resource_count(action_collection),
+ "deprecations" => data_collector.deprecations.to_a,
+ }
+
+ if run_status&.exception
+ message["error"] = {
+ "class" => run_status.exception.class,
+ "message" => run_status.exception.message,
+ "backtrace" => run_status.exception.backtrace,
+ "description" => data_collector.error_description,
+ }
+ end
+
+ message
+ end
+
+ private
+
+ # @return [Integer] the number of resources successfully updated in the chef-client run
+ def updated_resource_count(action_collection)
+ return 0 if action_collection.nil?
+ action_collection.filtered_collection(up_to_date: false, skipped: false, unprocessed: false, failed: false).count
+ end
+
+ # @return [Array<Chef::ActionCollection::ActionRecord>] list of all action_records for all resources
+ def action_records(action_collection)
+ return [] if action_collection.nil?
+ action_collection.action_records
+ end
+
+ # @return [Array<Hash>] list of all action_records rendered as a Hash for sending to JSON
+ def all_action_records(action_collection)
+ action_records(action_collection).map { |rec| action_record_for_json(rec) }
+ end
+
+ # @return [Hash] the Hash representation of the action_record for sending as JSON
+ def action_record_for_json(action_record)
+ new_resource = action_record.new_resource
+ current_resource = action_record.current_resource
+
+ hash = {
+ "type" => new_resource.resource_name.to_sym,
+ "name" => new_resource.name.to_s,
+ "id" => safe_resource_identity(new_resource),
+ "after" => safe_state_for_resource_reporter(new_resource),
+ "before" => safe_state_for_resource_reporter(current_resource),
+ "duration" => action_record.elapsed_time.nil? ? "" : (action_record.elapsed_time * 1000).to_i.to_s,
+ "delta" => new_resource.respond_to?(:diff) && updated_or_failed?(action_record) ? new_resource.diff : "",
+ "ignore_failure" => new_resource.ignore_failure,
+ "result" => action_record.action.to_s,
+ "status" => action_record_status_for_json(action_record),
+ }
+
+ if new_resource.cookbook_name
+ hash["cookbook_name"] = new_resource.cookbook_name
+ hash["cookbook_version"] = new_resource.cookbook_version.version
+ hash["recipe_name"] = new_resource.recipe_name
+ end
+
+ hash["conditional"] = action_record.conditional.to_text if action_record.status == :skipped
+ hash["error_message"] = action_record.exception.message unless action_record.exception.nil?
+
+ hash
+ end
+
+ # If the identity property of a resource has been lazied (via a lazy name resource) evaluating it
+ # for an unprocessed resource (where the preconditions have not been met) may cause the lazy
+ # evaluator to throw -- and would otherwise crash the data collector.
+ #
+ # @return [String] the resource's identity property
+ #
+ def safe_resource_identity(new_resource)
+ new_resource.identity.to_s
+ rescue => e
+ "unknown identity (due to #{e.class})"
+ end
+
+ # FIXME: This is likely necessary due to the same lazy issue with properties and failing resources?
+ #
+ # @return [Hash] the resource's reported state properties
+ #
+ def safe_state_for_resource_reporter(resource)
+ resource ? resource.state_for_resource_reporter : {}
+ rescue
+ {}
+ end
+
+ # Helper to convert action record status (symbols) to strings for the Data Collector server.
+ # Does a bit of necessary underscores-to-dashes conversion to comply with the Data Collector API.
+ #
+ # @return [String] resource status (
+ #
+ def action_record_status_for_json(action_record)
+ action = action_record.status.to_s
+ action = "up-to-date" if action == "up_to_date"
+ action
+ end
+
+ # @return [Boolean] True if the resource was updated or failed
+ def updated_or_failed?(action_record)
+ action_record.status == :updated || action_record.status == :failed
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/data_collector/run_start_message.rb b/lib/chef/data_collector/run_start_message.rb
new file mode 100644
index 0000000000..448043a48d
--- /dev/null
+++ b/lib/chef/data_collector/run_start_message.rb
@@ -0,0 +1,60 @@
+#
+# Copyright:: Copyright 2012-2019, Chef Software 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/data_collector/message_helpers"
+
+class Chef
+ class DataCollector
+ module RunStartMessage
+ extend Chef::DataCollector::MessageHelpers
+
+ # This module encapsulates rendering the run_start_message given the state gathered in the data_collector.
+ # It is deliberately a stateless module and is deliberately not mixed into the data_collector and only
+ # uses the public api methods of the data_collector.
+ #
+ # No external code should call this module directly.
+ #
+ # @api private
+ class << self
+
+ # Construct the message payload that is sent to the DataCollector server at the
+ # start of a Chef run.
+ #
+ # @param data_collector [Chef::DataCollector::Reporter] the calling data_collector instance
+ #
+ # @return [Hash] A hash containing the run start message data.
+ #
+ def construct_message(data_collector)
+ run_status = data_collector.run_status
+ node = data_collector.node
+ {
+ "chef_server_fqdn" => URI(Chef::Config[:chef_server_url]).host,
+ "entity_uuid" => Chef::Config[:chef_guid],
+ "id" => run_status&.run_id,
+ "message_version" => "1.0.0",
+ "message_type" => "run_start",
+ "node_name" => node&.name || data_collector.node_name,
+ "organization_name" => organization,
+ "run_id" => run_status&.run_id,
+ "source" => solo_run? ? "chef_solo" : "chef_client",
+ "start_time" => run_status.start_time.utc.iso8601,
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb
index 0886d63152..3b0b70c9b9 100644
--- a/lib/chef/event_dispatch/base.rb
+++ b/lib/chef/event_dispatch/base.rb
@@ -29,7 +29,7 @@ class Chef
class Base
# Called at the very start of a Chef Run
- def run_start(version)
+ def run_start(version, run_status)
end
def run_started(run_status)
@@ -44,6 +44,7 @@ class Chef
end
# Called right after ohai runs.
+ # NOTE: the node object here is always nil because of when it is called
def ohai_completed(node)
end
@@ -73,6 +74,10 @@ class Chef
# TODO: def node_run_list_overridden(*args)
+ # Called once the node is loaded by the policy builder
+ def node_load_success(node)
+ end
+
# Failed to load node data from the server
def node_load_failed(node_name, exception, config)
end
@@ -163,6 +168,10 @@ class Chef
## TODO: add callbacks for overall cookbook eval start and complete.
+ # Called immediately after creating the run_context and before any cookbook compilation happens
+ def cookbook_compilation_start(run_context)
+ end
+
# Called when library file loading starts
def library_load_start(file_count)
end
@@ -263,10 +272,18 @@ class Chef
def recipe_load_complete
end
+ # This is called after all cookbook compilation phases are completed.
+ def cookbook_compilation_complete(run_context)
+ end
+
# Called before convergence starts
def converge_start(run_context)
end
+ # Callback hook for handlers to register their interest in the action_collection
+ def action_collection_registration(action_collection)
+ end
+
# Called when the converge phase is finished.
def converge_complete
end
diff --git a/lib/chef/event_dispatch/dispatcher.rb b/lib/chef/event_dispatch/dispatcher.rb
index 69419a393b..d69a7c76e8 100644
--- a/lib/chef/event_dispatch/dispatcher.rb
+++ b/lib/chef/event_dispatch/dispatcher.rb
@@ -10,19 +10,44 @@ class Chef
class Dispatcher < Base
attr_reader :subscribers
+ attr_reader :event_list
def initialize(*subscribers)
@subscribers = subscribers
+ @event_list = []
end
# Add a new subscriber to the list of registered subscribers
def register(subscriber)
- @subscribers << subscriber
+ subscribers << subscriber
+ end
+
+ def unregister(subscriber)
+ subscribers.reject! { |x| x == subscriber }
+ end
+
+ def enqueue(method_name, *args)
+ event_list << [ method_name, *args ]
+ process_events_until_done unless @in_call
+ end
+
+ (Base.instance_methods - Object.instance_methods).each do |method_name|
+ class_eval <<-EOM
+ def #{method_name}(*args)
+ enqueue(#{method_name.inspect}, *args)
+ end
+ EOM
+ end
+
+ # Special case deprecation, since it needs to know its caller
+ def deprecation(message, location = caller(2..2)[0])
+ enqueue(:deprecation, message, location)
end
# Check to see if we are dispatching to a formatter
+ # @api private
def formatter?
- @subscribers.any? { |s| s.respond_to?(:is_formatter?) && s.is_formatter? }
+ subscribers.any? { |s| s.respond_to?(:is_formatter?) && s.is_formatter? }
end
####
@@ -30,9 +55,11 @@ class Chef
# define the forwarding in one go:
#
+ # @api private
def call_subscribers(method_name, *args)
- @subscribers.each do |s|
- # Skip new/unsupported event names.
+ @in_call = true
+ subscribers.each do |s|
+ # Skip new/unsupported event names
next if !s.respond_to?(method_name)
mth = s.method(method_name)
# Trim arguments to match what the subscriber expects to allow
@@ -43,20 +70,19 @@ class Chef
mth.call(*args)
end
end
+ ensure
+ @in_call = false
end
- (Base.instance_methods - Object.instance_methods).each do |method_name|
- class_eval <<-EOM
- def #{method_name}(*args)
- call_subscribers(#{method_name.inspect}, *args)
- end
- EOM
- end
+ private
- # Special case deprecation, since it needs to know its caller
- def deprecation(message, location = caller(2..2)[0])
- call_subscribers(:deprecation, message, location)
+ # events are allowed to enqueue chained events, so pop them off until
+ # empty, rather than iterating over the list.
+ #
+ def process_events_until_done
+ call_subscribers(*event_list.shift) until event_list.empty?
end
+
end
end
end
diff --git a/lib/chef/event_loggers/windows_eventlog.rb b/lib/chef/event_loggers/windows_eventlog.rb
index 950477556a..e084fac684 100644
--- a/lib/chef/event_loggers/windows_eventlog.rb
+++ b/lib/chef/event_loggers/windows_eventlog.rb
@@ -1,7 +1,7 @@
#
# Author:: Jay Mundrawala (<jdm@chef.io>)
#
-# Copyright:: Copyright 2014-2016, Chef Software, Inc.
+# Copyright:: Copyright 2014-2019, Chef Software Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -45,7 +45,7 @@ class Chef
@eventlog = ::Win32::EventLog.open("Application")
end
- def run_start(version)
+ def run_start(version, run_status)
@eventlog.report_event(
event_type: ::Win32::EventLog::INFO_TYPE,
source: SOURCE,
diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb
index 19a6fafae6..936738f147 100644
--- a/lib/chef/formatters/doc.rb
+++ b/lib/chef/formatters/doc.rb
@@ -42,7 +42,7 @@ class Chef
message
end
- def run_start(version)
+ def run_start(version, run_status)
puts_line "Starting Chef Client, version #{version}"
puts_line "OpenSSL FIPS 140 mode enabled" if Chef::Config[:fips]
end
diff --git a/lib/chef/formatters/minimal.rb b/lib/chef/formatters/minimal.rb
index c8fc504eb0..e182189f7d 100644
--- a/lib/chef/formatters/minimal.rb
+++ b/lib/chef/formatters/minimal.rb
@@ -26,7 +26,7 @@ class Chef
end
# Called at the very start of a Chef Run
- def run_start(version)
+ def run_start(version, run_status)
puts_line "Starting Chef Client, version #{version}"
puts_line "OpenSSL FIPS 140 mode enabled" if Chef::Config[:fips]
end
diff --git a/lib/chef/node.rb b/lib/chef/node.rb
index 87418b5732..9b67f27f14 100644
--- a/lib/chef/node.rb
+++ b/lib/chef/node.rb
@@ -2,7 +2,7 @@
# Author:: Christopher Brown (<cb@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
-# Copyright:: Copyright 2008-2018, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,6 +19,7 @@
#
require "forwardable"
+require "securerandom"
require "chef/config"
require "chef/nil_argument"
require "chef/mixin/params_validate"
@@ -338,7 +339,7 @@ class Chef
logger.debug("Platform is #{platform} version #{version}")
automatic[:platform] = platform
automatic[:platform_version] = version
- automatic[:chef_guid] = Chef::Config[:chef_guid]
+ automatic[:chef_guid] = Chef::Config[:chef_guid] || ( Chef::Config[:chef_guid] = node_uuid )
automatic[:name] = name
automatic[:chef_environment] = chef_environment
end
@@ -687,5 +688,24 @@ class Chef
data
end
+ # Returns a UUID that uniquely identifies this node for reporting reasons.
+ #
+ # The node is read in from disk if it exists, or it's generated if it does
+ # does not exist.
+ #
+ # @return [String] UUID for the node
+ #
+ def node_uuid
+ path = File.expand_path(Chef::Config[:chef_guid_path])
+ dir = File.dirname(path)
+
+ unless File.exists?(path)
+ FileUtils.mkdir_p(dir)
+ File.write(path, SecureRandom.uuid)
+ end
+
+ File.open(path).first.chomp
+ end
+
end
end
diff --git a/lib/chef/policy_builder/dynamic.rb b/lib/chef/policy_builder/dynamic.rb
index 8ce4f25bfa..2c49f41656 100644
--- a/lib/chef/policy_builder/dynamic.rb
+++ b/lib/chef/policy_builder/dynamic.rb
@@ -1,6 +1,6 @@
#
# Author:: Daniel DeLeo (<dan@chef.io>)
-# Copyright:: Copyright 2015-2016, Chef Software, Inc.
+# Copyright:: Copyright 2015-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -74,6 +74,7 @@ class Chef
select_implementation(node)
implementation.finish_load_node(node)
node
+ events.node_load_success(node)
rescue Exception => e
events.node_load_failed(node_name, e, config)
raise
diff --git a/lib/chef/policy_builder/expand_node_object.rb b/lib/chef/policy_builder/expand_node_object.rb
index c91c74b047..839c3bb526 100644
--- a/lib/chef/policy_builder/expand_node_object.rb
+++ b/lib/chef/policy_builder/expand_node_object.rb
@@ -3,7 +3,7 @@
# Author:: Tim Hinderliter (<tim@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Daniel DeLeo (<dan@chef.io>)
-# Copyright:: Copyright 2008-2017, Chef Software Inc.
+# Copyright:: Copyright 2008-2018, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -68,6 +68,11 @@ class Chef
Chef.set_run_context(run_context)
end
+ # This not only creates the run_context but this is where we kick off
+ # compiling the entire expanded run_list, loading all the libraries, resources,
+ # attribute files and recipes, and constructing the entire resource collection.
+ # (FIXME: break up creating the run_context and compiling the cookbooks)
+ #
def setup_run_context(specific_recipes = nil)
if Chef::Config[:solo_legacy_mode]
Chef::Cookbook::FileVendor.fetch_from_disk(Chef::Config[:cookbook_path])
@@ -88,18 +93,20 @@ class Chef
run_context = Chef::RunContext.new(node, cookbook_collection, @events)
end
- # TODO: this is really obviously not the place for this
- # FIXME: need same edits
+ # TODO: move this into the cookbook_compilation_start hook
setup_chef_class(run_context)
- # TODO: this is not the place for this. It should be in Runner or
- # CookbookCompiler or something.
+ events.cookbook_compilation_start(run_context)
+
run_context.load(@run_list_expansion)
if specific_recipes
specific_recipes.each do |recipe_file|
run_context.load_recipe_file(recipe_file)
end
end
+
+ events.cookbook_compilation_complete(run_context)
+
run_context
end
diff --git a/lib/chef/policy_builder/policyfile.rb b/lib/chef/policy_builder/policyfile.rb
index 3e7462f0ed..bedbe651a1 100644
--- a/lib/chef/policy_builder/policyfile.rb
+++ b/lib/chef/policy_builder/policyfile.rb
@@ -192,8 +192,12 @@ class Chef
setup_chef_class(run_context)
+ events.cookbook_compilation_start(run_context)
+
run_context.load(run_list_expansion_ish)
+ events.cookbook_compilation_complete(run_context)
+
setup_chef_class(run_context)
run_context
end
diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb
index c857d76c02..e08b2b0bc3 100644
--- a/lib/chef/resource.rb
+++ b/lib/chef/resource.rb
@@ -140,6 +140,7 @@ class Chef
@guard_interpreter = nil
@default_guard_interpreter = :default
@elapsed_time = 0
+ @executed_by_runner = false
end
#
@@ -453,6 +454,11 @@ class Chef
attr_reader :elapsed_time
#
+ # @return [Boolean] If the resource was executed by the runner
+ #
+ attr_accessor :executed_by_runner
+
+ #
# The guard interpreter that will be used to process `only_if` and `not_if`
# statements. If left unset, the #default_guard_interpreter will be used.
#
@@ -1184,8 +1190,10 @@ class Chef
# Internal Resource Interface (for Chef)
#
- FORBIDDEN_IVARS = [:@run_context, :@logger, :@not_if, :@only_if, :@enclosing_provider, :@description, :@introduced, :@examples, :@validation_message, :@deprecated, :@default_description, :@skip_docs].freeze
- HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@logger, :@name, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider, :@description, :@introduced, :@examples, :@validation_message, :@deprecated, :@default_description, :@skip_docs].freeze
+ # FORBIDDEN_IVARS do not show up when the resource is converted to JSON (ie. hidden from data_collector and sending to the chef server via #to_json/to_h/as_json/inspect)
+ FORBIDDEN_IVARS = [:@run_context, :@logger, :@not_if, :@only_if, :@enclosing_provider, :@description, :@introduced, :@examples, :@validation_message, :@deprecated, :@default_description, :@skip_docs, :@executed_by_runner].freeze
+ # HIDDEN_IVARS do not show up when the resource is displayed to the user as text (ie. in the error inspector output via #to_text)
+ HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@logger, :@name, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider, :@description, :@introduced, :@examples, :@validation_message, :@deprecated, :@default_description, :@skip_docs, :@executed_by_runner].freeze
include Chef::Mixin::ConvertToClassName
extend Chef::Mixin::ConvertToClassName
diff --git a/lib/chef/resource_collection/resource_list.rb b/lib/chef/resource_collection/resource_list.rb
index 75305f63a6..3c993d6e55 100644
--- a/lib/chef/resource_collection/resource_list.rb
+++ b/lib/chef/resource_collection/resource_list.rb
@@ -1,6 +1,6 @@
#
# Author:: Tyler Ball (<tball@chef.io>)
-# Copyright:: Copyright 2014-2016, Chef Software, Inc.
+# Copyright:: Copyright 2014-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/lib/chef/resource_reporter.rb b/lib/chef/resource_reporter.rb
index 3198f84d12..01e4073549 100644
--- a/lib/chef/resource_reporter.rb
+++ b/lib/chef/resource_reporter.rb
@@ -3,7 +3,7 @@
# Author:: Prajakta Purohit (prajakta@chef.io>)
# Auther:: Tyler Cloke (<tyler@opscode.com>)
#
-# Copyright:: Copyright 2012-2018, Chef Software Inc.
+# Copyright:: Copyright 2012-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,88 +25,40 @@ require "chef/event_dispatch/base"
class Chef
class ResourceReporter < EventDispatch::Base
+ def for_json(action_record)
+ new_resource = action_record.new_resource
+ current_resource = action_record.current_resource
- ResourceReport = Struct.new(:new_resource,
- :current_resource,
- :action,
- :exception,
- :elapsed_time) do
-
- 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
+ as_hash = {}
+ as_hash["type"] = new_resource.resource_name.to_sym
+ as_hash["name"] = new_resource.name.to_s
+ as_hash["id"] = new_resource.identity.to_s
+ as_hash["after"] = new_resource.state_for_resource_reporter
+ as_hash["before"] = current_resource ? current_resource.state_for_resource_reporter : {}
+ as_hash["duration"] = ( action_record.elapsed_time * 1000 ).to_i
+ as_hash["delta"] = new_resource.diff if new_resource.respond_to?("diff")
+ as_hash["delta"] = "" if as_hash["delta"].nil?
- def self.new_for_exception(new_resource, action)
- report = new
- report.new_resource = new_resource
- report.action = action
- report
+ # TODO: rename as "action"
+ as_hash["result"] = action_record.action.to_s
+ if new_resource.cookbook_name
+ as_hash["cookbook_name"] = new_resource.cookbook_name
+ as_hash["cookbook_version"] = new_resource.cookbook_version.version
end
- # Future: Some resources store state information that does not convert nicely
- # to json. We can't call a resource's state method here, since there are conflicts
- # with some LWRPs, so we can't override a resource's state method to return
- # json-friendly state data.
- #
- # The registry key resource returns json-friendly state data through its state
- # attribute, and uses a read-only variable for fetching true state data. If
- # we have conflicts with other resources reporting json incompatible state, we
- # may want to extend the state_attrs API with the ability to rename POST'd
- # attrs.
- def for_json
- as_hash = {}
- as_hash["type"] = new_resource.resource_name.to_sym
- as_hash["name"] = new_resource.name.to_s
- as_hash["id"] = new_resource.identity.to_s
- as_hash["after"] = new_resource.state_for_resource_reporter
- as_hash["before"] = current_resource ? current_resource.state_for_resource_reporter : {}
- 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
- if new_resource.cookbook_name
- as_hash["cookbook_name"] = new_resource.cookbook_name
- as_hash["cookbook_version"] = new_resource.cookbook_version.version
- end
-
- as_hash
- end
-
- def finish
- self.elapsed_time = new_resource.elapsed_time
- end
-
- def success?
- !exception
- end
- end # End class ResouceReport
+ as_hash
+ end
- attr_reader :updated_resources
attr_reader :status
attr_reader :exception
attr_reader :error_descriptions
+ attr_reader :action_collection
+ attr_reader :rest_client
PROTOCOL_VERSION = "0.1.0".freeze
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
+ @pending_update = nil
@status = "success"
@exception = nil
@rest_client = rest_client
@@ -120,8 +72,8 @@ class Chef
if reporting_enabled?
begin
resource_history_url = "reports/nodes/#{node_name}/runs"
- server_response = @rest_client.post(resource_history_url, { action: :start, run_id: run_id,
- start_time: start_time.to_s }, headers)
+ server_response = rest_client.post(resource_history_url, { action: :start, run_id: run_id,
+ start_time: start_time.to_s }, headers)
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
handle_error_starting_run(e, resource_history_url)
end
@@ -157,62 +109,13 @@ class Chef
end
end
- @reporting_enabled = false
+ @runs_endpoint_failed = true
end
def run_id
@run_status.run_id
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
-
- # Verify if the resource has sensitive data
- # and create a new blank resource with only
- # the name so we can report it back without
- # sensitive data
- if @pending_update.new_resource.sensitive
- klass = @pending_update.new_resource.class
- resource_name = @pending_update.new_resource.name
- @pending_update.new_resource = klass.new(resource_name)
- end
-
- @updated_resources << @pending_update
- @pending_update = nil
- end
- end
-
def run_completed(node)
@status = "success"
post_reporting_data
@@ -232,6 +135,11 @@ class Chef
@expanded_run_list = run_list_expansion
end
+ def action_collection_registration(action_collection)
+ @action_collection = action_collection
+ action_collection.register(self) if reporting_enabled?
+ end
+
def post_reporting_data
if reporting_enabled?
run_data = prepare_run_data
@@ -242,7 +150,7 @@ class Chef
Chef::Log.trace("Sending compressed run data...")
# Since we're posting compressed data we can not directly call post which expects JSON
begin
- @rest_client.raw_request(:POST, resource_history_url, headers({ "Content-Encoding" => "gzip" }), compressed_data)
+ rest_client.raw_request(:POST, resource_history_url, headers({ "Content-Encoding" => "gzip" }), compressed_data)
rescue StandardError => e
if e.respond_to? :response
Chef::FileCache.store("failed-reporting-data.json", Chef::JSONCompat.to_json_pretty(run_data), 0640)
@@ -273,15 +181,24 @@ class Chef
@run_status.end_time
end
+ # get only the top level resources and strip out the subcollections
+ def updated_resources
+ @updated_resources ||= action_collection.filtered_collection(max_nesting: 0, up_to_date: false, skipped: false, unprocessed: false)
+ end
+
+ def total_res_count
+ updated_resources.count
+ end
+
def prepare_run_data
run_data = {}
run_data["action"] = "end"
- run_data["resources"] = updated_resources.map do |resource_record|
- resource_record.for_json
+ run_data["resources"] = updated_resources.map do |action_record|
+ for_json(action_record)
end
run_data["status"] = @status
run_data["run_list"] = Chef::JSONCompat.to_json(@run_status.node.run_list)
- run_data["total_res_count"] = @total_res_count.to_s
+ run_data["total_res_count"] = total_res_count.to_s
run_data["data"] = {}
run_data["start_time"] = start_time.to_s
run_data["end_time"] = end_time.to_s
@@ -313,18 +230,10 @@ class Chef
@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
+ def reporting_enabled?
+ Chef::Config[:enable_reporting] && !Chef::Config[:why_run] && !@runs_endpoint_failed
end
def encode_gzip(data)
diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb
index b940bfecbd..e407a0e7be 100644
--- a/lib/chef/run_context.rb
+++ b/lib/chef/run_context.rb
@@ -2,7 +2,7 @@
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
-# Copyright:: Copyright 2008-2017, Chef Software Inc.
+# Copyright:: Copyright 2008-2018, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -104,6 +104,7 @@ class Chef
#
attr_reader :resource_collection
+ attr_accessor :action_collection
#
# The list of control groups to execute during the audit phase
#
@@ -603,6 +604,8 @@ class Chef
class ChildRunContext < RunContext
extend Forwardable
def_delegators :parent_run_context, *%w{
+ action_collection
+ action_collection=
cancel_reboot
config
cookbook_collection
diff --git a/lib/chef/run_context/cookbook_compiler.rb b/lib/chef/run_context/cookbook_compiler.rb
index c3cee5841f..1bd612e721 100644
--- a/lib/chef/run_context/cookbook_compiler.rb
+++ b/lib/chef/run_context/cookbook_compiler.rb
@@ -1,6 +1,6 @@
#
# Author:: Daniel DeLeo (<dan@chef.io>)
-# Copyright:: Copyright 2012-2018, Chef Software Inc.
+# Copyright:: Copyright 2012-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/lib/chef/run_status.rb b/lib/chef/run_status.rb
index 37b10fb9be..575d31159b 100644
--- a/lib/chef/run_status.rb
+++ b/lib/chef/run_status.rb
@@ -1,6 +1,6 @@
#
# Author:: Daniel DeLeo (<dan@chef.io>)
-# Copyright:: Copyright 2010-2018, Chef Software Inc.
+# Copyright:: Copyright 2010-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -53,6 +53,7 @@ class Chef::RunStatus
# sets +end_time+ to the current time
def stop_clock
+ @start_time ||= Time.now # if we failed so early we didn't get a start time
@end_time = Time.now
end
diff --git a/lib/chef/runner.rb b/lib/chef/runner.rb
index 1c82439b57..d5dab75dfd 100644
--- a/lib/chef/runner.rb
+++ b/lib/chef/runner.rb
@@ -2,7 +2,7 @@
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
# Author:: Tim Hinderliter (<tim@chef.io>)
-# Copyright:: Copyright 2008-2017, Chef Software Inc.
+# Copyright:: Copyright 2008-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -95,7 +95,11 @@ class Chef
# Execute each resource.
run_context.resource_collection.execute_each_resource do |resource|
- Array(resource.action).each { |action| run_action(resource, action) }
+ begin
+ Array(resource.action).each { |action| run_action(resource, action) }
+ ensure
+ resource.executed_by_runner = true
+ end
end
rescue Exception => e