1 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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
|
require 'hiera/util'
require 'hiera/interpolate'
begin
require 'deep_merge/rails_compat'
rescue LoadError
end
class Hiera
module Backend
class Backend1xWrapper
def initialize(wrapped)
@wrapped = wrapped
end
def lookup(key, scope, order_override, resolution_type, context)
Hiera.debug("Using Hiera 1.x backend API to access instance of class #{@wrapped.class.name}. Lookup recursion will not be detected")
value = @wrapped.lookup(key, scope, order_override, resolution_type.is_a?(Hash) ? :hash : resolution_type)
# The most likely cause when an old backend returns nil is that the key was not found. In any case, it is
# impossible to know the difference between that and a found nil. The throw here preserves the old behavior.
throw (:no_such_key) if value.nil?
value
end
end
class << self
# Data lives in /var/lib/hiera by default. If a backend
# supplies a datadir in the config it will be used and
# subject to variable expansion based on scope
def datadir(backend, scope)
backend = backend.to_sym
if Config[backend] && Config[backend][:datadir]
dir = Config[backend][:datadir]
else
dir = Hiera::Util.var_dir
end
if !dir.is_a?(String)
raise(Hiera::InvalidConfigurationError,
"datadir for #{backend} cannot be an array")
end
interpolate_config(dir, scope, nil)
end
# Finds the path to a datafile based on the Backend#datadir
# and extension
#
# If the file is not found nil is returned
def datafile(backend, scope, source, extension)
datafile_in(datadir(backend, scope), source, extension)
end
# @api private
def datafile_in(datadir, source, extension)
file = File.join(datadir, "#{source}.#{extension}")
if File.exist?(file)
file
else
Hiera.debug("Cannot find datafile #{file}, skipping")
nil
end
end
# Constructs a list of data sources to search
#
# If you give it a specific hierarchy it will just use that
# else it will use the global configured one, failing that
# it will just look in the 'common' data source.
#
# An override can be supplied that will be pre-pended to the
# hierarchy.
#
# The source names will be subject to variable expansion based
# on scope
def datasources(scope, override=nil, hierarchy=nil)
if hierarchy
hierarchy = [hierarchy]
elsif Config.include?(:hierarchy)
hierarchy = [Config[:hierarchy]].flatten
else
hierarchy = ["common"]
end
hierarchy.insert(0, override) if override
hierarchy.flatten.map do |source|
source = interpolate_config(source, scope, override)
yield(source) unless source == "" or source =~ /(^\/|\/\/|\/$)/
end
end
# Constructs a list of data files to search
#
# If you give it a specific hierarchy it will just use that
# else it will use the global configured one, failing that
# it will just look in the 'common' data source.
#
# An override can be supplied that will be pre-pended to the
# hierarchy.
#
# The source names will be subject to variable expansion based
# on scope
#
# Only files that exist will be returned. If the file is missing, then
# the block will not receive the file.
#
# @yield [String, String] the source string and the name of the resulting file
# @api public
def datasourcefiles(backend, scope, extension, override=nil, hierarchy=nil)
datadir = Backend.datadir(backend, scope)
Backend.datasources(scope, override, hierarchy) do |source|
Hiera.debug("Looking for data source #{source}")
file = datafile_in(datadir, source, extension)
if file
yield source, file
end
end
end
# Parse a string like <code>'%{foo}'</code> against a supplied
# scope and additional scope. If either scope or
# extra_scope includes the variable 'foo', then it will
# be replaced else an empty string will be placed.
#
# If both scope and extra_data has "foo", then the value in scope
# will be used.
#
# @param data [String] The string to perform substitutions on.
# This will not be modified, instead a new string will be returned.
# @param scope [#[]] The primary source of data for substitutions.
# @param extra_data [#[]] The secondary source of data for substitutions.
# @param context [#[]] Context can include :recurse_guard and :order_override.
# @return [String] A copy of the data with all instances of <code>%{...}</code> replaced.
#
# @api public
def parse_string(data, scope, extra_data={}, context={:recurse_guard => nil, :order_override => nil})
Hiera::Interpolate.interpolate(data, scope, extra_data, context)
end
# Parses a answer received from data files
#
# Ultimately it just pass the data through parse_string but
# it makes some effort to handle arrays of strings as well
def parse_answer(data, scope, extra_data={}, context={:recurse_guard => nil, :order_override => nil})
if data.is_a?(Numeric) or data.is_a?(TrueClass) or data.is_a?(FalseClass)
return data
elsif data.is_a?(String)
return parse_string(data, scope, extra_data, context)
elsif data.is_a?(Hash)
answer = {}
data.each_pair do |key, val|
interpolated_key = parse_string(key, scope, extra_data, context)
answer[interpolated_key] = parse_answer(val, scope, extra_data, context)
end
return answer
elsif data.is_a?(Array)
answer = []
data.each do |item|
answer << parse_answer(item, scope, extra_data, context)
end
return answer
end
end
def resolve_answer(answer, resolution_type)
case resolution_type
when :array
[answer].flatten.uniq.compact
when :hash
answer # Hash structure should be preserved
else
answer
end
end
# Merges two hashes answers with the given or configured merge behavior. Behavior can be given
# by passing _resolution_type_ as a Hash
#
# :merge_behavior: {:native|:deep|:deeper}
#
# Deep merge options use the Hash utility function provided by [deep_merge](https://github.com/danielsdeleo/deep_merge)
# It uses the compatibility mode [deep_merge](https://github.com/danielsdeleo/deep_merge#using-deep_merge-in-rails)
#
# :native => Native Hash.merge
# :deep => Use Hash.deeper_merge
# :deeper => Use Hash.deeper_merge!
#
# @param left [Hash] left side of the merge
# @param right [Hash] right side of the merge
# @param resolution_type [String,Hash] The merge type, or if hash, the merge behavior and options
# @return [Hash] The merged result
# @see Hiera#lookup
#
def merge_answer(left,right,resolution_type=nil)
behavior, options =
if resolution_type.is_a?(Hash)
merge = resolution_type.clone
[merge.delete(:behavior), merge]
else
[Config[:merge_behavior], Config[:deep_merge_options] || {}]
end
case behavior
when :deeper,'deeper'
left.deeper_merge!(right, options)
when :deep,'deep'
left.deeper_merge(right, options)
else # Native and undefined
left.merge(right)
end
end
# Calls out to all configured backends in the order they
# were specified. The first one to answer will win.
#
# This lets you declare multiple backends, a possible
# use case might be in Puppet where a Puppet module declares
# default data using in-module data while users can override
# using JSON/YAML etc. By layering the backends and putting
# the Puppet one last you can override module author data
# easily.
#
# Backend instances are cached so if you need to connect to any
# databases then do so in your constructor, future calls to your
# backend will not create new instances
# @param key [String] The key to lookup. May be quoted with single or double quotes to avoid subkey traversal on dot characters
# @param scope [#[]] The primary source of data for substitutions.
# @param order_override [#[],nil] An override that will be pre-pended to the hierarchy definition.
# @param resolution_type [Symbol,Hash,nil] One of :hash, :array,:priority or a Hash with deep merge behavior and options
# @param context [#[]] Context used for internal processing
# @return [Object] The value that corresponds to the given key or nil if no such value cannot be found
#
def lookup(key, default, scope, order_override, resolution_type, context = {:recurse_guard => nil})
@backends ||= {}
answer = nil
# order_override is kept as an explicit argument for backwards compatibility, but should be specified
# in the context for internal handling.
context ||= {}
order_override ||= context[:order_override]
context[:order_override] ||= order_override
strategy = resolution_type.is_a?(Hash) ? :hash : resolution_type
segments = Util.split_key(key) { |problem| ArgumentError.new("#{problem} in key: #{key}") }
subsegments = nil
if segments.size > 1
unless strategy.nil? || strategy == :priority
raise ArgumentError, "Resolution type :#{strategy} is illegal when accessing values using dotted keys. Offending key was '#{key}'"
end
subsegments = segments.drop(1)
end
found = false
Config[:backends].each do |backend|
backend_constant = "#{backend.capitalize}_backend"
if constants.include?(backend_constant) || constants.include?(backend_constant.to_sym)
backend = (@backends[backend] ||= find_backend(backend_constant))
found_in_backend = false
new_answer = catch(:no_such_key) do
if subsegments.nil?
value = backend.lookup(segments[0], scope, order_override, resolution_type, context)
elsif backend.respond_to?(:lookup_with_segments)
value = backend.lookup_with_segments(segments, scope, order_override, resolution_type, context)
else
value = backend.lookup(segments[0], scope, order_override, resolution_type, context)
value = qualified_lookup(subsegments, value, key) unless subsegments.nil?
end
found_in_backend = true
value
end
next unless found_in_backend
found = true
case strategy
when :array
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
answer ||= []
answer << new_answer
when :hash
raise Exception, "Hiera type mismatch for key '#{key}': expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
answer ||= {}
answer = merge_answer(new_answer, answer, resolution_type)
else
answer = new_answer
break
end
end
end
answer = resolve_answer(answer, strategy) unless answer.nil?
answer = parse_string(default, scope, {}, context) if !found && default.is_a?(String)
return default if !found && answer.nil?
return answer
end
def clear!
@backends = {}
end
def qualified_lookup(segments, hash, full_key = nil)
value = hash
segments.each do |segment|
throw :no_such_key if value.nil?
if segment =~ /^[0-9]+$/
segment = segment.to_i
unless value.instance_of?(Array)
suffix = full_key.nil? ? '' : " from key '#{full_key}'"
raise Exception,
"Hiera type mismatch: Got #{value.class.name} when Array was expected to access value using '#{segment}'#{suffix}"
end
throw :no_such_key unless segment < value.size
else
unless value.respond_to?(:'[]') && !(value.instance_of?(Array) || value.instance_of?(String))
suffix = full_key.nil? ? '' : " from key '#{full_key}'"
raise Exception,
"Hiera type mismatch: Got #{value.class.name} when a hash-like object was expected to access value using '#{segment}'#{suffix}"
end
throw :no_such_key unless value.include?(segment)
end
value = value[segment]
end
value
end
def find_backend(backend_constant)
backend = Backend.const_get(backend_constant).new
return backend.method(:lookup).arity == 4 ? Backend1xWrapper.new(backend) : backend
end
private :find_backend
def interpolate_config(entry, scope, override)
if @config_lookup_context.nil?
@config_lookup_context = { :is_interpolate_config => true, :order_override => override, :recurse_guard => Hiera::RecursiveGuard.new }
begin
Hiera::Interpolate.interpolate(entry, scope, {}, @config_lookup_context)
ensure
@config_lookup_context = nil
end
else
# Nested call (will happen when interpolate method 'hiera' is used)
Hiera::Interpolate.interpolate(entry, scope, {}, @config_lookup_context.merge(:order_override => override))
end
end
end
end
end
|