hiera 2.0.0-x86-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.rb +115 -0
- data/lib/hiera/backend.rb +325 -0
- data/lib/hiera/backend/json_backend.rb +58 -0
- data/lib/hiera/backend/yaml_backend.rb +63 -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/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 +142 -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])
|
data/lib/hiera.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class Hiera
|
4
|
+
require "hiera/error"
|
5
|
+
require "hiera/version"
|
6
|
+
require "hiera/config"
|
7
|
+
require "hiera/util"
|
8
|
+
require "hiera/backend"
|
9
|
+
require "hiera/console_logger"
|
10
|
+
require "hiera/puppet_logger"
|
11
|
+
require "hiera/noop_logger"
|
12
|
+
require "hiera/fallback_logger"
|
13
|
+
require "hiera/filecache"
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_reader :logger
|
17
|
+
|
18
|
+
# Loggers are pluggable, just provide a class called
|
19
|
+
# Hiera::Foo_logger and respond to :warn and :debug
|
20
|
+
#
|
21
|
+
# See hiera-puppet for an example that uses the Puppet
|
22
|
+
# loging system instead of our own
|
23
|
+
def logger=(logger)
|
24
|
+
require "hiera/#{logger}_logger"
|
25
|
+
|
26
|
+
@logger = Hiera::FallbackLogger.new(
|
27
|
+
Hiera.const_get("#{logger.capitalize}_logger"),
|
28
|
+
Hiera::Console_logger)
|
29
|
+
rescue Exception => e
|
30
|
+
@logger = Hiera::Console_logger
|
31
|
+
warn("Failed to load #{logger} logger: #{e.class}: #{e}")
|
32
|
+
end
|
33
|
+
|
34
|
+
def warn(msg); @logger.warn(msg); end
|
35
|
+
def debug(msg); @logger.debug(msg); end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :options, :config
|
39
|
+
|
40
|
+
# If the config option is a string its assumed to be a filename,
|
41
|
+
# else a hash of what would have been in the YAML config file
|
42
|
+
def initialize(options={})
|
43
|
+
options[:config] ||= File.join(Util.config_dir, 'hiera.yaml')
|
44
|
+
|
45
|
+
@config = Config.load(options[:config])
|
46
|
+
|
47
|
+
Config.load_backends
|
48
|
+
end
|
49
|
+
|
50
|
+
# Calls the backends to do the actual lookup.
|
51
|
+
#
|
52
|
+
# The _scope_ can be anything that responds to `[]`, if you have input
|
53
|
+
# data like a Puppet Scope that does not you can wrap that data in a
|
54
|
+
# class that has a `[]` method that fetches the data from your source.
|
55
|
+
# See hiera-puppet for an example of this.
|
56
|
+
#
|
57
|
+
# The order-override will insert as first in the hierarchy a data source
|
58
|
+
# of your choice.
|
59
|
+
#
|
60
|
+
# Possible values for the _resolution_type_ parameter:
|
61
|
+
#
|
62
|
+
# - _:priority_ - This is the default. First found value is returned and no merge is performed
|
63
|
+
# - _:array_ - An array merge lookup assembles a value from every matching level of the hierarchy. It retrieves all
|
64
|
+
# of the (string or array) values for a given key, then flattens them into a single array of unique values.
|
65
|
+
# If _priority_ lookup can be thought of as a “default with overrides” pattern, _array_ merge lookup can be though
|
66
|
+
# of as “default with additions.”
|
67
|
+
# - _:hash_ - A hash merge lookup assembles a value from every matching level of the hierarchy. It retrieves all of
|
68
|
+
# the (hash) values for a given key, then merges the hashes into a single hash. Hash merge lookups will fail with
|
69
|
+
# an error if any of the values found in the data sources are strings or arrays. It only works when every value
|
70
|
+
# found is a hash. The actual merge behavior is determined by looking up the keys `:merge_behavior` and
|
71
|
+
# `:deep_merge_options` in the Hiera config. `:merge_behavior` can be set to `:deep`, :deeper` or `:native`
|
72
|
+
# (explained in detail below).
|
73
|
+
# - _{ deep merge options }_ - Configured values for `:merge_behavior` and `:deep_merge_options`will be completely
|
74
|
+
# ignored. Instead the _resolution_type_ will be a `:hash` merge where the `:merge_behavior` will be the value
|
75
|
+
# keyed by `:behavior` in the given hash and the `:deep_merge_options` will be the remaining top level entries of
|
76
|
+
# that same hash.
|
77
|
+
#
|
78
|
+
# Valid behaviors for the _:hash_ resolution type:
|
79
|
+
#
|
80
|
+
# - _native_ - Performs a simple hash-merge by overwriting keys of lower lookup priority.
|
81
|
+
# - _deeper_ - In a deeper hash merge, Hiera recursively merges keys and values in each source hash. For each key,
|
82
|
+
# if the value is:
|
83
|
+
# - only present in one source hash, it goes into the final hash.
|
84
|
+
# - a string/number/boolean and exists in two or more source hashes, the highest priority value goes into
|
85
|
+
# the final hash.
|
86
|
+
# - an array and exists in two or more source hashes, the values from each source are merged into a single
|
87
|
+
# array and de-duplicated (but not automatically flattened, as in an array merge lookup).
|
88
|
+
# - a hash and exists in two or more source hashes, the values from each source are recursively merged, as
|
89
|
+
# though they were source hashes.
|
90
|
+
# - mismatched between two or more source hashes, we haven’t validated the behavior. It should act as
|
91
|
+
# described in the deep_merge gem documentation.
|
92
|
+
# - _deep_ - In a deep hash merge, Hiera behaves the same as for _deeper_, except that when a string/number/boolean
|
93
|
+
# exists in two or more source hashes, the lowest priority value goes into the final hash. This is considered
|
94
|
+
# largely useless and should be avoided. Use _deeper_ instead.
|
95
|
+
#
|
96
|
+
# The _merge_ can be given as a hash with the mandatory key `:strategy` to denote the actual strategy. This
|
97
|
+
# is useful for the `:deeper` and `:deep` strategy since they can use additional options to control the behavior.
|
98
|
+
# The options can be passed as top level keys in the `merge` parameter when it is a given as a hash. Recognized
|
99
|
+
# options are:
|
100
|
+
#
|
101
|
+
# - 'knockout_prefix' Set to string value to signify prefix which deletes elements from existing element. Defaults is _undef_
|
102
|
+
# - 'sort_merged_arrays' Set to _true_ to sort all arrays that are merged together. Default is _false_
|
103
|
+
# - 'unpack_arrays' Set to string value used as a deliminator to join all array values and then split them again. Default is _undef_
|
104
|
+
# - 'merge_hash_arrays' Set to _true_ to merge hashes within arrays. Default is _false_
|
105
|
+
#
|
106
|
+
# @param key [String] The key to lookup
|
107
|
+
# @param default [Object,nil] The value to return when there is no match for _key_
|
108
|
+
# @param scope [#[],nil] The scope to use for the lookup
|
109
|
+
# @param order_override [#[]] An override that will considered the first source of lookup
|
110
|
+
# @param resolution_type [String,Hash<Symbol,String>] Symbolic resolution type or deep merge configuration
|
111
|
+
# @return [Object] The found value or the given _default_ value
|
112
|
+
def lookup(key, default, scope, order_override=nil, resolution_type=:priority)
|
113
|
+
Backend.lookup(key, default, scope, order_override, resolution_type)
|
114
|
+
end
|
115
|
+
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
|