summaryrefslogtreecommitdiff
path: root/lib/chef/resource/chef_handler.rb
blob: a006b2648a796b1db43f476a29f538a89b761cfe (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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
#
# Author:: Seth Chisamore <schisamo@chef.io>
# Copyright:: Copyright (c) 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.
# 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 "../resource"
require "chef-utils/dist" unless defined?(ChefUtils::Dist)

class Chef
  class Resource
    class ChefHandler < Chef::Resource
      unified_mode true

      provides(:chef_handler) { true }

      description "Use the **chef_handler** resource to enable handlers during a #{ChefUtils::Dist::Infra::PRODUCT} run. The resource allows arguments to be passed to #{ChefUtils::Dist::Infra::PRODUCT}, which then applies the conditions defined by the custom handler to the node attribute data collected during a #{ChefUtils::Dist::Infra::PRODUCT} run, and then processes the handler based on that data.\nThe **chef_handler** resource is typically defined early in a node's run-list (often being the first item). This ensures that all of the handlers will be available for the entire #{ChefUtils::Dist::Infra::PRODUCT} run."
      introduced "14.0"
      examples <<~'DOC'
      **Enable the 'MyHandler' handler**

      The following example shows how to enable a fictional 'MyHandler' handler which is located on disk at `/etc/chef/my_handler.rb`. The handler will be configured to run with Chef Infra Client and will be passed values to the handler's initializer method:

      ```ruby
      chef_handler 'MyHandler' do
        source '/etc/chef/my_handler.rb' # the file should already be at this path
        arguments path: '/var/chef/reports'
        action :enable
      end
      ```

      **Enable handlers during the compile phase**

      ```ruby
      chef_handler 'Chef::Handler::JsonFile' do
        source 'chef/handler/json_file'
        arguments path: '/var/chef/reports'
        action :enable
        compile_time true
      end
      ```

      **Handle only exceptions**

      ```ruby
      chef_handler 'Chef::Handler::JsonFile' do
        source 'chef/handler/json_file'
        arguments path: '/var/chef/reports'
        type exception: true
        action :enable
      end
      ```

      **Cookbook Versions (a custom handler)**

      [@juliandunn](https://github.com/juliandunn) created a custom report handler that logs all of the cookbooks and cookbook versions that were used during a Chef Infra Client run, and then reports after the run is complete.

      cookbook_versions.rb:

      The following custom handler defines how cookbooks and cookbook versions that are used during a Chef Infra Client run will be compiled into a report using the `Chef::Log` class in Chef Infra Client:

      ```ruby
      require 'chef/log'

      module Chef
        class CookbookVersionsHandler < Chef::Handler
          def report
            cookbooks = run_context.cookbook_collection
            Chef::Log.info('Cookbooks and versions run: #{cookbooks.map {|x| x.name.to_s + ' ' + x.version }}')
          end
        end
      end
      ```

      default.rb:

      The following recipe is added to the run-list for every node on which a list of cookbooks and versions will be generated as report output after every Chef Infra Client run.

      ```ruby
      cookbook_file '/etc/chef/cookbook_versions.rb' do
        source 'cookbook_versions.rb'
        action :create
      end

      chef_handler 'Chef::CookbookVersionsHandler' do
        source '/etc/chef/cookbook_versions.rb'
        type report: true
        action :enable
      end
      ```

      This recipe will generate report output similar to the following:

      ```
      [2013-11-26T03:11:06+00:00] INFO: Chef Infra Client Run complete in 0.300029878 seconds
      [2013-11-26T03:11:06+00:00] INFO: Running report handlers
      [2013-11-26T03:11:06+00:00] INFO: Cookbooks and versions run: ["cookbook_versions_handler 1.0.0"]
      [2013-11-26T03:11:06+00:00] INFO: Report handlers complete
      ```

      **JsonFile Handler**

      The JsonFile handler is available from the `chef_handler` cookbook and can be used with exceptions and reports. It serializes run status data to a JSON file. This handler may be enabled in one of the following ways.

      By adding the following lines of Ruby code to either the client.rb file or the solo.rb file, depending on how Chef Infra Client is being run:

      ```ruby
      require 'chef/handler/json_file'
      report_handlers << Chef::Handler::JsonFile.new(path: '/var/chef/reports')
      exception_handlers << Chef::Handler::JsonFile.new(path: '/var/chef/reports')
      ```

      By using the `chef_handler` resource in a recipe, similar to the following:

      ```ruby
      chef_handler 'Chef::Handler::JsonFile' do
        source 'chef/handler/json_file'
        arguments path: '/var/chef/reports'
        action :enable
      end
      ```

      After it has run, the run status data can be loaded and inspected via Interactive Ruby (IRb):

      ```
      irb(main):002:0> require 'json' => true
      irb(main):003:0> require 'chef' => true
      irb(main):004:0> r = JSON.parse(IO.read('/var/chef/reports/chef-run-report-20110322060731.json')) => ... output truncated
      irb(main):005:0> r.keys => ['end_time', 'node', 'updated_resources', 'exception', 'all_resources', 'success', 'elapsed_time', 'start_time', 'backtrace']
      irb(main):006:0> r['elapsed_time'] => 0.00246
      ```

      Register the JsonFile handler

      ```ruby
      chef_handler 'Chef::Handler::JsonFile' do
        source 'chef/handler/json_file'
        arguments path: '/var/chef/reports'
        action :enable
      end
      ```

      **ErrorReport Handler**

      The ErrorReport handler is built into Chef Infra Client and can be used for both exceptions and reports. It serializes error report data to a JSON file. This handler may be enabled in one of the following ways.

      By adding the following lines of Ruby code to either the client.rb file or the solo.rb file, depending on how Chef Infra Client is being run:

      ```ruby
      require 'chef/handler/error_report'
      report_handlers << Chef::Handler::ErrorReport.new
      exception_handlers << Chef::Handler::ErrorReport.new
      ```

      By using the `chef_handler` resource in a recipe, similar to the following:

      ```ruby
      chef_handler 'Chef::Handler::ErrorReport' do
        source 'chef/handler/error_report'
        action :enable
      end
      ```
      DOC

      property :class_name, String,
        description: "The name of the handler class. This can be module name-spaced.",
        name_property: true

      property :source, String,
        description: "The full path to the handler file. Can also be a gem path if the handler ships as part of a Ruby gem."

      property :arguments, [Array, Hash],
        description: "Arguments to pass the handler's class initializer.",
        default: lazy { [] }

      property :type, Hash,
        description: "The type of handler to register as, i.e. :report, :exception or both.",
        default: { report: true, exception: true }

      # supports means a different thing in chef-land so we renamed it but
      # wanted to make sure we didn't break the world
      alias_method :supports, :type

      # This action needs to find an rb file that presumably contains the indicated class in it and the
      # load that file. It then instantiates that class by name and registers it as a handler.
      action :enable do
        description "Enables the handler for the current #{ChefUtils::Dist::Infra::PRODUCT} run on the current node"

        class_name = new_resource.class_name
        new_resource.type.each do |type, enable|
          next unless enable

          unregister_handler(type, class_name)
        end

        handler = nil

        require new_resource.source unless new_resource.source.nil?

        _, klass = get_class(class_name)
        handler = klass.send(:new, *collect_args(new_resource.arguments))

        new_resource.type.each do |type, enable|
          next unless enable

          register_handler(type, handler)
        end
      end

      action :disable do
        description "Disables the handler for the current #{ChefUtils::Dist::Infra::PRODUCT} run on the current node"

        new_resource.type.each_key do |type|
          unregister_handler(type, new_resource.class_name)
        end
      end

      action_class do
        # Registers a handler in Chef::Config.
        #
        # @param handler_type [Symbol] such as :report or :exception.
        # @param handler [Chef::Handler] handler to register.
        def register_handler(handler_type, handler)
          Chef::Log.info("Enabling #{handler.class.name} as a #{handler_type} handler.")
          Chef::Config.send("#{handler_type}_handlers") << handler
        end

        # Removes all handlers that match the given class name in Chef::Config.
        #
        # @param handler_type [Symbol] such as :report or :exception.
        # @param class_full_name [String] such as 'Chef::Handler::ErrorReport'.
        #
        # @return [void]
        def unregister_handler(handler_type, class_full_name)
          Chef::Config.send("#{handler_type}_handlers").delete_if do |v|
            # avoid a bit of log spam
            if v.class.name == class_full_name
              Chef::Log.info("Disabling #{class_full_name} as a #{handler_type} handler.")
              true
            end
          end
        end

        # Walks down the namespace hierarchy to return the class object for the given class name.
        # If the class is not available, NameError is thrown.
        #
        # @param class_full_name [String] full class name such as 'Chef::Handler::Foo' or 'MyHandler'.
        #
        # @return [Array] parent class and child class.
        def get_class(class_full_name)
          ancestors = class_full_name.split("::")
          class_name = ancestors.pop

          # We need to search the ancestors only for the first/uppermost namespace of the class, so we
          # need to enable the #const_get inherit parameter only when we are searching in Kernel scope
          # (see COOK-4117).
          parent = ancestors.inject(Kernel) { |scope, const_name| scope.const_get(const_name, scope === Kernel) }
          child = parent.const_get(class_name, parent === Kernel)
          [parent, child]
        end

        def collect_args(resource_args = [])
          if resource_args.is_a? Array
            resource_args
          else
            [resource_args]
          end
        end
      end
    end
  end
end