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.
- data/lib/hiera/backend/consul_backend.rb +290 -0
- data/lib/puppet/parser/functions/consul_info.rb +52 -0
- metadata +178 -0
@@ -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:
|