murakumo 0.1.0

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