lib/facter/framework/core/cache_manager.rb
# frozen_string_literal: true
module Facter
class CacheManager
def initialize
@groups = {}
@log = Log.new(self)
@fact_groups = Facter::FactGroups.new
@cache_dir = LegacyFacter::Util::Config.facts_cache_dir
end
def resolve_facts(searched_facts)
return searched_facts, [] if (!File.directory?(@cache_dir) || !Options[:cache]) && Options[:ttls].any?
facts = []
searched_facts.delete_if do |fact|
res = resolve_fact(fact)
if res
facts << res
true
else
false
end
end
[searched_facts, facts.flatten]
end
def cache_facts(resolved_facts)
return unless Options[:cache] && Options[:ttls].any?
@groups = {}
resolved_facts.each do |fact|
cache_fact(fact)
end
begin
write_cache unless @groups.empty?
rescue Errno::EACCES => e
@log.warn("Could not write cache: #{e.message}")
end
end
def fact_cache_enabled?(fact_name)
fact = @fact_groups.get_fact(fact_name)
if fact
!fact[:ttls].nil?
else
false
end
# fact_group = @fact_groups.get_fact_group(fact_name)
# delete_cache(fact_group) if fact_group && !cached
end
private
def resolve_fact(searched_fact)
fact_name = if searched_fact.file
File.basename(searched_fact.file)
else
searched_fact.name
end
return unless fact_cache_enabled?(fact_name)
fact = @fact_groups.get_fact(fact_name)
return if external_fact_in_custom_group?(searched_fact, fact_name, fact)
return unless fact
return unless check_ttls?(fact[:group], fact[:ttls])
read_fact(searched_fact, fact[:group])
end
def external_fact_in_custom_group?(searched_fact, fact_name, fact)
if searched_fact.type == :file && fact[:group] != fact_name
@log.error("Cannot cache '#{fact_name}' fact from '#{fact[:group]}' group. "\
'Caching custom group is not supported for external facts.')
return true
end
false
end
def read_fact(searched_fact, fact_group)
data = nil
Facter::Framework::Benchmarking::Timer.measure(searched_fact.name, 'cached') do
data = read_group_json(fact_group)
end
return unless data
unless searched_fact.file
return unless valid_format_version?(searched_fact, data, fact_group)
delete_cache(fact_group) unless data.keys.grep(/#{searched_fact.name}/).any?
# data.fetch(searched_fact.name) { delete_cache(fact_group) }
end
@log.debug("loading cached values for #{searched_fact.name} facts")
create_facts(searched_fact, data)
end
def valid_format_version?(searched_fact, data, fact_group)
unless data['cache_format_version'] == 1
@log.debug("The fact #{searched_fact.name} could not be read from the cache, \
cache_format_version is incorrect!")
delete_cache(fact_group)
return false
end
true
end
def create_facts(searched_fact, data)
if searched_fact.type == :file
resolve_external_fact(searched_fact, data)
else
return unless data[searched_fact.name]
[Facter::ResolvedFact.new(searched_fact.name, data[searched_fact.name], searched_fact.type,
searched_fact.user_query)]
end
end
def resolve_external_fact(searched_fact, data)
facts = []
data.each do |fact_name, fact_value|
next if fact_name == 'cache_format_version'
fact = Facter::ResolvedFact.new(fact_name, fact_value, searched_fact.type,
searched_fact.user_query)
fact.file = searched_fact.file
facts << fact
end
facts
end
def cache_fact(fact)
fact_name = if fact.file
File.basename(fact.file)
else
fact.name
end
group_name = @fact_groups.get_fact_group(fact_name)
return unless group_name
return unless fact_cache_enabled?(fact_name)
@groups[group_name] ||= {}
@groups[group_name][fact.name] = fact.value
end
def write_cache
unless File.directory?(@cache_dir)
require 'fileutils'
FileUtils.mkdir_p(@cache_dir)
end
@groups.each do |group_name, data|
next unless check_ttls?(group_name, @fact_groups.get_group_ttls(group_name))
cache_file_name = File.join(@cache_dir, group_name)
next if facts_already_cached?(cache_file_name, data)
@log.debug("caching values for #{group_name} facts")
data['cache_format_version'] = 1
File.write(cache_file_name, JSON.pretty_generate(data))
end
end
def facts_already_cached?(cache_file_name, data)
if File.readable?(cache_file_name)
file = Facter::Util::FileHelper.safe_read(cache_file_name)
begin
cached_data = JSON.parse(file) unless file.nil?
return true if (data.keys - cached_data.keys).empty?
rescue JSON::ParserError => e
@log.debug("Failed to read cache file #{cache_file_name}. Detail: #{e.message}")
rescue NoMethodError => e
@log.debug("No keys found in #{cache_file_name}. Detail: #{e.message}")
end
end
false
end
def read_group_json(group_name)
return @groups[group_name] if @groups.key?(group_name)
cache_file_name = File.join(@cache_dir, group_name)
data = nil
file = Facter::Util::FileHelper.safe_read(cache_file_name)
begin
data = JSON.parse(file) unless file.nil?
rescue JSON::ParserError
delete_cache(group_name)
end
@groups[group_name] = data
end
def check_ttls?(group_name, ttls)
return false unless ttls
cache_file_name = File.join(@cache_dir, group_name)
if File.readable?(cache_file_name)
file_time = File.mtime(cache_file_name)
expire_date = file_time + ttls
return true if expire_date > Time.now
File.delete(cache_file_name)
end
@log.debug("#{group_name} facts cache file expired, missing or is corrupt")
true
end
def delete_cache(group_name)
cache_file_name = File.join(@cache_dir, group_name)
begin
File.delete(cache_file_name) if File.readable?(cache_file_name)
rescue Errno::EACCES, Errno::EROFS => e
@log.warn("Could not delete cache: #{e.message}")
end
end
end
end