# frozen_string_literal: true # A Facter plugin that loads external facts. # # Default Unix Directories: # /opt/puppetlabs/custom_facts/facts.d, /etc/custom_facts/facts.d, /etc/puppetlabs/custom_facts/facts.d # # Beginning with Facter 3, only /opt/puppetlabs/custom_facts/facts.d will be a default external fact # directory in Unix. # # Default Windows Direcotires: # C:\ProgramData\Puppetlabs\custom_facts\facts.d (2008) # C:\Documents and Settings\All Users\Application Data\Puppetlabs\custom_facts\facts.d (2003) # # Can also load from command-line specified directory # # Facts can be in the form of JSON, YAML or Text files # and any executable that returns key=value pairs. require 'yaml' module LegacyFacter module Util class DirectoryLoader class NoSuchDirectoryError < RuntimeError end # This value makes it highly likely that external facts will take # precedence over all other facts EXTERNAL_FACT_WEIGHT = 10_000 # Directory for fact loading attr_reader :directories def initialize(dir = LegacyFacter::Util::Config.external_facts_dirs, weight = EXTERNAL_FACT_WEIGHT) @directories = [dir].flatten @weight = weight @log ||= Facter::Log.new(self) end # Load facts from files in fact directory using the relevant parser classes to # parse them. def load(collection) weight = @weight searched_facts, cached_facts = load_directory_entries(collection) load_cached_facts(collection, cached_facts, weight) load_searched_facts(collection, searched_facts, weight) end private def log @log ||= Facter::Log.new(self) end def load_directory_entries(_collection) cm = Facter::CacheManager.new facts = [] entries.each do |file| basename = File.basename(file) next if file_blocked?(basename) if facts.find { |f| f.name == basename } && cm.fact_cache_enabled?(basename) Facter.log_exception(Exception.new("Caching is enabled for group \"#{basename}\" while "\ 'there are at least two external facts files with the same filename')) else searched_fact = Facter::SearchedFact.new(basename, nil, nil, :file) searched_fact.file = file facts << searched_fact end end cm.resolve_facts(facts) end def load_cached_facts(collection, cached_facts, weight) cached_facts.each do |cached_fact| collection.add(cached_fact.name, value: cached_fact.value, fact_type: :external, file: cached_fact.file) { has_weight(weight) } end end def load_searched_facts(collection, searched_facts, weight) searched_facts.each do |fact| parser = LegacyFacter::Util::Parser.parser_for(fact.file) next if parser.nil? data = resolve_fact(fact, parser) if data == false log.warn "Could not interpret fact file #{fact.file}" elsif (data == {}) || data.nil? log.debug( "Structured data fact file #{fact.file} was parsed but was either empty or an invalid filetype "\ '(valid filetypes are .yaml, .json, and .txt).' ) elsif !data.is_a?(Hash) log.error("Structured data fact file #{fact.file} was parsed but no key=>value data was returned.") else add_data(data, collection, fact, weight) end end end def add_data(data, collection, fact, weight) data.each do |key, value| collection.add( key, value: value, fact_type: :external, file: fact.file ) { has_weight(weight) } end end def resolve_fact(fact, parser) data = nil fact_name = File.basename(fact.file) Facter::Framework::Benchmarking::Timer.measure(fact_name) { data = parser.results } data end def entries dirs = @directories.select { |directory| File.directory?(directory) }.map do |directory| Dir.entries(directory).map { |directory_entry| File.join(directory, directory_entry) }.sort.reverse! end dirs.flatten.select { |f| should_parse?(f) } rescue Errno::ENOENT [] end def should_parse?(file) File.basename(file) !~ /^\./ end def file_blocked?(file) if Facter::Options[:blocked_facts].include? file Facter.debug("External fact file #{file} blocked.") return true end false end end end end