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.
@@ -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