hiera 2.0.0-x64-mingw32
Sign up to get free protection for your applications and to get access to all the features.
- 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
|