# # Author:: Adam Jacob () # Author:: Christopher Walters () # Author:: Christopher Brown () # Author:: Tim Hinderliter () # Copyright:: Copyright (c) 2008-2011 Opscode, 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/config' require 'chef/mixin/params_validate' require 'chef/mixin/path_sanity' require 'chef/log' require 'chef/rest' require 'chef/api_client' require 'chef/api_client/registration' require 'chef/node' require 'chef/role' require 'chef/file_cache' require 'chef/run_context' require 'chef/runner' require 'chef/run_status' require 'chef/cookbook/cookbook_collection' require 'chef/cookbook/file_vendor' require 'chef/cookbook/file_system_file_vendor' require 'chef/cookbook/remote_file_vendor' require 'chef/event_dispatch/dispatcher' require 'chef/event_loggers/base' require 'chef/event_loggers/windows_eventlog' require 'chef/formatters/base' require 'chef/formatters/doc' require 'chef/formatters/minimal' require 'chef/version' require 'chef/resource_reporter' require 'chef/run_lock' require 'chef/policy_builder' require 'chef/request_id' require 'chef/platform/rebooter' require 'ohai' require 'rbconfig' 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 include Chef::Mixin::PathSanity # IO stream that will be used as 'STDOUT' for formatters. Formatters are # configured during `initialize`, so this provides a convenience for # setting alternative IO stream during tests. STDOUT_FD = STDOUT # IO stream that will be used as 'STDERR' for formatters. Formatters are # configured during `initialize`, so this provides a convenience for # setting alternative IO stream during tests. STDERR_FD = STDERR # 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(¬ification_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(¬ification_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(¬ification_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 @events.run_started(run_status) end # Callback to fire notifications that the run completed successfully def run_completed_successfully success_handlers = self.class.run_completed_successfully_notifications success_handlers.each do |notification| notification.call(run_status) end end # Callback to fire notifications that the Chef run failed def run_failed failure_handlers = self.class.run_failed_notifications failure_handlers.each do |notification| notification.call(run_status) end end attr_accessor :node attr_accessor :ohai attr_accessor :rest attr_accessor :runner attr_reader :json_attribs attr_reader :run_status attr_reader :events # Creates a new Chef::Client. def initialize(json_attribs=nil, args={}) @json_attribs = json_attribs || {} @node = nil @run_status = nil @runner = nil @ohai = Ohai::System.new event_handlers = configure_formatters + configure_event_loggers event_handlers += Array(Chef::Config[:event_handlers]) @events = EventDispatch::Dispatcher.new(*event_handlers) @override_runlist = args.delete(:override_runlist) @specific_recipes = args.delete(:specific_recipes) if new_runlist = args.delete(:runlist) @json_attribs["run_list"] = new_runlist end end def configure_formatters formatters_for_run.map do |formatter_name, output_path| if output_path.nil? Chef::Formatters.new(formatter_name, STDOUT_FD, STDERR_FD) else io = File.open(output_path, "a+") io.sync = true Chef::Formatters.new(formatter_name, io, io) end end end def formatters_for_run if Chef::Config.formatters.empty? [default_formatter] else Chef::Config.formatters end end def default_formatter if (STDOUT.tty? && !Chef::Config[:force_logger]) || Chef::Config[:force_formatter] [:doc] else [:null] end end def configure_event_loggers if Chef::Config.disable_event_logger [] else Chef::Config.event_loggers.map do |evt_logger| case evt_logger when Symbol Chef::EventLoggers.new(evt_logger) when Class evt_logger.new else end end end end # Instantiates a Chef::Node object, possibly loading the node's prior state # when using chef-client. Delegates to policy_builder # # # === Returns # Chef::Node:: The node object for this chef run def load_node policy_builder.load_node @node = policy_builder.node end # Mutates the `node` object to prepare it for the chef run. Delegates to # policy_builder # # === Returns # Chef::Node:: The updated node object def build_node policy_builder.build_node @run_status = Chef::RunStatus.new(node, events) node end def setup_run_context run_context = policy_builder.setup_run_context(@specific_recipes) assert_cookbook_path_not_empty(run_context) run_status.run_context = run_context run_context end def sync_cookbooks policy_builder.sync_cookbooks end def policy_builder @policy_builder ||= Chef::PolicyBuilder.strategy.new(node_name, ohai.data, json_attribs, @override_runlist, events) end def save_updated_node if Chef::Config[:solo] # nothing to do elsif policy_builder.temporary_policy? Chef::Log.warn("Skipping final node save because override_runlist was given") else Chef::Log.debug("Saving the current state of node #{node_name}") @node.save end end def run_ohai ohai.all_plugins end def node_name name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:machinename] || ohai[:hostname] Chef::Config[:node_name] = name raise Chef::Exceptions::CannotDetermineNodeName unless name # node names > 90 bytes only work with authentication protocol >= 1.1 # see discussion in config.rb. if name.bytesize > 90 Chef::Config[:authentication_protocol_version] = "1.1" end name end # # === Returns # rest:: returns Chef::REST connection object def register(client_name=node_name, config=Chef::Config) if !config[:client_key] @events.skipping_registration(client_name, config) Chef::Log.debug("Client key is unspecified - skipping registration") elsif File.exists?(config[:client_key]) @events.skipping_registration(client_name, config) Chef::Log.debug("Client key #{config[:client_key]} is present - skipping registration") else @events.registration_start(node_name, config) Chef::Log.info("Client key #{config[:client_key]} is not present - registering") Chef::ApiClient::Registration.new(node_name, config[:client_key]).run @events.registration_completed end # We now have the client key, and should use it from now on. @rest = Chef::REST.new(config[:chef_server_url], client_name, config[:client_key]) @resource_reporter = Chef::ResourceReporter.new(@rest) @events.register(@resource_reporter) rescue Exception => e # TODO: munge exception so a semantic failure message can be given to the # user @events.registration_failed(client_name, e, config) raise end # Converges the node. # # === Returns # true:: Always returns true def converge(run_context) @events.converge_start(run_context) Chef::Log.debug("Converging node #{node_name}") @runner = Chef::Runner.new(run_context) runner.converge @events.converge_complete true rescue Exception # TODO: should this be a separate #converge_failed(exception) method? @events.converge_complete raise end # Expands the run list. Delegates to the policy_builder. # # Normally this does not need to be called from here, it will be called by # build_node. This is provided so external users (like the chefspec # project) can inject custom behavior into the run process. # # === Returns # RunListExpansion: A RunListExpansion or API compatible object. def expanded_run_list policy_builder.expand_run_list end def do_windows_admin_check if Chef::Platform.windows? Chef::Log.debug("Checking for administrator privileges....") if !has_admin_privileges? message = "chef-client doesn't have administrator privileges on node #{node_name}." if Chef::Config[:fatal_windows_admin_check] Chef::Log.fatal(message) Chef::Log.fatal("fatal_windows_admin_check is set to TRUE.") raise Chef::Exceptions::WindowsNotAdmin, message else Chef::Log.warn("#{message} This might cause unexpected resource failures.") end else Chef::Log.debug("chef-client has administrator privileges on node #{node_name}.") end end end # Do a full run for this Chef::Client. Calls: # # * run_ohai - Collect information about the system # * build_node - Get the last known state, merge with local changes # * register - If not in solo mode, make sure the server knows about this client # * sync_cookbooks - If not in solo mode, populate the local cache with the node's cookbooks # * converge - Bring this system up to date # # === Returns # true:: Always returns true. def run runlock = RunLock.new(Chef::Config.lockfile) runlock.acquire # don't add code that may fail before entering this section to be sure to release lock begin runlock.save_pid check_ssl_config request_id = Chef::RequestID.instance.request_id run_context = nil @events.run_start(Chef::VERSION) Chef::Log.info("*** Chef #{Chef::VERSION} ***") Chef::Log.info "Chef-client pid: #{Process.pid}" Chef::Log.debug("Chef-client request_id: #{request_id}") enforce_path_sanity run_ohai @events.ohai_completed(node) register unless Chef::Config[:solo] load_node build_node run_status.run_id = request_id run_status.start_clock Chef::Log.info("Starting Chef Run for #{node.name}") run_started do_windows_admin_check run_context = setup_run_context catch(:end_client_run_early) do converge(run_context) end save_updated_node run_status.stop_clock Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds") run_completed_successfully @events.run_completed(node) # rebooting has to be the last thing we do, no exceptions. Chef::Platform::Rebooter.reboot_if_needed!(node) true rescue Exception => e # CHEF-3336: Send the error first in case something goes wrong below and we don't know why Chef::Log.debug("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}") # If we failed really early, we may not have a run_status yet. Too early for these to be of much use. if run_status run_status.stop_clock run_status.exception = e run_failed end Chef::Application.debug_stacktrace(e) @events.run_failed(e) raise ensure Chef::RequestID.instance.reset_request_id request_id = nil @run_status = nil run_context = nil runlock.release GC.start end true end private def empty_directory?(path) !File.exists?(path) || (Dir.entries(path).size <= 2) end def is_last_element?(index, object) object.kind_of?(Array) ? index == object.size - 1 : true end def assert_cookbook_path_not_empty(run_context) if Chef::Config[:solo] # Check for cookbooks in the path given # Chef::Config[:cookbook_path] can be a string or an array # if it's an array, go through it and check each one, raise error at the last one if no files are found cookbook_paths = Array(Chef::Config[:cookbook_path]) Chef::Log.debug "Loading from cookbook_path: #{cookbook_paths.map { |path| File.expand_path(path) }.join(', ')}" if cookbook_paths.all? {|path| empty_directory?(path) } msg = "None of the cookbook paths set in Chef::Config[:cookbook_path], #{cookbook_paths.inspect}, contain any cookbooks" Chef::Log.fatal(msg) raise Chef::Exceptions::CookbookNotFound, msg end else Chef::Log.warn("Node #{node_name} has an empty run list.") if run_context.node.run_list.empty? end end def has_admin_privileges? require 'chef/win32/security' Chef::ReservedNames::Win32::Security.has_admin_privileges? end def check_ssl_config if Chef::Config[:ssl_verify_mode] == :verify_none and !Chef::Config[:verify_api_cert] Chef::Log.warn(<<-WARN) * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * SSL validation of HTTPS requests is disabled. HTTPS connections are still encrypted, but chef is not able to detect forged replies or man in the middle attacks. To fix this issue add an entry like this to your configuration file: ``` # Verify all HTTPS connections (recommended) ssl_verify_mode :verify_peer # OR, Verify only connections to chef-server verify_api_cert true ``` To check your SSL configuration, or troubleshoot errors, you can use the `knife ssl check` command like so: ``` knife ssl check -c #{Chef::Config.config_file} ``` * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * WARN end end end end # HACK cannot load this first, but it must be loaded. require 'chef/cookbook_loader' require 'chef/cookbook_version' require 'chef/cookbook/synchronizer'