hiera-1.3.0/0000755000004100000410000000000012243500207012626 5ustar www-datawww-datahiera-1.3.0/bin/0000755000004100000410000000000012243500207013376 5ustar www-datawww-datahiera-1.3.0/bin/hiera0000755000004100000410000001443312243500207014421 0ustar www-datawww-data#!/usr/bin/env ruby # CLI client for Hiera. # # To lookup the 'release' key for a node given Puppet YAML facts: # # $ hiera release 'rel/%{location}' --yaml some.node.yaml # # If the node yaml had a location fact the default would match that # else you can supply scope values on the command line # # $ hiera release 'rel/%{location}' location=dc2 --yaml some.node.yaml # Bundler and rubygems maintain a set of directories from which to # load gems. If Bundler is loaded, let it determine what can be # loaded. If it's not loaded, then use rubygems. But do this before # loading any hiera code, so that our gem loading system is sane. if not defined? ::Bundler begin require 'rubygems' rescue LoadError end end require 'hiera' require 'hiera/util' require 'optparse' require 'pp' options = { :default => nil, :config => File.join(Hiera::Util.config_dir, 'hiera.yaml'), :scope => {}, :key => nil, :verbose => false, :resolution_type => :priority } # Loads the scope from YAML or JSON files def load_scope(source, type=:yaml) case type when :mcollective begin require 'mcollective' include MCollective::RPC util = rpcclient("rpcutil") util.progress = false nodestats = util.custom_request("inventory", {}, source, {"identity" => source}).first raise "Failed to retrieve facts for node #{source}: #{nodestats[:statusmsg]}" unless nodestats[:statuscode] == 0 scope = nodestats[:data][:facts] rescue Exception => e STDERR.puts "MCollective lookup failed: #{e.class}: #{e}" exit 1 end when :yaml raise "Cannot find scope #{type} file #{source}" unless File.exist?(source) require 'yaml' # Attempt to load puppet in case we're going to be fed # Puppet yaml files begin require 'puppet' rescue end scope = YAML.load_file(source) # Puppet makes dumb yaml files that do not promote data reuse. scope = scope.values if scope.is_a?(Puppet::Node::Facts) when :json raise "Cannot find scope #{type} file #{source}" unless File.exist?(source) require 'json' scope = JSON.load(File.read(source)) when :inventory_service # For this to work the machine running the hiera command needs access to # /facts REST endpoint on your inventory server. This access is # controlled in auth.conf and identification is by the certname of the # machine running hiera commands. # # Another caveat is that if your inventory server isn't at the short dns # name of 'puppet' you will need to set the inventory_sever option in # your puppet.conf. Set it in either the master or main sections. It # is fine to have the inventory_server option set even if the config # doesn't have the fact_terminus set to rest. begin require 'puppet/util/run_mode' $puppet_application_mode = Puppet::Util::RunMode[:master] require 'puppet' Puppet.settings.parse Puppet::Node::Facts.indirection.terminus_class = :rest scope = YAML.load(Puppet::Node::Facts.indirection.find(source).to_yaml) # Puppet makes dumb yaml files that do not promote data reuse. scope = scope.values if scope.is_a?(Puppet::Node::Facts) rescue Exception => e STDERR.puts "Puppet inventory service lookup failed: #{e.class}: #{e}" exit 1 end else raise "Don't know how to load data type #{type}" end raise "Scope from #{type} file #{source} should be a Hash" unless scope.is_a?(Hash) scope end OptionParser.new do |opts| 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" opts.on("--version", "-V", "Version information") do puts Hiera.version exit end opts.on("--debug", "-d", "Show debugging information") do options[:verbose] = true end opts.on("--array", "-a", "Return all values as an array") do options[:resolution_type] = :array end opts.on("--hash", "-h", "Return all values as a hash") do options[:resolution_type] = :hash end opts.on("--config CONFIG", "-c", "Configuration file") do |v| if File.exist?(v) options[:config] = v else STDERR.puts "Cannot find config file: #{v}" exit 1 end end opts.on("--json SCOPE", "-j", "JSON format file to load scope from") do |v| begin options[:scope] = load_scope(v, :json) rescue Exception => e STDERR.puts "Could not load JSON scope: #{e.class}: #{e}" exit 1 end end opts.on("--yaml SCOPE", "-y", "YAML format file to load scope from") do |v| begin options[:scope] = load_scope(v) rescue Exception => e STDERR.puts "Could not load YAML scope: #{e.class}: #{e}" exit 1 end end opts.on("--mcollective IDENTITY", "-m", "Use facts from a node (via mcollective) as scope") do |v| begin options[:scope] = load_scope(v, :mcollective) rescue Exception => e STDERR.puts "Could not load MCollective scope: #{e.class}: #{e}" exit 1 end end opts.on("--inventory_service IDENTITY", "-i", "Use facts from a node (via Puppet's inventory service) as scope") do |v| begin options[:scope] = load_scope(v, :inventory_service) rescue Exception => e STDERR.puts "Could not load Puppet inventory service scope: #{e.class}: #{e}" exit 1 end end end.parse! # arguments can be: # # key default var=val another=val # # The var=val's assign scope unless ARGV.empty? options[:key] = ARGV.delete_at(0) ARGV.each do |arg| if arg =~ /^(.+?)=(.+?)$/ options[:scope][$1] = $2 else unless options[:default] options[:default] = arg.dup else STDERR.puts "Don't know how to parse scope argument: #{arg}" end end end else STDERR.puts "Please supply a data item to look up" exit 1 end begin hiera = Hiera.new(:config => options[:config]) rescue Exception => e if options[:verbose] raise else STDERR.puts "Failed to start Hiera: #{e.class}: #{e}" exit 1 end end unless options[:verbose] Hiera.logger = "noop" end ans = hiera.lookup(options[:key], options[:default], options[:scope], nil, options[:resolution_type]) if ans.is_a?(String) puts ans else pp ans end hiera-1.3.0/spec/0000755000004100000410000000000012243500207013560 5ustar www-datawww-datahiera-1.3.0/spec/spec_helper.rb0000644000004100000410000000242112243500207016375 0ustar www-datawww-data$:.insert(0, File.join([File.dirname(__FILE__), "..", "lib"])) require 'rubygems' require 'rspec' require 'mocha' require 'hiera' require 'tmpdir' RSpec.configure do |config| config.mock_with :mocha end # In ruby 1.8.5 Dir does not have mktmpdir defined, so this monkey patches # Dir to include the 1.8.7 definition of that method if it isn't already defined. # Method definition borrowed from ruby-1.8.7-p357/lib/ruby/1.8/tmpdir.rb unless Dir.respond_to?(:mktmpdir) def Dir.mktmpdir(prefix_suffix=nil, tmpdir=nil) case prefix_suffix when nil prefix = "d" suffix = "" when String prefix = prefix_suffix suffix = "" when Array prefix = prefix_suffix[0] suffix = prefix_suffix[1] else raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}" end tmpdir ||= Dir.tmpdir t = Time.now.strftime("%Y%m%d") n = nil begin path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}" path << "-#{n}" if n path << suffix Dir.mkdir(path, 0700) rescue Errno::EEXIST n ||= 0 n += 1 retry end if block_given? begin yield path ensure FileUtils.remove_entry_secure path end else path end end end hiera-1.3.0/spec/unit/0000755000004100000410000000000012243500207014537 5ustar www-datawww-datahiera-1.3.0/spec/unit/fallback_logger_spec.rb0000644000004100000410000000361612243500207021202 0ustar www-datawww-datarequire 'hiera/fallback_logger' describe Hiera::FallbackLogger do before :each do InMemoryLogger.reset SuitableLogger.reset end it "delegates #warn to the logger implemenation" do logger = Hiera::FallbackLogger.new(InMemoryLogger) logger.warn("the message") InMemoryLogger.warnings.should == ["the message"] end it "delegates #debug to the logger implemenation" do logger = Hiera::FallbackLogger.new(InMemoryLogger) logger.debug("the message") InMemoryLogger.debugs.should == ["the message"] end it "chooses the first logger that is suitable" do logger = Hiera::FallbackLogger.new(UnsuitableLogger, SuitableLogger) logger.warn("for the suitable logger") SuitableLogger.warnings.should include("for the suitable logger") end it "raises an error if no implementation is suitable" do expect do Hiera::FallbackLogger.new(UnsuitableLogger) end.to raise_error "No suitable logging implementation found." end it "issues a warning for each implementation that is not suitable" do Hiera::FallbackLogger.new(UnsuitableLogger, UnsuitableLogger, SuitableLogger) SuitableLogger.warnings.should == [ "Not using UnsuitableLogger. It does not report itself to be suitable.", "Not using UnsuitableLogger. It does not report itself to be suitable."] end # Preserves log messages in memory # and also serves as a "legacy" logger that has no # suitable? method class InMemoryLogger class << self attr_accessor :warnings, :debugs end def self.reset self.warnings = [] self.debugs = [] end def self.warn(message) self.warnings << message end def self.debug(message) self.debugs << message end end class UnsuitableLogger def self.suitable? false end end class SuitableLogger < InMemoryLogger def self.suitable? true end end end hiera-1.3.0/spec/unit/backend_spec.rb0000644000004100000410000005676312243500207017506 0ustar www-datawww-datarequire 'spec_helper' require 'hiera/util' class Hiera describe Backend do describe "#datadir" do it "interpolates any values in the configured value" do Config.load({:rspec => {:datadir => "/tmp/%{interpolate}"}}) dir = Backend.datadir(:rspec, { "interpolate" => "my_data" }) dir.should == "/tmp/my_data" end it "defaults to a directory in var" do Config.load({}) Backend.datadir(:rspec, {}).should == Hiera::Util.var_dir Config.load({:rspec => nil}) Backend.datadir(:rspec, {}).should == Hiera::Util.var_dir Config.load({:rspec => {}}) Backend.datadir(:rspec, {}).should == Hiera::Util.var_dir end it "fails when the datadir is an array" do Config.load({:rspec => {:datadir => []}}) expect do Backend.datadir(:rspec, {}) end.to raise_error(Hiera::InvalidConfigurationError, /datadir for rspec cannot be an array/) end end describe "#datafile" do it "translates a non-existant datafile into nil" do Hiera.expects(:debug).with("Cannot find datafile /nonexisting/test.yaml, skipping") Backend.expects(:datadir).returns("/nonexisting") Backend.datafile(:yaml, {}, "test", "yaml").should == nil end it "concatenates the datadir and datafile and format to produce the full datafile filename" do Backend.expects(:datadir).returns("/nonexisting") File.expects(:exist?).with("/nonexisting/test.yaml").returns(true) Backend.datafile(:yaml, {}, "test", "yaml").should == "/nonexisting/test.yaml" end end describe "#datasources" do it "iterates over the datasources in the order of the given hierarchy" do expected = ["one", "two"] Backend.datasources({}, nil, ["one", "two"]) do |backend| backend.should == expected.delete_at(0) end expected.empty?.should == true end it "uses the configured hierarchy no specific hierarchy is given" do Config.load(:hierarchy => "test") Backend.datasources({}) do |backend| backend.should == "test" end end it "defaults to a hierarchy of only 'common' if not configured or given" do Config.load({}) Backend.datasources({}) do |backend| backend.should == "common" end end it "prefixes the hierarchy with the override if an override is provided" do Config.load({}) expected = ["override", "common"] Backend.datasources({}, "override") do |backend| backend.should == expected.delete_at(0) end expected.empty?.should == true end it "parses the names of the hierarchy levels using the given scope" do Backend.expects(:parse_string).with("common", {:rspec => :tests}) Backend.datasources({:rspec => :tests}) { } end it "defaults to 'common' if the hierarchy contains no hierarchies with non-empty names" do Config.load({}) expected = ["common"] Backend.datasources({}, "%{rspec}") do |backend| backend.should == expected.delete_at(0) end expected.empty?.should == true end end describe "#parse_string" do it "passes nil through untouched" do Backend.parse_string(nil, {}).should == nil end it "does not modify the input data" do data = "%{value}" Backend.parse_string(data, { "value" => "replacement" }) data.should == "%{value}" end it "passes non-string data through untouched" do input = { "not a" => "string" } Backend.parse_string(input, {}).should == input end @scope_interpolation_tests = { "replace %{part1} and %{part2}" => "replace value of part1 and value of part2", "replace %{scope('part1')} and %{scope('part2')}" => "replace value of part1 and value of part2" } @scope_interpolation_tests.each do |input, expected| it "replaces interpolations with data looked up in the scope" do scope = {"part1" => "value of part1", "part2" => "value of part2"} Backend.parse_string(input, scope).should == expected end end it "replaces interpolations with data looked up in extra_data when scope does not contain the value" do input = "test_%{rspec}_test" Backend.parse_string(input, {}, {"rspec" => "extra"}).should == "test_extra_test" end it "prefers data from scope over data from extra_data" do input = "test_%{rspec}_test" Backend.parse_string(input, {"rspec" => "test"}, {"rspec" => "fail"}).should == "test_test_test" end @interprets_nil_in_scope_tests = { "test_%{rspec}_test" => "test__test", "test_%{scope('rspec')}_test" => "test__test" } @interprets_nil_in_scope_tests.each do |input, expected| it "interprets nil in scope as a non-value" do Backend.parse_string(input, {"rspec" => nil}).should == expected end end @interprets_false_in_scope_tests = { "test_%{rspec}_test" => "test_false_test", "test_%{scope('rspec')}_test" => "test_false_test" } @interprets_false_in_scope_tests.each do |input, expected| it "interprets false in scope as a real value" do input = "test_%{scope('rspec')}_test" Backend.parse_string(input, {"rspec" => false}).should == expected end end it "interprets false in extra_data as a real value" do input = "test_%{rspec}_test" Backend.parse_string(input, {}, {"rspec" => false}).should == "test_false_test" end it "interprets nil in extra_data as a non-value" do input = "test_%{rspec}_test" Backend.parse_string(input, {}, {"rspec" => nil}).should == "test__test" end @interprets_undefined_in_scope_tests = { "test_%{rspec}_test" => "test__test", "test_%{scope('rspec')}_test" => "test__test" } @interprets_undefined_in_scope_tests.each do |input, expected| it "interprets :undefined in scope as a non-value" do Backend.parse_string(input, {"rspec" => :undefined}).should == expected end end it "uses the value from extra_data when scope is :undefined" do input = "test_%{rspec}_test" Backend.parse_string(input, {"rspec" => :undefined}, { "rspec" => "extra" }).should == "test_extra_test" end @exact_lookup_tests = { "test_%{::rspec::data}_test" => "test_value_test", "test_%{scope('::rspec::data')}_test" => "test_value_test" } @exact_lookup_tests.each do |input, expected| it "looks up the interpolated value exactly as it appears in the input" do Backend.parse_string(input, {"::rspec::data" => "value"}).should == expected end end @surrounding_whitespace_tests = { "test_%{\trspec::data }_test" => "test_value_test", "test_%{scope('\trspec::data ')}_test" => "test_value_test" } @surrounding_whitespace_tests.each do |input, expected| it "does not remove any surrounding whitespace when parsing the key to lookup" do Backend.parse_string(input, {"\trspec::data " => "value"}).should == expected end end @leading_double_colon_tests = { "test_%{::rspec::data}_test" => "test__test", "test_%{scope('::rspec::data')}_test" => "test__test" } @leading_double_colon_tests.each do |input, expected| it "does not try removing leading :: when a full lookup fails (#17434)" do Backend.parse_string(input, {"rspec::data" => "value"}).should == expected end end @double_colon_key_tests = { "test_%{::rspec::data}_test" => "test__test", "test_%{scope('::rspec::data')}_test" => "test__test" } @double_colon_key_tests.each do |input, expected| it "does not try removing leading sections separated by :: when a full lookup fails (#17434)" do Backend.parse_string(input, {"data" => "value"}).should == expected end end it "does not try removing unknown, preceeding characters when looking up values" do input = "test_%{$var}_test" Backend.parse_string(input, {"$var" => "value"}).should == "test_value_test" end it "looks up recursively" do scope = {"rspec" => "%{first}", "first" => "%{last}", "last" => "final"} input = "test_%{rspec}_test" Backend.parse_string(input, scope).should == "test_final_test" end it "raises an error if the recursive lookup results in an infinite loop" do scope = {"first" => "%{second}", "second" => "%{first}"} input = "test_%{first}_test" expect do Backend.parse_string(input, scope) end.to raise_error Hiera::InterpolationLoop, "Detected in [first, second]" end it "replaces hiera interpolations with data looked up in hiera" do input = "%{hiera('key1')}" scope = {} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("key1", scope, nil, :priority).returns("answer") Backend.parse_string(input, scope).should == "answer" end end describe "#parse_answer" do it "interpolates values in strings" do input = "test_%{rspec}_test" Backend.parse_answer(input, {"rspec" => "test"}).should == "test_test_test" end it "interpolates each string in an array" do input = ["test_%{rspec}_test", "test_%{rspec}_test", ["test_%{rspec}_test"]] Backend.parse_answer(input, {"rspec" => "test"}).should == ["test_test_test", "test_test_test", ["test_test_test"]] end it "interpolates each string in a hash" do input = {"foo" => "test_%{rspec}_test", "bar" => "test_%{rspec}_test"} Backend.parse_answer(input, {"rspec" => "test"}).should == {"foo"=>"test_test_test", "bar"=>"test_test_test"} end it "interpolates string in hash keys" do input = {"%{rspec}" => "test"} Backend.parse_answer(input, {"rspec" => "foo"}).should == {"foo"=>"test"} end it "interpolates strings in nested hash keys" do input = {"topkey" => {"%{rspec}" => "test"}} Backend.parse_answer(input, {"rspec" => "foo"}).should == {"topkey"=>{"foo" => "test"}} end it "interpolates strings in a mixed structure of arrays and hashes" do input = {"foo" => "test_%{rspec}_test", "bar" => ["test_%{rspec}_test", "test_%{rspec}_test"]} Backend.parse_answer(input, {"rspec" => "test"}).should == {"foo"=>"test_test_test", "bar"=>["test_test_test", "test_test_test"]} end it "interpolates hiera lookups values in strings" do input = "test_%{hiera('rspec')}_test" scope = {} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority).returns("test") Backend.parse_answer(input, scope).should == "test_test_test" end it "interpolates hiera lookups in each string in an array" do input = ["test_%{hiera('rspec')}_test", "test_%{hiera('rspec')}_test", ["test_%{hiera('rspec')}_test"]] scope = {} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority).returns("test") Backend.parse_answer(input, scope).should == ["test_test_test", "test_test_test", ["test_test_test"]] end it "interpolates hiera lookups in each string in a hash" do input = {"foo" => "test_%{hiera('rspec')}_test", "bar" => "test_%{hiera('rspec')}_test"} scope = {} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority).returns("test") Backend.parse_answer(input, scope).should == {"foo"=>"test_test_test", "bar"=>"test_test_test"} end it "interpolates hiera lookups in string in hash keys" do input = {"%{hiera('rspec')}" => "test"} scope = {} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority).returns("foo") Backend.parse_answer(input, scope).should == {"foo"=>"test"} end it "interpolates hiera lookups in strings in nested hash keys" do input = {"topkey" => {"%{hiera('rspec')}" => "test"}} scope = {} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority).returns("foo") Backend.parse_answer(input, scope).should == {"topkey"=>{"foo" => "test"}} end it "interpolates hiera lookups in strings in a mixed structure of arrays and hashes" do input = {"foo" => "test_%{hiera('rspec')}_test", "bar" => ["test_%{hiera('rspec')}_test", "test_%{hiera('rspec')}_test"]} scope = {} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority).returns("test") Backend.parse_answer(input, scope).should == {"foo"=>"test_test_test", "bar"=>["test_test_test", "test_test_test"]} end it "interpolates hiera lookups and scope lookups in the same string" do input = {"foo" => "test_%{hiera('rspec')}_test", "bar" => "test_%{rspec2}_test"} scope = {"rspec2" => "scope_rspec"} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority).returns("hiera_rspec") Backend.parse_answer(input, scope).should == {"foo"=>"test_hiera_rspec_test", "bar"=>"test_scope_rspec_test"} end it "interpolates hiera and scope lookups with the same lookup query in a single string" do input = "test_%{hiera('rspec')}_test_%{rspec}" scope = {"rspec" => "scope_rspec"} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority).returns("hiera_rspec") Backend.parse_answer(input, scope).should == "test_hiera_rspec_test_scope_rspec" end it "passes integers unchanged" do input = 1 Backend.parse_answer(input, {"rspec" => "test"}).should == 1 end it "passes floats unchanged" do input = 0.233 Backend.parse_answer(input, {"rspec" => "test"}).should == 0.233 end it "passes the boolean true unchanged" do input = true Backend.parse_answer(input, {"rspec" => "test"}).should == true end it "passes the boolean false unchanged" do input = false Backend.parse_answer(input, {"rspec" => "test"}).should == false end it "interpolates lookups using single or double quotes" do input = "test_%{scope(\"rspec\")}_test_%{scope('rspec')}" scope = {"rspec" => "scope_rspec"} Backend.parse_answer(input, scope).should == "test_scope_rspec_test_scope_rspec" end end describe "#resolve_answer" do it "flattens and removes duplicate values from arrays during an array lookup" do Backend.resolve_answer(["foo", ["foo", "foo"], "bar"], :array).should == ["foo", "bar"] end it "returns the data unchanged during a priority lookup" do Backend.resolve_answer(["foo", ["foo", "foo"], "bar"], :priority).should == ["foo", ["foo", "foo"], "bar"] end end describe "#lookup" do before do Hiera.stubs(:debug) Hiera.stubs(:warn) end it "caches loaded backends" do Backend.clear! Hiera.expects(:debug).with(regexp_matches(/Hiera YAML backend starting/)).once Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend.lookup("key", "default", {}, nil, nil) Backend.lookup("key", "default", {}, nil, nil) end it "returns the answer from the backend" do Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with("key", {}, nil, nil).returns("answer") Backend.lookup("key", "default", {}, nil, nil).should == "answer" end it "retains the datatypes as returned by the backend" do Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with("stringval", {}, nil, nil).returns("string") Backend::Yaml_backend.any_instance.expects(:lookup).with("boolval", {}, nil, nil).returns(false) Backend::Yaml_backend.any_instance.expects(:lookup).with("numericval", {}, nil, nil).returns(1) Backend.lookup("stringval", "default", {}, nil, nil).should == "string" Backend.lookup("boolval", "default", {}, nil, nil).should == false Backend.lookup("numericval", "default", {}, nil, nil).should == 1 end it "calls to all backends till an answer is found" do backend = mock backend.expects(:lookup).returns("answer") Config.load({}) Config.instance_variable_set("@config", {:backends => ["yaml", "rspec"]}) Backend.instance_variable_set("@backends", {"rspec" => backend}) #Backend::Yaml_backend.any_instance.expects(:lookup).with("key", {"rspec" => "test"}, nil, nil) Backend.expects(:constants).returns(["Yaml_backend", "Rspec_backend"]).twice Backend.lookup("key", "test_%{rspec}", {"rspec" => "test"}, nil, nil).should == "answer" end it "calls to all backends till an answer is found when doing array lookups" do backend = mock backend.expects(:lookup).returns(["answer"]) Config.load({}) Config.instance_variable_set("@config", {:backends => ["yaml", "rspec"]}) Backend.instance_variable_set("@backends", {"rspec" => backend}) Backend.expects(:constants).returns(["Yaml_backend", "Rspec_backend"]).twice Backend.lookup("key", "notfound", {"rspec" => "test"}, nil, :array).should == ["answer"] end it "calls to all backends till an answer is found when doing hash lookups" do thehash = {:answer => "value"} backend = mock backend.expects(:lookup).returns(thehash) Config.load({}) Config.instance_variable_set("@config", {:backends => ["yaml", "rspec"]}) Backend.instance_variable_set("@backends", {"rspec" => backend}) Backend.expects(:constants).returns(["Yaml_backend", "Rspec_backend"]).twice Backend.lookup("key", "notfound", {"rspec" => "test"}, nil, :hash).should == thehash end it "builds a merged hash from all backends for hash searches" do backend1 = mock :lookup => {"a" => "answer"} backend2 = mock :lookup => {"b" => "bnswer"} Config.load({}) Config.instance_variable_set("@config", {:backends => ["first", "second"]}) Backend.instance_variable_set("@backends", {"first" => backend1, "second" => backend2}) Backend.stubs(:constants).returns(["First_backend", "Second_backend"]) Backend.lookup("key", {}, {"rspec" => "test"}, nil, :hash).should == {"a" => "answer", "b" => "bnswer"} end it "builds an array from all backends for array searches" do backend1 = mock :lookup => ["a", "b"] backend2 = mock :lookup => ["c", "d"] Config.load({}) Config.instance_variable_set("@config", {:backends => ["first", "second"]}) Backend.instance_variable_set("@backends", {"first" => backend1, "second" => backend2}) Backend.stubs(:constants).returns(["First_backend", "Second_backend"]) Backend.lookup("key", {}, {"rspec" => "test"}, nil, :array).should == ["a", "b", "c", "d"] end it "uses the earliest backend result for priority searches" do backend1 = mock backend1.stubs(:lookup).returns(["a", "b"]) backend2 = mock backend2.stubs(:lookup).returns(["c", "d"]) Config.load({}) Config.instance_variable_set("@config", {:backends => ["first", "second"]}) Backend.instance_variable_set("@backends", {"first" => backend1, "second" => backend2}) Backend.stubs(:constants).returns(["First_backend", "Second_backend"]) Backend.lookup("key", {}, {"rspec" => "test"}, nil, :priority).should == ["a", "b"] end it "parses the answers based on resolution_type" do Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend.expects(:resolve_answer).with("test_test", :priority).returns("parsed") Backend::Yaml_backend.any_instance.expects(:lookup).with("key", {"rspec" => "test"}, nil, :priority).returns("test_test") Backend.lookup("key", "test_%{rspec}", {"rspec" => "test"}, nil, :priority).should == "parsed" end it "returns the default with variables parsed if nothing is found" do Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with("key", {"rspec" => "test"}, nil, nil) Backend.lookup("key", "test_%{rspec}", {"rspec" => "test"}, nil, nil).should == "test_test" end it "keeps string default data as a string" do Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with("key", {}, nil, nil) Backend.lookup("key", "test", {}, nil, nil).should == "test" end it "keeps array default data as an array" do Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with("key", {}, nil, :array) Backend.lookup("key", ["test"], {}, nil, :array).should == ["test"] end it "keeps hash default data as a hash" do Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with("key", {}, nil, :hash) Backend.lookup("key", {"test" => "value"}, {}, nil, :hash).should == {"test" => "value"} end end describe '#merge_answer' do before do Hiera.stubs(:debug) Hiera.stubs(:warn) Config.stubs(:validate!) end it "uses Hash.merge when configured with :merge_behavior => :native" do Config.load({:merge_behavior => :native}) Hash.any_instance.expects(:merge).with({"b" => "bnswer"}).returns({"a" => "answer", "b" => "bnswer"}) Backend.merge_answer({"a" => "answer"},{"b" => "bnswer"}).should == {"a" => "answer", "b" => "bnswer"} end it "uses deep_merge! when configured with :merge_behavior => :deeper" do Config.load({:merge_behavior => :deeper}) Hash.any_instance.expects('deep_merge!').with({"b" => "bnswer"}).returns({"a" => "answer", "b" => "bnswer"}) Backend.merge_answer({"a" => "answer"},{"b" => "bnswer"}).should == {"a" => "answer", "b" => "bnswer"} end it "uses deep_merge when configured with :merge_behavior => :deep" do Config.load({:merge_behavior => :deep}) Hash.any_instance.expects('deep_merge').with({"b" => "bnswer"}).returns({"a" => "answer", "b" => "bnswer"}) Backend.merge_answer({"a" => "answer"},{"b" => "bnswer"}).should == {"a" => "answer", "b" => "bnswer"} end end end end hiera-1.3.0/spec/unit/config_spec.rb0000644000004100000410000000645012243500207017350 0ustar www-datawww-datarequire 'spec_helper' class Hiera describe Config do describe "#load" do let(:default_config) do { :backends => ["yaml"], :hierarchy => "common", :logger => "console", :merge_behavior=>:native } end it "should treat string sources as a filename" do expect { Config.load("/nonexisting") }.to raise_error end it "should raise an error for missing config files" do File.expects(:exist?).with("/nonexisting").returns(false) YAML.expects(:load_file).with("/nonexisting").never expect { Config.load("/nonexisting") }.to raise_error "Config file /nonexisting not found" end it "should attempt to YAML load config files" do File.expects(:exist?).with("/nonexisting").returns(true) YAML.expects(:load_file).with("/nonexisting").returns(YAML.load("---\n")) Config.load("/nonexisting") end it "should use defaults on empty YAML config file" do File.expects(:exist?).with("/nonexisting").returns(true) YAML.expects(:load_file).with("/nonexisting").returns(YAML.load("")) Config.load("/nonexisting").should == default_config end it "should use hash data as source if supplied" do config = Config.load({"rspec" => "test"}) config["rspec"].should == "test" end it "should merge defaults with the loaded or supplied config" do config = Config.load({}) config.should == {:backends => ["yaml"], :hierarchy => "common", :logger => "console", :merge_behavior=>:native} end it "should force :backends to be a flattened array" do Config.load({:backends => [["foo", ["bar"]]]}).should == {:backends => ["foo", "bar"], :hierarchy => "common", :logger => "console", :merge_behavior=>:native} end it "should load the supplied logger" do Hiera.expects(:logger=).with("foo") Config.load({:logger => "foo"}) end it "should default to the console logger" do Hiera.expects(:logger=).with("console") Config.load({}) end context "loading '/dev/null' as spec tests do" do before :each do # Simulate the behavior of YAML.load_file('/dev/null') in MRI 1.9.3p194 Config.stubs(:yaml_load_file). raises(TypeError, "no implicit conversion from nil to integer") end it "is not exceptional behavior" do expect { Config.load('/dev/null') }.to_not raise_error end end end describe "#load_backends" do it "should load each backend" do Config.load(:backends => ["One", "Two"]) Config.expects(:require).with("hiera/backend/one_backend") Config.expects(:require).with("hiera/backend/two_backend") Config.load_backends end it "should warn if it cant load a backend" do Config.load(:backends => ["one"]) Config.expects(:require).with("hiera/backend/one_backend").raises("fail") expect { Config.load_backends }.to raise_error("fail") end end describe "#include?" do it "should correctly report inclusion" do Config.load({}) Config.include?(:foo).should == false Config.include?(:logger).should == true end end end end hiera-1.3.0/spec/unit/puppet_logger_spec.rb0000644000004100000410000000120612243500207020751 0ustar www-datawww-datarequire 'hiera/puppet_logger' describe Hiera::Puppet_logger do it "is not suitable when Puppet is not defined" do ensure_puppet_not_defined Hiera::Puppet_logger.suitable?.should == false end it "is suitable when Puppet is defined" do ensure_puppet_defined Hiera::Puppet_logger.suitable?.should == true end after :each do ensure_puppet_not_defined end def ensure_puppet_defined if !Kernel.const_defined? :Puppet Kernel.const_set(:Puppet, "Fake Puppet") end end def ensure_puppet_not_defined if Kernel.const_defined? :Puppet Kernel.send(:remove_const, :Puppet) end end end hiera-1.3.0/spec/unit/version_spec.rb0000644000004100000410000000200412243500207017557 0ustar www-datawww-datarequire "spec_helper" require "hiera/version" require 'pathname' describe "Hiera.version Public API" do subject() { Hiera } before :each do Hiera.instance_eval do if @hiera_version @hiera_version = nil end end end context "without a VERSION file" do before :each do subject.stubs(:read_version_file).returns(nil) end it "is Hiera::VERSION" do subject.version.should == Hiera::VERSION end it "respects the version= setter" do subject.version = '1.2.3' subject.version.should == '1.2.3' end end context "with a VERSION file" do it "is the content of the file" do subject.expects(:read_version_file).with() do |path| pathname = Pathname.new(path) pathname.basename.to_s == "VERSION" end.returns('1.2.1-9-g9fda440') subject.version.should == '1.2.1-9-g9fda440' end it "respects the version= setter" do subject.version = '1.2.3' subject.version.should == '1.2.3' end end end hiera-1.3.0/spec/unit/console_logger_spec.rb0000644000004100000410000000070212243500207021076 0ustar www-datawww-datarequire 'spec_helper' class Hiera describe Console_logger do describe "#warn" do it "should warn to STDERR" do STDERR.expects(:puts).with("WARN: 0: foo") Time.expects(:now).returns(0) Console_logger.warn("foo") end it "should debug to STDERR" do STDERR.expects(:puts).with("DEBUG: 0: foo") Time.expects(:now).returns(0) Console_logger.debug("foo") end end end end hiera-1.3.0/spec/unit/util_spec.rb0000644000004100000410000000315212243500207017054 0ustar www-datawww-datarequire 'spec_helper' describe Hiera::Util do describe 'Hiera::Util.posix?' do it 'should return true on posix systems' do Etc.expects(:getpwuid).with(0).returns(true) Hiera::Util.posix?.should be_true end it 'should return false on non posix systems' do Etc.expects(:getpwuid).with(0).returns(nil) Hiera::Util.posix?.should be_false end end describe 'Hiera::Util.microsoft_windows?' do it 'should return false on posix systems' do Hiera::Util.expects(:file_alt_separator).returns(nil) Hiera::Util.microsoft_windows?.should be_false end end describe 'Hiera::Util.config_dir' do it 'should return the correct path for posix systems' do Hiera::Util.expects(:file_alt_separator).returns(nil) Hiera::Util.config_dir.should == '/etc' end it 'should return the correct path for microsoft windows systems' do Hiera::Util.expects(:microsoft_windows?).returns(true) Hiera::Util.expects(:common_appdata).returns('C:\\ProgramData') Hiera::Util.config_dir.should == 'C:\\ProgramData/PuppetLabs/hiera/etc' end end describe 'Hiera::Util.var_dir' do it 'should return the correct path for posix systems' do Hiera::Util.expects(:file_alt_separator).returns(nil) Hiera::Util.var_dir.should == '/var/lib/hiera' end it 'should return the correct path for microsoft windows systems' do Hiera::Util.expects(:microsoft_windows?).returns(true) Hiera::Util.expects(:common_appdata).returns('C:\\ProgramData') Hiera::Util.var_dir.should == 'C:\\ProgramData/PuppetLabs/hiera/var' end end end hiera-1.3.0/spec/unit/hiera_spec.rb0000644000004100000410000000420312243500207017165 0ustar www-datawww-datarequire 'spec_helper' require 'hiera/util' # This is only around for the logger setup tests module Hiera::Foo_logger end describe "Hiera" do describe "#logger=" do it "loads the given logger" do Hiera.expects(:require).with("hiera/foo_logger") Hiera.logger = "foo" end it "falls back to the Console logger when the logger could not be loaded" do Hiera.expects(:warn) Hiera.logger = "no_such_logger" Hiera.logger.should be Hiera::Console_logger end it "falls back to the Console logger when the logger class could not be found" do Hiera.expects(:warn) Hiera.expects(:require).with("hiera/no_constant_logger") Hiera.logger = "no_constant" Hiera.logger.should be Hiera::Console_logger end end describe "#warn" do it "delegates to the configured logger" do Hiera.logger = 'console' Hiera::Console_logger.expects(:warn).with("rspec") Hiera.warn("rspec") end end describe "#debug" do it "delegates to the configured logger" do Hiera.logger = 'console' Hiera::Console_logger.expects(:debug).with("rspec") Hiera.debug("rspec") end end describe "#initialize" do it "uses a default config file when none is provided" do config_file = File.join(Hiera::Util.config_dir, 'hiera.yaml') Hiera::Config.expects(:load).with(config_file) Hiera::Config.stubs(:load_backends) Hiera.new end it "passes the supplied config to the config class" do Hiera::Config.expects(:load).with({"test" => "rspec"}) Hiera::Config.stubs(:load_backends) Hiera.new(:config => {"test" => "rspec"}) end it "loads all backends on start" do Hiera::Config.stubs(:load) Hiera::Config.expects(:load_backends) Hiera.new end end describe "#lookup" do it "delegates to the Backend#lookup method" do Hiera::Config.stubs(:load) Hiera::Config.stubs(:load_backends) Hiera::Backend.expects(:lookup).with(:key, :default, :scope, :order_override, :resolution_type) Hiera.new.lookup(:key, :default, :scope, :order_override, :resolution_type) end end end hiera-1.3.0/spec/unit/filecache_spec.rb0000644000004100000410000000765212243500207020013 0ustar www-datawww-datarequire 'spec_helper' require 'tmpdir' class Hiera describe Filecache do before do @cache = Filecache.new end def write_file(file, contents) File.open(file, 'w') do |f| f.write(contents) end end describe "#read" do it "reads data from a file" do Dir.mktmpdir do |dir| file = File.join(dir, "testing") write_file(file, "my data") @cache.read(file).should == "my data" end end it "rereads data when the file changes" do Dir.mktmpdir do |dir| file = File.join(dir, "testing") write_file(file, "my data") @cache.read(file).should == "my data" write_file(file, "changed data") @cache.read(file).should == "changed data" end end it "uses the provided default when the type does not match the expected type" do Hiera.expects(:debug).with(regexp_matches(/String.*not.*Hash, setting defaults/)) Dir.mktmpdir do |dir| file = File.join(dir, "testing") write_file(file, "my data") data = @cache.read(file, Hash, { :testing => "hash" }) do |data| "a string" end data.should == { :testing => "hash" } end end it "traps any errors from the block and uses the default value" do Hiera.expects(:debug).with(regexp_matches(/Reading data.*failed:.*testing error/)) Dir.mktmpdir do |dir| file = File.join(dir, "testing") write_file(file, "my data") data = @cache.read(file, Hash, { :testing => "hash" }) do |data| raise ArgumentError, "testing error" end data.should == { :testing => "hash" } end end it "raises an error when there is no default given and there is a problem" do Dir.mktmpdir do |dir| file = File.join(dir, "testing") write_file(file, "my data") expect do @cache.read(file, Hash) do |data| raise ArgumentError, "testing error" end end.to raise_error(ArgumentError, "testing error") end end end describe "#read_file" do it "reads data from a file" do Dir.mktmpdir do |dir| file = File.join(dir, "testing") write_file(file, "my data") @cache.read_file(file).should == "my data" end end it "rereads data when the file changes" do Dir.mktmpdir do |dir| file = File.join(dir, "testing") write_file(file, "my data") @cache.read_file(file).should == "my data" write_file(file, "changed data") @cache.read_file(file).should == "changed data" end end it "errors when the type does not match the expected type" do Dir.mktmpdir do |dir| file = File.join(dir, "testing") write_file(file, "my data") expect do @cache.read_file(file, Hash) do |data| "a string" end end.to raise_error(TypeError) end end it "converts the read data using the block" do Dir.mktmpdir do |dir| file = File.join(dir, "testing") write_file(file, "my data") @cache.read_file(file, Hash) do |data| { :data => data } end.should == { :data => "my data" } end end it "errors when the file does not exist" do expect do @cache.read_file("/notexist") end.to raise_error(Errno::ENOENT) end it "propogates any errors from the block" do Dir.mktmpdir do |dir| file = File.join(dir, "testing") write_file(file, "my data") expect do @cache.read_file(file) do |data| raise ArgumentError, "testing error" end end.to raise_error(ArgumentError, "testing error") end end end end end hiera-1.3.0/spec/unit/backend/0000755000004100000410000000000012243500207016126 5ustar www-datawww-datahiera-1.3.0/spec/unit/backend/json_backend_spec.rb0000644000004100000410000000745612243500207022121 0ustar www-datawww-datarequire 'spec_helper' require 'hiera/backend/json_backend' class Hiera module Backend describe Json_backend do before do Hiera.stubs(:debug) Hiera.stubs(:warn) Hiera::Backend.stubs(:empty_answer).returns(nil) @cache = mock @backend = Json_backend.new(@cache) end describe "#initialize" do it "should announce its creation" do # because other specs checks this Hiera.expects(:debug).with("Hiera JSON backend starting") Json_backend.new end end describe "#lookup" do it "should look for data in all sources" do Backend.expects(:datasources).multiple_yields(["one"], ["two"]) Backend.expects(:datafile).with(:json, {}, "one", "json").returns(nil) Backend.expects(:datafile).with(:json, {}, "two", "json").returns(nil) @backend.lookup("key", {}, nil, :priority) end it "should retain the data types found in data files" do Backend.expects(:datasources).yields("one").times(3) Backend.expects(:datafile).with(:json, {}, "one", "json").returns("/nonexisting/one.json").times(3) File.stubs(:exist?).with("/nonexisting/one.json").returns(true) @cache.expects(:read_file).with("/nonexisting/one.json", Hash).returns({"stringval" => "string", "boolval" => true, "numericval" => 1}).times(3) @backend.lookup("stringval", {}, nil, :priority).should == "string" @backend.lookup("boolval", {}, nil, :priority).should == true @backend.lookup("numericval", {}, nil, :priority).should == 1 end it "should pick data earliest source that has it for priority searches" do scope = {"rspec" => "test"} Backend.expects(:datasources).multiple_yields(["one"], ["two"]) Backend.expects(:datafile).with(:json, scope, "one", "json").returns("/nonexisting/one.json") Backend.expects(:datafile).with(:json, scope, "two", "json").never File.stubs(:exist?).with("/nonexisting/one.json").returns(true) @cache.expects(:read_file).with("/nonexisting/one.json", Hash).returns({"key" => "test_%{rspec}"}) @backend.lookup("key", scope, nil, :priority).should == "test_test" end it "should build an array of all data sources for array searches" do Hiera::Backend.stubs(:empty_answer).returns([]) Backend.stubs(:parse_answer).with('answer', {}).returns("answer") Backend.expects(:datafile).with(:json, {}, "one", "json").returns("/nonexisting/one.json") Backend.expects(:datafile).with(:json, {}, "two", "json").returns("/nonexisting/two.json") Backend.expects(:datasources).multiple_yields(["one"], ["two"]) File.expects(:exist?).with("/nonexisting/one.json").returns(true) File.expects(:exist?).with("/nonexisting/two.json").returns(true) @cache.expects(:read_file).with("/nonexisting/one.json", Hash).returns({"key" => "answer"}) @cache.expects(:read_file).with("/nonexisting/two.json", Hash).returns({"key" => "answer"}) @backend.lookup("key", {}, nil, :array).should == ["answer", "answer"] end it "should parse the answer for scope variables" do Backend.stubs(:parse_answer).with('test_%{rspec}', {'rspec' => 'test'}).returns("test_test") Backend.expects(:datasources).yields("one") Backend.expects(:datafile).with(:json, {"rspec" => "test"}, "one", "json").returns("/nonexisting/one.json") File.expects(:exist?).with("/nonexisting/one.json").returns(true) @cache.expects(:read_file).with("/nonexisting/one.json", Hash).returns({"key" => "test_%{rspec}"}) @backend.lookup("key", {"rspec" => "test"}, nil, :priority).should == "test_test" end end end end end hiera-1.3.0/spec/unit/backend/yaml_backend_spec.rb0000644000004100000410000001702412243500207022102 0ustar www-datawww-datarequire 'spec_helper' require 'hiera/backend/yaml_backend' class Hiera module Backend describe Yaml_backend do before do Config.load({}) Hiera.stubs(:debug) Hiera.stubs(:warn) @cache = mock @backend = Yaml_backend.new(@cache) end describe "#initialize" do it "should announce its creation" do # because other specs checks this Hiera.expects(:debug).with("Hiera YAML backend starting") Yaml_backend.new end end describe "#lookup" do it "should look for data in all sources" do Backend.expects(:datasources).multiple_yields(["one"], ["two"]) Backend.expects(:datafile).with(:yaml, {}, "one", "yaml").returns(nil) Backend.expects(:datafile).with(:yaml, {}, "two", "yaml").returns(nil) @backend.lookup("key", {}, nil, :priority) end it "should pick data earliest source that has it for priority searches" do Backend.expects(:datasources).multiple_yields(["one"], ["two"]) Backend.expects(:datafile).with(:yaml, {}, "one", "yaml").returns("/nonexisting/one.yaml") Backend.expects(:datafile).with(:yaml, {}, "two", "yaml").returns(nil).never @cache.expects(:read_file).with("/nonexisting/one.yaml", Hash).returns({"key"=>"answer"}) File.stubs(:exist?).with("/nonexisting/one.yaml").returns(true) @backend.lookup("key", {}, nil, :priority).should == "answer" end it "should not look up missing data files" do Backend.expects(:datasources).multiple_yields(["one"]) Backend.expects(:datafile).with(:yaml, {}, "one", "yaml").returns(nil) YAML.expects(:load_file).never @backend.lookup("key", {}, nil, :priority) end it "should return nil for empty data files" do Backend.expects(:datasources).multiple_yields(["one"]) Backend.expects(:datafile).with(:yaml, {}, "one", "yaml").returns("/nonexisting/one.yaml") File.stubs(:exist?).with("/nonexisting/one.yaml").returns(true) @cache.expects(:read_file).with("/nonexisting/one.yaml", Hash).returns({}) @backend.lookup("key", {}, nil, :priority).should be_nil end it "should build an array of all data sources for array searches" do Backend.expects(:datasources).multiple_yields(["one"], ["two"]) Backend.expects(:datafile).with(:yaml, {}, "one", "yaml").returns("/nonexisting/one.yaml") Backend.expects(:datafile).with(:yaml, {}, "two", "yaml").returns("/nonexisting/two.yaml") File.stubs(:exist?).with("/nonexisting/one.yaml").returns(true) File.stubs(:exist?).with("/nonexisting/two.yaml").returns(true) @cache.expects(:read_file).with("/nonexisting/one.yaml", Hash).returns({"key"=>"answer"}) @cache.expects(:read_file).with("/nonexisting/two.yaml", Hash).returns({"key"=>"answer"}) @backend.lookup("key", {}, nil, :array).should == ["answer", "answer"] end it "should ignore empty hash of data sources for hash searches" do Backend.expects(:datasources).multiple_yields(["one"], ["two"]) Backend.expects(:datafile).with(:yaml, {}, "one", "yaml").returns("/nonexisting/one.yaml") Backend.expects(:datafile).with(:yaml, {}, "two", "yaml").returns("/nonexisting/two.yaml") File.stubs(:exist?).with("/nonexisting/one.yaml").returns(true) File.stubs(:exist?).with("/nonexisting/two.yaml").returns(true) @cache.expects(:read_file).with("/nonexisting/one.yaml", Hash).returns({}) @cache.expects(:read_file).with("/nonexisting/two.yaml", Hash).returns({"key"=>{"a"=>"answer"}}) @backend.lookup("key", {}, nil, :hash).should == {"a" => "answer"} end it "should build a merged hash of data sources for hash searches" do Backend.expects(:datasources).multiple_yields(["one"], ["two"]) Backend.expects(:datafile).with(:yaml, {}, "one", "yaml").returns("/nonexisting/one.yaml") Backend.expects(:datafile).with(:yaml, {}, "two", "yaml").returns("/nonexisting/two.yaml") File.stubs(:exist?).with("/nonexisting/one.yaml").returns(true) File.stubs(:exist?).with("/nonexisting/two.yaml").returns(true) @cache.expects(:read_file).with("/nonexisting/one.yaml", Hash).returns({"key"=>{"a"=>"answer"}}) @cache.expects(:read_file).with("/nonexisting/two.yaml", Hash).returns({"key"=>{"b"=>"answer", "a"=>"wrong"}}) @backend.lookup("key", {}, nil, :hash).should == {"a" => "answer", "b" => "answer"} end it "should fail when trying to << a Hash" do Backend.expects(:datasources).multiple_yields(["one"], ["two"]) Backend.expects(:datafile).with(:yaml, {}, "one", "yaml").returns("/nonexisting/one.yaml") Backend.expects(:datafile).with(:yaml, {}, "two", "yaml").returns("/nonexisting/two.yaml") File.stubs(:exist?).with("/nonexisting/one.yaml").returns(true) File.stubs(:exist?).with("/nonexisting/two.yaml").returns(true) @cache.expects(:read_file).with("/nonexisting/one.yaml", Hash).returns({"key"=>["a", "answer"]}) @cache.expects(:read_file).with("/nonexisting/two.yaml", Hash).returns({"key"=>{"a"=>"answer"}}) expect {@backend.lookup("key", {}, nil, :array)}.to raise_error(Exception, "Hiera type mismatch: expected Array and got Hash") end it "should fail when trying to merge an Array" do Backend.expects(:datasources).multiple_yields(["one"], ["two"]) Backend.expects(:datafile).with(:yaml, {}, "one", "yaml").returns("/nonexisting/one.yaml") Backend.expects(:datafile).with(:yaml, {}, "two", "yaml").returns("/nonexisting/two.yaml") File.stubs(:exist?).with("/nonexisting/one.yaml").returns(true) File.stubs(:exist?).with("/nonexisting/two.yaml").returns(true) @cache.expects(:read_file).with("/nonexisting/one.yaml", Hash).returns({"key"=>{"a"=>"answer"}}) @cache.expects(:read_file).with("/nonexisting/two.yaml", Hash).returns({"key"=>["a", "wrong"]}) expect { @backend.lookup("key", {}, nil, :hash) }.to raise_error(Exception, "Hiera type mismatch: expected Hash and got Array") end it "should parse the answer for scope variables" do Backend.expects(:datasources).yields("one") Backend.expects(:datafile).with(:yaml, {"rspec" => "test"}, "one", "yaml").returns("/nonexisting/one.yaml") File.stubs(:exist?).with("/nonexisting/one.yaml").returns(true) @cache.expects(:read_file).with("/nonexisting/one.yaml", Hash).returns({"key"=>"test_%{rspec}"}) @backend.lookup("key", {"rspec" => "test"}, nil, :priority).should == "test_test" end it "should retain datatypes found in yaml files" do Backend.expects(:datasources).yields("one").times(3) Backend.expects(:datafile).with(:yaml, {}, "one", "yaml").returns("/nonexisting/one.yaml").times(3) File.stubs(:exist?).with("/nonexisting/one.yaml").returns(true) yaml = "---\nstringval: 'string'\nboolval: true\nnumericval: 1" @cache.expects(:read_file).with("/nonexisting/one.yaml", Hash).times(3).returns({"boolval"=>true, "numericval"=>1, "stringval"=>"string"}) @backend.lookup("stringval", {}, nil, :priority).should == "string" @backend.lookup("boolval", {}, nil, :priority).should == true @backend.lookup("numericval", {}, nil, :priority).should == 1 end end end end end hiera-1.3.0/lib/0000755000004100000410000000000012243500207013374 5ustar www-datawww-datahiera-1.3.0/lib/hiera/0000755000004100000410000000000012243500207014464 5ustar www-datawww-datahiera-1.3.0/lib/hiera/fallback_logger.rb0000644000004100000410000000213012243500207020103 0ustar www-datawww-data# Select from a given list of loggers the first one that # it suitable and use that as the actual logger # # @api private class Hiera::FallbackLogger # Chooses the first suitable logger. For all of the loggers that are # unsuitable it will issue a warning using the suitable logger stating that # the unsuitable logger is not being used. # # @param implementations [Array] the implementations to choose from # @raises when there are no suitable loggers def initialize(*implementations) warnings = [] @implementation = implementations.find do |impl| if impl.respond_to?(:suitable?) if impl.suitable? true else warnings << "Not using #{impl.name}. It does not report itself to be suitable." false end else true end end if @implementation.nil? raise "No suitable logging implementation found." end warnings.each { |message| warn(message) } end def warn(message) @implementation.warn(message) end def debug(message) @implementation.debug(message) end end hiera-1.3.0/lib/hiera/filecache.rb0000644000004100000410000000514612243500207016722 0ustar www-datawww-dataclass Hiera class Filecache def initialize @cache = {} end # Reads a file, optionally parse it in some way check the # output type and set a default # # Simply invoking it this way will return the file contents # # data = read("/some/file") # # But as most cases of file reading in hiera involves some kind # of parsing through a serializer there's some help for those # cases: # # data = read("/some/file", Hash, {}) do |data| # JSON.parse(data) # end # # In this case it will read the file, parse it using JSON then # check that the end result is a Hash, if it's not a hash or if # reading/parsing fails it will return {} instead # # Prior to calling this method you should be sure the file exist def read(path, expected_type = Object, default=nil, &block) read_file(path, expected_type, &block) rescue TypeError => detail Hiera.debug("#{detail.message}, setting defaults") @cache[path][:data] = default rescue => detail error = "Reading data from #{path} failed: #{detail.class}: #{detail}" if default.nil? raise detail else Hiera.debug(error) @cache[path][:data] = default end end # Read a file when it changes. If a file is re-read and has not changed since the last time # then the last, processed, contents will be returned. # # The processed data can also be checked against an expected type. If the # type does not match a TypeError is raised. # # No error handling is done inside this method. Any failed reads or errors # in processing will be propagated to the caller def read_file(path, expected_type = Object) if stale?(path) data = File.read(path) @cache[path][:data] = block_given? ? yield(data) : data if !@cache[path][:data].is_a?(expected_type) raise TypeError, "Data retrieved from #{path} is #{data.class} not #{expected_type}" end end @cache[path][:data] end private def stale?(path) meta = path_metadata(path) @cache[path] ||= {:data => nil, :meta => nil} if @cache[path][:meta] == meta return false else @cache[path][:meta] = meta return true end end # This is based on the old caching in the YAML backend and has a # resolution of 1 second, changes made within the same second of # a previous read will be ignored def path_metadata(path) stat = File.stat(path) {:inode => stat.ino, :mtime => stat.mtime, :size => stat.size} end end end hiera-1.3.0/lib/hiera/interpolate.rb0000644000004100000410000000301612243500207017337 0ustar www-datawww-datarequire 'hiera/backend' class Hiera::Interpolate class << self INTERPOLATION = /%\{([^\}]*)\}/ METHOD_INTERPOLATION = /%\{(scope|hiera)\(['"]([^"']*)["']\)\}/ def interpolate(data, recurse_guard, scope, extra_data) if data.is_a?(String) && (match = data.match(INTERPOLATION)) interpolation_variable = match[1] recurse_guard.check(interpolation_variable) do interpolate_method, key = get_interpolation_method_and_key(data) interpolated_data = send(interpolate_method, data, key, scope, extra_data) interpolate(interpolated_data, recurse_guard, scope, extra_data) end else data end end def get_interpolation_method_and_key(data) if (match = data.match(METHOD_INTERPOLATION)) case match[1] when 'hiera' then [:hiera_interpolate, match[2]] when 'scope' then [:scope_interpolate, match[2]] end elsif (match = data.match(INTERPOLATION)) [:scope_interpolate, match[1]] end end private :get_interpolation_method_and_key def scope_interpolate(data, key, scope, extra_data) value = scope[key] if value.nil? || value == :undefined value = extra_data[key] end data.sub(INTERPOLATION, value.to_s) end private :scope_interpolate def hiera_interpolate(data, key, scope, extra_data) value = Hiera::Backend.lookup(key, nil, scope, nil, :priority) data.sub(METHOD_INTERPOLATION, value) end private :hiera_interpolate end end hiera-1.3.0/lib/hiera/recursive_guard.rb0000644000004100000410000000064412243500207020206 0ustar www-datawww-data# Allow for safe recursive lookup of values during variable interpolation. # # @api private class Hiera::InterpolationLoop < StandardError; end class Hiera::RecursiveGuard def initialize @seen = [] end def check(value, &block) if @seen.include?(value) raise Hiera::InterpolationLoop, "Detected in [#{@seen.join(', ')}]" end @seen.push(value) ret = yield @seen.pop ret end end hiera-1.3.0/lib/hiera/console_logger.rb0000644000004100000410000000037512243500207020017 0ustar www-datawww-dataclass Hiera module Console_logger class << self def warn(msg) STDERR.puts("WARN: %s: %s" % [Time.now.to_s, msg]) end def debug(msg) STDERR.puts("DEBUG: %s: %s" % [Time.now.to_s, msg]) end end end end hiera-1.3.0/lib/hiera/noop_logger.rb0000644000004100000410000000016612243500207017326 0ustar www-datawww-dataclass Hiera module Noop_logger class << self def warn(msg);end def debug(msg);end end end end hiera-1.3.0/lib/hiera/version.rb0000644000004100000410000000677112243500207016511 0ustar www-datawww-data# The version method and constant are isolated in hiera/version.rb so that a # simple `require 'hiera/version'` allows a rubygems gemspec or bundler # Gemfile to get the hiera version of the gem install. # # The version is programatically settable because we want to allow the # Raketasks and such to set the version based on the output of `git describe` class Hiera VERSION = "1.3.0" ## # version is a public API method intended to always provide a fast and # lightweight way to determine the version of hiera. # # The intent is that software external to hiera be able to determine the # hiera version with no side-effects. The expected use is: # # require 'hiera/version' # version = Hiera.version # # This function has the following ordering precedence. This precedence list # is designed to facilitate automated packaging tasks by simply writing to # the VERSION file in the same directory as this source file. # # 1. If a version has been explicitly assigned using the Hiera.version= # method, return that version. # 2. If there is a VERSION file, read the contents, trim any # trailing whitespace, and return that version string. # 3. Return the value of the Hiera::VERSION constant hard-coded into # the source code. # # If there is no VERSION file, the method must return the version string of # the nearest parent version that is an officially released version. That is # to say, if a branch named 3.1.x contains 25 patches on top of the most # recent official release of 3.1.1, then the version method must return the # string "3.1.1" if no "VERSION" file is present. # # By design the version identifier is _not_ intended to vary during the life # a process. There is no guarantee provided that writing to the VERSION file # while a Hiera process is running will cause the version string to be # updated. On the contrary, the contents of the VERSION are cached to reduce # filesystem accesses. # # The VERSION file is intended to be used by package maintainers who may be # applying patches or otherwise changing the software version in a manner # that warrants a different software version identifier. The VERSION file is # intended to be managed and owned by the release process and packaging # related tasks, and as such should not reside in version control. The # VERSION constant is intended to be version controlled in history. # # Ideally, this behavior will allow package maintainers to precisely specify # the version of the software they're packaging as in the following example: # # $ git describe --match "1.2.*" > lib/hiera/VERSION # $ ruby -r hiera/version -e 'puts Hiera.version' # 1.2.1-9-g9fda440 # # @api public # # @return [String] containing the hiera version, e.g. "1.2.1" def self.version version_file = File.join(File.dirname(__FILE__), 'VERSION') return @hiera_version if @hiera_version if version = read_version_file(version_file) @hiera_version = version end @hiera_version ||= VERSION end def self.version=(version) @hiera_version = version end ## # read_version_file reads the content of the "VERSION" file that lives in the # same directory as this source code file. # # @api private # # @return [String] for example: "1.6.14-6-gea42046" or nil if the VERSION # file does not exist. def self.read_version_file(path) if File.exists?(path) File.read(path).chomp end end private_class_method :read_version_file end hiera-1.3.0/lib/hiera/config.rb0000644000004100000410000000473212243500207016264 0ustar www-datawww-dataclass Hiera::Config class << self ## # load takes a string or hash as input, strings are treated as filenames # hashes are stored as data that would have been in the config file # # Unless specified it will only use YAML as backend with a single # 'common' hierarchy and console logger # # @return [Hash] representing the configuration. e.g. # {:backends => "yaml", :hierarchy => "common"} def load(source) @config = {:backends => "yaml", :hierarchy => "common", :merge_behavior => :native } if source.is_a?(String) if File.exist?(source) config = begin yaml_load_file(source) rescue TypeError => detail case detail.message when /no implicit conversion from nil to integer/ false else raise detail end end @config.merge! config if config else raise "Config file #{source} not found" end elsif source.is_a?(Hash) @config.merge! source end @config[:backends] = [ @config[:backends] ].flatten if @config.include?(:logger) Hiera.logger = @config[:logger].to_s else @config[:logger] = "console" Hiera.logger = "console" end self.validate! @config end def validate! case @config[:merge_behavior] when :deep,'deep',:deeper,'deeper' begin require "deep_merge" rescue LoadError Hiera.warn "Ignoring configured merge_behavior" Hiera.warn "Must have 'deep_merge' gem installed." @config[:merge_behavior] = :native end end end ## # yaml_load_file directly delegates to YAML.load_file and is intended to be # a private, internal method suitable for stubbing and mocking. # # @return [Object] return value of {YAML.load_file} def yaml_load_file(source) YAML.load_file(source) end private :yaml_load_file def load_backends @config[:backends].each do |backend| begin require "hiera/backend/#{backend.downcase}_backend" rescue LoadError => e Hiera.warn "Cannot load backend #{backend}: #{e}" end end end def include?(key) @config.include?(key) end def [](key) @config[key] end end end hiera-1.3.0/lib/hiera/puppet_logger.rb0000644000004100000410000000043612243500207017670 0ustar www-datawww-dataclass Hiera module Puppet_logger class << self def suitable? defined?(::Puppet) == "constant" end def warn(msg) Puppet.notice("hiera(): #{msg}") end def debug(msg) Puppet.debug("hiera(): #{msg}") end end end end hiera-1.3.0/lib/hiera/backend.rb0000644000004100000410000001566112243500207016411 0ustar www-datawww-datarequire 'hiera/util' require 'hiera/recursive_guard' 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) file = File.join([datadir(backend, scope), "#{source}.#{extension}"]) unless File.exist?(file) Hiera.debug("Cannot find datafile #{file}, skipping") return nil end return file 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 # 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, Hiera::RecursiveGuard.new, 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 hiera-1.3.0/lib/hiera/backend/0000755000004100000410000000000012243500207016053 5ustar www-datawww-datahiera-1.3.0/lib/hiera/backend/json_backend.rb0000644000004100000410000000330512243500207021021 0ustar www-datawww-dataclass Hiera module Backend class Json_backend def initialize(cache=nil) require 'json' Hiera.debug("Hiera JSON backend starting") @cache = cache || Filecache.new end def lookup(key, scope, order_override, resolution_type) answer = nil Hiera.debug("Looking up #{key} in JSON backend") Backend.datasources(scope, order_override) do |source| Hiera.debug("Looking for data source #{source}") jsonfile = Backend.datafile(:json, scope, source, "json") || next next unless File.exist?(jsonfile) data = @cache.read_file(jsonfile, Hash) do |data| JSON.parse(data) end next if data.empty? next unless data.include?(key) # for array resolution we just append to the array whatever # we find, we then goes onto the next file and keep adding to # the array # # for priority searches we break after the first found data item new_answer = Backend.parse_answer(data[key], scope) 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 = Backend.merge_answer(new_answer,answer) else answer = new_answer break end end return answer end end end end hiera-1.3.0/lib/hiera/backend/yaml_backend.rb0000644000004100000410000000400212243500207021005 0ustar www-datawww-dataclass Hiera module Backend class Yaml_backend def initialize(cache=nil) require 'yaml' Hiera.debug("Hiera YAML backend starting") @cache = cache || Filecache.new end def lookup(key, scope, order_override, resolution_type) answer = nil Hiera.debug("Looking up #{key} in YAML backend") Backend.datasources(scope, order_override) do |source| Hiera.debug("Looking for data source #{source}") yamlfile = Backend.datafile(:yaml, scope, source, "yaml") || next next unless File.exist?(yamlfile) data = @cache.read_file(yamlfile, Hash) do |data| YAML.load(data) end next if data.empty? next unless data.include?(key) # Extra logging that we found the key. This can be outputted # multiple times if the resolution type is array or hash but that # should be expected as the logging will then tell the user ALL the # places where the key is found. Hiera.debug("Found #{key} in #{source}") # for array resolution we just append to the array whatever # we find, we then goes onto the next file and keep adding to # the array # # for priority searches we break after the first found data item new_answer = Backend.parse_answer(data[key], scope) 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 = Backend.merge_answer(new_answer,answer) else answer = new_answer break end end return answer end end end end hiera-1.3.0/lib/hiera/error.rb0000644000004100000410000000014212243500207016137 0ustar www-datawww-dataclass Hiera class Error < StandardError; end class InvalidConfigurationError < Error; end end hiera-1.3.0/lib/hiera/util.rb0000644000004100000410000000151612243500207015771 0ustar www-datawww-dataclass Hiera module Util module_function def posix? require 'etc' Etc.getpwuid(0) != nil end def microsoft_windows? return false unless file_alt_separator begin require 'win32/dir' true rescue LoadError => err warn "Cannot run on Microsoft Windows without the win32-dir gem: #{err}" false end end def config_dir if microsoft_windows? File.join(common_appdata, 'PuppetLabs', 'hiera', 'etc') else '/etc' end end def var_dir if microsoft_windows? File.join(common_appdata, 'PuppetLabs', 'hiera', 'var') else '/var/lib/hiera' end end def file_alt_separator File::ALT_SEPARATOR end def common_appdata Dir::COMMON_APPDATA end end end hiera-1.3.0/lib/hiera.rb0000644000004100000410000000355512243500207015021 0ustar www-datawww-datarequire 'yaml' class Hiera require "hiera/error" require "hiera/version" require "hiera/config" require "hiera/util" require "hiera/backend" require "hiera/console_logger" require "hiera/puppet_logger" require "hiera/noop_logger" require "hiera/fallback_logger" require "hiera/filecache" class << self attr_reader :logger # Loggers are pluggable, just provide a class called # Hiera::Foo_logger and respond to :warn and :debug # # See hiera-puppet for an example that uses the Puppet # loging system instead of our own def logger=(logger) require "hiera/#{logger}_logger" @logger = Hiera::FallbackLogger.new( Hiera.const_get("#{logger.capitalize}_logger"), Hiera::Console_logger) rescue Exception => e @logger = Hiera::Console_logger warn("Failed to load #{logger} logger: #{e.class}: #{e}") end def warn(msg); @logger.warn(msg); end def debug(msg); @logger.debug(msg); end end attr_reader :options, :config # If the config option is a string its assumed to be a filename, # else a hash of what would have been in the YAML config file def initialize(options={}) options[:config] ||= File.join(Util.config_dir, 'hiera.yaml') @config = Config.load(options[:config]) Config.load_backends end # Calls the backends to do the actual lookup. # # The scope can be anything that responds to [], if you have input # data like a Puppet Scope that does not you can wrap that data in a # class that has a [] method that fetches the data from your source. # See hiera-puppet for an example of this. # # The order-override will insert as first in the hierarchy a data source # of your choice. def lookup(key, default, scope, order_override=nil, resolution_type=:priority) Backend.lookup(key, default, scope, order_override, resolution_type) end end hiera-1.3.0/metadata.yml0000644000004100000410000000476612243500207015146 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: hiera version: !ruby/object:Gem::Version version: 1.3.0 prerelease: platform: ruby authors: - Puppet Labs autorequire: bindir: bin cert_chain: [] date: 2013-11-21 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: json_pure requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' description: A pluggable data store for hierarcical data email: info@puppetlabs.com executables: - hiera extensions: [] extra_rdoc_files: [] files: - bin/hiera - lib/hiera/puppet_logger.rb - lib/hiera/console_logger.rb - lib/hiera/filecache.rb - lib/hiera/version.rb - lib/hiera/fallback_logger.rb - lib/hiera/error.rb - lib/hiera/interpolate.rb - lib/hiera/util.rb - lib/hiera/recursive_guard.rb - lib/hiera/backend.rb - lib/hiera/noop_logger.rb - lib/hiera/config.rb - lib/hiera/backend/yaml_backend.rb - lib/hiera/backend/json_backend.rb - lib/hiera.rb - COPYING - README.md - LICENSE - spec/unit/util_spec.rb - spec/unit/puppet_logger_spec.rb - spec/unit/version_spec.rb - spec/unit/config_spec.rb - spec/unit/backend_spec.rb - spec/unit/filecache_spec.rb - spec/unit/hiera_spec.rb - spec/unit/console_logger_spec.rb - spec/unit/backend/yaml_backend_spec.rb - spec/unit/backend/json_backend_spec.rb - spec/unit/fallback_logger_spec.rb - spec/spec_helper.rb homepage: https://github.com/puppetlabs/hiera licenses: [] post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 1.8.23 signing_key: specification_version: 3 summary: Light weight hierarchical data store test_files: - spec/unit/util_spec.rb - spec/unit/puppet_logger_spec.rb - spec/unit/version_spec.rb - spec/unit/config_spec.rb - spec/unit/backend_spec.rb - spec/unit/filecache_spec.rb - spec/unit/hiera_spec.rb - spec/unit/console_logger_spec.rb - spec/unit/backend/yaml_backend_spec.rb - spec/unit/backend/json_backend_spec.rb - spec/unit/fallback_logger_spec.rb - spec/spec_helper.rb hiera-1.3.0/LICENSE0000644000004100000410000000122512243500207013633 0ustar www-datawww-dataPuppet - Automating Configuration Management. Copyright (C) 2012 Puppet Labs Inc Puppet Labs can be contacted at: info@puppetlabs.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. hiera-1.3.0/README.md0000644000004100000410000002001512243500207014103 0ustar www-datawww-data# Hiera [![Build Status](https://travis-ci.org/puppetlabs/hiera.png?branch=master)](https://travis-ci.org/puppetlabs/hiera) A simple pluggable Hierarchical Database. - **Tutorials:** Check the docs directory for tutorials. ## Why? Hierarchical data is a good fit for the representation of infrastructure information. Consider the example of a typical company with 2 datacenters and on-site development, staging etc. All machines need: - ntp servers - sysadmin contacts By thinking about the data in a hierarchical manner you can resolve these to the most correct answer easily:
     /------------- DC1 -------------\             /------------- DC2 -------------\
    | ntpserver: ntp1.dc1.example.com |           | ntpserver: ntp1.dc2.example.com |
    | sysadmin: dc1noc@example.com    |           |                                 |
    | classes: users::dc1             |           | classes: users::dc2             |
     \-------------------------------/             \-------------------------------/
                                \                      /
                                  \                  /
                           /------------- COMMON -------------\
                          | ntpserver: 1.pool.ntp.org          |
                          | sysadmin: "sysadmin@%{domain}"     |
                          | classes: users::common             |
                           \----------------------------------/
In this simple example machines in DC1 and DC2 have their own NTP servers, additionaly DC1 has its own sysadmin contact - perhaps because its a remote DR site - while DC2 and all the other environments would revert to the common contact that would have the machines domain fact expanded into the result. The _classes_ variable can be searched using the array method which would build up a list of classes to include on a node based on the hierarchy. Machines in DC1 would have the classes _users::common_ and _users::dc1_. The other environment like development and staging would all use the public NTP infrastructure. This is the data model that extlookup() have promoted in Puppet, Hiera has taken this data model and extracted it into a standalone project that is pluggable and have a few refinements over extlookup. ## Enhancements over Extlookup Extlookup had just one backend, Hiera can be extended with your own backends and represent a few enhancements over the base Extlookup approach thanks to this. ### Multiple backends are queried If you have a YAML and Puppet backend loaded and your users provide module defaults in the Puppet backend you can use your YAML data to override the Puppet data. If the YAML doesnt provide an answer the Puppet backend will get an opportunity to provide an answer. ### More scope based variable expansion Extlookup could parse data like %{foo} into a scope lookup for the variable foo. Hiera retains this ability and any Arrays or Hashes will be recursively searched for all strings that will then be parsed. The datadir and defaults are now also subject to variable parsing based on scope. ### No CSV support by default We have not at present provided a backward compatible CSV backend. A converter to YAML or JSON should be written. When the CSV backend was first chosen for Puppet the Puppet language only supports strings and arrays of strings which mapped well to CSV. Puppet has become (a bit) better wrt data and can now handle hashes and arrays of hashes so it's a good time to retire the old data format. ### Array Searches Hiera can search through all the tiers in a hierarchy and merge the result into a single array. This is used in the hiera-puppet project to replace External Node Classifiers by creating a Hiera compatible include function. ## Future Enhancements * More backends should be created * A webservice that exposes the data * Tools to help maintain the data files. Ideally this would be Foreman and Dashboard with their own backends ## Installation Hiera is available as a Gem called _hiera_ and out of the box it comes with just a single YAML backend. Hiera is also available as a native package via apt (http://apt.puppetlabs.com) and yum (http://yum.puppetlabs.com). Instructions for adding these repositories can be found at http://docs.puppetlabs.com/guides/installation.html#debian-and-ubuntu and http://docs.puppetlabs.com/guides/installation.html#enterprise-linux respectively. At present JSON (github/ripienaar/hiera-json) and Puppet (hiera-puppet) backends are availble. ## Configuration You can configure Hiera using a YAML file or by providing it Hash data in your code. There isn't a default config path - the CLI script will probably assume _/etc/hiera.yaml_ though. The default data directory for file based storage is _/var/lib/hiera_. A sample configuration file can be seen here:
---
:backends:
  - yaml
  - puppet

:logger: console

:hierarchy:
  - "sites/%{location}"
  - common

:yaml:
   :datadir: /etc/puppet/hieradata

:puppet:
   :datasource: data
This configuration will require YAML files in _/etc/puppet/hieradata_ these need to contain Hash data, sample files matching the hierarchy described in the _Why?_ section are below: _/etc/puppet/hieradata/sites/dc1.yaml_:
---
ntpserver: ntp1.dc1.example.com
sysadmin: dc1noc@example.com
_/etc/puppet/hieradata/sites/dc2.yaml_:
---
ntpserver: ntp1.dc2.example.com
_/etc/puppet/hieradata/common.yaml_:
---
sysadmin: "sysadmin@%{domain}"
ntpserver: 1.pool.ntp.org
## Querying from CLI You can query your data from the CLI. By default the CLI expects a config file in _/etc/hiera.yaml_ but you can pass _--config_ to override that. This example searches Hiera for node data. Scope is loaded from a Puppet created YAML facts store as found on your Puppet Masters. If no data is found and the facts had a location=dc1 fact the default would be _sites/dc1_
$ hiera acme_version 'sites/%{location}' --yaml /var/lib/puppet/yaml/facts/example.com.yaml
You can also supply extra facts on the CLI, assuming Puppet facts did not have a location fact:
$ hiera acme_version 'sites/%{location}' location=dc1 --yaml /var/lib/puppet/yaml/facts/example.com.yaml
Or if you use MCollective you can fetch the scope from a remote node's facts:
$ hiera acme_version 'sites/%{location}' -m box.example.com
You can also do array merge searches on the CLI:
$ hiera -a classes location=dc1
["users::common", "users::dc1"]
## Querying from code This is the same query programatically as in the above CLI example:
require 'rubygems'
require 'hiera'
require 'puppet'

# load the facts for example.com
scope = YAML.load_file("/var/lib/puppet/yaml/facts/example.com.yaml").values

# create a new instance based on config file
hiera = Hiera.new(:config => "/etc/puppet/hiera.yaml")

# resolve the 'acme_version' variable based on scope
#
# given a fact location=dc1 in the facts file this will default to a branch sites/dc1
# and allow hierarchical overrides based on the hierarchy defined in the config file
puts "ACME Software Version: %s" % [ hiera.lookup("acme_version", "sites/%{location}", scope) ]
## Extending There exist 2 backends at present in addition to the bundled YAML one. ### JSON This can be found on github under _ripienaar/hiera-json_. This is a good example of file based backends as Hiera provides a number of helpers to make writing these trivial. ### Puppet This is much more complex and queries the data from the running Puppet state, it's found on GitHub under _ripienaar/hiera-puppet_. This is a good example to learn how to map your internal program state into what Hiera wants as I needed to do with the Puppet Scope. It includes a Puppet Parser Function to query the data from within Puppet. When used in Puppet you'd expect Hiera to log using the Puppet infrastructure, this plugin includes a Puppet Logger plugin for Hiera that uses the normal Puppet logging methods for all logging. ## License See LICENSE file. ## Support Please log tickets and issues at our [Projects site](http://projects.puppetlabs.com) hiera-1.3.0/COPYING0000644000004100000410000002614112243500207013665 0ustar www-datawww-data Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2010, 2011 R.I.Pienaar, Puppet Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.