Skip to content

Commit cb61aba

Browse files
committed
add callbacks to Chef::Client to notify on chef run start/stop
* Chef::Client adds callbacks for run start, success, and failure * refactor report/notification handlers to use the callbacks * refactor cookbook cache cleanup to use the callbacks * solution for CHEF-1397 will use the callbacks for cache cleanup
1 parent 65192fd commit cb61aba

6 files changed

Lines changed: 333 additions & 162 deletions

File tree

‎chef/lib/chef/application/client.rb‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,8 @@ def run_application
224224
raise
225225
rescue Exception => e
226226
if Chef::Config[:interval]
227-
Chef::Log.error("#{e.class}")
228-
Chef::Log.fatal("#{e}\n#{e.backtrace.join("\n")}")
229-
Chef::Log.fatal("Sleeping for #{Chef::Config[:interval]} seconds before trying again")
227+
Chef::Log.error("#{e.class}:#{e}\n#{e.backtrace.join("\n")}")
228+
Chef::Log.error("Sleeping for #{Chef::Config[:interval]} seconds before trying again")
230229
sleep Chef::Config[:interval]
231230
retry
232231
else

‎chef/lib/chef/client.rb‎

Lines changed: 82 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,73 @@ class Chef
4040
# The main object in a Chef run. Preps a Chef::Node and Chef::RunContext,
4141
# syncs cookbooks if necessary, and triggers convergence.
4242
class Client
43+
44+
# Clears all notifications for client run status events.
45+
# Primarily for testing purposes.
46+
def self.clear_notifications
47+
@run_start_notifications = nil
48+
@run_completed_successfully_notifications = nil
49+
@run_failed_notifications = nil
50+
end
51+
52+
# The list of notifications to be run when the client run starts.
53+
def self.run_start_notifications
54+
@run_start_notifications ||= []
55+
end
56+
57+
# The list of notifications to be run when the client run completes
58+
# successfully.
59+
def self.run_completed_successfully_notifications
60+
@run_completed_successfully_notifications ||= []
61+
end
62+
63+
# The list of notifications to be run when the client run fails.
64+
def self.run_failed_notifications
65+
@run_failed_notifications ||= []
66+
end
67+
68+
# Add a notification for the 'client run started' event. The notification
69+
# is provided as a block. The current Chef::RunStatus object will be passed
70+
# to the notification_block when the event is triggered.
71+
def self.when_run_starts(&notification_block)
72+
run_start_notifications << notification_block
73+
end
74+
75+
# Add a notification for the 'client run success' event. The notification
76+
# is provided as a block. The current Chef::RunStatus object will be passed
77+
# to the notification_block when the event is triggered.
78+
def self.when_run_completes_successfully(&notification_block)
79+
run_completed_successfully_notifications << notification_block
80+
end
81+
82+
# Add a notification for the 'client run failed' event. The notification
83+
# is provided as a block. The current Chef::RunStatus is passed to the
84+
# notification_block when the event is triggered.
85+
def self.when_run_fails(&notification_block)
86+
run_failed_notifications << notification_block
87+
end
88+
89+
# Callback to fire notifications that the Chef run is starting
90+
def run_started
91+
self.class.run_start_notifications.each do |notification|
92+
notification.call(run_status)
93+
end
94+
end
95+
96+
# Callback to fire notifications that the run completed successfully
97+
def run_completed_successfully
98+
self.class.run_completed_successfully_notifications.each do |notification|
99+
notification.call(run_status)
100+
end
101+
end
102+
103+
# Callback to fire notifications that the Chef run failed
104+
def run_failed
105+
self.class.run_failed_notifications.each do |notification|
106+
notification.call(run_status)
107+
end
108+
end
109+
43110
attr_accessor :node
44111
attr_accessor :ohai
45112
attr_accessor :rest
@@ -49,10 +116,13 @@ class Client
49116
# TODO: timh/cw: 5-19-2010: json_attribs should be moved to RunContext?
50117
attr_reader :json_attribs
51118

119+
attr_reader :run_status
120+
52121
# Creates a new Chef::Client.
53122
def initialize(json_attribs=nil)
54123
@json_attribs = json_attribs
55124
@node = nil
125+
@run_status = nil
56126
@runner = nil
57127
@ohai = Ohai::System.new
58128
end
@@ -75,9 +145,10 @@ def run
75145
build_node
76146

77147
begin
78-
run_status = Chef::RunStatus.new(node)
148+
79149
run_status.start_clock
80150
Chef::Log.info("Starting Chef Run (Version #{Chef::VERSION})")
151+
run_started
81152

82153
if Chef::Config[:solo]
83154
Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest) }
@@ -86,18 +157,12 @@ def run
86157
assert_cookbook_path_not_empty(run_context)
87158
converge(run_context)
88159
else
89-
# Keep track of the filenames that we use in both eager cookbook
90-
# downloading (during sync_cookbooks) and lazy (during the run
91-
# itself, through FileVendor). After the run is over, clean up the
92-
# cache.
93-
valid_cache_entries = Hash.new
94-
95160
# Sync_cookbooks eagerly loads all files except files and templates.
96161
# It returns the cookbook_hash -- the return result from
97162
# /nodes/#{nodename}/cookbooks -- which we will use for our
98163
# run_context.
99-
Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, rest, valid_cache_entries) }
100-
cookbook_hash = sync_cookbooks(valid_cache_entries)
164+
Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, rest) }
165+
cookbook_hash = sync_cookbooks
101166
run_context = Chef::RunContext.new(node, Chef::CookbookCollection.new(cookbook_hash))
102167
run_status.run_context = run_context
103168

@@ -106,41 +171,23 @@ def run
106171
converge(run_context)
107172
Chef::Log.debug("Saving the current state of node #{node_name}")
108173
@node.save
109-
110-
cleanup_file_cache(valid_cache_entries)
111174
end
112175

113176
run_status.stop_clock
114177
Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds")
115-
run_report_handlers(run_status)
178+
run_completed_successfully
116179
true
117180
rescue Exception => e
118181
run_status.stop_clock
119182
run_status.exception = e
120-
run_exception_handlers(run_status)
121-
Chef::Log.error("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}")
183+
run_failed
184+
Chef::Log.debug("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}")
122185
raise
123186
ensure
124187
run_status = nil
125188
end
126189
end
127190

128-
def run_report_handlers(run_status)
129-
Chef::Log.info("Running report handlers")
130-
Array(Chef::Config[:report_handlers]).each do |handler|
131-
handler.run_report_safely(run_status)
132-
end
133-
Chef::Log.info("Report handlers complete")
134-
end
135-
136-
def run_exception_handlers(run_status)
137-
Chef::Log.error("Running exception handlers")
138-
Array(Chef::Config[:exception_handlers]).each do |handler|
139-
handler.run_report_safely(run_status)
140-
end
141-
Chef::Log.error("Exception handlers complete")
142-
end
143-
144191
def run_ohai
145192
ohai.all_plugins
146193
end
@@ -171,10 +218,13 @@ def build_node
171218
@node = Chef::Node.find_or_create(node_name)
172219
end
173220

221+
174222
@node.consume_external_attrs(ohai.data, @json_attribs)
175223
@node.save unless Chef::Config[:solo]
176224
@node.reset_defaults_and_overrides
177225

226+
@run_status = Chef::RunStatus.new(@node)
227+
178228
@node
179229
end
180230

@@ -196,104 +246,17 @@ def register
196246
#
197247
# === Returns
198248
# true:: Always returns true
199-
def sync_cookbooks(valid_cache_entries)
249+
def sync_cookbooks
200250
Chef::Log.debug("Synchronizing cookbooks")
201251
cookbook_hash = rest.get_rest("nodes/#{node_name}/cookbooks")
202-
Chef::Log.debug("Cookbooks to load: #{cookbook_hash.inspect}")
203-
204-
# Remove all cookbooks no longer relevant to this node
205-
Chef::FileCache.find(File.join(%w{cookbooks ** *})).each do |cache_file|
206-
cache_file =~ /^cookbooks\/([^\/]+)\//
207-
unless cookbook_hash.has_key?($1)
208-
Chef::Log.info("Removing #{cache_file} from the cache; its cookbook is no longer needed on this client.")
209-
Chef::FileCache.delete(cache_file)
210-
end
211-
end
212-
213-
# Synchronize each of the node's cookbooks, and add to the
214-
# valid_cache_entries hash.
215-
cookbook_hash.values.each do |cookbook|
216-
sync_cookbook_file_cache(cookbook, valid_cache_entries)
217-
end
252+
Chef::CookbookVersion.sync_cookbooks(cookbook_hash)
218253

219254
# register the file cache path in the cookbook path so that CookbookLoader actually picks up the synced cookbooks
220255
Chef::Config[:cookbook_path] = File.join(Chef::Config[:file_cache_path], "cookbooks")
221256

222257
cookbook_hash
223258
end
224259

225-
# Update the file caches for a given cache segment. Takes a segment name
226-
# and a hash that matches one of the cookbooks/_attribute_files style
227-
# remote file listings.
228-
#
229-
# === Parameters
230-
# cookbook<Chef::Cookbook>:: The cookbook to update
231-
# valid_cache_entries<Hash>:: Out-param; Added to this hash are the files that
232-
# were referred to by this cookbook
233-
def sync_cookbook_file_cache(cookbook, valid_cache_entries)
234-
Chef::Log.debug("Synchronizing cookbook #{cookbook.name}")
235-
236-
# files and templates are lazily loaded, and will be done later.
237-
eager_segments = Chef::CookbookVersion::COOKBOOK_SEGMENTS.dup
238-
eager_segments.delete(:files)
239-
eager_segments.delete(:templates)
240-
241-
eager_segments.each do |segment|
242-
segment_filenames = Array.new
243-
cookbook.manifest[segment].each do |manifest_record|
244-
# segment = cookbook segment
245-
# remote_list = list of file hashes
246-
#
247-
# We need the list of known good attribute files, so we can delete any that are
248-
# just laying about.
249-
250-
cache_filename = File.join("cookbooks", cookbook.name, manifest_record['path'])
251-
valid_cache_entries[cache_filename] = true
252-
253-
current_checksum = nil
254-
if Chef::FileCache.has_key?(cache_filename)
255-
current_checksum = Chef::CookbookVersion.checksum_cookbook_file(Chef::FileCache.load(cache_filename, false))
256-
end
257-
258-
# If the checksums are different between on-disk (current) and on-server
259-
# (remote, per manifest), do the update. This will also execute if there
260-
# is no current checksum.
261-
if current_checksum != manifest_record['checksum']
262-
raw_file = rest.get_rest(manifest_record[:url], true)
263-
264-
Chef::Log.info("Storing updated #{cache_filename} in the cache.")
265-
Chef::FileCache.move_to(raw_file.path, cache_filename)
266-
else
267-
Chef::Log.debug("Not storing #{cache_filename}, as the cache is up to date.")
268-
end
269-
270-
# make the segment filenames a full path.
271-
full_path_cache_filename = Chef::FileCache.load(cache_filename, false)
272-
segment_filenames << full_path_cache_filename
273-
end
274-
275-
# replace segment filenames with a full-path one.
276-
if segment.to_sym == :recipes
277-
cookbook.recipe_filenames = segment_filenames
278-
elsif segment.to_sym == :attributes
279-
cookbook.attribute_filenames = segment_filenames
280-
else
281-
cookbook.segment_filenames(segment).replace(segment_filenames)
282-
end
283-
end
284-
end
285-
286-
def cleanup_file_cache(valid_cache_entries)
287-
# Delete each file in the cache that we didn't encounter in the
288-
# manifest.
289-
Chef::FileCache.find(File.join(%w{cookbooks ** *})).each do |cache_filename|
290-
unless valid_cache_entries[cache_filename]
291-
Chef::Log.info("Removing #{cache_filename} from the cache; it is no longer on the server.")
292-
Chef::FileCache.delete(cache_filename)
293-
end
294-
end
295-
end
296-
297260
# Converges the node.
298261
#
299262
# === Returns

‎chef/lib/chef/cookbook/remote_file_vendor.rb‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,10 @@ class Cookbook
2525
# if not available, loading them from the remote server.
2626
class RemoteFileVendor < FileVendor
2727

28-
def initialize(manifest, rest, valid_cache_entries)
28+
def initialize(manifest, rest)
2929
@manifest = manifest
3030
@cookbook_name = @manifest[:cookbook_name]
3131
@rest = rest
32-
@valid_cache_entries = valid_cache_entries
3332
end
3433

3534
# Implements abstract base's requirement. It looks in the
@@ -50,7 +49,7 @@ def get_filename(filename)
5049

5150
# update valid_cache_entries so the upstream cache cleaner knows what
5251
# we've used.
53-
@valid_cache_entries[cache_filename] = true
52+
validate_cached_copy(cache_filename)
5453

5554
current_checksum = nil
5655
if Chef::FileCache.has_key?(cache_filename)
@@ -74,6 +73,14 @@ def get_filename(filename)
7473
# return the filename, not the contents (second argument= false)
7574
full_path_cache_filename
7675
end
76+
77+
def validate_cached_copy(cache_filename)
78+
valid_cache_entries[cache_filename] = true
79+
end
80+
81+
def valid_cache_entries
82+
Chef::CookbookVersion.valid_cache_entries
83+
end
7784

7885
end
7986
end

0 commit comments

Comments
 (0)