hiera 1.2.0.rc1 → 1.2.0.rc2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of hiera might be problematic. Click here for more details.

data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Hiera
2
2
 
3
+ [![Build Status](https://travis-ci.org/puppetlabs/hiera.png?branch=master)](https://travis-ci.org/puppetlabs/hiera)
4
+
3
5
  A simple pluggable Hierarchical Database.
4
6
 
5
7
  -
@@ -1,14 +1,16 @@
1
1
  require 'yaml'
2
2
 
3
3
  class Hiera
4
- VERSION = "1.2.0-rc1"
4
+ VERSION = "1.2.0-rc2"
5
5
 
6
- autoload :Config, "hiera/config"
7
- autoload :Util, "hiera/util"
8
- autoload :Backend, "hiera/backend"
9
- autoload :Console_logger, "hiera/console_logger"
10
- autoload :Puppet_logger, "hiera/puppet_logger"
11
- autoload :Noop_logger, "hiera/noop_logger"
6
+ require "hiera/config"
7
+ require "hiera/util"
8
+ require "hiera/backend"
9
+ require "hiera/console_logger"
10
+ require "hiera/puppet_logger"
11
+ require "hiera/noop_logger"
12
+ require "hiera/fallback_logger"
13
+ require "hiera/filecache"
12
14
 
13
15
  class << self
14
16
  attr_reader :logger
@@ -23,13 +25,13 @@ class Hiera
23
25
  # See hiera-puppet for an example that uses the Puppet
24
26
  # loging system instead of our own
25
27
  def logger=(logger)
26
- loggerclass = "#{logger.capitalize}_logger"
28
+ require "hiera/#{logger}_logger"
27
29
 
28
- require "hiera/#{logger}_logger" unless constants.include?(loggerclass)
29
-
30
- @logger = const_get(loggerclass)
30
+ @logger = Hiera::FallbackLogger.new(
31
+ Hiera.const_get("#{logger.capitalize}_logger"),
32
+ Hiera::Console_logger)
31
33
  rescue Exception => e
32
- @logger = Console_logger
34
+ @logger = Hiera::Console_logger
33
35
  warn("Failed to load #{logger} logger: #{e.class}: #{e}")
34
36
  end
35
37
 
@@ -1,4 +1,6 @@
1
1
  require 'hiera/util'
2
+ require 'hiera/recursive_lookup'
3
+
2
4
  begin
3
5
  require 'deep_merge'
4
6
  rescue LoadError
@@ -6,6 +8,8 @@ end
6
8
 
7
9
  class Hiera
8
10
  module Backend
11
+ INTERPOLATION = /%\{([^\}]*)\}/
12
+
9
13
  class << self
10
14
  # Data lives in /var/lib/hiera by default. If a backend
11
15
  # supplies a datadir in the config it will be used and
@@ -65,44 +69,38 @@ class Hiera
65
69
  end
66
70
  end
67
71
 
68
- # Parse a string like '%{foo}' against a supplied
72
+ # Parse a string like <code>'%{foo}'</code> against a supplied
69
73
  # scope and additional scope. If either scope or
70
- # extra_scope includes the varaible 'foo' it will
74
+ # extra_scope includes the variable 'foo', then it will
71
75
  # be replaced else an empty string will be placed.
72
76
  #
73
- # If both scope and extra_data has "foo" scope
74
- # will win. See hiera-puppet for an example of
75
- # this to make hiera aware of additional non scope
76
- # variables
77
+ # If both scope and extra_data has "foo", then the value in scope
78
+ # will be used.
79
+ #
80
+ # @param data [String] The string to perform substitutions on.
81
+ # This will not be modified, instead a new string will be returned.
82
+ # @param scope [#[]] The primary source of data for substitutions.
83
+ # @param extra_data [#[]] The secondary source of data for substitutions.
84
+ # @return [String] A copy of the data with all instances of <code>%{...}</code> replaced.
85
+ #
86
+ # @api public
77
87
  def parse_string(data, scope, extra_data={})
78
- return nil unless data
79
-
80
- tdata = data.clone
81
-
82
- if tdata.is_a?(String)
83
- while tdata =~ /%\{(.+?)\}/
84
- begin
85
- var = $1
86
-
87
- val = ""
88
-
89
- # Puppet can return :undefined for unknown scope vars,
90
- # If it does then we still need to evaluate extra_data
91
- # before returning an empty string.
92
- scope_val = scope[var]
93
- if !scope_val.nil? && scope_val != :undefined
94
- val = scope_val
95
- elsif extra_data[var]
96
- val = extra_data[var]
97
- end
98
- end until val != "" || var !~ /::(.+)/
88
+ interpolate(data, Hiera::RecursiveLookup.new(scope, extra_data))
89
+ end
99
90
 
100
- tdata.gsub!(/%\{(::)?#{var}\}/, val)
91
+ def interpolate(data, values)
92
+ if data.is_a?(String)
93
+ data.gsub(INTERPOLATION) do
94
+ name = $1
95
+ values.lookup(name) do |value|
96
+ interpolate(value, values)
97
+ end
101
98
  end
99
+ else
100
+ data
102
101
  end
103
-
104
- return tdata
105
102
  end
103
+ private :interpolate
106
104
 
107
105
  # Parses a answer received from data files
108
106
  #
@@ -1,10 +1,12 @@
1
1
  class Hiera
2
2
  module Backend
3
3
  class Json_backend
4
- def initialize
4
+ def initialize(cache=nil)
5
5
  require 'json'
6
6
 
7
7
  Hiera.debug("Hiera JSON backend starting")
8
+
9
+ @cache = cache || Filecache.new
8
10
  end
9
11
 
10
12
  def lookup(key, scope, order_override, resolution_type)
@@ -17,7 +19,11 @@ class Hiera
17
19
 
18
20
  jsonfile = Backend.datafile(:json, scope, source, "json") || next
19
21
 
20
- data = JSON.parse(File.read(jsonfile))
22
+ next unless File.exist?(jsonfile)
23
+
24
+ data = @cache.read(jsonfile, Hash, {}) do |data|
25
+ JSON.parse(data)
26
+ end
21
27
 
22
28
  next if data.empty?
23
29
  next unless data.include?(key)
@@ -1,11 +1,11 @@
1
1
  class Hiera
2
2
  module Backend
3
3
  class Yaml_backend
4
- def initialize
4
+ def initialize(cache=nil)
5
5
  require 'yaml'
6
6
  Hiera.debug("Hiera YAML backend starting")
7
- @data = Hash.new
8
- @cache = Hash.new
7
+
8
+ @cache = cache || Filecache.new
9
9
  end
10
10
 
11
11
  def lookup(key, scope, order_override, resolution_type)
@@ -17,20 +17,14 @@ class Hiera
17
17
  Hiera.debug("Looking for data source #{source}")
18
18
  yamlfile = Backend.datafile(:yaml, scope, source, "yaml") || next
19
19
 
20
- # If you call stale? BEFORE you do encounter the YAML.load_file line
21
- # it will populate the @cache variable and return true. The second
22
- # time you call it, it will return false because @cache has been
23
- # populated. Because of this there are two conditions to check:
24
- # is @data[yamlfile] populated AND is the cache stale.
25
- if @data[yamlfile]
26
- @data[yamlfile] = YAML.load_file(yamlfile) if stale?(yamlfile)
27
- else
28
- @data[yamlfile] = YAML.load_file(yamlfile)
20
+ next unless File.exist?(yamlfile)
21
+
22
+ data = @cache.read(yamlfile, Hash, {}) do |data|
23
+ YAML.load(data)
29
24
  end
30
25
 
31
- next if ! @data[yamlfile]
32
- next if @data[yamlfile].empty?
33
- next unless @data[yamlfile].include?(key)
26
+ next if data.empty?
27
+ next unless data.include?(key)
34
28
 
35
29
  # Extra logging that we found the key. This can be outputted
36
30
  # multiple times if the resolution type is array or hash but that
@@ -43,7 +37,7 @@ class Hiera
43
37
  # the array
44
38
  #
45
39
  # for priority searches we break after the first found data item
46
- new_answer = Backend.parse_answer(@data[yamlfile][key], scope)
40
+ new_answer = Backend.parse_answer(data[key], scope)
47
41
  case resolution_type
48
42
  when :array
49
43
  raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
@@ -61,18 +55,6 @@ class Hiera
61
55
 
62
56
  return answer
63
57
  end
64
-
65
- def stale?(yamlfile)
66
- # NOTE: The mtime change in a file MUST be > 1 second before being
67
- # recognized as stale. File mtime changes within 1 second will
68
- # not be recognized.
69
- stat = File.stat(yamlfile)
70
- current = { 'inode' => stat.ino, 'mtime' => stat.mtime, 'size' => stat.size }
71
- return false if @cache[yamlfile] == current
72
-
73
- @cache[yamlfile] = current
74
- return true
75
- end
76
58
  end
77
59
  end
78
60
  end
@@ -0,0 +1,41 @@
1
+ # Select from a given list of loggers the first one that
2
+ # it suitable and use that as the actual logger
3
+ #
4
+ # @api private
5
+ class Hiera::FallbackLogger
6
+ # Chooses the first suitable logger. For all of the loggers that are
7
+ # unsuitable it will issue a warning using the suitable logger stating that
8
+ # the unsuitable logger is not being used.
9
+ #
10
+ # @param implementations [Array<Hiera::Logger>] the implementations to choose from
11
+ # @raises when there are no suitable loggers
12
+ def initialize(*implementations)
13
+ warnings = []
14
+ @implementation = implementations.find do |impl|
15
+ if impl.respond_to?(:suitable?)
16
+ if impl.suitable?
17
+ true
18
+ else
19
+ warnings << "Not using #{impl.name}. It does not report itself to be suitable."
20
+ false
21
+ end
22
+ else
23
+ true
24
+ end
25
+ end
26
+
27
+ if @implementation.nil?
28
+ raise "No suitable logging implementation found."
29
+ end
30
+
31
+ warnings.each { |message| warn(message) }
32
+ end
33
+
34
+ def warn(message)
35
+ @implementation.warn(message)
36
+ end
37
+
38
+ def debug(message)
39
+ @implementation.debug(message)
40
+ end
41
+ end
@@ -0,0 +1,74 @@
1
+ class Hiera
2
+ class Filecache
3
+ def initialize
4
+ @cache = {}
5
+ end
6
+
7
+ # Reads a file, optionally parse it in some way check the
8
+ # output type and set a default
9
+ #
10
+ # Simply invoking it this way will return the file contents
11
+ #
12
+ # data = read("/some/file")
13
+ #
14
+ # But as most cases of file reading in hiera involves some kind
15
+ # of parsing through a serializer there's some help for those
16
+ # cases:
17
+ #
18
+ # data = read("/some/file", Hash, {}) do |data|
19
+ # JSON.parse(data)
20
+ # end
21
+ #
22
+ # In this case it will read the file, parse it using JSON then
23
+ # check that the end result is a Hash, if it's not a hash or if
24
+ # reading/parsing fails it will return {} instead
25
+ #
26
+ # Prior to calling this method you should be sure the file exist
27
+ def read(path, expected_type=nil, default=nil)
28
+ @cache[path] ||= {:data => nil, :meta => path_metadata(path)}
29
+
30
+ if File.exist?(path) && !@cache[path][:data] || stale?(path)
31
+ if block_given?
32
+ begin
33
+ @cache[path][:data] = yield(File.read(path))
34
+ rescue => e
35
+ Hiera.debug("Reading data from %s failed: %s: %S" % [path, e.class, e.to_s])
36
+ @cache[path][:data] = default
37
+ end
38
+ else
39
+ @cache[path][:data] = File.read(path)
40
+ end
41
+ end
42
+
43
+ if block_given? && !expected_type.nil?
44
+ unless @cache[path][:data].is_a?(expected_type)
45
+ Hiera.debug("Data retrieved from %s is not a %s, setting defaults" % [path, expected_type])
46
+ @cache[path][:data] = default
47
+ end
48
+ end
49
+
50
+ @cache[path][:data]
51
+ end
52
+
53
+ def stale?(path)
54
+ meta = path_metadata(path)
55
+
56
+ @cache[path] ||= {:data => nil, :meta => nil}
57
+
58
+ if @cache[path][:meta] == meta
59
+ return false
60
+ else
61
+ @cache[path][:meta] = meta
62
+ return true
63
+ end
64
+ end
65
+
66
+ # This is based on the old caching in the YAML backend and has a
67
+ # resolution of 1 second, changes made within the same second of
68
+ # a previous read will be ignored
69
+ def path_metadata(path)
70
+ stat = File.stat(path)
71
+ {:inode => stat.ino, :mtime => stat.mtime, :size => stat.size}
72
+ end
73
+ end
74
+ end
@@ -1,6 +1,10 @@
1
1
  class Hiera
2
2
  module Puppet_logger
3
3
  class << self
4
+ def suitable?
5
+ Kernel.const_defined?(:Puppet)
6
+ end
7
+
4
8
  def warn(msg)
5
9
  Puppet.notice("hiera(): #{msg}")
6
10
  end
@@ -0,0 +1,31 @@
1
+ # Allow for safe recursive lookup of values during variable interpolation.
2
+ #
3
+ # @api private
4
+ class Hiera::RecursiveLookup
5
+ def initialize(scope, extra_data)
6
+ @seen = []
7
+ @scope = scope
8
+ @extra_data = extra_data
9
+ end
10
+
11
+ def lookup(name, &block)
12
+ if @seen.include?(name)
13
+ raise Exception, "Interpolation loop detected in [#{@seen.join(', ')}]"
14
+ end
15
+ @seen.push(name)
16
+ ret = yield(current_value)
17
+ @seen.pop
18
+ ret
19
+ end
20
+
21
+ def current_value
22
+ name = @seen.last
23
+
24
+ scope_val = @scope[name]
25
+ if scope_val.nil? || scope_val == :undefined
26
+ @extra_data[name]
27
+ else
28
+ scope_val
29
+ end
30
+ end
31
+ end
@@ -8,7 +8,8 @@ class Hiera
8
8
  Hiera.stubs(:debug)
9
9
  Hiera.stubs(:warn)
10
10
  Hiera::Backend.stubs(:empty_answer).returns(nil)
11
- @backend = Json_backend.new
11
+ @cache = mock
12
+ @backend = Json_backend.new(@cache)
12
13
  end
13
14
 
14
15
  describe "#initialize" do
@@ -30,13 +31,9 @@ class Hiera
30
31
  it "should retain the data types found in data files" do
31
32
  Backend.expects(:datasources).yields("one").times(3)
32
33
  Backend.expects(:datafile).with(:json, {}, "one", "json").returns("/nonexisting/one.json").times(3)
33
- File.expects(:read).with("/nonexisting/one.json").returns('{"stringval":"string",
34
- "boolval":true,
35
- "numericval":1}').times(3)
34
+ File.stubs(:exist?).with("/nonexisting/one.json").returns(true)
36
35
 
37
- Backend.stubs(:parse_answer).with('string', {}).returns('string')
38
- Backend.stubs(:parse_answer).with(true, {}).returns(true)
39
- Backend.stubs(:parse_answer).with(1, {}).returns(1)
36
+ @cache.expects(:read).with("/nonexisting/one.json", Hash, {}).returns({"stringval" => "string", "boolval" => true, "numericval" => 1}).times(3)
40
37
 
41
38
  @backend.lookup("stringval", {}, nil, :priority).should == "string"
42
39
  @backend.lookup("boolval", {}, nil, :priority).should == true
@@ -45,13 +42,12 @@ class Hiera
45
42
 
46
43
  it "should pick data earliest source that has it for priority searches" do
47
44
  scope = {"rspec" => "test"}
48
- Backend.stubs(:parse_answer).with('answer', scope).returns("answer")
49
- Backend.stubs(:parse_answer).with('test_%{rspec}', scope).returns("test_test")
50
45
  Backend.expects(:datasources).multiple_yields(["one"], ["two"])
51
46
  Backend.expects(:datafile).with(:json, scope, "one", "json").returns("/nonexisting/one.json")
52
- Backend.expects(:datafile).with(:json, scope, "two", "json").returns(nil).never
53
- File.expects(:read).with("/nonexisting/one.json").returns("one.json")
54
- JSON.expects(:parse).with("one.json").returns({"key" => "test_%{rspec}"})
47
+ Backend.expects(:datafile).with(:json, scope, "two", "json").never
48
+
49
+ File.stubs(:exist?).with("/nonexisting/one.json").returns(true)
50
+ @cache.expects(:read).with("/nonexisting/one.json", Hash, {}).returns({"key" => "test_%{rspec}"})
55
51
 
56
52
  @backend.lookup("key", scope, nil, :priority).should == "test_test"
57
53
  end
@@ -64,11 +60,11 @@ class Hiera
64
60
 
65
61
  Backend.expects(:datasources).multiple_yields(["one"], ["two"])
66
62
 
67
- File.expects(:read).with("/nonexisting/one.json").returns("one.json")
68
- File.expects(:read).with("/nonexisting/two.json").returns("two.json")
63
+ File.expects(:exist?).with("/nonexisting/one.json").returns(true)
64
+ File.expects(:exist?).with("/nonexisting/two.json").returns(true)
69
65
 
70
- JSON.expects(:parse).with("one.json").returns({"key" => "answer"})
71
- JSON.expects(:parse).with("two.json").returns({"key" => "answer"})
66
+ @cache.expects(:read).with("/nonexisting/one.json", Hash, {}).returns({"key" => "answer"})
67
+ @cache.expects(:read).with("/nonexisting/two.json", Hash, {}).returns({"key" => "answer"})
72
68
 
73
69
  @backend.lookup("key", {}, nil, :array).should == ["answer", "answer"]
74
70
  end
@@ -78,8 +74,8 @@ class Hiera
78
74
  Backend.expects(:datasources).yields("one")
79
75
  Backend.expects(:datafile).with(:json, {"rspec" => "test"}, "one", "json").returns("/nonexisting/one.json")
80
76
 
81
- File.expects(:read).with("/nonexisting/one.json").returns("one.json")
82
- JSON.expects(:parse).with("one.json").returns({"key" => "test_%{rspec}"})
77
+ File.expects(:exist?).with("/nonexisting/one.json").returns(true)
78
+ @cache.expects(:read).with("/nonexisting/one.json", Hash, {}).returns({"key" => "test_%{rspec}"})
83
79
 
84
80
  @backend.lookup("key", {"rspec" => "test"}, nil, :priority).should == "test_test"
85
81
  end