hiera-backend-consul_backend 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,290 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'json'
4
+ require 'base64'
5
+
6
+ class ConfigurationError < ArgumentError
7
+ end
8
+
9
+ # Hiera backend for Consul
10
+ class Hiera
11
+ module Backend
12
+ class Consul_backend
13
+ @api_version = 'v1'
14
+
15
+ class << self
16
+ attr_reader :api_version
17
+ end
18
+
19
+ attr_reader :host, :consul
20
+
21
+ def initialize
22
+ begin
23
+ @config = Config[:consul]
24
+ rescue StandardError => e
25
+ raise ConfigurationError, "[hiera-consul]: config cannot be read: #{e}"
26
+ end
27
+
28
+ verify_config!
29
+ parse_hosts!
30
+
31
+ @consul = setup_consul
32
+ use_ssl!
33
+
34
+ @cache = {}
35
+ build_cache!
36
+ end
37
+
38
+ def lookup(key, scope, order_override, _resolution_type)
39
+ answer = nil
40
+
41
+ paths = resolve_paths(key, scope, order_override)
42
+ paths.unshift(order_override) if order_override
43
+
44
+ filtered_paths = filter_paths(paths, key)
45
+
46
+ filtered_paths.each do |path|
47
+ return @cache[key] if path == 'services' && @cache.key?(key)
48
+
49
+ debug("Lookup #{path}/#{key} on #{@host}:#{@config[:port]}")
50
+
51
+ answer = wrapquery("#{path}/#{key}")
52
+ break if answer
53
+ end
54
+
55
+ answer
56
+ end
57
+
58
+ def resolve_paths(key, scope, order_override)
59
+ if @config[:base]
60
+ Backend.datasources(scope, order_override) do |source|
61
+ url = "#{@config[:base]}/#{source}"
62
+ Backend.parse_string(url, scope, 'key' => key)
63
+ end
64
+ elsif @config[:paths]
65
+ @config[:paths].map { |p| Backend.parse_string(p, scope, 'key' => key) }
66
+ else
67
+ probable_source = @config[:base] ? :base : :paths
68
+ raise ConfigurationError, "[hiera-consul]: There is an issue with your hierarchy. Please check #{probable_source} configuration"
69
+ exit 1
70
+ end
71
+ end
72
+
73
+ def verify_config!
74
+ return true if
75
+ @config[:host] && @config[:port] && (@config[:paths] || @config[:base])
76
+ raise ConfigurationError, '[hiera-consul]: Missing minimum configuration, please check hiera.yaml'
77
+ end
78
+
79
+ def parse_hosts!
80
+ @hosts =
81
+ if @config[:host].is_a?(String)
82
+ [@config[:host]]
83
+ else
84
+ @config[:host]
85
+ end
86
+ end
87
+
88
+ def setup_consul
89
+ fail '[hiera-consul]: No consul server is available' if @hosts.empty?
90
+ @host = @hosts.shift
91
+
92
+ debug "Trying #{@host}"
93
+ @consul = Net::HTTP.new(@host, @config[:port])
94
+ @consul.read_timeout = @config[:http_read_timeout] || 10
95
+ @consul.open_timeout = @config[:http_connect_timeout] || 10
96
+ @consul
97
+ end
98
+
99
+ def consul_fallback
100
+ debug "Could not reach #{@host}, retrying with #{@hosts.first}"
101
+ setup_consul
102
+ end
103
+
104
+ def use_ssl!
105
+ if @config[:use_ssl]
106
+ @consul.use_ssl = true
107
+ config_ssl!
108
+ else
109
+ @consul.use_ssl = false
110
+ end
111
+ end
112
+
113
+ def config_ssl!
114
+ msg = '[hiera-consul]: use_ssl is enabled but no ssl_cert is set'
115
+ fail msg unless @config[:ssl_cert]
116
+
117
+ ssl_verify!
118
+ ssl_store!
119
+ ssl_key!
120
+ ssl_cert!
121
+ end
122
+
123
+ def ssl_verify!
124
+ if @config[:ssl_verify]
125
+ @consul.verify_mode = OpenSSL::SSL::VERIFY_PEER
126
+ else
127
+ @consul.verify_mode = OpenSSL::SSL::VERIFY_NONE
128
+ end
129
+ end
130
+
131
+ def store
132
+ return @store if @store
133
+ ssl_store!
134
+ @store
135
+ end
136
+
137
+ def ssl_store!
138
+ @store = OpenSSL::X509::Store.new
139
+ @store.add_cert(OpenSSL::X509::Certificate.new(File.read(@config[:ssl_ca_cert])))
140
+ @consul.cert_store = @store
141
+ end
142
+
143
+ def ssl_key!
144
+ debug("ssl_key: #{File.expand_path(@config[:ssl_key])}")
145
+ @consul.key = OpenSSL::PKey::RSA.new(File.read(@config[:ssl_key]))
146
+ end
147
+
148
+ def ssl_cert!
149
+ debug("ssl_cert: #{File.expand_path(@config[:ssl_cert])}")
150
+ @consul.cert = OpenSSL::X509::Certificate.new(File.read(@config[:ssl_cert]))
151
+ end
152
+
153
+ def filter_paths(paths, key)
154
+ paths.reduce([]) do |acc, path|
155
+ if "#{path}/#{key}".match('//')
156
+ # Check that we are not looking somewhere that will make hiera
157
+ # crash subsequent lookups
158
+ debug("The specified path #{path}/#{key} is malformed, skipping")
159
+ elsif path !~ %r{^/v\d/(catalog|kv)/}
160
+ # We only support querying the catalog or the kv store
161
+ debug("We only support queries to catalog and kv and you asked #{path}, skipping")
162
+ else
163
+ acc << path
164
+ end
165
+ acc
166
+ end
167
+ end
168
+
169
+ def parse_result(res)
170
+ # Consul always returns an array
171
+ res_array = JSON.parse(res)
172
+
173
+ # See if we are a k/v return or a catalog return
174
+ unless res_array.length > 0
175
+ debug('Jumped as array empty')
176
+ return nil
177
+ end
178
+
179
+ if res_array.first.include? 'Value'
180
+ Base64.decode64(res_array.first['Value'])
181
+ else
182
+ res_array
183
+ end
184
+ end
185
+
186
+ def wrapquery(path)
187
+ httpreq = Net::HTTP::Get.new("#{path}#{token(path)}")
188
+ result = request(httpreq)
189
+
190
+ if result.nil?
191
+ debug('No response from any server')
192
+ return nil
193
+ end
194
+
195
+ unless result.is_a?(Net::HTTPSuccess)
196
+ debug("HTTP response code was #{result.code}")
197
+ return nil
198
+ end
199
+
200
+ if result.body == 'null'
201
+ debug('Jumped as consul null is not valid')
202
+ return nil
203
+ end
204
+
205
+ debug("Answer was #{result.body}")
206
+ parse_result(result.body)
207
+ end
208
+
209
+ # Token is passed only when querying kv store
210
+ def token(path)
211
+ "?token=#{@config[:token]}" if @config[:token] && path =~ %r{^/v\d/kv/}
212
+ end
213
+
214
+ def request(httpreq)
215
+ @consul.request(httpreq)
216
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, Errno::ECONNREFUSED => e
217
+ if @hosts.length >= 1
218
+ consul_fallback
219
+ retry
220
+ else
221
+ debug('Could not connect to Consul')
222
+ raise Exception, e.message unless @config[:failure] == 'graceful'
223
+ return nil
224
+ end
225
+ end
226
+
227
+ def query_services
228
+ path = "/#{self.class.api_version}/catalog/services"
229
+ debug("Querying #{@host}#{path}")
230
+ wrapquery(path)
231
+ end
232
+
233
+ def query_service(key)
234
+ path = "/#{self.class.api_version}/catalog/service/#{key}"
235
+ debug("Querying #{path}")
236
+ wrapquery(path)
237
+ end
238
+
239
+ def build_cache!
240
+ services = query_services
241
+ return nil unless services.is_a? Hash
242
+
243
+ services.each do |key, _|
244
+ cache_service(key)
245
+ end
246
+
247
+ debug("Cache: #{@cache}")
248
+ end
249
+
250
+ def cache_service(key)
251
+ service = query_service(key)
252
+ return nil unless service.is_a?(Array)
253
+
254
+ service.each do |node_hash|
255
+ node = node_hash['Node']
256
+ cache_node(key, node, node_hash)
257
+ end
258
+ end
259
+
260
+ # Store the value of a particular node
261
+ def cache_node(key, node, node_hash)
262
+ node_hash.each do |property, value|
263
+ next if property == 'ServiceID'
264
+
265
+ update_cache(key, value, property, node)
266
+ end
267
+ end
268
+
269
+ def update_cache(key, value, property, node)
270
+ @cache["#{key}_#{property}_#{node}"] = value unless property == 'Node'
271
+
272
+ if @cache.key?("#{key}_#{property}")
273
+ @cache["#{key}_#{property}_array"].push(value)
274
+ else
275
+ # Value of the first registered node
276
+ @cache["#{key}_#{property}"] = value
277
+
278
+ # Values of all nodes
279
+ @cache["#{key}_#{property}_array"] = [value]
280
+ end
281
+ end
282
+
283
+ private
284
+
285
+ def debug(msg)
286
+ Hiera.debug("[hiera-consul]: #{msg}")
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,52 @@
1
+ module Puppet::Parser::Functions
2
+ newfunction(:consul_info, :type => :rvalue, :doc => <<-EOS
3
+ Parse the incoming consul info and return a value
4
+ EOS
5
+ ) do |args|
6
+
7
+ data = args[0]
8
+ field = args[1]
9
+ if args[2]
10
+ separator = args[2]
11
+ else
12
+ separator = ":"
13
+ end
14
+ debug("consul-info() :: Determined that my separator is \"#{separator}\"")
15
+
16
+ if field.is_a?(Array)
17
+ field_iterator = field
18
+ debug("consul-info() :: Field is an Array, importing as it is #{field_iterator}")
19
+ elsif field.is_a?(String)
20
+ field_iterator = []
21
+ field_iterator.push(field)
22
+ debug("consul-info() :: Field is a text string, converting to array #{field_iterator}")
23
+ elsif field.is_a?(Hash)
24
+ raise(Puppet::ParseError, 'consul_info() does not accept a hash as your field argument')
25
+ end
26
+
27
+ if data.is_a?(Hash)
28
+ myendstring = ""
29
+ debug ("consul-info() :: Data is a hash")
30
+ field_iterator.each do |myfield|
31
+ myendstring << "#{data[myfield]}#{separator}"
32
+ end
33
+ myreturn = myendstring.gsub(/#{Regexp.escape(separator)}$/, '')
34
+ elsif data.is_a?(Array)
35
+ debug ("consul_info() :: Data is an array")
36
+ myreturn = []
37
+ data.each do |mydata|
38
+ myendstring = ""
39
+ field_iterator.each do |myfield|
40
+ myendstring << "#{mydata[myfield]}#{separator}"
41
+ end
42
+ myreturn << myendstring.gsub(/#{Regexp.escape(separator)}$/, '')
43
+ end
44
+ else
45
+ raise(Puppet::ParseError, "consul_info() does not know how to treat data #{data}")
46
+ end
47
+
48
+ debug("consul_info() returning #{myreturn}")
49
+ return myreturn
50
+
51
+ end
52
+ end
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hiera-backend-consul_backend
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Gwilliam
9
+ - Marc Cluet
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2015-10-22 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: 1.1.1
31
+ - !ruby/object:Gem::Dependency
32
+ name: hiera
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: '1.0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: puppet
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ - !ruby/object:Gem::Dependency
80
+ name: puppetlabs_spec_helper
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: rspec
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ~>
101
+ - !ruby/object:Gem::Version
102
+ version: '3.3'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: '3.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: webmock
129
+ requirement: !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ~>
133
+ - !ruby/object:Gem::Version
134
+ version: '1.22'
135
+ type: :development
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ~>
141
+ - !ruby/object:Gem::Version
142
+ version: '1.22'
143
+ description: A hiera backend that queries consul's service discovery catalog and distributed
144
+ k/v store
145
+ email: dhgwilliam@gmail.com
146
+ executables: []
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - ./lib/hiera/backend/consul_backend.rb
151
+ - ./lib/puppet/parser/functions/consul_info.rb
152
+ homepage: https://github.com/dhgwilliam/hiera-consul
153
+ licenses:
154
+ - Apache 2.0
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ none: false
161
+ requirements:
162
+ - - ! '>='
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ none: false
167
+ requirements:
168
+ - - ! '>='
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubyforge_project:
173
+ rubygems_version: 1.8.23.2
174
+ signing_key:
175
+ specification_version: 3
176
+ summary: A hiera backend to query consul
177
+ test_files: []
178
+ has_rdoc: