# 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) cached = 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 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 => e @log.warn("Could not delete cache: #{e.message}") end end end end