# frozen_string_literal: true require_relative '../../puppet/resource/status' class Puppet::Transaction::ResourceHarness NO_ACTION = Object.new extend Forwardable def_delegators :@transaction, :relationship_graph attr_reader :transaction def initialize(transaction) @transaction = transaction @persistence = transaction.persistence end def evaluate(resource) status = Puppet::Resource::Status.new(resource) begin context = ResourceApplicationContext.from_resource(resource, status) perform_changes(resource, context) if status.changed? && !resource.noop? cache(resource, :synced, Time.now) resource.flush if resource.respond_to?(:flush) end rescue => detail status.failed_because(detail) ensure status.evaluation_time = Time.now - status.time end status end def scheduled?(resource) return true if Puppet[:ignoreschedules] schedule = schedule(resource) return true unless schedule # We use 'checked' here instead of 'synced' because otherwise we'll # end up checking most resources most times, because they will generally # have been synced a long time ago (e.g., a file only gets updated # once a month on the server and its schedule is daily; the last sync time # will have been a month ago, so we'd end up checking every run). schedule.match?(cached(resource, :checked).to_i) end def schedule(resource) unless resource.catalog resource.warning _("Cannot schedule without a schedule-containing catalog") return nil end name = resource[:schedule] return nil unless name resource.catalog.resource(:schedule, name) || resource.fail(_("Could not find schedule %{name}") % { name: name }) end # Used mostly for scheduling and auditing at this point. def cached(resource, name) Puppet::Util::Storage.cache(resource)[name] end # Used mostly for scheduling and auditing at this point. def cache(resource, name, value) Puppet::Util::Storage.cache(resource)[name] = value end private def perform_changes(resource, context) cache(resource, :checked, Time.now) # Record the current state in state.yml. context.audited_params.each do |param| cache(resource, param, context.current_values[param]) end ensure_param = resource.parameter(:ensure) if ensure_param && ensure_param.should ensure_event = sync_if_needed(ensure_param, context) else ensure_event = NO_ACTION end if ensure_event == NO_ACTION if context.resource_present? resource.properties.each do |param| sync_if_needed(param, context) end else resource.debug("Nothing to manage: no ensure and the resource doesn't exist") end end capture_audit_events(resource, context) persist_system_values(resource, context) end # We persist the last known values for the properties of a resource after resource # application. # @param [Puppet::Type] resource resource whose values we are to persist. # @param [ResourceApplicationContext] context the application context to operate on. def persist_system_values(resource, context) param_to_event = {} context.status.events.each do |ev| param_to_event[ev.property] = ev end context.system_value_params.each do |pname, param| @persistence.set_system_value(resource.ref, pname.to_s, new_system_value(param, param_to_event[pname.to_s], @persistence.get_system_value(resource.ref, pname.to_s))) end end def sync_if_needed(param, context) historical_value = context.historical_values[param.name] current_value = context.current_values[param.name] do_audit = context.audited_params.include?(param.name) begin if param.should && !param.safe_insync?(current_value) event = create_change_event(param, current_value, historical_value) if do_audit event = audit_event(event, param, context) end brief_audit_message = audit_message(param, do_audit, historical_value, current_value) if param.noop noop(event, param, current_value, brief_audit_message) else sync(event, param, current_value, brief_audit_message) end event else NO_ACTION end rescue => detail # Execution will continue on StandardErrors, just store the event Puppet.log_exception(detail) event = create_change_event(param, current_value, historical_value) event.status = "failure" event.message = param.format(_("change from %s to %s failed: "), param.is_to_s(current_value), param.should_to_s(param.should)) + detail.to_s event rescue Exception => detail # Execution will halt on Exceptions, they get raised to the application event = create_change_event(param, current_value, historical_value) event.status = "failure" event.message = param.format(_("change from %s to %s failed: "), param.is_to_s(current_value), param.should_to_s(param.should)) + detail.to_s raise ensure if event name = param.name.to_s event.message ||= _("could not create change error message for %{name}") % { name: name } event.calculate_corrective_change(@persistence.get_system_value(context.resource.ref, name)) event.message << ' (corrective)' if event.corrective_change context.record(event) event.send_log context.synced_params << param.name end end end def create_change_event(property, current_value, historical_value) options = {} should = property.should if property.sensitive options[:previous_value] = current_value.nil? ? nil : '[redacted]' options[:desired_value] = should.nil? ? nil : '[redacted]' options[:historical_value] = historical_value.nil? ? nil : '[redacted]' else options[:previous_value] = current_value options[:desired_value] = should options[:historical_value] = historical_value end property.event(options) end # This method is an ugly hack because, given a Time object with nanosecond # resolution, roundtripped through YAML serialization, the Time object will # be truncated to microseconds. # For audit purposes, this code special cases this comparison, and compares # the two objects by their second and microsecond components. tv_sec is the # number of seconds since the epoch, and tv_usec is only the microsecond # portion of time. def are_audited_values_equal(a, b) a == b || (a.is_a?(Time) && b.is_a?(Time) && a.tv_sec == b.tv_sec && a.tv_usec == b.tv_usec) end private :are_audited_values_equal # Populate an existing event with audit information. # # @param event [Puppet::Transaction::Event] The event to be populated. # @param property [Puppet::Property] The property being audited. # @param context [ResourceApplicationContext] # # @return [Puppet::Transaction::Event] The given event, populated with the audit information. def audit_event(event, property, context) event.audited = true event.status = "audit" # The event we've been provided might have been redacted so we need to use the state stored within # the resource application context to see if an event was actually generated. if !are_audited_values_equal(context.historical_values[property.name], context.current_values[property.name]) event.message = property.format(_("audit change: previously recorded value %s has been changed to %s"), property.is_to_s(event.historical_value), property.is_to_s(event.previous_value)) end event end def audit_message(param, do_audit, historical_value, current_value) if do_audit && historical_value && !are_audited_values_equal(historical_value, current_value) param.format(_(" (previously recorded value was %s)"), param.is_to_s(historical_value)) else "" end end def noop(event, param, current_value, audit_message) event.message = param.format(_("current_value %s, should be %s (noop)"), param.is_to_s(current_value), param.should_to_s(param.should)) + audit_message.to_s event.status = "noop" end def sync(event, param, current_value, audit_message) param.sync if param.sensitive event.message = param.format(_("changed %s to %s"), param.is_to_s(current_value), param.should_to_s(param.should)) + audit_message.to_s else event.message = "#{param.change_to_s(current_value, param.should)}#{audit_message}" end event.status = "success" end def capture_audit_events(resource, context) context.audited_params.each do |param_name| if context.historical_values.include?(param_name) if !are_audited_values_equal(context.historical_values[param_name], context.current_values[param_name]) && !context.synced_params.include?(param_name) parameter = resource.parameter(param_name) event = audit_event(create_change_event(parameter, context.current_values[param_name], context.historical_values[param_name]), parameter, context) event.send_log context.record(event) end else property = resource.property(param_name) property.notice(property.format(_("audit change: newly-recorded value %s"), context.current_values[param_name])) end end end # Given an event and its property, calculate the system_value to persist # for future calculations. # @param [Puppet::Transaction::Event] event event to use for processing # @param [Puppet::Property] property correlating property # @param [Object] old_system_value system_value from last transaction # @return [Object] system_value to be used for next transaction def new_system_value(property, event, old_system_value) if event && event.status != "success" # For non-success events, we persist the old_system_value if it is defined, # or use the event previous_value. # If we're using the event previous_value, we ensure that it's # an array. This is needed because properties assume that their # `should` value is an array, and we will use this value later # on in property insync? logic. event_value = [event.previous_value] unless event.previous_value.is_a?(Array) old_system_value.nil? ? event_value : old_system_value else # For non events, or for success cases, we just want to store # the parameters agent value. # We use instance_variable_get here because we want this process to bypass any # munging/unmunging or validation that the property might try to do, since those # operations may not be correctly implemented for custom types. property.instance_variable_get(:@should) end end # @api private ResourceApplicationContext = Struct.new(:resource, :current_values, :historical_values, :audited_params, :synced_params, :status, :system_value_params) do def self.from_resource(resource, status) ResourceApplicationContext.new(resource, resource.retrieve_resource.to_hash, Puppet::Util::Storage.cache(resource).dup, (resource[:audit] || []).map { |p| p.to_sym }, [], status, resource.parameters.select { |_n, p| p.is_a?(Puppet::Property) && !p.sensitive }) end def resource_present? resource.present?(current_values) end def record(event) status << event end end end