lib/facter/custom_facts/util/directory_loader.rb
# 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