continuent-tools-core 0.0.1

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,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