hiera-backend-consul_backend 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: