hiera 0.3.0 → 1.0.0rc4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of hiera might be problematic. Click here for more details.
- data/CHANGELOG +112 -0
- data/COPYING +202 -0
- data/README.md +237 -0
- data/bin/hiera +160 -156
- data/lib/hiera.rb +58 -50
- data/lib/hiera/backend.rb +174 -161
- data/lib/hiera/backend/yaml_backend.rb +67 -45
- data/lib/hiera/config.rb +43 -41
- data/lib/hiera/console_logger.rb +9 -9
- data/lib/hiera/noop_logger.rb +8 -0
- data/lib/hiera/puppet_logger.rb +10 -5
- data/lib/hiera/util.rb +47 -0
- data/spec/spec_helper.rb +42 -6
- data/spec/unit/backend/yaml_backend_spec.rb +153 -104
- data/spec/unit/backend_spec.rb +309 -255
- data/spec/unit/config_spec.rb +85 -79
- data/spec/unit/console_logger_spec.rb +13 -13
- data/spec/unit/hiera_spec.rb +57 -54
- data/spec/unit/util_spec.rb +49 -0
- metadata +21 -10
data/lib/hiera/backend.rb
CHANGED
@@ -1,174 +1,187 @@
|
|
1
|
-
|
2
|
-
module Backend
|
3
|
-
class << self
|
4
|
-
# Data lives in /var/lib/hiera by default. If a backend
|
5
|
-
# supplies a datadir in the config it will be used and
|
6
|
-
# subject to variable expansion based on scope
|
7
|
-
def datadir(backend, scope)
|
8
|
-
backend = backend.to_sym
|
9
|
-
default = "/var/lib/hiera"
|
10
|
-
|
11
|
-
if Config.include?(backend)
|
12
|
-
parse_string(Config[backend][:datadir] || default, scope)
|
13
|
-
else
|
14
|
-
parse_string(default, scope)
|
15
|
-
end
|
16
|
-
end
|
1
|
+
require 'hiera/util'
|
17
2
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
3
|
+
class Hiera
|
4
|
+
module Backend
|
5
|
+
class << self
|
6
|
+
# Data lives in /var/lib/hiera by default. If a backend
|
7
|
+
# supplies a datadir in the config it will be used and
|
8
|
+
# subject to variable expansion based on scope
|
9
|
+
def datadir(backend, scope)
|
10
|
+
backend = backend.to_sym
|
11
|
+
default = Hiera::Util.var_dir
|
12
|
+
|
13
|
+
if Config.include?(backend)
|
14
|
+
parse_string(Config[backend][:datadir] || default, scope)
|
15
|
+
else
|
16
|
+
parse_string(default, scope)
|
17
|
+
end
|
18
|
+
end
|
24
19
|
|
25
|
-
|
26
|
-
|
20
|
+
# Finds the path to a datafile based on the Backend#datadir
|
21
|
+
# and extension
|
22
|
+
#
|
23
|
+
# If the file is not found nil is returned
|
24
|
+
def datafile(backend, scope, source, extension)
|
25
|
+
file = File.join([datadir(backend, scope), "#{source}.#{extension}"])
|
27
26
|
|
28
|
-
|
29
|
-
|
27
|
+
unless File.exist?(file)
|
28
|
+
Hiera.debug("Cannot find datafile #{file}, skipping")
|
30
29
|
|
31
|
-
|
32
|
-
|
30
|
+
return nil
|
31
|
+
end
|
33
32
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
33
|
+
return file
|
34
|
+
end
|
35
|
+
|
36
|
+
# Constructs a list of data sources to search
|
37
|
+
#
|
38
|
+
# If you give it a specific hierarchy it will just use that
|
39
|
+
# else it will use the global configured one, failing that
|
40
|
+
# it will just look in the 'common' data source.
|
41
|
+
#
|
42
|
+
# An override can be supplied that will be pre-pended to the
|
43
|
+
# hierarchy.
|
44
|
+
#
|
45
|
+
# The source names will be subject to variable expansion based
|
46
|
+
# on scope
|
47
|
+
def datasources(scope, override=nil, hierarchy=nil)
|
48
|
+
if hierarchy
|
49
|
+
hierarchy = [hierarchy]
|
50
|
+
elsif Config.include?(:hierarchy)
|
51
|
+
hierarchy = [Config[:hierarchy]].flatten
|
52
|
+
else
|
53
|
+
hierarchy = ["common"]
|
54
|
+
end
|
45
55
|
|
46
|
-
|
47
|
-
#
|
48
|
-
# If you give it a specific hierarchy it will just use that
|
49
|
-
# else it will use the global configured one, failing that
|
50
|
-
# it will just look in the 'common' data source.
|
51
|
-
#
|
52
|
-
# An override can be supplied that will be pre-pended to the
|
53
|
-
# hierarchy.
|
54
|
-
#
|
55
|
-
# The source names will be subject to variable expansion based
|
56
|
-
# on scope
|
57
|
-
def datasources(scope, override=nil, hierarchy=nil)
|
58
|
-
if hierarchy
|
59
|
-
hierarchy = [hierarchy]
|
60
|
-
elsif Config.include?(:hierarchy)
|
61
|
-
hierarchy = [Config[:hierarchy]].flatten
|
62
|
-
else
|
63
|
-
hierarchy = ["common"]
|
64
|
-
end
|
65
|
-
|
66
|
-
hierarchy.insert(0, override) if override
|
67
|
-
|
68
|
-
hierarchy.flatten.map do |source|
|
69
|
-
source = parse_string(source, scope)
|
70
|
-
yield(source) unless source == "" or source =~ /(^\/|\/\/|\/$)/
|
71
|
-
end
|
72
|
-
end
|
56
|
+
hierarchy.insert(0, override) if override
|
73
57
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
58
|
+
hierarchy.flatten.map do |source|
|
59
|
+
source = parse_string(source, scope)
|
60
|
+
yield(source) unless source == "" or source =~ /(^\/|\/\/|\/$)/
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Parse a string like '%{foo}' against a supplied
|
65
|
+
# scope and additional scope. If either scope or
|
66
|
+
# extra_scope includes the varaible 'foo' it will
|
67
|
+
# be replaced else an empty string will be placed.
|
68
|
+
#
|
69
|
+
# If both scope and extra_data has "foo" scope
|
70
|
+
# will win. See hiera-puppet for an example of
|
71
|
+
# this to make hiera aware of additional non scope
|
72
|
+
# variables
|
73
|
+
def parse_string(data, scope, extra_data={})
|
74
|
+
return nil unless data
|
75
|
+
|
76
|
+
tdata = data.clone
|
77
|
+
|
78
|
+
if tdata.is_a?(String)
|
79
|
+
while tdata =~ /%\{(.+?)\}/
|
80
|
+
begin
|
81
|
+
var = $1
|
82
|
+
|
83
|
+
val = ""
|
84
|
+
|
85
|
+
# Puppet can return :undefined for unknown scope vars,
|
86
|
+
# If it does then we still need to evaluate extra_data
|
87
|
+
# before returning an empty string.
|
88
|
+
if scope[var] && scope[var] != :undefined
|
89
|
+
val = scope[var]
|
90
|
+
elsif extra_data[var]
|
91
|
+
val = extra_data[var]
|
92
|
+
end
|
93
|
+
end until val != "" || var !~ /::(.+)/
|
94
|
+
|
95
|
+
tdata.gsub!(/%\{(::)?#{var}\}/, val)
|
96
|
+
end
|
97
|
+
end
|
102
98
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
99
|
+
return tdata
|
100
|
+
end
|
101
|
+
|
102
|
+
# Parses a answer received from data files
|
103
|
+
#
|
104
|
+
# Ultimately it just pass the data through parse_string but
|
105
|
+
# it makes some effort to handle arrays of strings as well
|
106
|
+
def parse_answer(data, scope, extra_data={})
|
107
|
+
if data.is_a?(Numeric) or data.is_a?(TrueClass) or data.is_a?(FalseClass)
|
108
|
+
return data
|
109
|
+
elsif data.is_a?(String)
|
110
|
+
return parse_string(data, scope, extra_data)
|
111
|
+
elsif data.is_a?(Hash)
|
112
|
+
answer = {}
|
113
|
+
data.each_pair do |key, val|
|
114
|
+
answer[key] = parse_answer(val, scope, extra_data)
|
115
|
+
end
|
116
|
+
|
117
|
+
return answer
|
118
|
+
elsif data.is_a?(Array)
|
119
|
+
answer = []
|
120
|
+
data.each do |item|
|
121
|
+
answer << parse_answer(item, scope, extra_data)
|
122
|
+
end
|
123
|
+
|
124
|
+
return answer
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def resolve_answer(answer, resolution_type)
|
129
|
+
case resolution_type
|
130
|
+
when :array
|
131
|
+
[answer].flatten.uniq.compact
|
132
|
+
when :hash
|
133
|
+
answer # Hash structure should be preserved
|
134
|
+
else
|
135
|
+
answer
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Calls out to all configured backends in the order they
|
140
|
+
# were specified. The first one to answer will win.
|
141
|
+
#
|
142
|
+
# This lets you declare multiple backends, a possible
|
143
|
+
# use case might be in Puppet where a Puppet module declares
|
144
|
+
# default data using in-module data while users can override
|
145
|
+
# using JSON/YAML etc. By layering the backends and putting
|
146
|
+
# the Puppet one last you can override module author data
|
147
|
+
# easily.
|
148
|
+
#
|
149
|
+
# Backend instances are cached so if you need to connect to any
|
150
|
+
# databases then do so in your constructor, future calls to your
|
151
|
+
# backend will not create new instances
|
152
|
+
def lookup(key, default, scope, order_override, resolution_type)
|
153
|
+
@backends ||= {}
|
154
|
+
answer = nil
|
155
|
+
|
156
|
+
Config[:backends].each do |backend|
|
157
|
+
if constants.include?("#{backend.capitalize}_backend") || constants.include?("#{backend.capitalize}_backend".to_sym)
|
158
|
+
@backends[backend] ||= Backend.const_get("#{backend.capitalize}_backend").new
|
159
|
+
new_answer = @backends[backend].lookup(key, scope, order_override, resolution_type)
|
160
|
+
|
161
|
+
if not new_answer.nil?
|
162
|
+
case resolution_type
|
163
|
+
when :array
|
164
|
+
raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
|
165
|
+
answer ||= []
|
166
|
+
answer << new_answer
|
167
|
+
when :hash
|
168
|
+
raise Exception, "Hiera type mismatch: expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
|
169
|
+
answer ||= {}
|
170
|
+
answer = new_answer.merge answer
|
171
|
+
else
|
172
|
+
answer = new_answer
|
173
|
+
break
|
174
|
+
end
|
127
175
|
end
|
176
|
+
end
|
177
|
+
end
|
128
178
|
|
129
|
-
|
130
|
-
|
131
|
-
when :array
|
132
|
-
[answer].flatten.uniq.compact
|
133
|
-
when :hash
|
134
|
-
answer # Hash structure should be preserved
|
135
|
-
else
|
136
|
-
answer
|
137
|
-
end
|
138
|
-
end
|
179
|
+
answer = resolve_answer(answer, resolution_type) unless answer.nil?
|
180
|
+
answer = parse_string(default, scope) if answer.nil? and default.is_a?(String)
|
139
181
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
# This lets you declare multiple backends, a possible
|
144
|
-
# use case might be in Puppet where a Puppet module declares
|
145
|
-
# default data using in-module data while users can override
|
146
|
-
# using JSON/YAML etc. By layering the backends and putting
|
147
|
-
# the Puppet one last you can override module author data
|
148
|
-
# easily.
|
149
|
-
#
|
150
|
-
# Backend instances are cached so if you need to connect to any
|
151
|
-
# databases then do so in your constructor, future calls to your
|
152
|
-
# backend will not create new instances
|
153
|
-
def lookup(key, default, scope, order_override, resolution_type)
|
154
|
-
@backends ||= {}
|
155
|
-
answer = nil
|
156
|
-
|
157
|
-
Config[:backends].each do |backend|
|
158
|
-
if constants.include?("#{backend.capitalize}_backend")
|
159
|
-
@backends[backend] ||= Backend.const_get("#{backend.capitalize}_backend").new
|
160
|
-
answer = @backends[backend].lookup(key, scope, order_override, resolution_type)
|
161
|
-
|
162
|
-
break if answer
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
answer = resolve_answer(answer, resolution_type)
|
167
|
-
answer = parse_string(default, scope) if answer.nil?
|
168
|
-
|
169
|
-
return default if answer == empty_answer(resolution_type)
|
170
|
-
return answer
|
171
|
-
end
|
172
|
-
end
|
182
|
+
return default if answer.nil?
|
183
|
+
return answer
|
184
|
+
end
|
173
185
|
end
|
186
|
+
end
|
174
187
|
end
|
@@ -1,49 +1,71 @@
|
|
1
1
|
class Hiera
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
2
|
+
module Backend
|
3
|
+
class Yaml_backend
|
4
|
+
def initialize
|
5
|
+
require 'yaml'
|
6
|
+
Hiera.debug("Hiera YAML backend starting")
|
7
|
+
@data = Hash.new
|
8
|
+
@cache = Hash.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def lookup(key, scope, order_override, resolution_type)
|
12
|
+
answer = nil
|
13
|
+
|
14
|
+
Hiera.debug("Looking up #{key} in YAML backend")
|
15
|
+
|
16
|
+
Backend.datasources(scope, order_override) do |source|
|
17
|
+
Hiera.debug("Looking for data source #{source}")
|
18
|
+
yamlfile = Backend.datafile(:yaml, scope, source, "yaml") || next
|
19
|
+
|
20
|
+
# If you call stale? BEFORE you do encounter the YAML.load_file line
|
21
|
+
# it will populate the @cache variable and return true. The second
|
22
|
+
# time you call it, it will return false because @cache has been
|
23
|
+
# populated. Because of this there are two conditions to check:
|
24
|
+
# is @data[yamlfile] populated AND is the cache stale.
|
25
|
+
if @data[yamlfile]
|
26
|
+
@data[yamlfile] = YAML.load_file(yamlfile) if stale?(yamlfile)
|
27
|
+
else
|
28
|
+
@data[yamlfile] = YAML.load_file(yamlfile)
|
29
|
+
end
|
30
|
+
|
31
|
+
next if ! @data[yamlfile]
|
32
|
+
next if @data[yamlfile].empty?
|
33
|
+
next unless @data[yamlfile].include?(key)
|
34
|
+
# for array resolution we just append to the array whatever
|
35
|
+
# we find, we then goes onto the next file and keep adding to
|
36
|
+
# the array
|
37
|
+
#
|
38
|
+
# for priority searches we break after the first found data item
|
39
|
+
new_answer = Backend.parse_answer(@data[yamlfile][key], scope)
|
40
|
+
case resolution_type
|
41
|
+
when :array
|
42
|
+
raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
|
43
|
+
answer ||= []
|
44
|
+
answer << new_answer
|
45
|
+
when :hash
|
46
|
+
raise Exception, "Hiera type mismatch: expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
|
47
|
+
answer ||= {}
|
48
|
+
answer = new_answer.merge answer
|
49
|
+
else
|
50
|
+
answer = new_answer
|
51
|
+
break
|
52
|
+
end
|
47
53
|
end
|
54
|
+
|
55
|
+
return answer
|
56
|
+
end
|
57
|
+
|
58
|
+
def stale?(yamlfile)
|
59
|
+
# NOTE: The mtime change in a file MUST be > 1 second before being
|
60
|
+
# recognized as stale. File mtime changes within 1 second will
|
61
|
+
# not be recognized.
|
62
|
+
stat = File.stat(yamlfile)
|
63
|
+
current = { 'inode' => stat.ino, 'mtime' => stat.mtime, 'size' => stat.size }
|
64
|
+
return false if @cache[yamlfile] == current
|
65
|
+
|
66
|
+
@cache[yamlfile] = current
|
67
|
+
return true
|
68
|
+
end
|
48
69
|
end
|
70
|
+
end
|
49
71
|
end
|