hiera 2.0.0-x64-mingw32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/COPYING +202 -0
- data/LICENSE +18 -0
- data/README.md +276 -0
- data/bin/hiera +248 -0
- data/lib/hiera/backend/json_backend.rb +58 -0
- data/lib/hiera/backend/yaml_backend.rb +63 -0
- data/lib/hiera/backend.rb +325 -0
- data/lib/hiera/config.rb +90 -0
- data/lib/hiera/console_logger.rb +13 -0
- data/lib/hiera/error.rb +4 -0
- data/lib/hiera/fallback_logger.rb +41 -0
- data/lib/hiera/filecache.rb +86 -0
- data/lib/hiera/interpolate.rb +98 -0
- data/lib/hiera/noop_logger.rb +8 -0
- data/lib/hiera/puppet_logger.rb +17 -0
- data/lib/hiera/recursive_guard.rb +20 -0
- data/lib/hiera/util.rb +47 -0
- data/lib/hiera/version.rb +89 -0
- data/lib/hiera.rb +115 -0
- data/spec/spec_helper.rb +78 -0
- data/spec/unit/backend/json_backend_spec.rb +85 -0
- data/spec/unit/backend/yaml_backend_spec.rb +138 -0
- data/spec/unit/backend_spec.rb +743 -0
- data/spec/unit/config_spec.rb +118 -0
- data/spec/unit/console_logger_spec.rb +19 -0
- data/spec/unit/fallback_logger_spec.rb +80 -0
- data/spec/unit/filecache_spec.rb +142 -0
- data/spec/unit/fixtures/interpolate/config/hiera.yaml +6 -0
- data/spec/unit/fixtures/interpolate/data/niltest.yaml +2 -0
- data/spec/unit/fixtures/interpolate/data/recursive.yaml +3 -0
- data/spec/unit/fixtures/override/config/hiera.yaml +5 -0
- data/spec/unit/fixtures/override/data/alternate.yaml +1 -0
- data/spec/unit/fixtures/override/data/common.yaml +2 -0
- data/spec/unit/hiera_spec.rb +81 -0
- data/spec/unit/interpolate_spec.rb +36 -0
- data/spec/unit/puppet_logger_spec.rb +31 -0
- data/spec/unit/util_spec.rb +49 -0
- data/spec/unit/version_spec.rb +44 -0
- metadata +128 -0
data/bin/hiera
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# CLI client for Hiera.
|
4
|
+
#
|
5
|
+
# To lookup the 'release' key for a node given Puppet YAML facts:
|
6
|
+
#
|
7
|
+
# $ hiera release 'rel/%{location}' --yaml some.node.yaml
|
8
|
+
#
|
9
|
+
# If the node yaml had a location fact the default would match that
|
10
|
+
# else you can supply scope values on the command line
|
11
|
+
#
|
12
|
+
# $ hiera release 'rel/%{location}' location=dc2 --yaml some.node.yaml
|
13
|
+
|
14
|
+
# Bundler and rubygems maintain a set of directories from which to
|
15
|
+
# load gems. If Bundler is loaded, let it determine what can be
|
16
|
+
# loaded. If it's not loaded, then use rubygems. But do this before
|
17
|
+
# loading any hiera code, so that our gem loading system is sane.
|
18
|
+
if not defined? ::Bundler
|
19
|
+
begin
|
20
|
+
require 'rubygems'
|
21
|
+
rescue LoadError
|
22
|
+
end
|
23
|
+
end
|
24
|
+
require 'hiera'
|
25
|
+
require 'hiera/util'
|
26
|
+
require 'optparse'
|
27
|
+
require 'pp'
|
28
|
+
|
29
|
+
options = {
|
30
|
+
:default => nil,
|
31
|
+
:config => File.join(Hiera::Util.config_dir, 'hiera.yaml'),
|
32
|
+
:scope => {},
|
33
|
+
:key => nil,
|
34
|
+
:verbose => false,
|
35
|
+
:resolution_type => :priority,
|
36
|
+
:format => :ruby
|
37
|
+
}
|
38
|
+
|
39
|
+
initial_scopes = Array.new
|
40
|
+
|
41
|
+
# Loads the scope from YAML or JSON files
|
42
|
+
def load_scope(source, type=:yaml)
|
43
|
+
case type
|
44
|
+
when :mcollective
|
45
|
+
begin
|
46
|
+
require 'mcollective'
|
47
|
+
|
48
|
+
include MCollective::RPC
|
49
|
+
|
50
|
+
util = rpcclient("rpcutil")
|
51
|
+
util.progress = false
|
52
|
+
nodestats = util.custom_request("inventory", {}, source, {"identity" => source}).first
|
53
|
+
|
54
|
+
raise "Failed to retrieve facts for node #{source}: #{nodestats[:statusmsg]}" unless nodestats[:statuscode] == 0
|
55
|
+
|
56
|
+
scope = nodestats[:data][:facts]
|
57
|
+
rescue Exception => e
|
58
|
+
STDERR.puts "MCollective lookup failed: #{e.class}: #{e}"
|
59
|
+
exit 1
|
60
|
+
end
|
61
|
+
|
62
|
+
when :yaml
|
63
|
+
raise "Cannot find scope #{type} file #{source}" unless File.exist?(source)
|
64
|
+
|
65
|
+
require 'yaml'
|
66
|
+
|
67
|
+
# Attempt to load puppet in case we're going to be fed
|
68
|
+
# Puppet yaml files
|
69
|
+
begin
|
70
|
+
require 'puppet'
|
71
|
+
rescue
|
72
|
+
end
|
73
|
+
|
74
|
+
scope = YAML.load_file(source)
|
75
|
+
|
76
|
+
# Puppet makes dumb yaml files that do not promote data reuse.
|
77
|
+
scope = scope.values if scope.is_a?(Puppet::Node::Facts)
|
78
|
+
|
79
|
+
when :json
|
80
|
+
raise "Cannot find scope #{type} file #{source}" unless File.exist?(source)
|
81
|
+
|
82
|
+
require 'json'
|
83
|
+
|
84
|
+
scope = JSON.load(File.read(source))
|
85
|
+
|
86
|
+
when :inventory_service
|
87
|
+
# For this to work the machine running the hiera command needs access to
|
88
|
+
# /facts REST endpoint on your inventory server. This access is
|
89
|
+
# controlled in auth.conf and identification is by the certname of the
|
90
|
+
# machine running hiera commands.
|
91
|
+
#
|
92
|
+
# Another caveat is that if your inventory server isn't at the short dns
|
93
|
+
# name of 'puppet' you will need to set the inventory_sever option in
|
94
|
+
# your puppet.conf. Set it in either the master or main sections. It
|
95
|
+
# is fine to have the inventory_server option set even if the config
|
96
|
+
# doesn't have the fact_terminus set to rest.
|
97
|
+
begin
|
98
|
+
require 'puppet/util/run_mode'
|
99
|
+
$puppet_application_mode = Puppet::Util::RunMode[:master]
|
100
|
+
require 'puppet'
|
101
|
+
Puppet.settings.parse
|
102
|
+
Puppet::Node::Facts.indirection.terminus_class = :rest
|
103
|
+
scope = YAML.load(Puppet::Node::Facts.indirection.find(source).to_yaml)
|
104
|
+
# Puppet makes dumb yaml files that do not promote data reuse.
|
105
|
+
scope = scope.values if scope.is_a?(Puppet::Node::Facts)
|
106
|
+
rescue Exception => e
|
107
|
+
STDERR.puts "Puppet inventory service lookup failed: #{e.class}: #{e}"
|
108
|
+
exit 1
|
109
|
+
end
|
110
|
+
else
|
111
|
+
raise "Don't know how to load data type #{type}"
|
112
|
+
end
|
113
|
+
|
114
|
+
raise "Scope from #{type} file #{source} should be a Hash" unless scope.is_a?(Hash)
|
115
|
+
|
116
|
+
scope
|
117
|
+
end
|
118
|
+
|
119
|
+
def output_answer(ans, format)
|
120
|
+
case format
|
121
|
+
when :json
|
122
|
+
require 'json'
|
123
|
+
puts JSON.dump(ans)
|
124
|
+
when :ruby
|
125
|
+
if ans.is_a?(String)
|
126
|
+
puts ans
|
127
|
+
else
|
128
|
+
pp ans
|
129
|
+
end
|
130
|
+
when :yaml
|
131
|
+
require 'yaml'
|
132
|
+
puts ans.to_yaml
|
133
|
+
else
|
134
|
+
STDERR.puts "Unknown output format: #{v}"
|
135
|
+
exit 1
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
OptionParser.new do |opts|
|
140
|
+
opts.banner = "Usage: hiera [options] key [default value] [variable='text'...]\n\nThe default value will be used if no value is found for the key. Scope variables\nwill be interpolated into %{variable} placeholders in the hierarchy and in\nreturned values.\n\n"
|
141
|
+
|
142
|
+
opts.on("--version", "-V", "Version information") do
|
143
|
+
puts Hiera.version
|
144
|
+
exit
|
145
|
+
end
|
146
|
+
|
147
|
+
opts.on("--debug", "-d", "Show debugging information") do
|
148
|
+
options[:verbose] = true
|
149
|
+
end
|
150
|
+
|
151
|
+
opts.on("--array", "-a", "Return all values as an array") do
|
152
|
+
options[:resolution_type] = :array
|
153
|
+
end
|
154
|
+
|
155
|
+
opts.on("--hash", "-h", "Return all values as a hash") do
|
156
|
+
options[:resolution_type] = :hash
|
157
|
+
end
|
158
|
+
|
159
|
+
opts.on("--config CONFIG", "-c", "Configuration file") do |v|
|
160
|
+
if File.exist?(v)
|
161
|
+
options[:config] = v
|
162
|
+
else
|
163
|
+
STDERR.puts "Cannot find config file: #{v}"
|
164
|
+
exit 1
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
opts.on("--json SCOPE", "-j", "JSON format file to load scope from") do |v|
|
169
|
+
initial_scopes << { :type => :json, :value => v, :name => "JSON" }
|
170
|
+
end
|
171
|
+
|
172
|
+
opts.on("--yaml SCOPE", "-y", "YAML format file to load scope from") do |v|
|
173
|
+
initial_scopes << { :type => :yaml, :value => v, :name => "YAML" }
|
174
|
+
end
|
175
|
+
|
176
|
+
opts.on("--mcollective IDENTITY", "-m", "Use facts from a node (via mcollective) as scope") do |v|
|
177
|
+
initial_scopes << { :type => :mcollective, :value => v, :name => "Mcollective" }
|
178
|
+
end
|
179
|
+
|
180
|
+
opts.on("--inventory_service IDENTITY", "-i", "Use facts from a node (via Puppet's inventory service) as scope") do |v|
|
181
|
+
initial_scopes << { :type => :inventory_service, :value => v, :name => "Puppet inventory service" }
|
182
|
+
end
|
183
|
+
|
184
|
+
opts.on("--format TYPE", "-f", "Output the result in a specific format (ruby, yaml or json); default is 'ruby'") do |v|
|
185
|
+
options[:format] = case v
|
186
|
+
when 'json', 'ruby', 'yaml'
|
187
|
+
v.to_sym
|
188
|
+
else
|
189
|
+
STDERR.puts "Unknown output format: #{v}"
|
190
|
+
exit 1
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end.parse!
|
194
|
+
|
195
|
+
unless initial_scopes.empty?
|
196
|
+
initial_scopes.each { |this_scope|
|
197
|
+
# Load initial scope
|
198
|
+
begin
|
199
|
+
options[:scope] = load_scope(this_scope[:value], this_scope[:type])
|
200
|
+
rescue Exception => e
|
201
|
+
STDERR.puts "Could not load #{this_scope[:name]} scope: #{e.class}: #{e}"
|
202
|
+
exit 1
|
203
|
+
end
|
204
|
+
}
|
205
|
+
end
|
206
|
+
|
207
|
+
# arguments can be:
|
208
|
+
#
|
209
|
+
# key default var=val another=val
|
210
|
+
#
|
211
|
+
# The var=val's assign scope
|
212
|
+
unless ARGV.empty?
|
213
|
+
options[:key] = ARGV.delete_at(0)
|
214
|
+
|
215
|
+
ARGV.each do |arg|
|
216
|
+
if arg =~ /^(.+?)=(.+?)$/
|
217
|
+
options[:scope][$1] = $2
|
218
|
+
else
|
219
|
+
unless options[:default]
|
220
|
+
options[:default] = arg.dup
|
221
|
+
else
|
222
|
+
STDERR.puts "Don't know how to parse scope argument: #{arg}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
else
|
227
|
+
STDERR.puts "Please supply a data item to look up"
|
228
|
+
exit 1
|
229
|
+
end
|
230
|
+
|
231
|
+
begin
|
232
|
+
hiera = Hiera.new(:config => options[:config])
|
233
|
+
rescue Exception => e
|
234
|
+
if options[:verbose]
|
235
|
+
raise
|
236
|
+
else
|
237
|
+
STDERR.puts "Failed to start Hiera: #{e.class}: #{e}"
|
238
|
+
exit 1
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
unless options[:verbose]
|
243
|
+
Hiera.logger = "noop"
|
244
|
+
end
|
245
|
+
|
246
|
+
ans = hiera.lookup(options[:key], options[:default], options[:scope], nil, options[:resolution_type])
|
247
|
+
|
248
|
+
output_answer(ans, options[:format])
|
@@ -0,0 +1,58 @@
|
|
1
|
+
class Hiera
|
2
|
+
module Backend
|
3
|
+
class Json_backend
|
4
|
+
def initialize(cache=nil)
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
Hiera.debug("Hiera JSON backend starting")
|
8
|
+
|
9
|
+
@cache = cache || Filecache.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def lookup(key, scope, order_override, resolution_type, context)
|
13
|
+
answer = nil
|
14
|
+
found = false
|
15
|
+
|
16
|
+
Hiera.debug("Looking up #{key} in JSON backend")
|
17
|
+
|
18
|
+
Backend.datasources(scope, order_override) do |source|
|
19
|
+
Hiera.debug("Looking for data source #{source}")
|
20
|
+
|
21
|
+
jsonfile = Backend.datafile(:json, scope, source, "json") || next
|
22
|
+
|
23
|
+
next unless File.exist?(jsonfile)
|
24
|
+
|
25
|
+
data = @cache.read_file(jsonfile, Hash) do |data|
|
26
|
+
JSON.parse(data)
|
27
|
+
end
|
28
|
+
|
29
|
+
next if data.empty?
|
30
|
+
next unless data.include?(key)
|
31
|
+
found = true
|
32
|
+
|
33
|
+
# for array resolution we just append to the array whatever
|
34
|
+
# we find, we then goes onto the next file and keep adding to
|
35
|
+
# the array
|
36
|
+
#
|
37
|
+
# for priority searches we break after the first found data item
|
38
|
+
new_answer = Backend.parse_answer(data[key], scope, {}, context)
|
39
|
+
case resolution_type.is_a?(Hash) ? :hash : resolution_type
|
40
|
+
when :array
|
41
|
+
raise Exception, "Hiera type mismatch for key '#{key}': expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
|
42
|
+
answer ||= []
|
43
|
+
answer << new_answer
|
44
|
+
when :hash
|
45
|
+
raise Exception, "Hiera type mismatch for key '#{key}': expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
|
46
|
+
answer ||= {}
|
47
|
+
answer = Backend.merge_answer(new_answer, answer, resolution_type)
|
48
|
+
else
|
49
|
+
answer = new_answer
|
50
|
+
break
|
51
|
+
end
|
52
|
+
end
|
53
|
+
throw :no_such_key unless found
|
54
|
+
return answer
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class Hiera
|
2
|
+
module Backend
|
3
|
+
class Yaml_backend
|
4
|
+
def initialize(cache=nil)
|
5
|
+
require 'yaml'
|
6
|
+
Hiera.debug("Hiera YAML backend starting")
|
7
|
+
|
8
|
+
@cache = cache || Filecache.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def lookup(key, scope, order_override, resolution_type, context)
|
12
|
+
answer = nil
|
13
|
+
found = false
|
14
|
+
|
15
|
+
Hiera.debug("Looking up #{key} in YAML backend")
|
16
|
+
|
17
|
+
Backend.datasourcefiles(:yaml, scope, "yaml", order_override) do |source, yamlfile|
|
18
|
+
data = @cache.read_file(yamlfile, Hash) do |data|
|
19
|
+
YAML.load(data) || {}
|
20
|
+
end
|
21
|
+
|
22
|
+
next if data.empty?
|
23
|
+
next unless data.include?(key)
|
24
|
+
found = true
|
25
|
+
|
26
|
+
# Extra logging that we found the key. This can be outputted
|
27
|
+
# multiple times if the resolution type is array or hash but that
|
28
|
+
# should be expected as the logging will then tell the user ALL the
|
29
|
+
# places where the key is found.
|
30
|
+
Hiera.debug("Found #{key} in #{source}")
|
31
|
+
|
32
|
+
# for array resolution we just append to the array whatever
|
33
|
+
# we find, we then goes onto the next file and keep adding to
|
34
|
+
# the array
|
35
|
+
#
|
36
|
+
# for priority searches we break after the first found data item
|
37
|
+
new_answer = Backend.parse_answer(data[key], scope, {}, context)
|
38
|
+
case resolution_type.is_a?(Hash) ? :hash : resolution_type
|
39
|
+
when :array
|
40
|
+
raise Exception, "Hiera type mismatch for key '#{key}': expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
|
41
|
+
answer ||= []
|
42
|
+
answer << new_answer
|
43
|
+
when :hash
|
44
|
+
raise Exception, "Hiera type mismatch for key '#{key}': expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
|
45
|
+
answer ||= {}
|
46
|
+
answer = Backend.merge_answer(new_answer, answer, resolution_type)
|
47
|
+
else
|
48
|
+
answer = new_answer
|
49
|
+
break
|
50
|
+
end
|
51
|
+
end
|
52
|
+
throw :no_such_key unless found
|
53
|
+
return answer
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def file_exists?(path)
|
59
|
+
File.exist? path
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,325 @@
|
|
1
|
+
require 'hiera/util'
|
2
|
+
require 'hiera/interpolate'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'deep_merge'
|
6
|
+
rescue LoadError
|
7
|
+
end
|
8
|
+
|
9
|
+
class Hiera
|
10
|
+
module Backend
|
11
|
+
class Backend1xWrapper
|
12
|
+
def initialize(wrapped)
|
13
|
+
@wrapped = wrapped
|
14
|
+
end
|
15
|
+
|
16
|
+
def lookup(key, scope, order_override, resolution_type, context)
|
17
|
+
Hiera.debug("Using Hiera 1.x backend API to access instance of class #{@wrapped.class.name}. Lookup recursion will not be detected")
|
18
|
+
value = @wrapped.lookup(key, scope, order_override, resolution_type.is_a?(Hash) ? :hash : resolution_type)
|
19
|
+
|
20
|
+
# The most likely cause when an old backend returns nil is that the key was not found. In any case, it is
|
21
|
+
# impossible to know the difference between that and a found nil. The throw here preserves the old behavior.
|
22
|
+
throw (:no_such_key) if value.nil?
|
23
|
+
value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
# Data lives in /var/lib/hiera by default. If a backend
|
29
|
+
# supplies a datadir in the config it will be used and
|
30
|
+
# subject to variable expansion based on scope
|
31
|
+
def datadir(backend, scope)
|
32
|
+
backend = backend.to_sym
|
33
|
+
|
34
|
+
if Config[backend] && Config[backend][:datadir]
|
35
|
+
dir = Config[backend][:datadir]
|
36
|
+
else
|
37
|
+
dir = Hiera::Util.var_dir
|
38
|
+
end
|
39
|
+
|
40
|
+
if !dir.is_a?(String)
|
41
|
+
raise(Hiera::InvalidConfigurationError,
|
42
|
+
"datadir for #{backend} cannot be an array")
|
43
|
+
end
|
44
|
+
|
45
|
+
parse_string(dir, scope)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Finds the path to a datafile based on the Backend#datadir
|
49
|
+
# and extension
|
50
|
+
#
|
51
|
+
# If the file is not found nil is returned
|
52
|
+
def datafile(backend, scope, source, extension)
|
53
|
+
datafile_in(datadir(backend, scope), source, extension)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @api private
|
57
|
+
def datafile_in(datadir, source, extension)
|
58
|
+
file = File.join(datadir, "#{source}.#{extension}")
|
59
|
+
|
60
|
+
if File.exist?(file)
|
61
|
+
file
|
62
|
+
else
|
63
|
+
Hiera.debug("Cannot find datafile #{file}, skipping")
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Constructs a list of data sources to search
|
69
|
+
#
|
70
|
+
# If you give it a specific hierarchy it will just use that
|
71
|
+
# else it will use the global configured one, failing that
|
72
|
+
# it will just look in the 'common' data source.
|
73
|
+
#
|
74
|
+
# An override can be supplied that will be pre-pended to the
|
75
|
+
# hierarchy.
|
76
|
+
#
|
77
|
+
# The source names will be subject to variable expansion based
|
78
|
+
# on scope
|
79
|
+
def datasources(scope, override=nil, hierarchy=nil)
|
80
|
+
if hierarchy
|
81
|
+
hierarchy = [hierarchy]
|
82
|
+
elsif Config.include?(:hierarchy)
|
83
|
+
hierarchy = [Config[:hierarchy]].flatten
|
84
|
+
else
|
85
|
+
hierarchy = ["common"]
|
86
|
+
end
|
87
|
+
|
88
|
+
hierarchy.insert(0, override) if override
|
89
|
+
|
90
|
+
hierarchy.flatten.map do |source|
|
91
|
+
source = parse_string(source, scope, {}, :order_override => override)
|
92
|
+
yield(source) unless source == "" or source =~ /(^\/|\/\/|\/$)/
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Constructs a list of data files to search
|
97
|
+
#
|
98
|
+
# If you give it a specific hierarchy it will just use that
|
99
|
+
# else it will use the global configured one, failing that
|
100
|
+
# it will just look in the 'common' data source.
|
101
|
+
#
|
102
|
+
# An override can be supplied that will be pre-pended to the
|
103
|
+
# hierarchy.
|
104
|
+
#
|
105
|
+
# The source names will be subject to variable expansion based
|
106
|
+
# on scope
|
107
|
+
#
|
108
|
+
# Only files that exist will be returned. If the file is missing, then
|
109
|
+
# the block will not receive the file.
|
110
|
+
#
|
111
|
+
# @yield [String, String] the source string and the name of the resulting file
|
112
|
+
# @api public
|
113
|
+
def datasourcefiles(backend, scope, extension, override=nil, hierarchy=nil)
|
114
|
+
datadir = Backend.datadir(backend, scope)
|
115
|
+
Backend.datasources(scope, override, hierarchy) do |source|
|
116
|
+
Hiera.debug("Looking for data source #{source}")
|
117
|
+
file = datafile_in(datadir, source, extension)
|
118
|
+
|
119
|
+
if file
|
120
|
+
yield source, file
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Parse a string like <code>'%{foo}'</code> against a supplied
|
126
|
+
# scope and additional scope. If either scope or
|
127
|
+
# extra_scope includes the variable 'foo', then it will
|
128
|
+
# be replaced else an empty string will be placed.
|
129
|
+
#
|
130
|
+
# If both scope and extra_data has "foo", then the value in scope
|
131
|
+
# will be used.
|
132
|
+
#
|
133
|
+
# @param data [String] The string to perform substitutions on.
|
134
|
+
# This will not be modified, instead a new string will be returned.
|
135
|
+
# @param scope [#[]] The primary source of data for substitutions.
|
136
|
+
# @param extra_data [#[]] The secondary source of data for substitutions.
|
137
|
+
# @param context [#[]] Context can include :recurse_guard and :order_override.
|
138
|
+
# @return [String] A copy of the data with all instances of <code>%{...}</code> replaced.
|
139
|
+
#
|
140
|
+
# @api public
|
141
|
+
def parse_string(data, scope, extra_data={}, context={:recurse_guard => nil, :order_override => nil})
|
142
|
+
Hiera::Interpolate.interpolate(data, scope, extra_data, context)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Parses a answer received from data files
|
146
|
+
#
|
147
|
+
# Ultimately it just pass the data through parse_string but
|
148
|
+
# it makes some effort to handle arrays of strings as well
|
149
|
+
def parse_answer(data, scope, extra_data={}, context={:recurse_guard => nil, :order_override => nil})
|
150
|
+
if data.is_a?(Numeric) or data.is_a?(TrueClass) or data.is_a?(FalseClass)
|
151
|
+
return data
|
152
|
+
elsif data.is_a?(String)
|
153
|
+
return parse_string(data, scope, extra_data, context)
|
154
|
+
elsif data.is_a?(Hash)
|
155
|
+
answer = {}
|
156
|
+
data.each_pair do |key, val|
|
157
|
+
interpolated_key = parse_string(key, scope, extra_data, context)
|
158
|
+
answer[interpolated_key] = parse_answer(val, scope, extra_data, context)
|
159
|
+
end
|
160
|
+
|
161
|
+
return answer
|
162
|
+
elsif data.is_a?(Array)
|
163
|
+
answer = []
|
164
|
+
data.each do |item|
|
165
|
+
answer << parse_answer(item, scope, extra_data, context)
|
166
|
+
end
|
167
|
+
|
168
|
+
return answer
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def resolve_answer(answer, resolution_type)
|
173
|
+
case resolution_type
|
174
|
+
when :array
|
175
|
+
[answer].flatten.uniq.compact
|
176
|
+
when :hash
|
177
|
+
answer # Hash structure should be preserved
|
178
|
+
else
|
179
|
+
answer
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Merges two hashes answers with the given or configured merge behavior. Behavior can be given
|
184
|
+
# by passing _resolution_type_ as a Hash
|
185
|
+
#
|
186
|
+
# :merge_behavior: {:native|:deep|:deeper}
|
187
|
+
#
|
188
|
+
# Deep merge options use the Hash utility function provided by [deep_merge](https://github.com/danielsdeleo/deep_merge)
|
189
|
+
#
|
190
|
+
# :native => Native Hash.merge
|
191
|
+
# :deep => Use Hash.deep_merge
|
192
|
+
# :deeper => Use Hash.deep_merge!
|
193
|
+
#
|
194
|
+
# @param left [Hash] left side of the merge
|
195
|
+
# @param right [Hash] right side of the merge
|
196
|
+
# @param resolution_type [String,Hash] The merge type, or if hash, the merge behavior and options
|
197
|
+
# @return [Hash] The merged result
|
198
|
+
# @see Hiera#lookup
|
199
|
+
#
|
200
|
+
def merge_answer(left,right,resolution_type=nil)
|
201
|
+
behavior, options =
|
202
|
+
if resolution_type.is_a?(Hash)
|
203
|
+
merge = resolution_type.clone
|
204
|
+
[merge.delete(:behavior), merge]
|
205
|
+
else
|
206
|
+
[Config[:merge_behavior], Config[:deep_merge_options] || {}]
|
207
|
+
end
|
208
|
+
|
209
|
+
case behavior
|
210
|
+
when :deeper,'deeper'
|
211
|
+
left.deep_merge!(right, options)
|
212
|
+
when :deep,'deep'
|
213
|
+
left.deep_merge(right, options)
|
214
|
+
else # Native and undefined
|
215
|
+
left.merge(right)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Calls out to all configured backends in the order they
|
220
|
+
# were specified. The first one to answer will win.
|
221
|
+
#
|
222
|
+
# This lets you declare multiple backends, a possible
|
223
|
+
# use case might be in Puppet where a Puppet module declares
|
224
|
+
# default data using in-module data while users can override
|
225
|
+
# using JSON/YAML etc. By layering the backends and putting
|
226
|
+
# the Puppet one last you can override module author data
|
227
|
+
# easily.
|
228
|
+
#
|
229
|
+
# Backend instances are cached so if you need to connect to any
|
230
|
+
# databases then do so in your constructor, future calls to your
|
231
|
+
# backend will not create new instances
|
232
|
+
|
233
|
+
# @param key [String] The key to lookup
|
234
|
+
# @param scope [#[]] The primary source of data for substitutions.
|
235
|
+
# @param order_override [#[],nil] An override that will be pre-pended to the hierarchy definition.
|
236
|
+
# @param resolution_type [Symbol,Hash,nil] One of :hash, :array,:priority or a Hash with deep merge behavior and options
|
237
|
+
# @param context [#[]] Context used for internal processing
|
238
|
+
# @return [Object] The value that corresponds to the given key or nil if no such value cannot be found
|
239
|
+
#
|
240
|
+
def lookup(key, default, scope, order_override, resolution_type, context = {:recurse_guard => nil})
|
241
|
+
@backends ||= {}
|
242
|
+
answer = nil
|
243
|
+
|
244
|
+
# order_override is kept as an explicit argument for backwards compatibility, but should be specified
|
245
|
+
# in the context for internal handling.
|
246
|
+
context ||= {}
|
247
|
+
order_override ||= context[:order_override]
|
248
|
+
context[:order_override] ||= order_override
|
249
|
+
|
250
|
+
strategy = resolution_type.is_a?(Hash) ? :hash : resolution_type
|
251
|
+
|
252
|
+
segments = key.split('.')
|
253
|
+
subsegments = nil
|
254
|
+
if segments.size > 1
|
255
|
+
raise ArgumentError, "Resolution type :#{strategy} is illegal when doing segmented key lookups" unless strategy.nil? || strategy == :priority
|
256
|
+
subsegments = segments.drop(1)
|
257
|
+
end
|
258
|
+
|
259
|
+
found = false
|
260
|
+
Config[:backends].each do |backend|
|
261
|
+
backend_constant = "#{backend.capitalize}_backend"
|
262
|
+
if constants.include?(backend_constant) || constants.include?(backend_constant.to_sym)
|
263
|
+
backend = (@backends[backend] ||= find_backend(backend_constant))
|
264
|
+
found_in_backend = false
|
265
|
+
new_answer = catch(:no_such_key) do
|
266
|
+
value = backend.lookup(segments[0], scope, order_override, resolution_type, context)
|
267
|
+
value = qualified_lookup(subsegments, value) unless subsegments.nil?
|
268
|
+
found_in_backend = true
|
269
|
+
value
|
270
|
+
end
|
271
|
+
next unless found_in_backend
|
272
|
+
found = true
|
273
|
+
|
274
|
+
case strategy
|
275
|
+
when :array
|
276
|
+
raise Exception, "Hiera type mismatch for key '#{key}': expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
|
277
|
+
answer ||= []
|
278
|
+
answer << new_answer
|
279
|
+
when :hash
|
280
|
+
raise Exception, "Hiera type mismatch for key '#{key}': expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
|
281
|
+
answer ||= {}
|
282
|
+
answer = merge_answer(new_answer, answer, resolution_type)
|
283
|
+
else
|
284
|
+
answer = new_answer
|
285
|
+
break
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
answer = resolve_answer(answer, strategy) unless answer.nil?
|
291
|
+
answer = parse_string(default, scope, {}, context) if !found && default.is_a?(String)
|
292
|
+
|
293
|
+
return default if !found && answer.nil?
|
294
|
+
return answer
|
295
|
+
end
|
296
|
+
|
297
|
+
def clear!
|
298
|
+
@backends = {}
|
299
|
+
end
|
300
|
+
|
301
|
+
def qualified_lookup(segments, hash)
|
302
|
+
value = hash
|
303
|
+
segments.each do |segment|
|
304
|
+
throw :no_such_key if value.nil?
|
305
|
+
if segment =~ /^[0-9]+$/
|
306
|
+
segment = segment.to_i
|
307
|
+
raise Exception, "Hiera type mismatch: Got #{value.class.name} when Array was expected enable lookup using key '#{segment}'" unless value.instance_of?(Array)
|
308
|
+
throw :no_such_key unless segment < value.size
|
309
|
+
else
|
310
|
+
raise Exception, "Hiera type mismatch: Got #{value.class.name} when a non Array object that responds to '[]' was expected to enable lookup using key '#{segment}'" unless value.respond_to?(:'[]') && !value.instance_of?(Array);
|
311
|
+
throw :no_such_key unless value.include?(segment)
|
312
|
+
end
|
313
|
+
value = value[segment]
|
314
|
+
end
|
315
|
+
value
|
316
|
+
end
|
317
|
+
|
318
|
+
def find_backend(backend_constant)
|
319
|
+
backend = Backend.const_get(backend_constant).new
|
320
|
+
return backend.method(:lookup).arity == 4 ? Backend1xWrapper.new(backend) : backend
|
321
|
+
end
|
322
|
+
private :find_backend
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|