lib/chef/client.rb in chef-0.9.8 vs lib/chef/client.rb in chef-0.9.10.rc.0

- old
+ new

@@ -38,23 +38,93 @@ class Chef # == Chef::Client # The main object in a Chef run. Preps a Chef::Node and Chef::RunContext, # syncs cookbooks if necessary, and triggers convergence. class Client + + # Clears all notifications for client run status events. + # Primarily for testing purposes. + def self.clear_notifications + @run_start_notifications = nil + @run_completed_successfully_notifications = nil + @run_failed_notifications = nil + end + + # The list of notifications to be run when the client run starts. + def self.run_start_notifications + @run_start_notifications ||= [] + end + + # The list of notifications to be run when the client run completes + # successfully. + def self.run_completed_successfully_notifications + @run_completed_successfully_notifications ||= [] + end + + # The list of notifications to be run when the client run fails. + def self.run_failed_notifications + @run_failed_notifications ||= [] + end + + # Add a notification for the 'client run started' event. The notification + # is provided as a block. The current Chef::RunStatus object will be passed + # to the notification_block when the event is triggered. + def self.when_run_starts(&notification_block) + run_start_notifications << notification_block + end + + # Add a notification for the 'client run success' event. The notification + # is provided as a block. The current Chef::RunStatus object will be passed + # to the notification_block when the event is triggered. + def self.when_run_completes_successfully(&notification_block) + run_completed_successfully_notifications << notification_block + end + + # Add a notification for the 'client run failed' event. The notification + # is provided as a block. The current Chef::RunStatus is passed to the + # notification_block when the event is triggered. + def self.when_run_fails(&notification_block) + run_failed_notifications << notification_block + end + + # Callback to fire notifications that the Chef run is starting + def run_started + self.class.run_start_notifications.each do |notification| + notification.call(run_status) + end + end + + # Callback to fire notifications that the run completed successfully + def run_completed_successfully + self.class.run_completed_successfully_notifications.each do |notification| + notification.call(run_status) + end + end + + # Callback to fire notifications that the Chef run failed + def run_failed + self.class.run_failed_notifications.each do |notification| + notification.call(run_status) + end + end + attr_accessor :node attr_accessor :ohai attr_accessor :rest attr_accessor :runner #-- # TODO: timh/cw: 5-19-2010: json_attribs should be moved to RunContext? attr_reader :json_attribs + attr_reader :run_status + # Creates a new Chef::Client. def initialize(json_attribs=nil) @json_attribs = json_attribs @node = nil + @run_status = nil @runner = nil @ohai = Ohai::System.new end # Do a full run for this Chef::Client. Calls: @@ -73,76 +143,53 @@ run_ohai register unless Chef::Config[:solo] build_node begin - run_status = Chef::RunStatus.new(node) + run_status.start_clock Chef::Log.info("Starting Chef Run (Version #{Chef::VERSION})") + run_started if Chef::Config[:solo] Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::FileSystemFileVendor.new(manifest) } run_context = Chef::RunContext.new(node, Chef::CookbookCollection.new(Chef::CookbookLoader.new)) run_status.run_context = run_context assert_cookbook_path_not_empty(run_context) converge(run_context) else - # Keep track of the filenames that we use in both eager cookbook - # downloading (during sync_cookbooks) and lazy (during the run - # itself, through FileVendor). After the run is over, clean up the - # cache. - valid_cache_entries = Hash.new - # Sync_cookbooks eagerly loads all files except files and templates. # It returns the cookbook_hash -- the return result from # /nodes/#{nodename}/cookbooks -- which we will use for our # run_context. - Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, rest, valid_cache_entries) } - cookbook_hash = sync_cookbooks(valid_cache_entries) + Chef::Cookbook::FileVendor.on_create { |manifest| Chef::Cookbook::RemoteFileVendor.new(manifest, rest) } + cookbook_hash = sync_cookbooks run_context = Chef::RunContext.new(node, Chef::CookbookCollection.new(cookbook_hash)) run_status.run_context = run_context assert_cookbook_path_not_empty(run_context) converge(run_context) Chef::Log.debug("Saving the current state of node #{node_name}") @node.save - - cleanup_file_cache(valid_cache_entries) end run_status.stop_clock Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds") - run_report_handlers(run_status) + run_completed_successfully true rescue Exception => e run_status.stop_clock run_status.exception = e - run_exception_handlers(run_status) - Chef::Log.error("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}") + run_failed + Chef::Log.debug("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}") raise ensure run_status = nil end end - def run_report_handlers(run_status) - Chef::Log.info("Running report handlers") - Array(Chef::Config[:report_handlers]).each do |handler| - handler.run_report_safely(run_status) - end - Chef::Log.info("Report handlers complete") - end - - def run_exception_handlers(run_status) - Chef::Log.error("Running exception handlers") - Array(Chef::Config[:exception_handlers]).each do |handler| - handler.run_report_safely(run_status) - end - Chef::Log.error("Exception handlers complete") - end - def run_ohai ohai.all_plugins end def node_name @@ -169,14 +216,17 @@ @node = Chef::Node.build(node_name) else @node = Chef::Node.find_or_create(node_name) end - @node.process_external_attrs(ohai.data, @json_attribs) + + @node.consume_external_attrs(ohai.data, @json_attribs) @node.save unless Chef::Config[:solo] @node.reset_defaults_and_overrides + @run_status = Chef::RunStatus.new(@node) + @node end # # === Returns @@ -194,108 +244,21 @@ # Synchronizes all the cookbooks from the chef-server. # # === Returns # true:: Always returns true - def sync_cookbooks(valid_cache_entries) + def sync_cookbooks Chef::Log.debug("Synchronizing cookbooks") cookbook_hash = rest.get_rest("nodes/#{node_name}/cookbooks") - Chef::Log.debug("Cookbooks to load: #{cookbook_hash.inspect}") - - # Remove all cookbooks no longer relevant to this node - Chef::FileCache.find(File.join(%w{cookbooks ** *})).each do |cache_file| - cache_file =~ /^cookbooks\/([^\/]+)\// - unless cookbook_hash.has_key?($1) - Chef::Log.info("Removing #{cache_file} from the cache; its cookbook is no longer needed on this client.") - Chef::FileCache.delete(cache_file) - end - end + Chef::CookbookVersion.sync_cookbooks(cookbook_hash) - # Synchronize each of the node's cookbooks, and add to the - # valid_cache_entries hash. - cookbook_hash.values.each do |cookbook| - sync_cookbook_file_cache(cookbook, valid_cache_entries) - end - # register the file cache path in the cookbook path so that CookbookLoader actually picks up the synced cookbooks Chef::Config[:cookbook_path] = File.join(Chef::Config[:file_cache_path], "cookbooks") cookbook_hash end - # Update the file caches for a given cache segment. Takes a segment name - # and a hash that matches one of the cookbooks/_attribute_files style - # remote file listings. - # - # === Parameters - # cookbook<Chef::Cookbook>:: The cookbook to update - # valid_cache_entries<Hash>:: Out-param; Added to this hash are the files that - # were referred to by this cookbook - def sync_cookbook_file_cache(cookbook, valid_cache_entries) - Chef::Log.debug("Synchronizing cookbook #{cookbook.name}") - - # files and templates are lazily loaded, and will be done later. - eager_segments = Chef::CookbookVersion::COOKBOOK_SEGMENTS.dup - eager_segments.delete(:files) - eager_segments.delete(:templates) - - eager_segments.each do |segment| - segment_filenames = Array.new - cookbook.manifest[segment].each do |manifest_record| - # segment = cookbook segment - # remote_list = list of file hashes - # - # We need the list of known good attribute files, so we can delete any that are - # just laying about. - - cache_filename = File.join("cookbooks", cookbook.name, manifest_record['path']) - valid_cache_entries[cache_filename] = true - - current_checksum = nil - if Chef::FileCache.has_key?(cache_filename) - current_checksum = Chef::CookbookVersion.checksum_cookbook_file(Chef::FileCache.load(cache_filename, false)) - end - - # If the checksums are different between on-disk (current) and on-server - # (remote, per manifest), do the update. This will also execute if there - # is no current checksum. - if current_checksum != manifest_record['checksum'] - raw_file = rest.get_rest(manifest_record[:url], true) - - Chef::Log.info("Storing updated #{cache_filename} in the cache.") - Chef::FileCache.move_to(raw_file.path, cache_filename) - else - Chef::Log.debug("Not storing #{cache_filename}, as the cache is up to date.") - end - - # make the segment filenames a full path. - full_path_cache_filename = Chef::FileCache.load(cache_filename, false) - segment_filenames << full_path_cache_filename - end - - # replace segment filenames with a full-path one. - if segment.to_sym == :recipes - cookbook.recipe_filenames = segment_filenames - elsif segment.to_sym == :attributes - cookbook.attribute_filenames = segment_filenames - else - cookbook.segment_filenames(segment).replace(segment_filenames) - end - end - end - - def cleanup_file_cache(valid_cache_entries) - # Delete each file in the cache that we didn't encounter in the - # manifest. - Chef::FileCache.find(File.join(%w{cookbooks ** *})).each do |cache_filename| - unless valid_cache_entries[cache_filename] - Chef::Log.info("Removing #{cache_filename} from the cache; it is no longer on the server.") - Chef::FileCache.delete(cache_filename) - end - end - end - # Converges the node. # # === Returns # true:: Always returns true def converge(run_context) @@ -323,10 +286,10 @@ Chef::Log.debug "loading from cookbook_path: #{Array(Chef::Config[:cookbook_path]).map { |path| File.expand_path(path) }.join(', ')}" Array(Chef::Config[:cookbook_path]).each_with_index do |cookbook_path, index| if directory_not_empty?(cookbook_path) break else - msg = "No cookbook found in #{Chef::Config[:cookbook_path].inspect}, make sure cookboook_path is set correctly." + msg = "No cookbook found in #{Chef::Config[:cookbook_path].inspect}, make sure cookbook_path is set correctly." Chef::Log.fatal(msg) raise Chef::Exceptions::CookbookNotFound, msg if is_last_element?(index, Chef::Config[:cookbook_path]) end end else