summaryrefslogtreecommitdiff
path: root/lib/chef/data_collector/run_end_message.rb
blob: 6050f61ad43f2ae826545a6043cd8c3cc2756448 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#
# Copyright:: Copyright (c) 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_relative "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 || [],
            "cookbooks" => ( node && node["cookbooks"] ) || {},
            "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