continuent-tools-core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,177 @@
1
+ class TungstenStatus
2
+ include TungstenAPI
3
+
4
+ DATASERVICE = "dataservice"
5
+ COORDINATOR = "coordinator"
6
+ ROUTERS = "routers"
7
+ DATASOURCES = "datasources"
8
+ REPLICATORS = "replicators"
9
+ MANAGER = "manager"
10
+ REPLICATOR = "replicator"
11
+ MASTER = "master"
12
+ DATASERVER = "dataserver"
13
+ STATUS = "status"
14
+ HOSTNAME = "hostname"
15
+ ROLE = "role"
16
+ SEQNO = "seqno"
17
+ LATENCY = "latency"
18
+
19
+ SERVICE_TYPE = "service_type"
20
+ REPLICATION_SERVICE = :replication
21
+ PHYSICAL_SERVICE = :physical
22
+ COMPOSITE_SERVICE = :composite
23
+
24
+ def initialize(install, dataservice = nil)
25
+ @install = install
26
+ @dataservice = dataservice
27
+ @service_type = nil
28
+ @props = nil
29
+ end
30
+
31
+ def parse
32
+ if @props != nil
33
+ return
34
+ end
35
+
36
+ if @dataservice == nil
37
+ if @install.dataservices().size > 1
38
+ raise "Unable to parse trepctl because there are multiple dataservices defined for this replicator. Try specifying a specific replication service."
39
+ end
40
+ @dataservice = @install.dataservices()[0]
41
+ end
42
+
43
+ if @install.is_manager?()
44
+ unless @install.is_running?("manager")
45
+ raise "Unable to provide status for #{@dataservice} from #{@install.hostname()}:#{@install.root()} because the manager is not running"
46
+ end
47
+ parse_manager()
48
+ elsif @install.is_replicator?()
49
+ unless @install.is_running?("replicator")
50
+ raise "Unable to provide status for #{@dataservice} from #{@install.hostname()}:#{@install.root()} because the replicator is not running"
51
+ end
52
+ parse_replicator()
53
+ else
54
+ raise "Unable to provide status for #{@dataservice} from #{@install.hostname()}:#{@install.root()} because it is not a database server"
55
+ end
56
+ end
57
+
58
+ def parse_manager
59
+ @props = Properties.new()
60
+
61
+ mgr = TungstenDataserviceManager.new(@install.mgr_api_uri())
62
+ result = mgr.get(@dataservice, 'status')
63
+ result = result["outputPayload"]["dataServiceState"]
64
+
65
+ @props.setProperty(DATASERVICE, @dataservice)
66
+ if result["composite"] == true
67
+ @props.setProperty(SERVICE_TYPE, COMPOSITE_SERVICE)
68
+ else
69
+ @props.setProperty(SERVICE_TYPE, PHYSICAL_SERVICE)
70
+ end
71
+ @props.setProperty(COORDINATOR, {
72
+ "host" => result["coordinator"],
73
+ "mode" => result["policyManagerMode"]
74
+ })
75
+ @props.setProperty(DATASOURCES, result["dataSources"])
76
+ @props.setProperty(REPLICATORS, result["replicators"])
77
+ end
78
+
79
+ def parse_replicator
80
+ @props = Properties.new()
81
+
82
+ @props.setProperty(DATASERVICE, @dataservice)
83
+ @props.setProperty(SERVICE_TYPE, REPLICATION_SERVICE)
84
+ r_props = JSON.parse(TU.cmd_result("#{@install.trepctl(@dataservice)} status -json"))
85
+ @props.setProperty([REPLICATORS, @install.hostname()], r_props)
86
+ end
87
+
88
+ def name
89
+ self.parse()
90
+ return @props.getProperty(DATASERVICE)
91
+ end
92
+
93
+ def coordinator
94
+ self.parse()
95
+ return @props.getProperty(['coordinator','host'])
96
+ end
97
+
98
+ def policy
99
+ self.parse()
100
+ return @props.getProperty(['coordinator','mode'])
101
+ end
102
+
103
+ def datasources
104
+ self.parse()
105
+ return @props.getPropertyOr([DATASOURCES], {}).keys()
106
+ end
107
+
108
+ def replicators
109
+ self.parse()
110
+ return @props.getPropertyOr([REPLICATORS], {}).keys()
111
+ end
112
+
113
+ def datasource_role(hostname)
114
+ datasource_value(hostname, 'role')
115
+ end
116
+
117
+ def datasource_status(hostname)
118
+ datasource_value(hostname, 'state')
119
+ end
120
+
121
+ def replicator_role(hostname)
122
+ replicator_value(hostname, 'role')
123
+ end
124
+
125
+ def replicator_status(hostname)
126
+ replicator_value(hostname, 'state')
127
+ end
128
+
129
+ def replicator_latency(hostname)
130
+ replicator_value(hostname, 'appliedLatency').to_f()
131
+ end
132
+
133
+ def datasource_value(hostname, argument)
134
+ self.parse()
135
+ return @props.getProperty([DATASOURCES, hostname, argument])
136
+ end
137
+
138
+ def replicator_value(hostname, argument)
139
+ self.parse()
140
+ return @props.getProperty([REPLICATORS, hostname, argument])
141
+ end
142
+
143
+ def is_replication?
144
+ self.parse()
145
+ return @props.getProperty(SERVICE_TYPE) == REPLICATION_SERVICE
146
+ end
147
+
148
+ def is_physical?
149
+ self.parse()
150
+ return @props.getProperty(SERVICE_TYPE) == PHYSICAL_SERVICE
151
+ end
152
+
153
+ def is_composite?
154
+ self.parse()
155
+ return @props.getProperty(SERVICE_TYPE) == COMPOSITE_SERVICE
156
+ end
157
+
158
+ def to_s
159
+ self.parse()
160
+ return @props.to_s()
161
+ end
162
+
163
+ def to_hash
164
+ self.parse()
165
+ return @props.props
166
+ end
167
+
168
+ def output()
169
+ self.parse()
170
+ TU.output(self.to_s)
171
+ end
172
+
173
+ def force_output()
174
+ self.parse()
175
+ TU.force_output(self.to_s)
176
+ end
177
+ end
@@ -0,0 +1,523 @@
1
+ class TungstenUtil
2
+ include Singleton
3
+ attr_accessor :remaining_arguments, :extra_arguments
4
+
5
+ def initialize()
6
+ super()
7
+
8
+ @logger_threshold = Logger::NOTICE
9
+ @previous_option_arguments = {}
10
+ @ssh_options = {}
11
+ @display_help = false
12
+ @num_errors = 0
13
+ @json_interface = false
14
+ @json_message_cache = []
15
+
16
+ # Create a temporary file to hold log contents
17
+ @log = Tempfile.new("tlog")
18
+ # Unlink the file so that no other process can read this log
19
+ @log.unlink()
20
+
21
+ arguments = ARGV.dup
22
+
23
+ if arguments.size() > 0
24
+ @extra_arguments = []
25
+
26
+ # This fixes an error that was coming up reading certain characters wrong
27
+ # The root cause is unknown but this allowed Ruby to parse the
28
+ # arguments properly
29
+ send_to_extra_arguments = false
30
+ arguments = arguments.map{|arg|
31
+ newarg = ''
32
+ arg.split("").each{|b|
33
+ unless b.getbyte(0)<33 || b.getbyte(0)>127 then
34
+ newarg.concat(b)
35
+ end
36
+ }
37
+
38
+ if newarg == "--"
39
+ send_to_extra_arguments = true
40
+ nil
41
+ elsif send_to_extra_arguments == true
42
+ @extra_arguments << newarg
43
+ nil
44
+ else
45
+ newarg
46
+ end
47
+ }
48
+
49
+ opts=OptionParser.new
50
+ opts.on("-i", "--info") {@logger_threshold = Logger::INFO}
51
+ opts.on("-n", "--notice") {@logger_threshold = Logger::NOTICE}
52
+ opts.on("-q", "--quiet") {@logger_threshold = Logger::WARN}
53
+ opts.on("-v", "--verbose") {@logger_threshold = Logger::DEBUG}
54
+ opts.on("--json") { @json_interface = true }
55
+ opts.on("-h", "--help") { @display_help = true }
56
+ opts.on("--net-ssh-option String") {|val|
57
+ val_parts = val.split("=")
58
+ if val_parts.length() !=2
59
+ error "Invalid value #{val} given for '--net-ssh-option'. There should be a key/value pair joined by a single =."
60
+ end
61
+
62
+ if val_parts[0] == "timeout"
63
+ val_parts[1] = val_parts[1].to_i
64
+ end
65
+
66
+ @ssh_options[val_parts[0].to_sym] = val_parts[1]
67
+ }
68
+
69
+ log(JSON.pretty_generate(arguments))
70
+ @remaining_arguments = run_option_parser(opts, arguments)
71
+ else
72
+ @remaining_arguments = []
73
+ @extra_arguments = []
74
+ end
75
+ end
76
+
77
+ def exit(code = 0)
78
+ if @json_interface == true
79
+ puts JSON.pretty_generate({
80
+ "rc" => code,
81
+ "messages" => @json_message_cache
82
+ })
83
+ end
84
+
85
+ Kernel.exit(code)
86
+ end
87
+
88
+ def display_help?
89
+ (@display_help == true)
90
+ end
91
+
92
+ def display_help
93
+ write_header("Global Options", nil)
94
+ output_usage_line("--directory", "Use this installed Tungsten directory as the base for all operations")
95
+ output_usage_line("--quiet, -q")
96
+ output_usage_line("--info, -i")
97
+ output_usage_line("--notice, -n")
98
+ output_usage_line("--verbose, -v")
99
+ output_usage_line("--help, -h", "Display this message")
100
+ output_usage_line("--json", "Provide return code and logging messages as a JSON object after the script finishes")
101
+ output_usage_line("--net-ssh-option=key=value", "Set the Net::SSH option for remote system calls", nil, nil, "Valid options can be found at http://net-ssh.github.com/ssh/v2/api/classes/Net/SSH.html#M000002")
102
+ end
103
+
104
+ def get_autocomplete_arguments
105
+ [
106
+ '--directory',
107
+ '--quiet', '-q',
108
+ '--info', '-i',
109
+ '--notice', '-n',
110
+ '--verbose', '-v',
111
+ '--help', '-h',
112
+ '--json',
113
+ '--net-ssh-option='
114
+ ]
115
+ end
116
+
117
+ def get_base_path
118
+ File.expand_path("#{File.dirname(__FILE__)}/../../../..")
119
+ end
120
+
121
+ def enable_output?
122
+ true
123
+ end
124
+
125
+ def output(content)
126
+ write(content, nil)
127
+ end
128
+
129
+ def force_output(content)
130
+ log(content)
131
+
132
+ puts(content)
133
+ $stdout.flush()
134
+ end
135
+
136
+ def log(content = nil)
137
+ if content == nil
138
+ @log
139
+ else
140
+ @log.puts DateTime.now.to_s + " " + content
141
+ @log.flush
142
+ end
143
+ end
144
+
145
+ def set_log_path(path)
146
+ TU.mkdir_if_absent(File.dirname(path))
147
+ old_log = @log
148
+ old_log.rewind()
149
+
150
+ @log = File.open(path, "w")
151
+ @log.puts(old_log.read())
152
+ end
153
+
154
+ def reset_errors
155
+ @num_errors = 0
156
+ end
157
+
158
+ def is_valid?
159
+ (@num_errors == 0)
160
+ end
161
+
162
+ def write(content="", level=Logger::INFO, hostname = nil, add_prefix = true)
163
+ if content.is_a?(Array)
164
+ content.each{
165
+ |c|
166
+ write(c, level, hostname, add_prefix)
167
+ }
168
+ return
169
+ end
170
+
171
+ unless content == "" || level == nil || add_prefix == false
172
+ content = "#{get_log_level_prefix(level, hostname)}#{content}"
173
+ end
174
+
175
+ # Attempt to determine the level for this message based on it's content
176
+ # If it is forwarded from another Tungsten script it will have a prefix
177
+ # so we know to use stdout or stderr
178
+ if level == nil
179
+ level = parse_log_level(content)
180
+ end
181
+
182
+ if level == Logger::ERROR
183
+ @num_errors = @num_errors + 1
184
+ end
185
+
186
+ log(content)
187
+
188
+ if enable_log_level?(level)
189
+ if @json_interface == true
190
+ @json_message_cache << content
191
+ else
192
+ if enable_output?()
193
+ if level != nil && level > Logger::NOTICE
194
+ $stdout.puts(content)
195
+ $stdout.flush()
196
+ else
197
+ $stdout.puts(content)
198
+ $stdout.flush()
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ # Write a header
206
+ def write_header(content, level=Logger::INFO)
207
+ write("#####################################################################", level, nil, false)
208
+ write("# #{content}", level, nil, false)
209
+ write("#####################################################################", level, nil, false)
210
+ end
211
+
212
+ def info(message, hostname = nil)
213
+ write(message, Logger::INFO, hostname)
214
+ end
215
+
216
+ def notice(message, hostname = nil)
217
+ write(message, Logger::NOTICE, hostname)
218
+ end
219
+
220
+ def warning(message, hostname = nil)
221
+ write(message, Logger::WARN, hostname)
222
+ end
223
+
224
+ def error(message, hostname = nil)
225
+ write(message, Logger::ERROR, hostname)
226
+ end
227
+
228
+ def exception(e, hostname = nil)
229
+ error(e.to_s(), hostname)
230
+ debug(e, hostname)
231
+ end
232
+
233
+ def debug(message, hostname = nil)
234
+ if message.is_a?(StandardError)
235
+ message = message.to_s() + ":\n" + message.backtrace.join("\n")
236
+ end
237
+ write(message, Logger::DEBUG, hostname)
238
+ end
239
+
240
+ def get_log_level_prefix(level=Logger::INFO, hostname = nil)
241
+ case level
242
+ when Logger::ERROR then prefix = "ERROR"
243
+ when Logger::WARN then prefix = "WARN "
244
+ when Logger::DEBUG then prefix = "DEBUG"
245
+ when Logger::NOTICE then prefix = "NOTE "
246
+ else
247
+ prefix = "INFO "
248
+ end
249
+
250
+ if hostname == nil
251
+ "#{prefix} >> "
252
+ else
253
+ "#{prefix} >> #{hostname} >> "
254
+ end
255
+ end
256
+
257
+ def parse_log_level(line)
258
+ prefix = line[0,5]
259
+
260
+ case prefix.strip
261
+ when "ERROR" then Logger::ERROR
262
+ when "WARN" then Logger::WARN
263
+ when "DEBUG" then Logger::DEBUG
264
+ when "NOTE" then Logger::NOTICE
265
+ when "INFO" then Logger::INFO
266
+ else
267
+ nil
268
+ end
269
+ end
270
+
271
+ # Split a log line into the log level and actual message
272
+ # This is used when forwarding log messages from a remote commmand
273
+ def split_log_content(content)
274
+ level = parse_log_level(content)
275
+ if level != nil
276
+ prefix = get_log_level_prefix(level)
277
+ content = content[prefix.length, content.length]
278
+ end
279
+
280
+ return [level, content]
281
+ end
282
+
283
+ def enable_log_level?(level=Logger::INFO)
284
+ if level == nil
285
+ true
286
+ elsif level < @logger_threshold
287
+ false
288
+ else
289
+ true
290
+ end
291
+ end
292
+
293
+ def get_log_level
294
+ @logger_threshold
295
+ end
296
+
297
+ def set_log_level(level=Logger::INFO)
298
+ @logger_threshold = level
299
+ end
300
+
301
+ def whoami
302
+ if ENV['USER']
303
+ ENV['USER']
304
+ elsif ENV['LOGNAME']
305
+ ENV['LOGNAME']
306
+ else
307
+ `whoami 2>/dev/null`.chomp
308
+ end
309
+ end
310
+
311
+ # Run the OptionParser object against @remaining_arguments or the
312
+ # {arguments} passed in. If no {arguments} value is given, the function
313
+ # will read from @remaining_arguments and update it with any remaining
314
+ # values
315
+ def run_option_parser(opts, arguments = nil, allow_invalid_options = true, invalid_option_prefix = nil)
316
+ if arguments == nil
317
+ use_remaining_arguments = true
318
+ arguments = @remaining_arguments
319
+ else
320
+ use_remaining_arguments = false
321
+ end
322
+
323
+ # Collect the list of options and remove the first two that are
324
+ # created by default
325
+ option_lists = opts.stack()
326
+ option_lists.shift()
327
+ option_lists.shift()
328
+
329
+ option_lists.each{
330
+ |ol|
331
+
332
+ ol.short.keys().each{
333
+ |arg|
334
+ if @previous_option_arguments.has_key?(arg)
335
+ error("The -#{arg} argument has already been captured")
336
+ end
337
+ @previous_option_arguments[arg] = true
338
+ }
339
+ ol.long.keys().each{
340
+ |arg|
341
+ if @previous_option_arguments.has_key?(arg)
342
+ error("The --#{arg} argument has already been captured")
343
+ end
344
+ @previous_option_arguments[arg] = true
345
+ }
346
+ }
347
+
348
+ remainder = []
349
+ while arguments.size() > 0
350
+ begin
351
+ arguments = opts.order!(arguments)
352
+
353
+ # The next argument does not have a dash so the OptionParser
354
+ # ignores it, we will add it to the stack and continue
355
+ if arguments.size() > 0 && (arguments[0] =~ /-.*/) != 0
356
+ remainder << arguments.shift()
357
+ end
358
+ rescue OptionParser::InvalidOption => io
359
+ if allow_invalid_options
360
+ # Prepend the invalid option onto the arguments array
361
+ remainder = remainder + io.recover([])
362
+
363
+ # The next argument does not have a dash so the OptionParser
364
+ # ignores it, we will add it to the stack and continue
365
+ if arguments.size() > 0 && (arguments[0] =~ /-.*/) != 0
366
+ remainder << arguments.shift()
367
+ end
368
+ else
369
+ if invalid_option_prefix != nil
370
+ io.reason = invalid_option_prefix
371
+ end
372
+
373
+ raise io
374
+ end
375
+ rescue => e
376
+ exception(e)
377
+ raise "Argument parsing failed: #{e.to_s()}"
378
+ end
379
+ end
380
+
381
+ if use_remaining_arguments == true
382
+ @remaining_arguments = remainder
383
+ return nil
384
+ else
385
+ return remainder
386
+ end
387
+ end
388
+
389
+ # Returns [width, height] of terminal when detected, nil if not detected.
390
+ # Think of this as a simpler version of Highline's Highline::SystemExtensions.terminal_size()
391
+ def detect_terminal_size
392
+ unless @terminal_size
393
+ if (ENV['COLUMNS'] =~ /^\d+$/) && (ENV['LINES'] =~ /^\d+$/)
394
+ @terminal_size = [ENV['COLUMNS'].to_i, ENV['LINES'].to_i]
395
+ elsif (RUBY_PLATFORM =~ /java/ || (!STDIN.tty? && ENV['TERM'])) && command_exists?('tput')
396
+ @terminal_size = [`tput cols`.to_i, `tput lines`.to_i]
397
+ elsif STDIN.tty? && command_exists?('stty')
398
+ @terminal_size = `stty size`.scan(/\d+/).map { |s| s.to_i }.reverse
399
+ else
400
+ @terminal_size = [80, 30]
401
+ end
402
+ end
403
+
404
+ return @terminal_size
405
+ rescue => e
406
+ [80, 30]
407
+ end
408
+
409
+ # Display information about the argument formatting {msg} so all lines
410
+ # appear formatted correctly
411
+ def output_usage_line(argument, msg = "", default = nil, max_line = nil, additional_help = "")
412
+ if max_line == nil
413
+ max_line = detect_terminal_size()[0]-5
414
+ end
415
+
416
+ if msg.is_a?(String)
417
+ msg = msg.split("\n").join(" ")
418
+ else
419
+ msg = msg.to_s()
420
+ end
421
+
422
+ msg = msg.gsub(/^\s+/, "").gsub(/\s+$/, $/)
423
+
424
+ if default.to_s() != ""
425
+ if msg != ""
426
+ msg += " "
427
+ end
428
+
429
+ msg += "[#{default}]"
430
+ end
431
+
432
+ if argument.length > 28 || (argument.length + msg.length > max_line)
433
+ output(argument)
434
+
435
+ wrapped_lines(msg, 29).each{
436
+ |line|
437
+ output(line)
438
+ }
439
+ else
440
+ output(format("%-29s", argument) + " " + msg)
441
+ end
442
+
443
+ if additional_help.to_s != ""
444
+ additional_help = additional_help.split("\n").map!{
445
+ |line|
446
+ line.strip()
447
+ }.join(' ')
448
+ additional_help.split("<br>").each{
449
+ |line|
450
+ output_usage_line("", line, nil, max_line)
451
+ }
452
+ end
453
+ end
454
+
455
+ # Break {msg} into lines that have {offset} leading spaces and do
456
+ # not exceed {max_line}
457
+ def wrapped_lines(msg, offset = 0, max_line = nil)
458
+ if max_line == nil
459
+ max_line = detect_terminal_size()[0]-5
460
+ end
461
+ if offset == 0
462
+ default_line = ""
463
+ else
464
+ line_format = "%-#{offset}s"
465
+ default_line = format(line_format, " ")
466
+ end
467
+
468
+ lines = []
469
+ words = msg.split(' ')
470
+
471
+ force_add_word = true
472
+ line = default_line.dup()
473
+ while words.length() > 0
474
+ if !force_add_word && line.length() + words[0].length() > max_line
475
+ lines << line
476
+ line = default_line.dup()
477
+ force_add_word = true
478
+ else
479
+ if line == ""
480
+ line = words.shift()
481
+ else
482
+ line += " " + words.shift()
483
+ end
484
+ force_add_word = false
485
+ end
486
+ end
487
+ lines << line
488
+
489
+ return lines
490
+ end
491
+
492
+ def to_identifier(str)
493
+ str.tr('.', '_').tr('-', '_').tr('/', '_').tr('\\', '_').downcase()
494
+ end
495
+
496
+ # Create a directory if it is absent.
497
+ def mkdir_if_absent(dirname)
498
+ if dirname == nil
499
+ return
500
+ end
501
+
502
+ if File.exists?(dirname)
503
+ if File.directory?(dirname)
504
+ debug("Found directory, no need to create: #{dirname}")
505
+ else
506
+ raise "Directory already exists as a file: #{dirname}"
507
+ end
508
+ else
509
+ debug("Creating missing directory: #{dirname}")
510
+ cmd_result("mkdir -p #{dirname}")
511
+ end
512
+ end
513
+
514
+ def pluralize(array, singular, plural = nil)
515
+ if plural == nil
516
+ singular
517
+ elsif array.size() > 1
518
+ plural
519
+ else
520
+ singular
521
+ end
522
+ end
523
+ end