Update stdlib and concat to 6.1.0 both
[mirror/dsa-puppet.git] / 3rdparty / modules / stdlib / lib / facter / facter_dot_d.rb
1 # @summary
2 #   A Facter plugin that loads facts from /etc/facter/facts.d
3 #   and /etc/puppetlabs/facter/facts.d.
4 #
5 # Facts can be in the form of JSON, YAML or Text files
6 # and any executable that returns key=value pairs.
7 #
8 # In the case of scripts you can also create a file that
9 # contains a cache TTL.  For foo.sh store the ttl as just
10 # a number in foo.sh.ttl
11 #
12 # The cache is stored in $libdir/facts_dot_d.cache as a mode
13 # 600 file and will have the end result of not calling your
14 # fact scripts more often than is needed
15 class Facter::Util::DotD
16   require 'yaml'
17   # These will be nil if Puppet is not available.
18   def initialize(dir = '/etc/facts.d', cache_file = File.join(Puppet[:libdir], 'facts_dot_d.cache'))
19     @dir = dir
20     @cache_file = cache_file
21     @cache = nil
22     @types = { '.txt' => :txt, '.json' => :json, '.yaml' => :yaml }
23   end
24
25   # entries
26   def entries
27     Dir.entries(@dir).reject { |f| f =~ %r{^\.|\.ttl$} }.sort.map { |f| File.join(@dir, f) }
28   rescue
29     []
30   end
31
32   # fact_type
33   # @param file
34   def fact_type(file)
35     extension = File.extname(file)
36
37     type = @types[extension] || :unknown
38
39     type = :script if type == :unknown && File.executable?(file)
40
41     type
42   end
43
44   # txt_parser
45   # @param file
46   def txt_parser(file)
47     File.readlines(file).each do |line|
48       next unless line =~ %r{^([^=]+)=(.+)$}
49       var = Regexp.last_match(1)
50       val = Regexp.last_match(2)
51
52       Facter.add(var) do
53         setcode { val }
54       end
55     end
56   rescue StandardError => e
57     Facter.warn("Failed to handle #{file} as text facts: #{e.class}: #{e}")
58   end
59
60   # json_parser
61   # @param file
62   def json_parser(file)
63     begin
64       require 'json'
65     rescue LoadError
66       retry if require 'rubygems'
67       raise
68     end
69
70     JSON.parse(File.read(file)).each_pair do |f, v|
71       Facter.add(f) do
72         setcode { v }
73       end
74     end
75   rescue StandardError => e
76     Facter.warn("Failed to handle #{file} as json facts: #{e.class}: #{e}")
77   end
78
79   # yaml_parser
80   # @param file
81   def yaml_parser(file)
82     require 'yaml'
83
84     YAML.load_file(file).each_pair do |f, v|
85       Facter.add(f) do
86         setcode { v }
87       end
88     end
89   rescue StandardError => e
90     Facter.warn("Failed to handle #{file} as yaml facts: #{e.class}: #{e}")
91   end
92
93   # script_parser
94   # @param file
95   def script_parser(file)
96     result = cache_lookup(file)
97     ttl = cache_time(file)
98
99     if result
100       Facter.debug("Using cached data for #{file}")
101     else
102       result = Facter::Util::Resolution.exec(file)
103
104       if ttl > 0
105         Facter.debug("Updating cache for #{file}")
106         cache_store(file, result)
107         cache_save!
108       end
109     end
110
111     result.split("\n").each do |line|
112       next unless line =~ %r{^(.+)=(.+)$}
113       var = Regexp.last_match(1)
114       val = Regexp.last_match(2)
115
116       Facter.add(var) do
117         setcode { val }
118       end
119     end
120   rescue StandardError => e
121     Facter.warn("Failed to handle #{file} as script facts: #{e.class}: #{e}")
122     Facter.debug(e.backtrace.join("\n\t"))
123   end
124
125   # cache_save
126   def cache_save!
127     cache = load_cache
128     File.open(@cache_file, 'w', 0o600) { |f| f.write(YAML.dump(cache)) }
129   rescue # rubocop:disable Lint/HandleExceptions
130   end
131
132   # cache_store
133   # @param file
134   def cache_store(file, data)
135     load_cache
136
137     @cache[file] = { :data => data, :stored => Time.now.to_i }
138   rescue # rubocop:disable Lint/HandleExceptions
139   end
140
141   # cache_lookup
142   # @param file
143   def cache_lookup(file)
144     cache = load_cache
145
146     return nil if cache.empty?
147
148     ttl = cache_time(file)
149
150     return nil unless cache[file]
151     now = Time.now.to_i
152
153     return cache[file][:data] if ttl == -1
154     return cache[file][:data] if (now - cache[file][:stored]) <= ttl
155     return nil
156   rescue
157     return nil
158   end
159
160   # cache_time
161   # @param file
162   def cache_time(file)
163     meta = file + '.ttl'
164
165     return File.read(meta).chomp.to_i
166   rescue
167     return 0
168   end
169
170   # load_cache
171   def load_cache
172     @cache ||= if File.exist?(@cache_file)
173                  YAML.load_file(@cache_file)
174                else
175                  {}
176                end
177
178     return @cache
179   rescue
180     @cache = {}
181     return @cache
182   end
183
184   # create
185   def create
186     entries.each do |fact|
187       type = fact_type(fact)
188       parser = "#{type}_parser"
189
190       next unless respond_to?("#{type}_parser")
191       Facter.debug("Parsing #{fact} using #{parser}")
192
193       send(parser, fact)
194     end
195   end
196 end
197
198 mdata = Facter.version.match(%r{(\d+)\.(\d+)\.(\d+)})
199 if mdata
200   (major, minor, _patch) = mdata.captures.map { |v| v.to_i }
201   if major < 2
202     # Facter 1.7 introduced external facts support directly
203     unless major == 1 && minor > 6
204       Facter::Util::DotD.new('/etc/facter/facts.d').create
205       Facter::Util::DotD.new('/etc/puppetlabs/facter/facts.d').create
206
207       # Windows has a different configuration directory that defaults to a vendor
208       # specific sub directory of the %COMMON_APPDATA% directory.
209       if Dir.const_defined? 'COMMON_APPDATA' # rubocop:disable Metrics/BlockNesting : Any attempt to alter this breaks it
210         windows_facts_dot_d = File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'facter', 'facts.d')
211         Facter::Util::DotD.new(windows_facts_dot_d).create
212       end
213     end
214   end
215 end