diff options
Diffstat (limited to 'lib')
25 files changed, 930 insertions, 272 deletions
diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index a5faee9d35..551e26e303 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -279,6 +279,12 @@ class Chef::Application::Client < Chef::Application Chef::Config[:chef_server_url] = config[:chef_server_url] if config.has_key? :chef_server_url Chef::Config.local_mode = config[:local_mode] if config.has_key?(:local_mode) + + if Chef::Config.has_key?(:chef_repo_path) && Chef::Config.chef_repo_path.nil? + Chef::Config.delete(:chef_repo_path) + Chef::Log.warn "chef_repo_path was set in a config file but was empty. Assuming #{Chef::Config.chef_repo_path}" + end + if Chef::Config.local_mode && !Chef::Config.has_key?(:cookbook_path) && !Chef::Config.has_key?(:chef_repo_path) Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Dir.pwd) end diff --git a/lib/chef/audit/audit_reporter.rb b/lib/chef/audit/audit_reporter.rb index a4f84ed7eb..b74bf07b8b 100644 --- a/lib/chef/audit/audit_reporter.rb +++ b/lib/chef/audit/audit_reporter.rb @@ -34,6 +34,7 @@ class Chef @rest_client = rest_client # Ruby 1.9.3 and above "enumerate their values in the order that the corresponding keys were inserted." @ordered_control_groups = Hash.new + @audit_phase_error = nil end def run_context @@ -59,6 +60,7 @@ class Chef # known control groups. def audit_phase_failed(error) # The stacktrace information has already been logged elsewhere + @audit_phase_error = error Chef::Log.debug("Audit Reporter failed.") ordered_control_groups.each do |name, control_group| audit_data.add_control_group(control_group) @@ -70,7 +72,9 @@ class Chef end def run_failed(error) - post_auditing_data(error) + # Audit phase errors are captured when audit_phase_failed gets called. + # The error passed here isn't relevant to auditing, so we ignore it. + post_auditing_data end def control_group_started(name) @@ -98,7 +102,7 @@ class Chef private - def post_auditing_data(error = nil) + def post_auditing_data unless auditing_enabled? Chef::Log.debug("Audit Reports are disabled. Skipping sending reports.") return @@ -116,8 +120,10 @@ class Chef Chef::Log.debug("Sending audit report (run-id: #{audit_data.run_id})") run_data = audit_data.to_hash - if error - run_data[:error] = "#{error.class.to_s}: #{error.message}\n#{error.backtrace.join("\n")}" + if @audit_phase_error + error_info = "#{@audit_phase_error.class}: #{@audit_phase_error.message}" + error_info << "\n#{@audit_phase_error.backtrace.join("\n")}" if @audit_phase_error.backtrace + run_data[:error] = error_info end Chef::Log.debug "Audit Report:\n#{Chef::JSONCompat.to_json_pretty(run_data)}" @@ -163,7 +169,6 @@ class Chef def iso8601ify(time) time.utc.iso8601.to_s end - end end end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 0764d3f3ba..51e78e60a9 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -50,6 +50,7 @@ require 'chef/run_lock' require 'chef/policy_builder' require 'chef/request_id' require 'chef/platform/rebooter' +require 'chef/mixin/deprecation' require 'ohai' require 'rbconfig' @@ -60,108 +61,100 @@ class Chef 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 + extend Chef::Mixin::Deprecation - # 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 + # + # The status of the Chef run. + # + # @return [Chef::RunStatus] + # + attr_reader :run_status - # 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 + # + # The node represented by this client. + # + # @return [Chef::Node] + # + def node + run_status.node 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) + def node=(value) + run_status.node = value 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 + # + # The ohai system used by this client. + # + # @return [Ohai::System] + # + attr_reader :ohai - # 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 + # + # The rest object used to communicate with the Chef server. + # + # @return [Chef::REST] + # + attr_reader :rest - attr_accessor :node - attr_accessor :ohai - attr_accessor :rest + # + # The runner used to converge. + # + # @return [Chef::Runner] + # attr_accessor :runner + # + # Extra node attributes that were applied to the node. + # + # @return [Hash] + # attr_reader :json_attribs - attr_reader :run_status + + # + # The event dispatcher for the Chef run, including any configured output + # formatters and event loggers. + # + # @return [EventDispatch::Dispatcher] + # + # @see Chef::Formatters + # @see Chef::Config#formatters + # @see Chef::Config#stdout + # @see Chef::Config#stderr + # @see Chef::Config#force_logger + # @see Chef::Config#force_formatter + # TODO add stdout, stderr, and default formatters to Chef::Config so the + # defaults aren't calculated here. Remove force_logger and force_formatter + # from this code. + # @see Chef::EventLoggers + # @see Chef::Config#disable_event_logger + # @see Chef::Config#event_loggers + # @see Chef::Config#event_handlers + # attr_reader :events + # # Creates a new Chef::Client. + # + # @param json_attribs [Hash] Node attributes to layer into the node when it is + # fetched. + # @param args [Hash] Options: + # @option args [Array<RunList::RunListItem>] :override_runlist A runlist to + # use instead of the node's embedded run list. + # @option args [Array<String>] :specific_recipes A list of recipe file paths + # to load after the run list has been loaded. + # def initialize(json_attribs=nil, args={}) @json_attribs = json_attribs || {} - @node = 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) + # TODO it seems like a bad idea to be deletin' other peoples' hashes. @override_runlist = args.delete(:override_runlist) @specific_recipes = args.delete(:specific_recipes) - @run_status = Chef::RunStatus.new(node, events) + @run_status = Chef::RunStatus.new(nil, events) if new_runlist = args.delete(:runlist) @json_attribs["run_list"] = new_runlist @@ -175,6 +168,173 @@ class Chef Chef.set_resource_priority_map(Chef::Platform::ResourcePriorityMap.instance) end + # + # Do a full run for this Chef::Client. + # + # Locks the run while doing its job. + # + # Fires run_start before doing anything and fires run_completed or + # run_failed when finished. Also notifies client listeners of run_started + # at the beginning of Compile, and run_completed_successfully or run_failed + # when all is complete. + # + # Phase 1: Setup + # -------------- + # Gets information about the system and the run we are doing. + # + # 1. Run ohai to collect system information. + # 2. Register / connect to the Chef server (unless in solo mode). + # 3. Retrieve the node (or create a new one). + # 4. Merge in json_attribs, Chef::Config.environment, and override_run_list. + # + # @see #run_ohai + # @see #load_node + # @see #build_node + # @see Chef::Config#lockfile + # @see Chef::RunLock#acquire + # + # Phase 2: Compile + # ---------------- + # Decides *what* we plan to converge by compiling recipes. + # + # 1. Sync required cookbooks to the local cache. + # 2. Load libraries from all cookbooks. + # 3. Load attributes from all cookbooks. + # 4. Load LWRPs from all cookbooks. + # 5. Load resource definitions from all cookbooks. + # 6. Load recipes in the run list. + # 7. Load recipes from the command line. + # + # @see #setup_run_context Syncs and compiles cookbooks. + # @see Chef::CookbookCompiler#compile + # + # Phase 3: Converge + # ----------------- + # Brings the system up to date. + # + # 1. Converge the resources built from recipes in Phase 2. + # 2. Save the node. + # 3. Reboot if we were asked to. + # + # @see #converge_and_save + # @see Chef::Runner + # + # Phase 4: Audit + # -------------- + # Runs 'control_group' audits in recipes. This entire section can be enabled or disabled with config. + # + # 1. 'control_group' DSL collects audits during Phase 2 + # 2. Audits are run using RSpec + # 3. Errors are collected and reported using the formatters + # + # @see #run_audits + # @see Chef::Audit::Runner#run + # + # @raise [Chef::Exceptions::RunFailedWrappingError] If converge or audit failed. + # + # @see Chef::Config#enforce_path_sanity + # @see Chef::Config#solo + # @see Chef::Config#audit_mode + # + # @return Always returns true. + # + def run + run_error = nil + + runlock = RunLock.new(Chef::Config.lockfile) + # TODO feels like acquire should have its own block arg for this + runlock.acquire + # don't add code that may fail before entering this section to be sure to release lock + begin + runlock.save_pid + + 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 + + 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 + + if Chef::Config[:audit_mode] != :audit_only + converge_error = converge_and_save(run_context) + end + + if Chef::Config[:why_run] == true + # why_run should probably be renamed to why_converge + Chef::Log.debug("Not running controls in 'why_run' mode - this mode is used to see potential converge changes") + elsif Chef::Config[:audit_mode] != :disabled + audit_error = run_audits(run_context) + end + + # Raise converge_error so run_failed reporters/events are processed. + raise converge_error if converge_error + + 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) + rescue Exception => run_error + # CHEF-3336: Send the error first in case something goes wrong below and we don't know why + Chef::Log.debug("Re-raising exception: #{run_error.class} - #{run_error.message}\n#{run_error.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 = run_error + run_failed + end + events.run_failed(run_error) + ensure + Chef::RequestID.instance.reset_request_id + request_id = nil + @run_status = nil + run_context = nil + runlock.release + GC.start + end + + # Raise audit, converge, and other errors here so that we exit + # with the proper exit status code and everything gets raised + # as a RunFailedWrappingError + if run_error || converge_error || audit_error + error = if run_error == converge_error + Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) + else + Chef::Exceptions::RunFailedWrappingError.new(run_error, converge_error, audit_error) + end + error.fill_backtrace + Chef::Application.debug_stacktrace(error) + raise error + end + + true + end + + # + # Private API + # TODO make this stuff protected or private + # + + # @api private def configure_formatters formatters_for_run.map do |formatter_name, output_path| if output_path.nil? @@ -187,6 +347,7 @@ class Chef end end + # @api private def formatters_for_run if Chef::Config.formatters.empty? [default_formatter] @@ -195,6 +356,7 @@ class Chef end end + # @api private def default_formatter if (STDOUT.tty? && !Chef::Config[:force_logger]) || Chef::Config[:force_formatter] [:doc] @@ -203,6 +365,7 @@ class Chef end end + # @api private def configure_event_loggers if Chef::Config.disable_event_logger [] @@ -219,8 +382,9 @@ class Chef end end - # Resource repoters send event information back to the chef server for processing. - # Can only be called after we have a @rest object + # Resource reporters send event information back to the chef server for + # processing. Can only be called after we have a @rest object + # @api private def register_reporters [ Chef::ResourceReporter.new(rest), @@ -230,43 +394,123 @@ class Chef end end + # + # Callback to fire notifications that the Chef run is starting + # + # @api private + # + 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 + # + # @api private + # + 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 + # + # @api private + # + def run_failed + failure_handlers = self.class.run_failed_notifications + failure_handlers.each do |notification| + notification.call(run_status) + end + end + + # # Instantiates a Chef::Node object, possibly loading the node's prior state - # when using chef-client. Delegates to policy_builder. Injects the built node - # into the Chef class. + # when using chef-client. Sets Chef.node to the new node. # # @return [Chef::Node] The node object for this Chef run + # + # @see Chef::PolicyBuilder#load_node + # + # @api private + # def load_node policy_builder.load_node - @node = policy_builder.node - Chef.set_node(@node) + run_status.node = policy_builder.node + Chef.set_node(policy_builder.node) node end - # Mutates the `node` object to prepare it for the chef run. Delegates to - # policy_builder + # + # Mutates the `node` object to prepare it for the chef run. # # @return [Chef::Node] The updated node object + # + # @see Chef::PolicyBuilder#build_node + # + # @api private + # def build_node policy_builder.build_node - @run_status.node = node + run_status.node = node node end + # + # Sync cookbooks to local cache. + # + # TODO this appears to be unused. + # + # @see Chef::PolicyBuilder#sync_cookbooks + # + # @api private + # + def sync_cookbooks + policy_builder.sync_cookbooks + end + + # + # Sets up the run context. + # + # @see Chef::PolicyBuilder#setup_run_context + # + # @return The newly set up run context + # + # @api private def setup_run_context - run_context = policy_builder.setup_run_context(@specific_recipes) + 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 - + # + # The PolicyBuilder strategy for figuring out run list and cookbooks. + # + # @return [Chef::PolicyBuilder::Policyfile, Chef::PolicyBuilder::ExpandNodeObject] + # + # @api private + # def policy_builder - @policy_builder ||= Chef::PolicyBuilder.strategy.new(node_name, ohai.data, json_attribs, @override_runlist, events) + @policy_builder ||= Chef::PolicyBuilder.strategy.new(node_name, ohai.data, json_attribs, override_runlist, events) end + # + # Save the updated node to Chef. + # + # Does not save if we are in solo mode or using override_runlist. + # + # @see Chef::Node#save + # @see Chef::Config#solo + # + # @api private + # def save_updated_node if Chef::Config[:solo] # nothing to do @@ -274,16 +518,46 @@ class Chef 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 + node.save end end + # + # Run ohai plugins. Runs all ohai plugins unless minimal_ohai is specified. + # + # Sends the ohai_completed event when finished. + # + # @see Chef::EventDispatcher# + # @see Chef::Config#minimal_ohai + # + # @api private + # def run_ohai filter = Chef::Config[:minimal_ohai] ? %w[fqdn machinename hostname platform platform_version os os_version] : nil ohai.all_plugins(filter) - @events.ohai_completed(node) + events.ohai_completed(node) end + # + # Figure out the node name we are working with. + # + # It tries these, in order: + # - Chef::Config.node_name + # - ohai[:fqdn] + # - ohai[:machinename] + # - ohai[:hostname] + # + # If we are running against a server with authentication protocol < 1.0, we + # *require* authentication protocol version 1.1. + # + # @raise [Chef::Exceptions::CannotDetermineNodeName] If the node name is not + # set and cannot be determined via ohai. + # + # @see Chef::Config#node_name + # @see Chef::Config#authentication_protocol_version + # + # @api private + # def node_name name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:machinename] || ohai[:hostname] Chef::Config[:node_name] = name @@ -292,6 +566,8 @@ class Chef # node names > 90 bytes only work with authentication protocol >= 1.1 # see discussion in config.rb. + # TODO use a computed default in Chef::Config to determine this instead of + # setting it. if name.bytesize > 90 Chef::Config[:authentication_protocol_version] = "1.1" end @@ -300,46 +576,86 @@ class Chef end # - # === Returns - # rest<Chef::REST>:: returns Chef::REST connection object + # Determine our private key and set up the connection to the Chef server. + # + # Skips registration and fires the `skipping_registration` event if + # Chef::Config.client_key is unspecified or already exists. + # + # If Chef::Config.client_key does not exist, we register the client with the + # Chef server and fire the registration_start and registration_completed events. + # + # @return [Chef::REST] The server connection object. + # + # @see Chef::Config#chef_server_url + # @see Chef::Config#client_key + # @see Chef::ApiClient::Registration#run + # @see Chef::EventDispatcher#skipping_registration + # @see Chef::EventDispatcher#registration_start + # @see Chef::EventDispatcher#registration_completed + # @see Chef::EventDispatcher#registration_failed + # + # @api private + # def register(client_name=node_name, config=Chef::Config) if !config[:client_key] - @events.skipping_registration(client_name, config) + 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) + 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) + 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 + 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]) register_reporters rescue Exception => e + # TODO this should probably only ever fire if we *started* registration. + # Move it to the block above. # TODO: munge exception so a semantic failure message can be given to the # user - @events.registration_failed(client_name, e, config) + events.registration_failed(client_name, e, config) raise end - # Converges the node. # - # === Returns - # The thrown exception, if there was one. If this returns nil the converge was successful. + # Converges all compiled resources. + # + # Fires the converge_start, converge_complete and converge_failed events. + # + # If the exception `:end_client_run_early` is thrown during convergence, it + # does not mark the run complete *or* failed, and returns `nil` + # + # @param run_context The run context. + # + # @return The thrown exception, if we are in audit mode. `nil` means the + # converge was successful or ended early. + # + # @raise Any converge exception, unless we are in audit mode, in which case + # we *return* the exception. + # + # @see Chef::Runner#converge + # @see Chef::Config#audit_mode + # @see Chef::EventDispatch#converge_start + # @see Chef::EventDispatch#converge_complete + # @see Chef::EventDispatch#converge_failed + # + # @api private + # def converge(run_context) converge_exception = nil catch(:end_client_run_early) do begin - @events.converge_start(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 + @runner.converge + events.converge_complete rescue Exception => e - @events.converge_failed(e) + events.converge_failed(e) raise e if Chef::Config[:audit_mode] == :disabled converge_exception = e end @@ -347,8 +663,28 @@ class Chef converge_exception end + # + # Converge the node via and then save it if successful. + # + # @param run_context The run context. + # + # @return The thrown exception, if we are in audit mode. `nil` means the + # converge was successful or ended early. + # + # @raise Any converge or node save exception, unless we are in audit mode, + # in which case we *return* the exception. + # + # @see #converge + # @see #save_updated_mode + # @see Chef::Config#audit_mode + # + # @api private + # # We don't want to change the old API on the `converge` method to have it perform # saving. So we wrap it in this method. + # TODO given this seems to be pretty internal stuff, how badly do we need to + # split this stuff up? + # def converge_and_save(run_context) converge_exception = converge(run_context) unless converge_exception @@ -362,37 +698,67 @@ class Chef converge_exception end + # + # Run the audit phase. + # + # Triggers the audit_phase_start, audit_phase_complete and + # audit_phase_failed events. + # + # @param run_context The run context. + # + # @return Any thrown exceptions. `nil` if successful. + # + # @see Chef::Audit::Runner#run + # @see Chef::EventDispatch#audit_phase_start + # @see Chef::EventDispatch#audit_phase_complete + # @see Chef::EventDispatch#audit_phase_failed + # + # @api private + # def run_audits(run_context) - audit_exception = nil begin - @events.audit_phase_start(run_status) + events.audit_phase_start(run_status) Chef::Log.info("Starting audit phase") auditor = Chef::Audit::Runner.new(run_context) auditor.run if auditor.failed? - raise Chef::Exceptions::AuditsFailed.new(auditor.num_failed, auditor.num_total) + audit_exception = Chef::Exceptions::AuditsFailed.new(auditor.num_failed, auditor.num_total) + events.audit_phase_failed(audit_exception) + else + events.audit_phase_complete end - @events.audit_phase_complete rescue Exception => e Chef::Log.error("Audit phase failed with error message: #{e.message}") - @events.audit_phase_failed(e) + events.audit_phase_failed(e) audit_exception = e end audit_exception 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. + # Expands the run list. + # + # @return [Chef::RunListExpansion] The expanded run list. + # + # @see Chef::PolicyBuilder#expand_run_list # - # === Returns - # RunListExpansion: A RunListExpansion or API compatible object. def expanded_run_list policy_builder.expand_run_list end + # + # Check if the user has Administrator privileges on windows. + # + # Throws an error if the user is not an admin, and + # `Chef::Config.fatal_windows_admin_check` is true. + # + # @raise [Chef::Exceptions::WindowsNotAdmin] If the user is not an admin. + # + # @see Chef::platform#windows? + # @see Chef::Config#fatal_windows_admin_check + # + # @api private + # def do_windows_admin_check if Chef::Platform.windows? Chef::Log.debug("Checking for administrator privileges....") @@ -412,99 +778,121 @@ class Chef 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 - - 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 - - register unless Chef::Config[:solo] - - load_node - - build_node + # Notification registration + class<<self + # + # Add a listener for the 'client run started' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_starts(¬ification_block) + run_start_notifications << notification_block + end - run_status.run_id = request_id - run_status.start_clock - Chef::Log.info("Starting Chef Run for #{node.name}") - run_started + # + # Add a listener for the 'client run success' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_completes_successfully(¬ification_block) + run_completed_successfully_notifications << notification_block + end - do_windows_admin_check + # + # Add a listener for the 'client run failed' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_fails(¬ification_block) + run_failed_notifications << notification_block + end - run_context = setup_run_context + # + # Clears all listeners for client run status events. + # + # Primarily for testing purposes. + # + # @api private + # + def clear_notifications + @run_start_notifications = nil + @run_completed_successfully_notifications = nil + @run_failed_notifications = nil + end - if Chef::Config[:audit_mode] != :audit_only - converge_error = converge_and_save(run_context) - end + # + # TODO These seem protected to me. + # + + # + # Listeners to be run when the client run starts. + # + # @return [Array<Proc>] + # + # @api private + # + def run_start_notifications + @run_start_notifications ||= [] + end - if Chef::Config[:why_run] == true - # why_run should probably be renamed to why_converge - Chef::Log.debug("Not running controls in 'why_run' mode - this mode is used to see potential converge changes") - elsif Chef::Config[:audit_mode] != :disabled - audit_error = run_audits(run_context) - end + # + # Listeners to be run when the client run completes successfully. + # + # @return [Array<Proc>] + # + # @api private + # + def run_completed_successfully_notifications + @run_completed_successfully_notifications ||= [] + end - if converge_error || audit_error - e = Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) - e.fill_backtrace - raise e - end + # + # Listeners to be run when the client run fails. + # + # @return [Array<Proc>] + # + # @api private + # + def run_failed_notifications + @run_failed_notifications ||= [] + end + end - run_status.stop_clock - Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds") - run_completed_successfully - @events.run_completed(node) + # + # 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. + # + # @api private + # + STDOUT_FD = STDOUT - # rebooting has to be the last thing we do, no exceptions. - Chef::Platform::Rebooter.reboot_if_needed!(node) + # + # 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. + # + # @api private + # + STDERR_FD = STDERR - true + # + # Deprecated writers + # - 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 + include Chef::Mixin::Deprecation + deprecated_attr_writer :ohai, "There is no alternative. Leave ohai alone!" + deprecated_attr_writer :rest, "There is no alternative. Leave rest alone!" + deprecated_attr :runner, "There is no alternative. Leave runner alone!" private + attr_reader :override_runlist + attr_reader :specific_recipes + def empty_directory?(path) !File.exists?(path) || (Dir.entries(path).size <= 2) end @@ -536,7 +924,6 @@ class Chef Chef::ReservedNames::Win32::Security.has_admin_privileges? end - end end diff --git a/lib/chef/config.rb b/lib/chef/config.rb index 629553c8ab..9beb18b53e 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -51,4 +51,3 @@ class Chef end end - diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index 97f5088b5d..77646376ba 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -21,6 +21,7 @@ require 'chef/mixin/convert_to_class_name' require 'chef/exceptions' require 'chef/resource_builder' require 'chef/mixin/shell_out' +require 'chef/mixin/powershell_out' require 'chef/dsl/resources' require 'chef/dsl/definitions' @@ -33,6 +34,7 @@ class Chef module Recipe include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut # method_missing must live for backcompat purposes until Chef 13. def method_missing(method_symbol, *args, &block) diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb index 7274105802..93caa62a65 100644 --- a/lib/chef/event_dispatch/base.rb +++ b/lib/chef/event_dispatch/base.rb @@ -82,6 +82,11 @@ class Chef def node_load_completed(node, expanded_run_list, config) end + # Called after the Policyfile was loaded. This event only occurs when + # chef is in policyfile mode. + def policyfile_loaded(policy) + end + # Called before the cookbook collection is fetched from the server. def cookbook_resolution_start(expanded_run_list) end diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index da562e70f4..c0f4158db4 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -435,7 +435,7 @@ class Chef wrapped_errors.each_with_index do |e,i| backtrace << "#{i+1}) #{e.class} - #{e.message}" backtrace += e.backtrace if e.backtrace - backtrace << "" + backtrace << "" unless i == wrapped_errors.length - 1 end set_backtrace(backtrace) end diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb index 7144d00b5d..e63c764cbf 100644 --- a/lib/chef/formatters/doc.rb +++ b/lib/chef/formatters/doc.rb @@ -3,9 +3,9 @@ require 'chef/config' class Chef module Formatters - #-- - # TODO: not sold on the name, but the output is similar to what rspec calls - # "specdoc" + + # Formatter similar to RSpec's documentation formatter. Uses indentation to + # show context. class Doc < Formatters::Base attr_reader :start_time, :end_time, :successful_audits, :failed_audits @@ -93,6 +93,10 @@ class Chef def node_load_completed(node, expanded_run_list, config) end + def policyfile_loaded(policy) + puts_line "Using policy '#{policy["name"]}' at revision '#{policy["revision_id"]}'" + end + # Called before the cookbook collection is fetched from the server. def cookbook_resolution_start(expanded_run_list) puts_line "resolving cookbooks for run list: #{expanded_run_list.inspect}" @@ -184,8 +188,10 @@ class Chef puts_line "Audit phase exception:" indent puts_line "#{error.message}" - error.backtrace.each do |l| - puts_line l + if error.backtrace + error.backtrace.each do |l| + puts_line l + end end end diff --git a/lib/chef/knife/core/generic_presenter.rb b/lib/chef/knife/core/generic_presenter.rb index f3ea0f0d6c..2df9603faa 100644 --- a/lib/chef/knife/core/generic_presenter.rb +++ b/lib/chef/knife/core/generic_presenter.rb @@ -181,7 +181,7 @@ class Chef # Must check :[] before attr because spec can include # `keys` - want the key named `keys`, not a list of # available keys. - elsif data.respond_to?(:[]) + elsif data.respond_to?(:[]) && data.has_key?(attr) data = data[attr] elsif data.respond_to?(attr.to_sym) data = data.send(attr.to_sym) diff --git a/lib/chef/knife/core/subcommand_loader.rb b/lib/chef/knife/core/subcommand_loader.rb index 1f59515f44..a8705c724f 100644 --- a/lib/chef/knife/core/subcommand_loader.rb +++ b/lib/chef/knife/core/subcommand_loader.rb @@ -23,7 +23,7 @@ class Chef class SubcommandLoader MATCHES_CHEF_GEM = %r{/chef-[\d]+\.[\d]+\.[\d]+}.freeze - MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}/}.freeze + MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}(-\w+)?(-\w+)?/}.freeze attr_reader :chef_config_dir attr_reader :env diff --git a/lib/chef/mixin/deprecation.rb b/lib/chef/mixin/deprecation.rb index 489f27c339..a3eacf75cb 100644 --- a/lib/chef/mixin/deprecation.rb +++ b/lib/chef/mixin/deprecation.rb @@ -95,6 +95,30 @@ class Chef DeprecatedInstanceVariable.new(obj, name, level) end + def deprecated_attr(name, alternative) + deprecated_attr_reader(name, alternative) + deprecated_attr_writer(name, alternative) + end + + def deprecated_attr_reader(name, alternative, level=:warn) + define_method(name) do + Chef::Log.deprecation("#{self.class}.#{name} is deprecated. Support will be removed in a future release.") + Chef::Log.deprecation(alternative) + Chef::Log.deprecation("Called from:") + caller[0..3].each {|c| Chef::Log.deprecation(c)} + instance_variable_get("@#{name}") + end + end + + def deprecated_attr_writer(name, alternative, level=:warn) + define_method("#{name}=") do |value| + Chef::Log.deprecation("Writing to #{self.class}.#{name} with #{name}= is deprecated. Support will be removed in a future release.") + Chef::Log.deprecation(alternative) + Chef::Log.deprecation("Called from:") + caller[0..3].each {|c| Chef::Log.deprecation(c)} + instance_variable_set("@#{name}", value) + end + end end end end diff --git a/lib/chef/mixin/powershell_out.rb b/lib/chef/mixin/powershell_out.rb new file mode 100644 index 0000000000..e4f29c07c4 --- /dev/null +++ b/lib/chef/mixin/powershell_out.rb @@ -0,0 +1,98 @@ +#-- +# Copyright:: Copyright (c) 2015 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 'chef/mixin/shell_out' +require 'chef/mixin/windows_architecture_helper' + +class Chef + module Mixin + module PowershellOut + include Chef::Mixin::ShellOut + include Chef::Mixin::WindowsArchitectureHelper + + # Run a command under powershell with the same API as shell_out. The + # options hash is extended to take an "architecture" flag which + # can be set to :i386 or :x86_64 to force the windows architecture. + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def powershell_out(*command_args) + script = command_args.first + options = command_args.last.is_a?(Hash) ? command_args.last : nil + + run_command_with_os_architecture(script, options) + end + + # Run a command under powershell with the same API as shell_out! + # (raises exceptions on errors) + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def powershell_out!(*command_args) + cmd = powershell_out(*command_args) + cmd.error! + cmd + end + + private + + # Helper function to run shell_out and wrap it with the correct + # flags to possibly disable WOW64 redirection (which we often need + # because chef-client runs as a 32-bit app on 64-bit windows). + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def run_command_with_os_architecture(script, options) + options ||= {} + options = options.dup + arch = options.delete(:architecture) + + with_os_architecture(nil, architecture: arch) do + shell_out( + build_powershell_command(script), + options + ) + end + end + + # Helper to build a powershell command around the script to run. + # + # @param script [String] script to run + # @retrurn [String] powershell command to execute + def build_powershell_command(script) + flags = [ + # Hides the copyright banner at startup. + "-NoLogo", + # Does not present an interactive prompt to the user. + "-NonInteractive", + # Does not load the Windows PowerShell profile. + "-NoProfile", + # always set the ExecutionPolicy flag + # see http://technet.microsoft.com/en-us/library/ee176961.aspx + "-ExecutionPolicy Unrestricted", + # Powershell will hang if STDIN is redirected + # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected + "-InputFormat None" + ] + + "powershell.exe #{flags.join(' ')} -Command \"#{script}\"" + end + end + end +end diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb index a0ac34f627..c5f3e1bd79 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -42,7 +42,7 @@ class Chef is_i386_process_on_x86_64_windows? end - def with_os_architecture(node) + def with_os_architecture(node, architecture: nil) node ||= begin os_arch = ENV['PROCESSOR_ARCHITEW6432'] || ENV['PROCESSOR_ARCHITECTURE'] @@ -51,9 +51,12 @@ class Chef n[:kernel][:machine] = os_arch == 'AMD64' ? :x86_64 : :i386 end end + + architecture ||= node_windows_architecture(node) + wow64_redirection_state = nil - if wow64_architecture_override_required?(node, node_windows_architecture(node)) + if wow64_architecture_override_required?(node, architecture) wow64_redirection_state = disable_wow64_file_redirection(node) end diff --git a/lib/chef/mixin/windows_env_helper.rb b/lib/chef/mixin/windows_env_helper.rb index 490b235065..a126801a28 100644 --- a/lib/chef/mixin/windows_env_helper.rb +++ b/lib/chef/mixin/windows_env_helper.rb @@ -21,11 +21,11 @@ require 'chef/exceptions' require 'chef/platform/query_helpers' require 'chef/win32/error' if Chef::Platform.windows? require 'chef/win32/api/system' if Chef::Platform.windows? +require 'chef/win32/api/unicode' if Chef::Platform.windows? class Chef module Mixin module WindowsEnvHelper - if Chef::Platform.windows? include Chef::ReservedNames::Win32::API::System end @@ -39,7 +39,16 @@ class Chef def broadcast_env_change flags = SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG - SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string('Environment').address, flags, 5000, nil) + # for why two calls, see: + # http://stackoverflow.com/questions/4968373/why-doesnt-sendmessagetimeout-update-the-environment-variables + if ( SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string('Environment').address, flags, 5000, nil) == 0 ) + Chef::ReservedNames::Win32::Error.raise! + end + if ( SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string( + Chef::ReservedNames::Win32::Unicode.utf8_to_wide('Environment') + ).address, flags, 5000, nil) == 0 ) + Chef::ReservedNames::Win32::Error.raise! + end end def expand_path(path) diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb index 03b1c9ca1e..b3948eac21 100644 --- a/lib/chef/platform/query_helpers.rb +++ b/lib/chef/platform/query_helpers.rb @@ -39,6 +39,11 @@ class Chef is_server_2003 end + def supports_powershell_execution_bypass?(node) + node[:languages] && node[:languages][:powershell] && + node[:languages][:powershell][:version].to_i >= 3 + end + def supports_dsc?(node) node[:languages] && node[:languages][:powershell] && node[:languages][:powershell][:version].to_i >= 4 diff --git a/lib/chef/policy_builder/policyfile.rb b/lib/chef/policy_builder/policyfile.rb index ac25b549be..5991e3ce10 100644 --- a/lib/chef/policy_builder/policyfile.rb +++ b/lib/chef/policy_builder/policyfile.rb @@ -119,6 +119,7 @@ class Chef @node = Chef::Node.find_or_create(node_name) validate_policyfile + events.policyfile_loaded(policy) node rescue Exception => e events.node_load_failed(node_name, e, Chef::Config) diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index 99d09d0507..42347a230e 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -22,6 +22,7 @@ require 'chef/mixin/convert_to_class_name' require 'chef/mixin/enforce_ownership_and_permissions' require 'chef/mixin/why_run' require 'chef/mixin/shell_out' +require 'chef/mixin/powershell_out' require 'chef/mixin/provides' require 'chef/platform/service_helpers' require 'chef/node_map' @@ -30,6 +31,7 @@ class Chef class Provider include Chef::Mixin::WhyRun include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut extend Chef::Mixin::Provides # supports the given resource and action (late binding) diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb index 416393ac60..4d5423d0e8 100644 --- a/lib/chef/provider/directory.rb +++ b/lib/chef/provider/directory.rb @@ -43,6 +43,9 @@ class Chef end def define_resource_requirements + # deep inside FAC we have to assert requirements, so call FACs hook to set that up + access_controls.define_resource_requirements + requirements.assert(:create) do |a| # Make sure the parent dir exists, or else fail. # for why run, print a message explaining the potential error. diff --git a/lib/chef/provider/dsc_resource.rb b/lib/chef/provider/dsc_resource.rb index 2812c154c6..5fa84a21e9 100644 --- a/lib/chef/provider/dsc_resource.rb +++ b/lib/chef/provider/dsc_resource.rb @@ -121,7 +121,14 @@ class Chef # however Invoke-DscResource is not correctly writing to that # stream and instead just dumping to stdout @converge_description = result.stdout - result.return_value[0]["InDesiredState"] + + if result.return_value.is_a?(Array) + # WMF Feb 2015 Preview + result.return_value[0]["InDesiredState"] + else + # WMF April 2015 Preview + result.return_value["InDesiredState"] + end end def set_resource diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index f9dcd6d80c..ed44dee6ae 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -24,71 +24,153 @@ class Chef provides :powershell_script, os: "windows" + def initialize (new_resource, run_context) + super(new_resource, run_context, '.ps1') + add_exit_status_wrapper + end + + def action_run + valid_syntax = validate_script_syntax! + super if valid_syntax + end + + def flags + # Must use -File rather than -Command to launch the script + # file created by the base class that contains the script + # code -- otherwise, powershell.exe does not propagate the + # error status of a failed Windows process that ran at the + # end of the script, it gets changed to '1'. + interpreter_flags = [default_interpreter_flags, '-File'].join(' ') + + if ! (@new_resource.flags.nil?) + interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') + end + + interpreter_flags + end + protected - EXIT_STATUS_EXCEPTION_HANDLER = "\ntrap [Exception] {write-error -exception ($_.Exception.Message);exit 1}".freeze - EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -ne $true) { if ( $LASTEXITCODE ) {exit $LASTEXITCODE} else { exit 1 }}".freeze - EXIT_STATUS_RESET_SCRIPT = "\n$global:LASTEXITCODE=$null".freeze - # Process exit codes are strange with PowerShell. Unless you - # explicitly call exit in Powershell, the powershell.exe - # interpreter returns only 0 for success or 1 for failure. Since - # we'd like to get specific exit codes from executable tools run - # with Powershell, we do some work using the automatic variables - # $? and $LASTEXITCODE to return the process exit code of the - # last process run in the script if it is the last command - # executed, otherwise 0 or 1 based on whether $? is set to true - # (success, where we return 0) or false (where we return 1). - def normalize_script_exit_status( code ) - target_code = ( EXIT_STATUS_EXCEPTION_HANDLER + - EXIT_STATUS_RESET_SCRIPT + - "\n" + - code.to_s + - EXIT_STATUS_NORMALIZATION_SCRIPT ) - convert_boolean_return = @new_resource.convert_boolean_return - self.code = <<EOH -new-variable -name interpolatedexitcode -visibility private -value $#{convert_boolean_return} -new-variable -name chefscriptresult -visibility private -$chefscriptresult = { -#{target_code} -}.invokereturnasis() -if ($interpolatedexitcode -and $chefscriptresult.gettype().name -eq 'boolean') { exit [int32](!$chefscriptresult) } else { exit 0 } -EOH - Chef::Log.debug("powershell_script provider called with script code:\n\n#{code}\n") + # Process exit codes are strange with PowerShell and require + # special handling to cover common use cases. + def add_exit_status_wrapper + self.code = wrapper_script + Chef::Log.debug("powershell_script provider called with script code:\n\n#{@new_resource.code}\n") Chef::Log.debug("powershell_script provider will execute transformed code:\n\n#{self.code}\n") end - public + def validate_script_syntax! + interpreter_arguments = default_interpreter_flags.join(' ') + Tempfile.open(['chef_powershell_script-user-code', '.ps1']) do | user_script_file | + user_script_file.puts("{#{@new_resource.code}}") + user_script_file.close - def initialize (new_resource, run_context) - super(new_resource, run_context, '.ps1') - normalize_script_exit_status(new_resource.code) + validation_command = "\"#{interpreter}\" #{interpreter_arguments} -Command #{user_script_file.path}" + + # For consistency with other script resources, allow even syntax errors + # to be suppressed if the returns attribute would have suppressed it + # at converge. + valid_returns = [0] + specified_returns = @new_resource.returns.is_a?(Integer) ? + [@new_resource.returns] : + @new_resource.returns + valid_returns.concat([1]) if specified_returns.include?(1) + + result = shell_out!(validation_command, {returns: valid_returns}) + result.exitstatus == 0 + end end - def flags - default_flags = [ + def default_interpreter_flags + # 'Bypass' is preferable since it doesn't require user input confirmation + # for files such as PowerShell modules downloaded from the + # Internet. However, 'Bypass' is not supported prior to + # PowerShell 3.0, so the fallback is 'Unrestricted' + execution_policy = Chef::Platform.supports_powershell_execution_bypass?(run_context.node) ? 'Bypass' : 'Unrestricted' + + [ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Unrestricted", + "-ExecutionPolicy #{execution_policy}", # Powershell will hang if STDIN is redirected # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected - "-InputFormat None", - # Must use -File rather than -Command to launch the script - # file created by the base class that contains the script - # code -- otherwise, powershell.exe does not propagate the - # error status of a failed Windows process that ran at the - # end of the script, it gets changed to '1'. - "-File" + "-InputFormat None" ] + end - interpreter_flags = default_flags.join(' ') + # A wrapper script is used to launch user-supplied script while + # still obtaining useful process exit codes. Unless you + # explicitly call exit in Powershell, the powershell.exe + # interpreter returns only 0 for success or 1 for failure. Since + # we'd like to get specific exit codes from executable tools run + # with Powershell, we do some work using the automatic variables + # $? and $LASTEXITCODE to return the process exit code of the + # last process run in the script if it is the last command + # executed, otherwise 0 or 1 based on whether $? is set to true + # (success, where we return 0) or false (where we return 1). + def wrapper_script +<<-EOH +# Chef Client wrapper for powershell_script resources - if ! (@new_resource.flags.nil?) - interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') - end +# LASTEXITCODE can be uninitialized -- make it explictly 0 +# to avoid incorrect detection of failure (non-zero) codes +$global:LASTEXITCODE = 0 - interpreter_flags +# Catch any exceptions -- without this, exceptions will result +# In a zero return code instead of the desired non-zero code +# that indicates a failure +trap [Exception] {write-error ($_.Exception.Message);exit 1} + +# Variable state that should not be accessible to the user code +new-variable -name interpolatedexitcode -visibility private -value $#{@new_resource.convert_boolean_return} +new-variable -name chefscriptresult -visibility private + +# Initialize a variable we use to capture $? inside a block +$global:lastcmdlet = $null + +# Execute the user's code in a script block -- +$chefscriptresult = +{ + #{@new_resource.code} + + # This assignment doesn't affect the block's return value + $global:lastcmdlet = $? +}.invokereturnasis() + +# Assume failure status of 1 -- success cases +# will have to override this +$exitstatus = 1 + +# If convert_boolean_return is enabled, the block's return value +# gets precedence in determining our exit status +if ($interpolatedexitcode -and $chefscriptresult -ne $null -and $chefscriptresult.gettype().name -eq 'boolean') +{ + $exitstatus = [int32](!$chefscriptresult) +} +elseif ($lastcmdlet) +{ + # Otherwise, a successful cmdlet execution defines the status + $exitstatus = 0 +} +elseif ( $LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0 ) +{ + # If the cmdlet status is failed, allow the Win32 status + # in $LASTEXITCODE to define exit status. This handles the case + # where no cmdlets, only Win32 processes have run since $? + # will be set to $false whenever a Win32 process returns a non-zero + # status. + $exitstatus = $LASTEXITCODE +} + +# If this script is launched with -File, the process exit +# status of PowerShell.exe will be $exitstatus. If it was +# launched with -Command, it will be 0 if $exitstatus was 0, +# 1 (i.e. failed) otherwise. +exit $exitstatus +EOH end + end end end diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb index 9204e3ef92..6c78f86fe0 100644 --- a/lib/chef/provider/service/freebsd.rb +++ b/lib/chef/provider/service/freebsd.rb @@ -147,7 +147,7 @@ class Chef # some scripts support multiple instances through symlinks such as openvpn. # We should get the service name from rcvar. Chef::Log.debug("name=\"service\" not found at #{init_command}. falling back to rcvar") - sn = shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] + shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] else # for why-run mode when the rcd_script is not there yet new_resource.service_name diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 2f5c2b7798..18759b29f7 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -37,6 +37,8 @@ require 'chef/resource_resolver' require 'chef/mixin/deprecation' require 'chef/mixin/provides' +require 'chef/mixin/shell_out' +require 'chef/mixin/powershell_out' class Chef class Resource @@ -51,6 +53,10 @@ class Chef include Chef::DSL::RebootPending extend Chef::Mixin::Provides + # This lets user code do things like `not_if { shell_out!("command") }` + include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut + # # The node the current Chef run is using. # diff --git a/lib/chef/resource_reporter.rb b/lib/chef/resource_reporter.rb index 7829bb4d70..7d13a5a5ce 100644 --- a/lib/chef/resource_reporter.rb +++ b/lib/chef/resource_reporter.rb @@ -213,7 +213,7 @@ class Chef # If we failed before we received the run_started callback, there's not much we can do # in terms of reporting if @run_status - post_reporting_data + post_reporting_data end end diff --git a/lib/chef/util/backup.rb b/lib/chef/util/backup.rb index 0cc009ca1f..6c95cedad7 100644 --- a/lib/chef/util/backup.rb +++ b/lib/chef/util/backup.rb @@ -78,8 +78,16 @@ class Chef Chef::Log.info("#{@new_resource} removed backup at #{backup_file}") end + def unsorted_backup_files + # If you replace this with Dir[], you will probably break Windows. + fn = Regexp.escape(::File.basename(path)) + Dir.entries(::File.dirname(backup_path)).select do |f| + !!(f =~ /\A#{fn}.chef-[0-9.]*\B/) + end.map {|f| ::File.join(::File.dirname(backup_path), f)} + end + def sorted_backup_files - Dir[Chef::Util::PathHelper.escape_glob(prefix, ".#{path}") + ".chef-*"].sort { |a,b| b <=> a } + unsorted_backup_files.sort { |a,b| b <=> a } end end end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index f7466084b6..f4cf71c002 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -21,7 +21,7 @@ class Chef CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '12.4.0.dev.0' + VERSION = '12.4.0.rc.0' end # |