# frozen_string_literal: true # This class represents a fact. Each fact has a name and multiple # {Facter::Util::Resolution resolutions}. # # Create facts using {Facter.add} # # @api public module Facter module Util class Fact # The location from where fact is resolved # @return [String] attr_reader :location # The name of the fact # @return [String] attr_reader :name # @return [String] # @deprecated attr_accessor :ldapname # Fact options e.g. fact_type attr_accessor :options # Weight of the resolution that was used to obtain the fact value attr_accessor :used_resolution_weight # Creates a new fact, with no resolution mechanisms. See {Facter.add} # for the public API for creating facts. # @param name [String] the fact name # @param options [Hash] optional parameters # @option options [String] :ldapname set the ldapname property on the fact # # @api private def initialize(name, options = {}) @name = LegacyFacter::Util::Normalization.normalize(name.to_s.downcase).intern @options = options.dup extract_ldapname_option!(options) @ldapname ||= @name.to_s @resolves = [] @searching = false @used_resolution_weight = 0 @value = nil end # Adds a new {Facter::Util::Resolution resolution}. This requires a # block, which will then be evaluated in the context of the new # resolution. # # @param options [Hash] A hash of options to set on the resolution # # @return [Facter::Util::Resolution] # # @api private def add(options = {}, &block) @options = @options.merge(options) @location = options[:file] @location ||= block.source_location if block_given? define_resolution(nil, options, &block) end # Define a new named resolution or return an existing resolution with # the given name. # # @param resolution_name [String] The name of the resolve to define or look up # @param options [Hash] A hash of options to set on the resolution # @return [Facter::Util::Resolution] # # @api public def define_resolution(resolution_name, options = {}, &block) resolution_type = options.delete(:type) || :simple resolve = create_or_return_resolution(resolution_name, resolution_type) resolve.options(options) unless options.empty? resolve.evaluate(&block) if block resolve rescue StandardError => e msg = "Unable to add resolve #{resolution_name.inspect} for fact '#{@name}': #{e.message}" msg += "\n#{e.backtrace.join("\n")}" if Options[:trace] log.error(msg, true) nil end # Retrieve an existing resolution by name # # @param name [String] # # @return [Facter::Util::Resolution, nil] The resolution if exists, nil if # it doesn't exist or name is nil def resolution(name) return nil if name.nil? @resolves.find { |resolve| resolve.name == name } end # Flushes any cached values. # # @return [void] # # @api private def flush @resolves.each(&:flush) @value = nil end # Returns the value for this fact. This searches all resolutions by # suitability and weight (see {Facter::Util::Resolution}). If no # suitable resolution is found, it returns nil. # # @api public def value return @value unless @value.nil? if @resolves.empty? log.debug format('No resolves for %s', name: @name) return nil end searching do suitable_resolutions = sort_by_weight(find_suitable_resolutions(@resolves)) Facter::Framework::Benchmarking::Timer.measure(@name) do @value = find_first_real_value(suitable_resolutions) end announce_when_no_suitable_resolution(suitable_resolutions) announce_when_no_value_found(@value) @value = resolve_value end @value end # @api private # @deprecated def extract_ldapname_option!(options) return unless options[:ldapname] log.warnonce('ldapname is deprecated and will be removed in a future version') self.ldapname = options.delete(:ldapname) end private def log @log ||= Facter::Log.new(self) end def resolve_value return Facter.core_value(name) if @value.nil? core_value = Facter.core_value(name) if @used_resolution_weight <= 0 core_value.nil? ? @value : core_value end # Are we in the midst of a search? def searching? @searching end # Lock our searching process, so we never get stuck in recursion. def searching raise "Caught recursion on #{@name}" if searching? # If we've gotten this far, we're not already searching, so go ahead and do so. @searching = true begin yield ensure @searching = false end end def find_suitable_resolutions(resolutions) resolutions.find_all(&:suitable?) end def sort_by_weight(resolutions) # sort resolutions: # - descending by weight # - multiple facts have the same weight but different types, the :external fact take precedence # - multiple facts with the same weight and type, the order is preserved. # note: sort_by with index is slower than .sort # we cannot use .sort because it is not stable: https://bugs.ruby-lang.org/issues/1089 # solution from: https://bugs.ruby-lang.org/issues/1089#note-10 # rubocop:disable Style/NestedTernaryOperator idx = 0 resolutions.sort_by do |x| [ -x.weight, x.respond_to?(:fact_type) ? x.fact_type == :external ? 0 : 1 : 1, idx += 1 ] end # rubocop:enable Style/NestedTernaryOperator end def find_first_real_value(resolutions) resolutions.each do |resolve| begin value = resolve.value rescue Facter::ResolveCustomFactError break end @used_resolution_weight = resolve.weight next if value.nil? log_fact_path(resolve) return value end nil end def log_fact_path(resolve) log.debug("#{resolve.fact_type} fact #{resolve.fact.name} was resolved from: #{resolve.last_evaluated}") end def announce_when_no_suitable_resolution(resolutions) return unless resolutions.empty? log.debug format('Found no suitable resolves of % for %s', resolver_length: @resolves.length, name: @name) end def announce_when_no_value_found(value) log.debug(format('value for %s is still nil', name: @name)) if value.nil? end def create_or_return_resolution(resolution_name, resolution_type) resolve = resolution(resolution_name) if resolve if resolution_type != resolve.resolution_type raise ArgumentError, "Cannot return resolution #{resolution_name} with type" \ " #{resolution_type}; already defined as #{resolve.resolution_type}" end else case resolution_type when :simple resolve = Facter::Util::Resolution.new(resolution_name, self) when :aggregate resolve = Facter::Core::Aggregate.new(resolution_name, self) else raise ArgumentError, "Expected resolution type to be one of (:simple, :aggregate) but was #{resolution_type}" end @resolves << resolve end resolve end end end end