require 'hiera/util'
require 'hiera/interpolate'
begin
require 'deep_merge'
rescue LoadError
end
class Hiera
module Backend
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
parse_string(dir, scope)
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 = parse_string(source, scope)
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 '%{foo}'
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.
# @return [String] A copy of the data with all instances of %{...}
replaced.
#
# @api public
def parse_string(data, scope, extra_data={})
Hiera::Interpolate.interpolate(data, scope, extra_data)
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={})
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)
elsif data.is_a?(Hash)
answer = {}
data.each_pair do |key, val|
interpolated_key = parse_string(key, scope, extra_data)
answer[interpolated_key] = parse_answer(val, scope, extra_data)
end
return answer
elsif data.is_a?(Array)
answer = []
data.each do |item|
answer << parse_answer(item, scope, extra_data)
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 configured merge behavior.
# :merge_behavior: {:native|:deep|:deeper}
#
# Deep merge options use the Hash utility function provided by [deep_merge](https://github.com/peritor/deep_merge)
#
# :native => Native Hash.merge
# :deep => Use Hash.deep_merge
# :deeper => Use Hash.deep_merge!
#
def merge_answer(left,right)
case Config[:merge_behavior]
when :deeper,'deeper'
left.deep_merge!(right)
when :deep,'deep'
left.deep_merge(right)
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
def lookup(key, default, scope, order_override, resolution_type)
@backends ||= {}
answer = nil
Config[:backends].each do |backend|
if constants.include?("#{backend.capitalize}_backend") || constants.include?("#{backend.capitalize}_backend".to_sym)
@backends[backend] ||= Backend.const_get("#{backend.capitalize}_backend").new
new_answer = @backends[backend].lookup(key, scope, order_override, resolution_type)
if not new_answer.nil?
case resolution_type
when :array
raise Exception, "Hiera type mismatch: 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: expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
answer ||= {}
answer = merge_answer(new_answer,answer)
else
answer = new_answer
break
end
end
end
end
answer = resolve_answer(answer, resolution_type) unless answer.nil?
answer = parse_string(default, scope) if answer.nil? and default.is_a?(String)
return default if answer.nil?
return answer
end
def clear!
@backends = {}
end
end
end
end