murakumo 0.1.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/README +57 -0
- data/bin/mrkmctl +5 -0
- data/bin/murakumo +5 -0
- data/etc/murakumo.server +95 -0
- data/etc/murakumo.yml.sample +33 -0
- data/lib/cli/mrkmctl.rb +74 -0
- data/lib/cli/mrkmctl_options.rb +122 -0
- data/lib/cli/murakumo.rb +20 -0
- data/lib/cli/murakumo_options.rb +158 -0
- data/lib/misc/murakumo_const.rb +17 -0
- data/lib/srv/murakumo_cloud.rb +457 -0
- data/lib/srv/murakumo_health_checker.rb +134 -0
- data/lib/srv/murakumo_health_checker_context.rb +33 -0
- data/lib/srv/murakumo_server.rb +102 -0
- metadata +142 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Murakumo
|
2
|
+
# Priority
|
3
|
+
MASTER = 1
|
4
|
+
BACKUP = 0
|
5
|
+
ORIGIN = -1
|
6
|
+
|
7
|
+
# Activity
|
8
|
+
ACTIVE = 1
|
9
|
+
INACTIVE = 0
|
10
|
+
|
11
|
+
ATTRIBUTES = {
|
12
|
+
:node_lifetime => [:node_lifetime, :to_f],
|
13
|
+
:send_interval => [:gossip_interval, :to_f],
|
14
|
+
:receive_timeout => [:receive_timeout, :to_f],
|
15
|
+
:log_level => nil,
|
16
|
+
}
|
17
|
+
end
|
@@ -0,0 +1,457 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'rgossip2'
|
3
|
+
require 'sqlite3'
|
4
|
+
|
5
|
+
require 'srv/murakumo_health_checker'
|
6
|
+
require 'misc/murakumo_const'
|
7
|
+
|
8
|
+
module Murakumo
|
9
|
+
|
10
|
+
class Cloud
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_reader :address
|
14
|
+
attr_reader :gossip
|
15
|
+
attr_reader :db
|
16
|
+
|
17
|
+
def initialize(options)
|
18
|
+
# オプションはインスタンス変数に保存
|
19
|
+
@options = options
|
20
|
+
|
21
|
+
# リソースレコードからホストのアドレスとデータを取り出す
|
22
|
+
host_data = options[:host]
|
23
|
+
@address = host_data.shift
|
24
|
+
host_data.concat [ORIGIN, ACTIVE]
|
25
|
+
alias_datas = options[:aliases].map {|r| r + [ACTIVE] }
|
26
|
+
|
27
|
+
# 名前は小文字に変換
|
28
|
+
datas = ([host_data] + alias_datas).map do |i|
|
29
|
+
name, ttl, priority, activity = i
|
30
|
+
name = name.downcase
|
31
|
+
[name, ttl, priority, activity]
|
32
|
+
end
|
33
|
+
|
34
|
+
# データベースを作成してレコードを更新
|
35
|
+
create_database
|
36
|
+
update(@address, datas)
|
37
|
+
|
38
|
+
# ゴシップオブジェクトを生成
|
39
|
+
@gossip = RGossip2.client({
|
40
|
+
:initial_nodes => options[:initial_nodes],
|
41
|
+
:address => @address,
|
42
|
+
:data => datas,
|
43
|
+
:auth_key => options[:auth_key],
|
44
|
+
:port => options[:gossip_port],
|
45
|
+
:node_lifetime => options[:gossip_node_lifetime],
|
46
|
+
:gossip_interval => options[:gossip_send_interval],
|
47
|
+
:receive_timeout => options[:gossip_receive_timeout],
|
48
|
+
:logger => options[:logger],
|
49
|
+
})
|
50
|
+
|
51
|
+
# ノードの更新をフック
|
52
|
+
@gossip.context.callback_handler = lambda do |act, addr, ts, dt|
|
53
|
+
case act
|
54
|
+
when :add, :comeback, :update
|
55
|
+
update(addr, dt)
|
56
|
+
when :delete
|
57
|
+
delete(addr)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# ヘルスチェック
|
62
|
+
@health_checkers = {}
|
63
|
+
|
64
|
+
if options.config_file and options.config_file['health-check']
|
65
|
+
health_check = options.config_file['health-check']
|
66
|
+
|
67
|
+
if health_check.kind_of?(Hash)
|
68
|
+
health_check.each do |name, conf|
|
69
|
+
checker = HealthChecker.new(name, self, options[:logger], conf)
|
70
|
+
@health_checkers[name] = checker
|
71
|
+
# ヘルスチェックはまだ起動しない
|
72
|
+
end
|
73
|
+
else
|
74
|
+
options[:logger].warn('configuration of a health check is not right')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Control of service
|
80
|
+
def_delegators :@gossip, :stop
|
81
|
+
|
82
|
+
def start
|
83
|
+
# デーモン化すると子プロセスはすぐ死ぬので
|
84
|
+
# このタイミングでヘルスチェックを起動
|
85
|
+
@health_checkers.each do |name, checker|
|
86
|
+
checker.start
|
87
|
+
end
|
88
|
+
|
89
|
+
@gossip.start
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_hash
|
93
|
+
keys = {
|
94
|
+
:auth_key => 'auth-key',
|
95
|
+
:dns_address => 'address',
|
96
|
+
:dns_port => 'port',
|
97
|
+
:initial_nodes => lambda {|v| ['initial-nodes', v.join(',')] },
|
98
|
+
:resolver => lambda {|v| [
|
99
|
+
'resolver',
|
100
|
+
v.instance_variable_get(:@config).instance_variable_get(:@config_info)[:nameserver].join(',')
|
101
|
+
]},
|
102
|
+
:socket => 'socket',
|
103
|
+
:max_ip_num => 'max-ip-num',
|
104
|
+
:log_path => 'log-path',
|
105
|
+
:log_level => 'log-level',
|
106
|
+
:gossip_port => 'gossip-port',
|
107
|
+
:gossip_node_lifetime => lambda {|v| [
|
108
|
+
'gossip-node-lifetime',
|
109
|
+
@gossip.context.node_lifetime
|
110
|
+
]},
|
111
|
+
:gossip_send_interval => lambda {|v| [
|
112
|
+
'gossip-send-interval',
|
113
|
+
@gossip.context.gossip_interval
|
114
|
+
]},
|
115
|
+
:gossip_receive_timeout => lambda {|v| [
|
116
|
+
'gossip-receive-timeout',
|
117
|
+
@gossip.context.receive_timeout
|
118
|
+
]},
|
119
|
+
}
|
120
|
+
|
121
|
+
hash = {}
|
122
|
+
|
123
|
+
keys.each do |k, name|
|
124
|
+
value = @options[k]
|
125
|
+
|
126
|
+
if value and name.respond_to?(:call)
|
127
|
+
name, value = name.call(value)
|
128
|
+
end
|
129
|
+
|
130
|
+
if value and (not value.kind_of?(String) or not value.empty?)
|
131
|
+
hash[name] = value
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
records = list_records
|
136
|
+
|
137
|
+
hash['host'] = records.find {|r| r[3] == ORIGIN }[0..2].join(',')
|
138
|
+
|
139
|
+
aliases = records.select {|r| r[3] != ORIGIN }.map do |r|
|
140
|
+
[r[1], r[2], (r[3] == MASTER ? 'master' : 'backup')].join(',')
|
141
|
+
end
|
142
|
+
|
143
|
+
hash['alias'] = aliases unless aliases.empty?
|
144
|
+
|
145
|
+
if @options.config_file and @options.config_file['health-check']
|
146
|
+
hash['health-check'] = @options.config_file['health-check']
|
147
|
+
end
|
148
|
+
|
149
|
+
return hash
|
150
|
+
end
|
151
|
+
|
152
|
+
def list_records
|
153
|
+
columns = %w(ip_address name ttl priority activity)
|
154
|
+
|
155
|
+
@db.execute(<<-EOS).map {|i| i.values_at(*columns) }
|
156
|
+
SELECT #{columns.join(', ')} FROM records ORDER BY ip_address, name
|
157
|
+
EOS
|
158
|
+
end
|
159
|
+
|
160
|
+
def add_or_rplace_records(records)
|
161
|
+
errmsg = nil
|
162
|
+
|
163
|
+
# 名前は小文字に変換
|
164
|
+
records = records.map do |i|
|
165
|
+
name, ttl, priority = i
|
166
|
+
name = name.downcase
|
167
|
+
[name, ttl, priority]
|
168
|
+
end
|
169
|
+
|
170
|
+
@gossip.transaction do
|
171
|
+
|
172
|
+
# 既存のホスト名は削除
|
173
|
+
@gossip.data.reject! do |d|
|
174
|
+
if records.any? {|r| r[0] == d[0] }
|
175
|
+
# オリジンのPriorityは変更不可
|
176
|
+
if d[2] == ORIGIN
|
177
|
+
records.each {|r| r[2] = ORIGIN if r[0] == d[0] }
|
178
|
+
end
|
179
|
+
|
180
|
+
true
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# データを更新
|
185
|
+
records = records.map {|r| r + [ACTIVE] }
|
186
|
+
@gossip.data.concat(records)
|
187
|
+
end # transaction
|
188
|
+
|
189
|
+
# データベースを更新
|
190
|
+
update(@address, records, true)
|
191
|
+
|
192
|
+
# ヘルスチェックがあれば開始
|
193
|
+
records.map {|i| i.first }.each do |name|
|
194
|
+
checker = @health_checkers[name]
|
195
|
+
|
196
|
+
if checker and not checker.alive?
|
197
|
+
checker.start
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
return [!errmsg, errmsg]
|
202
|
+
end
|
203
|
+
|
204
|
+
def delete_records(names)
|
205
|
+
errmsg = nil
|
206
|
+
|
207
|
+
# 名前は小文字に変換
|
208
|
+
names = names.map {|i| i.downcase }
|
209
|
+
|
210
|
+
@gossip.transaction do
|
211
|
+
# データを削除
|
212
|
+
@gossip.data.reject! do |d|
|
213
|
+
if names.any? {|n| n == d[0] }
|
214
|
+
if d[2] == ORIGIN
|
215
|
+
# オリジンは削除不可
|
216
|
+
errmsg = 'original host name cannot be deleted'
|
217
|
+
names.reject! {|n| n == d[0] }
|
218
|
+
false
|
219
|
+
else
|
220
|
+
true
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end # transaction
|
225
|
+
|
226
|
+
# データベースを更新
|
227
|
+
delete_by_names(@address, names)
|
228
|
+
|
229
|
+
# ヘルスチェックがあれば停止
|
230
|
+
names.each do |name|
|
231
|
+
checker = @health_checkers[name]
|
232
|
+
checker.stop if checker
|
233
|
+
end
|
234
|
+
|
235
|
+
return [!errmsg, errmsg]
|
236
|
+
end
|
237
|
+
|
238
|
+
def add_nodes(nodes)
|
239
|
+
errmsg = nil
|
240
|
+
|
241
|
+
nodes.each do |i|
|
242
|
+
@gossip.add_node(i)
|
243
|
+
end
|
244
|
+
|
245
|
+
return [!errmsg, errmsg]
|
246
|
+
end
|
247
|
+
|
248
|
+
def delete_nodes(nodes)
|
249
|
+
errmsg = nil
|
250
|
+
|
251
|
+
nodes.each do |i|
|
252
|
+
@gossip.delete_node(i)
|
253
|
+
end
|
254
|
+
|
255
|
+
return [!errmsg, errmsg]
|
256
|
+
end
|
257
|
+
|
258
|
+
def get_attr(name)
|
259
|
+
return unless ATTRIBUTES.has_key?(name)
|
260
|
+
|
261
|
+
if name == :log_level
|
262
|
+
if @gossip.logger
|
263
|
+
%w(debug info warn error fatal)[@gossip.logger.level]
|
264
|
+
else
|
265
|
+
nil
|
266
|
+
end
|
267
|
+
else
|
268
|
+
attr, conv = ATTRIBUTES[name]
|
269
|
+
@gossip.context.send(attr).to_s
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def set_attr(name, value)
|
274
|
+
return unless ATTRIBUTES.has_key?(name)
|
275
|
+
|
276
|
+
errmsg = nil
|
277
|
+
|
278
|
+
if name == :log_level
|
279
|
+
if @gossip.logger
|
280
|
+
@gossip.logger.level = %w(debug info warn error fatal).index(value.to_s)
|
281
|
+
end
|
282
|
+
else
|
283
|
+
attr, conv = ATTRIBUTES[name]
|
284
|
+
@gossip.context.send("#{attr}=", value.send(conv)).to_s
|
285
|
+
end
|
286
|
+
|
287
|
+
return [!errmsg, errmsg]
|
288
|
+
end
|
289
|
+
|
290
|
+
def close
|
291
|
+
# データベースをクローズ
|
292
|
+
@db.close
|
293
|
+
end
|
294
|
+
|
295
|
+
# Operation of storage
|
296
|
+
|
297
|
+
def update(address, datas, update_only = false)
|
298
|
+
return unless datas
|
299
|
+
|
300
|
+
datas.each do |i|
|
301
|
+
name, ttl, priority, activity = i
|
302
|
+
|
303
|
+
# 名前は小文字に変換
|
304
|
+
name = name.downcase
|
305
|
+
|
306
|
+
@db.execute(<<-EOS, address, name, ttl, priority, activity)
|
307
|
+
REPLACE INTO records (ip_address, name, ttl, priority, activity)
|
308
|
+
VALUES (?, ?, ?, ?, ?)
|
309
|
+
EOS
|
310
|
+
end
|
311
|
+
|
312
|
+
# データにないレコードは消す
|
313
|
+
unless update_only
|
314
|
+
names = datas.map {|i| "'#{i.first.downcase}'" }.join(',')
|
315
|
+
|
316
|
+
@db.execute(<<-EOS, address)
|
317
|
+
DELETE FROM records
|
318
|
+
WHERE ip_address = ? AND name NOT IN (#{names})
|
319
|
+
EOS
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
def delete(address)
|
324
|
+
@db.execute('DELETE FROM records WHERE ip_address = ?', address)
|
325
|
+
end
|
326
|
+
|
327
|
+
def delete_by_names(address, names)
|
328
|
+
names = names.map {|i| "'#{i.downcase}'" }.join(',')
|
329
|
+
|
330
|
+
@db.execute(<<-EOS, address)
|
331
|
+
DELETE FROM records
|
332
|
+
WHERE ip_address = ? AND name IN (#{names})
|
333
|
+
EOS
|
334
|
+
end
|
335
|
+
|
336
|
+
# Search of records
|
337
|
+
|
338
|
+
def address_exist?(name)
|
339
|
+
# 名前は小文字に変換
|
340
|
+
name = name.downcase
|
341
|
+
|
342
|
+
# シングルスレッドェ…
|
343
|
+
@address_records = @db.execute(<<-EOS, name, ACTIVE)
|
344
|
+
SELECT ip_address, ttl, priority FROM records
|
345
|
+
WHERE name = ? AND activity = ?
|
346
|
+
EOS
|
347
|
+
|
348
|
+
@address_records.length.nonzero?
|
349
|
+
end
|
350
|
+
|
351
|
+
def lookup_addresses(name)
|
352
|
+
records = nil
|
353
|
+
|
354
|
+
if @address_records.length == 1
|
355
|
+
# レコードが一件ならそれを返す
|
356
|
+
records = @address_records
|
357
|
+
else
|
358
|
+
# 優先度の高いレコードを検索
|
359
|
+
records = @address_records.select {|i| i['priority'] == MASTER }
|
360
|
+
|
361
|
+
# レコードが見つからなかった場合は優先度の低いレコードを選択
|
362
|
+
if records.empty?
|
363
|
+
records = @address_records.select {|i| i['priority'] == BACKUP }
|
364
|
+
end
|
365
|
+
|
366
|
+
# それでもレコードが見つからなかった場合はオリジンを選択
|
367
|
+
# ※このパスは通らない
|
368
|
+
records = @address_records if records.empty?
|
369
|
+
end
|
370
|
+
|
371
|
+
# IPアドレス、TTLを返す
|
372
|
+
return records.map {|i| i.values_at('ip_address', 'ttl') }
|
373
|
+
ensure
|
374
|
+
# エラー検出のため、一応クリア
|
375
|
+
@address_records = nil
|
376
|
+
end
|
377
|
+
|
378
|
+
def name_exist?(address)
|
379
|
+
address = x_ip_addr(address)
|
380
|
+
|
381
|
+
# シングルスレッドェ…
|
382
|
+
@name_records = @db.execute(<<-EOS, address, ACTIVE)
|
383
|
+
SELECT name, ttl, priority FROM records
|
384
|
+
WHERE ip_address = ? AND activity = ?
|
385
|
+
EOS
|
386
|
+
|
387
|
+
@name_records.length.nonzero?
|
388
|
+
end
|
389
|
+
|
390
|
+
def lookup_name(address)
|
391
|
+
record = nil
|
392
|
+
|
393
|
+
if @name_records.length == 1
|
394
|
+
# レコードが一件ならそれを返す
|
395
|
+
record = @name_records.first
|
396
|
+
else
|
397
|
+
# オリジンを検索
|
398
|
+
record = @name_records.find {|i| i['priority'] == ORIGIN }
|
399
|
+
|
400
|
+
# レコードが見つからなかった場合は優先度の高いレコード選択
|
401
|
+
unless record
|
402
|
+
record = @name_records.find {|i| i['priority'] == ACTIVE }
|
403
|
+
end
|
404
|
+
|
405
|
+
# それでもレコードが見つからなかった場合は優先度の低いレコードを選択
|
406
|
+
record = @name_records.first unless record
|
407
|
+
end
|
408
|
+
|
409
|
+
# ホスト名、TTLを返す
|
410
|
+
return record.values_at('name', 'ttl')
|
411
|
+
ensure
|
412
|
+
# エラー検出のため、一応クリア
|
413
|
+
@name_records = nil
|
414
|
+
end
|
415
|
+
|
416
|
+
private
|
417
|
+
|
418
|
+
# リソースレコードのデータベース作成
|
419
|
+
# もう少し並列処理に強いストレージに変えたいが…
|
420
|
+
def create_database
|
421
|
+
@db = SQLite3::Database.new(':memory:')
|
422
|
+
@db.type_translation = true
|
423
|
+
@db.results_as_hash = true
|
424
|
+
|
425
|
+
# リソースレコード用のテーブル
|
426
|
+
# (Typeは必要?)
|
427
|
+
@db.execute(<<-EOS)
|
428
|
+
CREATE TABLE records (
|
429
|
+
ip_address TEXT NOT NULL,
|
430
|
+
name TEXT NOT NULL,
|
431
|
+
ttl INTEGER NOT NULL,
|
432
|
+
priority INTEGER NOT NULL, /* MASTER:1, BACKUP:0, ORIGIN:-1 */
|
433
|
+
activity INTEGER NOT NULL, /* Active:1, Inactive:0 */
|
434
|
+
PRIMARY KEY (ip_address, name)
|
435
|
+
)
|
436
|
+
EOS
|
437
|
+
|
438
|
+
# インデックスを作成(必要?)
|
439
|
+
@db.execute(<<-EOS)
|
440
|
+
CREATE INDEX idx_name_act
|
441
|
+
ON records (name, activity)
|
442
|
+
EOS
|
443
|
+
|
444
|
+
@db.execute(<<-EOS)
|
445
|
+
CREATE INDEX idx_ip_act
|
446
|
+
ON records (ip_address, activity)
|
447
|
+
EOS
|
448
|
+
end
|
449
|
+
|
450
|
+
# 逆引き名の変換
|
451
|
+
def x_ip_addr(name)
|
452
|
+
name.sub(/\.in-addr\.arpa\Z/, '').split('.').reverse.join('.')
|
453
|
+
end
|
454
|
+
|
455
|
+
end # Cloud
|
456
|
+
|
457
|
+
end # Murakumo
|