hiera-2.0.0/0000755000175000017500000000000012517736341011620 5ustar jonasjonashiera-2.0.0/checksums.yaml.gz0000444000175000017500000000041412517736341015105 0ustar jonasjonasUeP9N@ | ǮNGG >hP*^φ,y.ox\||߯ڒev.].ƌ@lv< 9258S 67bxNrlթ|JS7ȝ5l@s=f!Pm&]dcEd 5e=tXKoK̞e}@} "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-2.0.0/spec/unit/console_logger_spec.rb0000644000175000017500000000070212517736341020070 0ustar jonasjonasrequire '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-2.0.0/spec/unit/backend_spec.rb0000644000175000017500000007765612517736341016504 0ustar jonasjonasrequire 'spec_helper' require 'hiera/util' class Hiera module Backend class Backend1x_backend def lookup(key, scope, order_override, resolution_type) ["a", "b"] end end end 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}, {}, {:order_override => nil}) 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" } @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 repeated occurances of the same lookup" do scope = {"rspec" => "value"} input = "it replaces %{rspec} and %{rspec}" Backend.parse_string(input, scope).should == "it replaces value and value" 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, instance_of(Hash)).returns("answer") Backend.parse_string(input, scope).should == "answer" end it "interpolation passes the order_override back into the backend" do Backend.expects(:lookup).with("lookup::key", nil, {}, "order_override_datasource", :priority, instance_of(Hash)) Backend.parse_string("%{hiera('lookup::key')}", {}, {}, {:order_override => "order_override_datasource"}) end it "replaces literal interpolations with their argument" do scope = {} input = "%{literal('%')}{rspec::data}" Backend.parse_string(input, scope).should == "%{rspec::data}" 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, instance_of(Hash)).returns("test") Backend.parse_answer(input, scope).should == "test_test_test" end it "interpolates alias lookups with non-string types" do input = "%{alias('rspec')}" scope = {} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority, instance_of(Hash)).returns(['test', 'test']) Backend.parse_answer(input, scope).should == ['test', 'test'] end it 'fails if alias interpolation is attempted in a string context with a prefix' do input = "stuff_before%{alias('rspec')}" scope = {} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority, instance_of(Hash)).returns(['test', 'test']) expect do Backend.parse_answer(input, scope).should == ['test', 'test'] end.to raise_error(Hiera::InterpolationInvalidValue, 'Cannot call alias in the string context') end it 'fails if alias interpolation is attempted in a string context with a postfix' do input = "%{alias('rspec')}_stiff after" scope = {} Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.stubs(:lookup).with("rspec", scope, nil, :priority, instance_of(Hash)).returns(['test', 'test']) expect do Backend.parse_answer(input, scope).should == ['test', 'test'] end.to raise_error(Hiera::InterpolationInvalidValue, 'Cannot call alias in the string context') 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, instance_of(Hash)).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, instance_of(Hash)).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, instance_of(Hash)).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, instance_of(Hash)).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, instance_of(Hash)).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, instance_of(Hash)).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, instance_of(Hash)).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, instance_of(Hash)).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, instance_of(Hash)).returns("string") Backend::Yaml_backend.any_instance.expects(:lookup).with("boolval", {}, nil, nil, instance_of(Hash)).returns(false) Backend::Yaml_backend.any_instance.expects(:lookup).with("numericval", {}, nil, nil, instance_of(Hash)).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, instance_of(Hash)).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, instance_of(Hash)).throws(:no_such_key) Backend.lookup("key", "test_%{rspec}", {"rspec" => "test"}, nil, nil).should == "test_test" end it "returns nil instead of the default when key is found with a nil value" do Config.load({:yaml => {:datadir => "/tmp"}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with("key", {"rspec" => "test"}, nil, nil, instance_of(Hash)).returns(nil) Backend.lookup("key", "test_%{rspec}", {"rspec" => "test"}, nil, nil).should == nil 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, instance_of(Hash)).throws(:no_such_key) 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, instance_of(Hash)).throws(:no_such_key) 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, instance_of(Hash)).throws(:no_such_key) Backend.lookup("key", {"test" => "value"}, {}, nil, :hash).should == {"test" => "value"} end it 'can use qualified key to lookup value in hash' do Config.load({:yaml => {:datadir => '/tmp'}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with('key', {}, nil, nil, instance_of(Hash)).returns({ 'test' => 'value'}) Backend.lookup('key.test', 'dflt', {}, nil, nil).should == 'value' end it 'can use qualified key to lookup value in array' do Config.load({:yaml => {:datadir => '/tmp'}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with('key', {}, nil, nil, instance_of(Hash)).returns([ 'first', 'second']) Backend.lookup('key.1', 'dflt', {}, nil, nil).should == 'second' end it 'will fail when qualified key is partially found but not expected hash' do Config.load({:yaml => {:datadir => '/tmp'}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with('key', {}, nil, nil, instance_of(Hash)).returns(['value 1', 'value 2']) expect do Backend.lookup('key.test', 'dflt', {}, nil, nil) end.to raise_error(Exception, /^Hiera type mismatch:/) end it 'will fail when qualified key used with resolution_type :hash' do expect do Backend.lookup('key.test', 'dflt', {}, nil, :hash) end.to raise_error(ArgumentError, /^Resolution type :hash is illegal/) end it 'will fail when qualified key used with resolution_type :array' do expect do Backend.lookup('key.test', 'dflt', {}, nil, :array) end.to raise_error(ArgumentError, /^Resolution type :array is illegal/) end it 'will succeed when qualified key used with resolution_type :priority' do Config.load({:yaml => {:datadir => '/tmp'}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with('key', {}, nil, :priority, instance_of(Hash)).returns({ 'test' => 'value'}) Backend.lookup('key.test', 'dflt', {}, nil, :priority).should == 'value' end it 'will fail when qualified key is partially found but not expected array' do Config.load({:yaml => {:datadir => '/tmp'}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with('key', {}, nil, nil, instance_of(Hash)).returns({ 'test' => 'value'}) expect do Backend.lookup('key.2', 'dflt', {}, nil, nil) end.to raise_error(Exception, /^Hiera type mismatch:/) end it 'will not fail when qualified key is partially not found' do Config.load({:yaml => {:datadir => '/tmp'}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with('key', {}, nil, nil, instance_of(Hash)).returns(nil) Backend.lookup('key.test', 'dflt', {}, nil, nil).should == 'dflt' end it 'will not fail when qualified key is array index out of bounds' do Config.load({:yaml => {:datadir => '/tmp'}}) Config.load_backends Backend::Yaml_backend.any_instance.expects(:lookup).with('key', {}, nil, nil, instance_of(Hash)).returns(['value 1', 'value 2']) Backend.lookup('key.33', 'dflt', {}, nil, nil).should == 'dflt' end it 'can use qualified key in interpolation to lookup value in hash' do Config.load({:yaml => {:datadir => '/tmp'}}) Config.load_backends Hiera::Backend.stubs(:datasourcefiles).yields('foo', 'bar') Hiera::Filecache.any_instance.expects(:read_file).at_most(2).returns({'key' => '%{hiera(\'some.subkey\')}', 'some' => { 'subkey' => 'value' }}) Backend.lookup('key', 'dflt', {}, nil, nil).should == 'value' end it 'can use qualified key in interpolated default and scope' do Config.load({:yaml => {:datadir => '/tmp'}}) Config.load_backends scope = { 'some' => { 'test' => 'value'}} Backend::Yaml_backend.any_instance.expects(:lookup).with('key', scope, nil, nil, instance_of(Hash)) Backend.lookup('key.notfound', '%{some.test}', scope, nil, nil).should == 'value' end it "handles older backend with 4 argument lookup" do Config.load({}) Config.instance_variable_set("@config", {:backends => ["Backend1x"]}) Hiera.expects(:debug).at_least_once.with(regexp_matches /Using Hiera 1.x backend/) Backend.lookup("key", {}, {"rspec" => "test"}, nil, :priority).should == ["a", "b"] 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 it "disregards configuration when 'merge' parameter is given as a Hash" 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"}, {:behavior => 'deeper' }).should == {"a" => "answer", "b" => "bnswer"} end it "propagates deep merge options when given Hash 'merge' parameter" do Hash.any_instance.expects('deep_merge!').with({"b" => "bnswer"}, { :knockout_prefix => '-' }).returns({"a" => "answer", "b" => "bnswer"}) Backend.merge_answer({"a" => "answer"},{"b" => "bnswer"}, {:behavior => 'deeper', :knockout_prefix => '-'}).should == {"a" => "answer", "b" => "bnswer"} end it "passes Config[:deep_merge_options] into calls to deep_merge" do Config.load({:merge_behavior => :deep, :deep_merge_options => { :knockout_prefix => '-' } }) Hash.any_instance.expects('deep_merge').with({"b" => "bnswer"}, {:knockout_prefix => '-'}).returns({"a" => "answer", "b" => "bnswer"}) Backend.merge_answer({"a" => "answer"},{"b" => "bnswer"}).should == {"a" => "answer", "b" => "bnswer"} end end end end hiera-2.0.0/spec/unit/interpolate_spec.rb0000644000175000017500000000266512517736341017427 0ustar jonasjonasrequire 'spec_helper' require 'hiera/util' describe "Hiera" do context "when doing interpolation" do let(:fixtures) { File.join(HieraSpec::FIXTURE_DIR, 'interpolate') } it 'should prevent endless recursion' do Hiera::Util.expects(:var_dir).at_least_once.returns(File.join(fixtures, 'data')) hiera = Hiera.new(:config => File.join(fixtures, 'config', 'hiera.yaml')) expect do hiera.lookup('foo', nil, {}) end.to raise_error Hiera::InterpolationLoop, 'Detected in [hiera("bar"), hiera("foo")]' end end context "when not finding value for interpolated key" do let(:fixtures) { File.join(HieraSpec::FIXTURE_DIR, 'interpolate') } it 'should resolve the interpolation to an empty string' do Hiera::Util.expects(:var_dir).at_least_once.returns(File.join(fixtures, 'data')) hiera = Hiera.new(:config => File.join(fixtures, 'config', 'hiera.yaml')) expect(hiera.lookup('niltest', nil, {})).to eq('Missing key ##. Key with nil ##') end end context "when doing interpolation with override" do let(:fixtures) { File.join(HieraSpec::FIXTURE_DIR, 'override') } it 'should resolve interpolation using the override' do Hiera::Util.expects(:var_dir).at_least_once.returns(File.join(fixtures, 'data')) hiera = Hiera.new(:config => File.join(fixtures, 'config', 'hiera.yaml')) expect(hiera.lookup('foo', nil, {}, 'alternate')).to eq('alternate') end end end hiera-2.0.0/spec/unit/config_spec.rb0000644000175000017500000000767112517736341016350 0ustar jonasjonasrequire '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", :unless => Hiera::Util.microsoft_windows? 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 Config.load('/dev/null') end end describe "if deep_merge can't be loaded" do let(:error_message) { "Must have 'deep_merge' gem installed for the configured merge_behavior." } before(:each) do Config.expects(:require).with("deep_merge").raises(LoadError, "unable to load") end it "should error if merge_behavior is 'deep'" do expect { Config.load(:merge_behavior => :deep) }.to raise_error(Hiera::Error, error_message) end it "should error if merge_behavior is 'deeper'" do expect { Config.load(:merge_behavior => :deeper) }.to raise_error(Hiera::Error, error_message) 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-2.0.0/spec/unit/util_spec.rb0000644000175000017500000000321212517736341016043 0ustar jonasjonasrequire '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/puppetlabs/code' 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/code' 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 == '/etc/puppetlabs/code/hieradata' 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/code/hieradata' end end end hiera-2.0.0/spec/unit/fallback_logger_spec.rb0000644000175000017500000000361612517736341020174 0ustar jonasjonasrequire '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-2.0.0/spec/unit/backend/0000755000175000017500000000000012517736341015120 5ustar jonasjonashiera-2.0.0/spec/unit/backend/json_backend_spec.rb0000644000175000017500000000762612517736341021112 0ustar jonasjonasrequire '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) expect { @backend.lookup("key", {}, nil, :priority, nil) }.to throw_symbol(:no_such_key) 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, nil).should == "string" @backend.lookup("boolval", {}, nil, :priority, nil).should == true @backend.lookup("numericval", {}, nil, :priority, nil).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, nil).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', {}, {}, anything).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, nil).should == ["answer", "answer"] end it "should parse the answer for scope variables" do Backend.stubs(:parse_answer).with('test_%{rspec}', {'rspec' => 'test'}, {}, anything).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, nil).should == "test_test" end end end end end hiera-2.0.0/spec/unit/backend/yaml_backend_spec.rb0000644000175000017500000001377412517736341021104 0ustar jonasjonasrequire 'spec_helper' require 'hiera/backend/yaml_backend' class Hiera module Backend class FakeCache attr_accessor :value def read(path, expected_type, default, &block) read_file(path, expected_type, &block) rescue => e default end def read_file(path, expected_type, &block) output = block.call(@value) if !output.is_a? expected_type raise TypeError end output end end describe Yaml_backend do before do Config.load({}) Hiera.stubs(:debug) Hiera.stubs(:warn) @cache = FakeCache.new @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 pick data earliest source that has it for priority searches" do Backend.expects(:datasourcefiles).with(:yaml, {}, "yaml", nil).yields(["one", "/nonexisting/one.yaml"]) @cache.value = "---\nkey: answer" @backend.lookup("key", {}, nil, :priority, nil).should == "answer" end describe "handling unexpected YAML values" do before do Backend.expects(:datasourcefiles).with(:yaml, {}, "yaml", nil).yields(["one", "/nonexisting/one.yaml"]) end it "throws :no_such_key when key is missing in YAML" do @cache.value = "---\n" expect { @backend.lookup("key", {}, nil, :priority, nil) }.to throw_symbol(:no_such_key) end it "returns nil when the YAML value is nil" do @cache.value = "key: ~\n" @backend.lookup("key", {}, nil, :priority, nil).should be_nil end it "throws :no_such_key when the YAML file is false" do @cache.value = "" expect { @backend.lookup("key", {}, nil, :priority, nil) }.to throw_symbol(:no_such_key) end it "raises a TypeError when the YAML value is not a hash" do @cache.value = "---\n[one, two, three]" expect { @backend.lookup("key", {}, nil, :priority, nil) }.to raise_error(TypeError) end end it "should build an array of all data sources for array searches" do Backend.expects(:datasourcefiles).with(:yaml, {}, "yaml", nil).multiple_yields(["one", "/nonexisting/one.yaml"], ["two", "/nonexisting/two.yaml"]) @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, nil).should == ["answer", "answer"] end it "should ignore empty hash of data sources for hash searches" do Backend.expects(:datasourcefiles).with(:yaml, {}, "yaml", nil).multiple_yields(["one", "/nonexisting/one.yaml"], ["two", "/nonexisting/two.yaml"]) @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, nil).should == {"a" => "answer"} end it "should build a merged hash of data sources for hash searches" do Backend.expects(:datasourcefiles).with(:yaml, {}, "yaml", nil).multiple_yields(["one", "/nonexisting/one.yaml"], ["two", "/nonexisting/two.yaml"]) @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, nil).should == {"a" => "answer", "b" => "answer"} end it "should fail when trying to << a Hash" do Backend.expects(:datasourcefiles).with(:yaml, {}, "yaml", nil).multiple_yields(["one", "/nonexisting/one.yaml"], ["two", "/nonexisting/two.yaml"]) @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, nil)}.to raise_error(Exception, "Hiera type mismatch for key 'key': expected Array and got Hash") end it "should fail when trying to merge an Array" do Backend.expects(:datasourcefiles).with(:yaml, {}, "yaml", nil).multiple_yields(["one", "/nonexisting/one.yaml"], ["two", "/nonexisting/two.yaml"]) @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, nil) }.to raise_error(Exception, "Hiera type mismatch for key 'key': expected Hash and got Array") end it "should parse the answer for scope variables" do Backend.expects(:datasourcefiles).with(:yaml, {"rspec" => "test"}, "yaml", nil).multiple_yields(["one", "/nonexisting/one.yaml"]) @cache.expects(:read_file).with("/nonexisting/one.yaml", Hash).returns({"key"=>"test_%{rspec}"}) @backend.lookup("key", {"rspec" => "test"}, nil, :priority, nil).should == "test_test" end it "should retain datatypes found in yaml files" do Backend.expects(:datasourcefiles).with(:yaml, {}, "yaml", nil).multiple_yields(["one", "/nonexisting/one.yaml"]).times(3) @cache.value = "---\nstringval: 'string'\nboolval: true\nnumericval: 1" @backend.lookup("stringval", {}, nil, :priority, nil).should == "string" @backend.lookup("boolval", {}, nil, :priority, nil).should == true @backend.lookup("numericval", {}, nil, :priority, nil).should == 1 end end end end end hiera-2.0.0/spec/unit/version_spec.rb0000644000175000017500000000200412517736341016551 0ustar jonasjonasrequire "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-2.0.0/spec/unit/hiera_spec.rb0000644000175000017500000000420312517736341016157 0ustar jonasjonasrequire '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-2.0.0/spec/unit/fixtures/0000755000175000017500000000000012517736341015402 5ustar jonasjonashiera-2.0.0/spec/unit/fixtures/interpolate/0000755000175000017500000000000012517736341017730 5ustar jonasjonashiera-2.0.0/spec/unit/fixtures/interpolate/config/0000755000175000017500000000000012517736341021175 5ustar jonasjonashiera-2.0.0/spec/unit/fixtures/interpolate/config/hiera.yaml0000644000175000017500000000007312517736341023151 0ustar jonasjonas:backends: - yaml :hierarchy: - recursive - niltest hiera-2.0.0/spec/unit/fixtures/interpolate/data/0000755000175000017500000000000012517736341020641 5ustar jonasjonashiera-2.0.0/spec/unit/fixtures/interpolate/data/recursive.yaml0000644000175000017500000000005712517736341023536 0ustar jonasjonasfoo: '%{hiera("bar")}' bar: '%{hiera("foo")}' hiera-2.0.0/spec/unit/fixtures/interpolate/data/niltest.yaml0000644000175000017500000000013312517736341023204 0ustar jonasjonasniltest: 'Missing key #%{hiera("knotfound")}#. Key with nil #%{hiera("knil")}#' knil: null hiera-2.0.0/spec/unit/fixtures/override/0000755000175000017500000000000012517736341017221 5ustar jonasjonashiera-2.0.0/spec/unit/fixtures/override/config/0000755000175000017500000000000012517736341020466 5ustar jonasjonashiera-2.0.0/spec/unit/fixtures/override/config/hiera.yaml0000644000175000017500000000005412517736341022441 0ustar jonasjonas:backends: - yaml :hierarchy: - common hiera-2.0.0/spec/unit/fixtures/override/data/0000755000175000017500000000000012517736341020132 5ustar jonasjonashiera-2.0.0/spec/unit/fixtures/override/data/common.yaml0000644000175000017500000000004512517736341022305 0ustar jonasjonasfoo: '%{hiera("bar")}' bar: 'common' hiera-2.0.0/spec/unit/fixtures/override/data/alternate.yaml0000644000175000017500000000002112517736341022766 0ustar jonasjonasbar: 'alternate' hiera-2.0.0/spec/unit/puppet_logger_spec.rb0000644000175000017500000000120612517736341017743 0ustar jonasjonasrequire '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-2.0.0/spec/spec_helper.rb0000644000175000017500000000425212517736341015373 0ustar jonasjonas$:.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 if Hiera::Util.microsoft_windows? && RUBY_VERSION =~ /^1\./ require 'win32console' config.output_stream = $stdout config.error_stream = $stderr config.formatters.each { |f| f.instance_variable_set(:@output, $stdout) } end config.after :suite do # Log the spec order to a file, but only if the LOG_SPEC_ORDER environment variable is # set. This should be enabled on Jenkins runs, as it can be used with Nick L.'s bisect # script to help identify and debug order-dependent spec failures. if ENV['LOG_SPEC_ORDER'] File.open("./spec_order.txt", "w") do |logfile| config.instance_variable_get(:@files_to_run).each { |f| logfile.puts f } end end end end # So everyone else doesn't have to include this base constant. module HieraSpec FIXTURE_DIR = File.join(dir = File.expand_path(File.dirname(__FILE__)), 'unit', 'fixtures') unless defined?(FIXTURE_DIR) 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-2.0.0/LICENSE0000644000175000017500000000123212517736341012623 0ustar jonasjonasPuppet - Automating Configuration Management. Copyright (C) 2012-2014 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-2.0.0/lib/0000755000175000017500000000000012517736341012366 5ustar jonasjonashiera-2.0.0/lib/hiera/0000755000175000017500000000000012517736341013456 5ustar jonasjonashiera-2.0.0/lib/hiera/config.rb0000644000175000017500000000463612517736341015261 0ustar jonasjonasclass 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 raise Hiera::Error, "Must have 'deep_merge' gem installed for the configured merge_behavior." 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-2.0.0/lib/hiera/console_logger.rb0000644000175000017500000000037512517736341017011 0ustar jonasjonasclass 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-2.0.0/lib/hiera/puppet_logger.rb0000644000175000017500000000043612517736341016662 0ustar jonasjonasclass 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-2.0.0/lib/hiera/backend/0000755000175000017500000000000012517736341015045 5ustar jonasjonashiera-2.0.0/lib/hiera/backend/yaml_backend.rb0000644000175000017500000000420412517736341020003 0ustar jonasjonasclass 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, context) answer = nil found = false Hiera.debug("Looking up #{key} in YAML backend") Backend.datasourcefiles(:yaml, scope, "yaml", order_override) do |source, yamlfile| data = @cache.read_file(yamlfile, Hash) do |data| YAML.load(data) || {} end next if data.empty? next unless data.include?(key) found = true # 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, {}, context) case resolution_type.is_a?(Hash) ? :hash : resolution_type when :array raise Exception, "Hiera type mismatch for key '#{key}': expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String answer ||= [] answer << new_answer when :hash raise Exception, "Hiera type mismatch for key '#{key}': expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash answer ||= {} answer = Backend.merge_answer(new_answer, answer, resolution_type) else answer = new_answer break end end throw :no_such_key unless found return answer end private def file_exists?(path) File.exist? path end end end end hiera-2.0.0/lib/hiera/backend/json_backend.rb0000644000175000017500000000361112517736341020013 0ustar jonasjonasclass 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, context) answer = nil found = false 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) found = true # 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, {}, context) case resolution_type.is_a?(Hash) ? :hash : resolution_type when :array raise Exception, "Hiera type mismatch for key '#{key}': expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String answer ||= [] answer << new_answer when :hash raise Exception, "Hiera type mismatch for key '#{key}': expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash answer ||= {} answer = Backend.merge_answer(new_answer, answer, resolution_type) else answer = new_answer break end end throw :no_such_key unless found return answer end end end end hiera-2.0.0/lib/hiera/fallback_logger.rb0000644000175000017500000000213012517736341017075 0ustar jonasjonas# 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-2.0.0/lib/hiera/recursive_guard.rb0000644000175000017500000000064412517736341017200 0ustar jonasjonas# 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-2.0.0/lib/hiera/filecache.rb0000644000175000017500000000514612517736341015714 0ustar jonasjonasclass 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-2.0.0/lib/hiera/backend.rb0000644000175000017500000003033612517736341015377 0ustar jonasjonasrequire 'hiera/util' require 'hiera/interpolate' begin require 'deep_merge' rescue LoadError end class Hiera module Backend class Backend1xWrapper def initialize(wrapped) @wrapped = wrapped end def lookup(key, scope, order_override, resolution_type, context) Hiera.debug("Using Hiera 1.x backend API to access instance of class #{@wrapped.class.name}. Lookup recursion will not be detected") value = @wrapped.lookup(key, scope, order_override, resolution_type.is_a?(Hash) ? :hash : resolution_type) # The most likely cause when an old backend returns nil is that the key was not found. In any case, it is # impossible to know the difference between that and a found nil. The throw here preserves the old behavior. throw (:no_such_key) if value.nil? value end end class << self # Data lives in /var/lib/hiera by default. If a backend # supplies a datadir in the config it will be used and # subject to variable expansion based on scope def datadir(backend, scope) backend = backend.to_sym if Config[backend] && Config[backend][:datadir] dir = Config[backend][:datadir] else dir = Hiera::Util.var_dir end if !dir.is_a?(String) raise(Hiera::InvalidConfigurationError, "datadir for #{backend} cannot be an array") end parse_string(dir, scope) end # Finds the path to a datafile based on the Backend#datadir # and extension # # If the file is not found nil is returned def datafile(backend, scope, source, extension) datafile_in(datadir(backend, scope), source, extension) end # @api private def datafile_in(datadir, source, extension) file = File.join(datadir, "#{source}.#{extension}") if File.exist?(file) file else Hiera.debug("Cannot find datafile #{file}, skipping") nil end end # Constructs a list of data sources to search # # If you give it a specific hierarchy it will just use that # else it will use the global configured one, failing that # it will just look in the 'common' data source. # # An override can be supplied that will be pre-pended to the # hierarchy. # # The source names will be subject to variable expansion based # on scope def datasources(scope, override=nil, hierarchy=nil) if hierarchy hierarchy = [hierarchy] elsif Config.include?(:hierarchy) hierarchy = [Config[:hierarchy]].flatten else hierarchy = ["common"] end hierarchy.insert(0, override) if override hierarchy.flatten.map do |source| source = parse_string(source, scope, {}, :order_override => override) yield(source) unless source == "" or source =~ /(^\/|\/\/|\/$)/ end end # Constructs a list of data files to search # # If you give it a specific hierarchy it will just use that # else it will use the global configured one, failing that # it will just look in the 'common' data source. # # An override can be supplied that will be pre-pended to the # hierarchy. # # The source names will be subject to variable expansion based # on scope # # Only files that exist will be returned. If the file is missing, then # the block will not receive the file. # # @yield [String, String] the source string and the name of the resulting file # @api public def datasourcefiles(backend, scope, extension, override=nil, hierarchy=nil) datadir = Backend.datadir(backend, scope) Backend.datasources(scope, override, hierarchy) do |source| Hiera.debug("Looking for data source #{source}") file = datafile_in(datadir, source, extension) if file yield source, file end end end # Parse a string like '%{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. # @param context [#[]] Context can include :recurse_guard and :order_override. # @return [String] A copy of the data with all instances of %{...} replaced. # # @api public def parse_string(data, scope, extra_data={}, context={:recurse_guard => nil, :order_override => nil}) Hiera::Interpolate.interpolate(data, scope, extra_data, context) end # Parses a answer received from data files # # Ultimately it just pass the data through parse_string but # it makes some effort to handle arrays of strings as well def parse_answer(data, scope, extra_data={}, context={:recurse_guard => nil, :order_override => nil}) if data.is_a?(Numeric) or data.is_a?(TrueClass) or data.is_a?(FalseClass) return data elsif data.is_a?(String) return parse_string(data, scope, extra_data, context) elsif data.is_a?(Hash) answer = {} data.each_pair do |key, val| interpolated_key = parse_string(key, scope, extra_data, context) answer[interpolated_key] = parse_answer(val, scope, extra_data, context) end return answer elsif data.is_a?(Array) answer = [] data.each do |item| answer << parse_answer(item, scope, extra_data, context) end return answer end end def resolve_answer(answer, resolution_type) case resolution_type when :array [answer].flatten.uniq.compact when :hash answer # Hash structure should be preserved else answer end end # Merges two hashes answers with the given or configured merge behavior. Behavior can be given # by passing _resolution_type_ as a Hash # # :merge_behavior: {:native|:deep|:deeper} # # Deep merge options use the Hash utility function provided by [deep_merge](https://github.com/danielsdeleo/deep_merge) # # :native => Native Hash.merge # :deep => Use Hash.deep_merge # :deeper => Use Hash.deep_merge! # # @param left [Hash] left side of the merge # @param right [Hash] right side of the merge # @param resolution_type [String,Hash] The merge type, or if hash, the merge behavior and options # @return [Hash] The merged result # @see Hiera#lookup # def merge_answer(left,right,resolution_type=nil) behavior, options = if resolution_type.is_a?(Hash) merge = resolution_type.clone [merge.delete(:behavior), merge] else [Config[:merge_behavior], Config[:deep_merge_options] || {}] end case behavior when :deeper,'deeper' left.deep_merge!(right, options) when :deep,'deep' left.deep_merge(right, options) else # Native and undefined left.merge(right) end end # Calls out to all configured backends in the order they # were specified. The first one to answer will win. # # This lets you declare multiple backends, a possible # use case might be in Puppet where a Puppet module declares # default data using in-module data while users can override # using JSON/YAML etc. By layering the backends and putting # the Puppet one last you can override module author data # easily. # # Backend instances are cached so if you need to connect to any # databases then do so in your constructor, future calls to your # backend will not create new instances # @param key [String] The key to lookup # @param scope [#[]] The primary source of data for substitutions. # @param order_override [#[],nil] An override that will be pre-pended to the hierarchy definition. # @param resolution_type [Symbol,Hash,nil] One of :hash, :array,:priority or a Hash with deep merge behavior and options # @param context [#[]] Context used for internal processing # @return [Object] The value that corresponds to the given key or nil if no such value cannot be found # def lookup(key, default, scope, order_override, resolution_type, context = {:recurse_guard => nil}) @backends ||= {} answer = nil # order_override is kept as an explicit argument for backwards compatibility, but should be specified # in the context for internal handling. context ||= {} order_override ||= context[:order_override] context[:order_override] ||= order_override strategy = resolution_type.is_a?(Hash) ? :hash : resolution_type segments = key.split('.') subsegments = nil if segments.size > 1 raise ArgumentError, "Resolution type :#{strategy} is illegal when doing segmented key lookups" unless strategy.nil? || strategy == :priority subsegments = segments.drop(1) end found = false Config[:backends].each do |backend| backend_constant = "#{backend.capitalize}_backend" if constants.include?(backend_constant) || constants.include?(backend_constant.to_sym) backend = (@backends[backend] ||= find_backend(backend_constant)) found_in_backend = false new_answer = catch(:no_such_key) do value = backend.lookup(segments[0], scope, order_override, resolution_type, context) value = qualified_lookup(subsegments, value) unless subsegments.nil? found_in_backend = true value end next unless found_in_backend found = true case strategy when :array raise Exception, "Hiera type mismatch for key '#{key}': expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String answer ||= [] answer << new_answer when :hash raise Exception, "Hiera type mismatch for key '#{key}': expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash answer ||= {} answer = merge_answer(new_answer, answer, resolution_type) else answer = new_answer break end end end answer = resolve_answer(answer, strategy) unless answer.nil? answer = parse_string(default, scope, {}, context) if !found && default.is_a?(String) return default if !found && answer.nil? return answer end def clear! @backends = {} end def qualified_lookup(segments, hash) value = hash segments.each do |segment| throw :no_such_key if value.nil? if segment =~ /^[0-9]+$/ segment = segment.to_i raise Exception, "Hiera type mismatch: Got #{value.class.name} when Array was expected enable lookup using key '#{segment}'" unless value.instance_of?(Array) throw :no_such_key unless segment < value.size else raise Exception, "Hiera type mismatch: Got #{value.class.name} when a non Array object that responds to '[]' was expected to enable lookup using key '#{segment}'" unless value.respond_to?(:'[]') && !value.instance_of?(Array); throw :no_such_key unless value.include?(segment) end value = value[segment] end value end def find_backend(backend_constant) backend = Backend.const_get(backend_constant).new return backend.method(:lookup).arity == 4 ? Backend1xWrapper.new(backend) : backend end private :find_backend end end end hiera-2.0.0/lib/hiera/version.rb0000644000175000017500000000677112517736341015503 0ustar jonasjonas# 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 = "2.0.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-2.0.0/lib/hiera/error.rb0000644000175000017500000000014212517736341015131 0ustar jonasjonasclass Hiera class Error < StandardError; end class InvalidConfigurationError < Error; end end hiera-2.0.0/lib/hiera/interpolate.rb0000644000175000017500000000663012517736341016336 0ustar jonasjonasrequire 'hiera/backend' require 'hiera/recursive_guard' class Hiera::InterpolationInvalidValue < StandardError; end class Hiera::Interpolate class << self INTERPOLATION = /%\{([^\}]*)\}/ METHOD_INTERPOLATION = /%\{(scope|hiera|literal|alias)\(['"]([^"']*)["']\)\}/ def interpolate(data, scope, extra_data, context) if data.is_a?(String) # Wrapping do_interpolation in a gsub block ensures we process # each interpolation site in isolation using separate recursion guards. context ||= {} new_context = context.clone new_context[:recurse_guard] ||= Hiera::RecursiveGuard.new data.gsub(INTERPOLATION) do |match| interp_val = do_interpolation(match, scope, extra_data, new_context) # Get interp method in case we are aliasing if data.is_a?(String) && (match = data.match(INTERPOLATION)) interpolate_method, key = get_interpolation_method_and_key(data) else interpolate_method = nil end if ( (interpolate_method == :alias_interpolate) and (!interp_val.is_a?(String)) ) if data.match("^#{INTERPOLATION}$") return interp_val else raise Hiera::InterpolationInvalidValue, "Cannot call alias in the string context" end else interp_val end end else data end end def do_interpolation(data, scope, extra_data, context) if data.is_a?(String) && (match = data.match(INTERPOLATION)) interpolation_variable = match[1] context[: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, context) # Halt recursion if we encounter a literal. return interpolated_data if interpolate_method == :literal_interpolate do_interpolation(interpolated_data, scope, extra_data, context) end else data end end private :do_interpolation 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]] when 'literal' then [:literal_interpolate, match[2]] when 'alias' then [:alias_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, context) segments = key.split('.') catch(:no_such_key) { return Hiera::Backend.qualified_lookup(segments, scope) } catch(:no_such_key) { Hiera::Backend.qualified_lookup(segments, extra_data) } end private :scope_interpolate def hiera_interpolate(data, key, scope, extra_data, context) Hiera::Backend.lookup(key, nil, scope, context[:order_override], :priority, context) end private :hiera_interpolate def literal_interpolate(data, key, scope, extra_data, context) key end private :literal_interpolate def alias_interpolate(data, key, scope, extra_data, context) Hiera::Backend.lookup(key, nil, scope, context[:order_override], :priority, context) end private :alias_interpolate end end hiera-2.0.0/lib/hiera/noop_logger.rb0000644000175000017500000000016612517736341016320 0ustar jonasjonasclass Hiera module Noop_logger class << self def warn(msg);end def debug(msg);end end end end hiera-2.0.0/lib/hiera/util.rb0000644000175000017500000000154512517736341014765 0ustar jonasjonasclass 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', 'code') else '/etc/puppetlabs/code' end end def var_dir if microsoft_windows? File.join(common_appdata, 'PuppetLabs', 'code', 'hieradata') else '/etc/puppetlabs/code/hieradata' end end def file_alt_separator File::ALT_SEPARATOR end def common_appdata Dir::COMMON_APPDATA end end end hiera-2.0.0/lib/hiera.rb0000644000175000017500000001374412517736341014014 0ustar jonasjonasrequire '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. # # Possible values for the _resolution_type_ parameter: # # - _:priority_ - This is the default. First found value is returned and no merge is performed # - _:array_ - An array merge lookup assembles a value from every matching level of the hierarchy. It retrieves all # of the (string or array) values for a given key, then flattens them into a single array of unique values. # If _priority_ lookup can be thought of as a “default with overrides” pattern, _array_ merge lookup can be though # of as “default with additions.” # - _:hash_ - A hash merge lookup assembles a value from every matching level of the hierarchy. It retrieves all of # the (hash) values for a given key, then merges the hashes into a single hash. Hash merge lookups will fail with # an error if any of the values found in the data sources are strings or arrays. It only works when every value # found is a hash. The actual merge behavior is determined by looking up the keys `:merge_behavior` and # `:deep_merge_options` in the Hiera config. `:merge_behavior` can be set to `:deep`, :deeper` or `:native` # (explained in detail below). # - _{ deep merge options }_ - Configured values for `:merge_behavior` and `:deep_merge_options`will be completely # ignored. Instead the _resolution_type_ will be a `:hash` merge where the `:merge_behavior` will be the value # keyed by `:behavior` in the given hash and the `:deep_merge_options` will be the remaining top level entries of # that same hash. # # Valid behaviors for the _:hash_ resolution type: # # - _native_ - Performs a simple hash-merge by overwriting keys of lower lookup priority. # - _deeper_ - In a deeper hash merge, Hiera recursively merges keys and values in each source hash. For each key, # if the value is: # - only present in one source hash, it goes into the final hash. # - a string/number/boolean and exists in two or more source hashes, the highest priority value goes into # the final hash. # - an array and exists in two or more source hashes, the values from each source are merged into a single # array and de-duplicated (but not automatically flattened, as in an array merge lookup). # - a hash and exists in two or more source hashes, the values from each source are recursively merged, as # though they were source hashes. # - mismatched between two or more source hashes, we haven’t validated the behavior. It should act as # described in the deep_merge gem documentation. # - _deep_ - In a deep hash merge, Hiera behaves the same as for _deeper_, except that when a string/number/boolean # exists in two or more source hashes, the lowest priority value goes into the final hash. This is considered # largely useless and should be avoided. Use _deeper_ instead. # # The _merge_ can be given as a hash with the mandatory key `:strategy` to denote the actual strategy. This # is useful for the `:deeper` and `:deep` strategy since they can use additional options to control the behavior. # The options can be passed as top level keys in the `merge` parameter when it is a given as a hash. Recognized # options are: # # - 'knockout_prefix' Set to string value to signify prefix which deletes elements from existing element. Defaults is _undef_ # - 'sort_merged_arrays' Set to _true_ to sort all arrays that are merged together. Default is _false_ # - 'unpack_arrays' Set to string value used as a deliminator to join all array values and then split them again. Default is _undef_ # - 'merge_hash_arrays' Set to _true_ to merge hashes within arrays. Default is _false_ # # @param key [String] The key to lookup # @param default [Object,nil] The value to return when there is no match for _key_ # @param scope [#[],nil] The scope to use for the lookup # @param order_override [#[]] An override that will considered the first source of lookup # @param resolution_type [String,Hash] Symbolic resolution type or deep merge configuration # @return [Object] The found value or the given _default_ value def lookup(key, default, scope, order_override=nil, resolution_type=:priority) Backend.lookup(key, default, scope, order_override, resolution_type) end end hiera-2.0.0/COPYING0000644000175000017500000002614112517736341012657 0ustar jonasjonas 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. hiera-2.0.0/bin/0000755000175000017500000000000012517736341012370 5ustar jonasjonashiera-2.0.0/bin/hiera0000755000175000017500000001552412517736341013415 0ustar jonasjonas#!/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, :format => :ruby } initial_scopes = Array.new # 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 def output_answer(ans, format) case format when :json require 'json' puts JSON.dump(ans) when :ruby if ans.is_a?(String) puts ans else pp ans end when :yaml require 'yaml' puts ans.to_yaml else STDERR.puts "Unknown output format: #{v}" exit 1 end 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| initial_scopes << { :type => :json, :value => v, :name => "JSON" } end opts.on("--yaml SCOPE", "-y", "YAML format file to load scope from") do |v| initial_scopes << { :type => :yaml, :value => v, :name => "YAML" } end opts.on("--mcollective IDENTITY", "-m", "Use facts from a node (via mcollective) as scope") do |v| initial_scopes << { :type => :mcollective, :value => v, :name => "Mcollective" } end opts.on("--inventory_service IDENTITY", "-i", "Use facts from a node (via Puppet's inventory service) as scope") do |v| initial_scopes << { :type => :inventory_service, :value => v, :name => "Puppet inventory service" } end opts.on("--format TYPE", "-f", "Output the result in a specific format (ruby, yaml or json); default is 'ruby'") do |v| options[:format] = case v when 'json', 'ruby', 'yaml' v.to_sym else STDERR.puts "Unknown output format: #{v}" exit 1 end end end.parse! unless initial_scopes.empty? initial_scopes.each { |this_scope| # Load initial scope begin options[:scope] = load_scope(this_scope[:value], this_scope[:type]) rescue Exception => e STDERR.puts "Could not load #{this_scope[:name]} scope: #{e.class}: #{e}" exit 1 end } end # 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]) output_answer(ans, options[:format]) hiera-2.0.0/metadata.yml0000644000175000017500000000611012517736341014121 0ustar jonasjonas--- !ruby/object:Gem::Specification name: hiera version: !ruby/object:Gem::Version version: 2.0.0 platform: ruby authors: - Puppet Labs autorequire: bindir: bin cert_chain: [] date: 2015-03-24 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: json_pure requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement 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/backend.rb - lib/hiera/backend/json_backend.rb - lib/hiera/backend/yaml_backend.rb - lib/hiera/config.rb - lib/hiera/console_logger.rb - lib/hiera/error.rb - lib/hiera/fallback_logger.rb - lib/hiera/filecache.rb - lib/hiera/interpolate.rb - lib/hiera/noop_logger.rb - lib/hiera/puppet_logger.rb - lib/hiera/recursive_guard.rb - lib/hiera/util.rb - lib/hiera/version.rb - lib/hiera.rb - COPYING - README.md - LICENSE - spec/spec_helper.rb - spec/unit/backend/json_backend_spec.rb - spec/unit/backend/yaml_backend_spec.rb - spec/unit/backend_spec.rb - spec/unit/config_spec.rb - spec/unit/console_logger_spec.rb - spec/unit/fallback_logger_spec.rb - spec/unit/filecache_spec.rb - spec/unit/fixtures/interpolate/config/hiera.yaml - spec/unit/fixtures/interpolate/data/niltest.yaml - spec/unit/fixtures/interpolate/data/recursive.yaml - spec/unit/fixtures/override/config/hiera.yaml - spec/unit/fixtures/override/data/alternate.yaml - spec/unit/fixtures/override/data/common.yaml - spec/unit/hiera_spec.rb - spec/unit/interpolate_spec.rb - spec/unit/puppet_logger_spec.rb - spec/unit/util_spec.rb - spec/unit/version_spec.rb homepage: https://github.com/puppetlabs/hiera licenses: [] metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.0.14 signing_key: specification_version: 4 summary: Light weight hierarchical data store test_files: - spec/spec_helper.rb - spec/unit/backend/json_backend_spec.rb - spec/unit/backend/yaml_backend_spec.rb - spec/unit/backend_spec.rb - spec/unit/config_spec.rb - spec/unit/console_logger_spec.rb - spec/unit/fallback_logger_spec.rb - spec/unit/filecache_spec.rb - spec/unit/fixtures/interpolate/config/hiera.yaml - spec/unit/fixtures/interpolate/data/niltest.yaml - spec/unit/fixtures/interpolate/data/recursive.yaml - spec/unit/fixtures/override/config/hiera.yaml - spec/unit/fixtures/override/data/alternate.yaml - spec/unit/fixtures/override/data/common.yaml - spec/unit/hiera_spec.rb - spec/unit/interpolate_spec.rb - spec/unit/puppet_logger_spec.rb - spec/unit/util_spec.rb - spec/unit/version_spec.rb hiera-2.0.0/README.md0000644000175000017500000002273512517736341013110 0ustar jonasjonas# 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. ### Qualified Key Lookup You can use a qualified key to lookup a value that is contained inside a hash or array:
$ hiera user
{"name"=>"kim", "home"=>"/home/kim"}
$ hiera user.name
kim
$ hiera ssh_users
["root", "jeff", "gary", "hunter"]
$ hiera ssh_users.0
root
## 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/puppetlabs/code/hieradata

:puppet:
   :datasource: data
This configuration will require YAML files in _/etc/puppetlabs/code/hieradata_ these need to contain Hash data, sample files matching the hierarchy described in the _Why?_ section are below: _/etc/puppetlabs/code/hieradata/sites/dc1.yaml_:
---
ntpserver: ntp1.dc1.example.com
sysadmin: dc1noc@example.com
_/etc/puppetlabs/code/hieradata/sites/dc2.yaml_:
---
ntpserver: ntp1.dc2.example.com
_/etc/puppetlabs/code/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 /opt/puppetlabs/puppet/cache/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 /opt/puppetlabs/puppet/cache/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("/opt/puppetlabs/puppet/cache/yaml/facts/example.com.yaml").values

# create a new instance based on config file
hiera = Hiera.new(:config => "/etc/puppetlabs/code/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 [JIRA tracker](http://tickets.puppetlabs.com). A [mailing list](https://groups.google.com/forum/?fromgroups#!forum/puppet-users) is available for asking questions and getting help from others. In addition there is an active #puppet channel on Freenode. We use semantic version numbers for our releases, and recommend that users stay as up-to-date as possible by upgrading to patch releases and minor releases as they become available. Bugfixes and ongoing development will occur in minor releases for the current major version. Security fixes will be backported to a previous major version on a best-effort basis, until the previous major version is no longer maintained. For example: If a security vulnerability is discovered in Hiera 1.3.0, we would fix it in the 1 series, most likely as 1.3.1. Maintainers would then make a best effort to backport that fix onto the latest Hiera release they carry. Long-term support, including security patches and bug fixes, is available for commercial customers. Please see the following page for more details: [Puppet Enterprise Support Lifecycle](http://puppetlabs.com/misc/puppet-enterprise-lifecycle)