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,553 @@
1
+ module TungstenAPI
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+
6
+
7
+
8
+ # SAMPLE USAGE:
9
+ # api_server='localhost:8090'
10
+ # service=chicago
11
+ #
12
+ # cctrl = TungstenDataserviceManager.new(api_server)
13
+ # cctrl.list(:text)
14
+ # puts ''
15
+ # json_obj = cctrl.get(service, 'policy')
16
+ # pp json_obj["message"]
17
+ #
18
+ # APICall.set_return_on_call_fail(:hash)
19
+ # json_obj = cctrl.post("#{service}/host1", 'promote')
20
+ # pp json_obj["message"] ## Failure message will come here
21
+ #
22
+ # begin
23
+ # APICall.set_return_on_call_fail(:raise)
24
+ # json_obj = cctrl.post(service}/host1', 'promote')
25
+ # pp json_obj["message"]
26
+ # rescue Exception => e
27
+ # puts e # failure message will coem here
28
+ # end
29
+
30
+
31
+ #
32
+ # This class defines an API call, as needed by the Tungsten Manager API.
33
+ # Up to a ppint, the class is fairly generic, as it defines how to create a URI
34
+ # with three to four segments.
35
+ #
36
+ # An APICall instance can run a 'get' or a 'post' call.
37
+ #
38
+ # Class methods:
39
+ # * set_api_root : defines which string is the root of the URI (default: 'manager')
40
+ # * set_return_on_call_fail: defines how we fail when a call does not succeed.
41
+ # You can set either :hash (default) or :raise. When :hash is defined, get and post
42
+ # always return a hash containing 'message' and 'httpStatus'. If :raise is chosen
43
+ # then all call failures will raise an exception
44
+ # * header : used to display the list of API calls
45
+ # * dashes : used to draw dashes below the header. (internally used by TungstenDataserviceManager::list)
46
+ #
47
+ # Public instance methods:
48
+ # * initialize (name, prefix, command, help, return_structure = :hash, type = :get)
49
+ # * description (display_mode ={:text|:hash:json} ) shows the API structure
50
+ # * make_uri (api_server, service) : creates a well formed URI, ready to submit
51
+ # * get (api_server, service) : returns the result of a 'get' call
52
+ # * post (api_server, service) : returns the result of a 'post' operation
53
+ #
54
+ class APICall
55
+
56
+ attr_accessor :name, :prefix, :command, :return_structure, :type
57
+ #
58
+ # By default, a call using this class will return a hash
59
+ # You can change this behavior by setting the fail type to :raise instead
60
+ #
61
+ @@return_on_call_fail=:hash
62
+
63
+ @@api_root = 'manager'
64
+
65
+ #
66
+ # This template is used to display a quick help about the API call
67
+ #
68
+ @@template = "%-15s %-4s %-10s %-10s %s"
69
+ #
70
+ # name
71
+ # type
72
+ # prefix
73
+ # command
74
+ # help
75
+
76
+ #
77
+ # Initialize the object.
78
+ # * name : how we call the API, even informally. This name is not used operationally
79
+ # * prefix: the portion of the call that needs to be inserted before the service name
80
+ # * command: what the api call responds to. For some calls, this part can be empty
81
+ # * type: either :get or :post
82
+ # * return_structure: so far, only :hash is supported
83
+ # * help: a brief description of what the API does
84
+ #
85
+ def initialize(name, prefix, command, help, return_structure = :hash, type = :get, ignore_service=false)
86
+ @name = name
87
+ @prefix = prefix
88
+ @command = command
89
+ @type = type # type can be :get, :post, :cmd
90
+ @returns = return_structure
91
+ @help = help
92
+ @ignore_service = ignore_service
93
+ # TODO : add expected structure
94
+ end
95
+
96
+ def from_hash (hash)
97
+ @name = hash[name]
98
+ @prefix = hash[prefix]
99
+ @command = hash[command]
100
+ @type = hash[type] # type can be :get, :post, :cmd
101
+ @returns = hash[return_structure]
102
+ @help = hash[help]
103
+ self
104
+ end
105
+
106
+ #
107
+ # Creates a well formed URI, ready to be used
108
+ #
109
+ def make_uri(api_server, service)
110
+ if (service && ! @ignore_service)
111
+ return "http://#{api_server}/#{@@api_root}/#{@prefix}/#{service}/#{@command}"
112
+ else
113
+ return "http://#{api_server}/#{@@api_root}/#{@command}"
114
+ end
115
+ end
116
+
117
+ #
118
+ # Class method. Defines how we behave in case of call failure
119
+ # By default, we ALWAYS return a :hash. We can also :raise
120
+ # an exception
121
+ #
122
+ def self.set_return_on_call_fail(return_on_call_fail)
123
+ @@return_on_call_fail = return_on_call_fail
124
+ end
125
+
126
+ #
127
+ # Defines the default API root in the URI.
128
+ # Currently it is 'manager'
129
+ #
130
+ def self.set_api_root(api_root)
131
+ @@api_root = api_root
132
+ end
133
+
134
+ #
135
+ # returns a header for the API call fields
136
+ #
137
+ def self.header ()
138
+ return sprintf(@@template , 'name', 'type', 'prefix', 'command' , 'help')
139
+ end
140
+
141
+ #
142
+ # returns a set of dashes to ptint below the header
143
+ #
144
+ def self.dashes ()
145
+ return sprintf(@@template , '----', '----', '------', '-------' , '----')
146
+ end
147
+
148
+ def to_s
149
+ return sprintf(@@template , @name, @type, @prefix, @command , @help)
150
+ end
151
+
152
+ def to_hash
153
+ {
154
+ :name.to_s => @name,
155
+ :type.to_s => @type,
156
+ :prefix.to_s => @prefix,
157
+ :command.to_s => @command,
158
+ :help.to_s => @help
159
+ }
160
+ end
161
+
162
+ #
163
+ # Returns a description of the API call, according to the display_mode:
164
+ # * :text (default) is a single line of text according to @@template
165
+ # * :hash is a Ruby hash of the API call contents
166
+ # * :json is a JSON representation of the above hash
167
+ #
168
+ def description (display_mode = :text)
169
+ if display_mode == :text
170
+ return self.to_s
171
+ end
172
+ if display_mode == :json
173
+ return JSON.generate(self.to_hash)
174
+ elsif display_mode == :hash
175
+ return self.to_hash
176
+ else
177
+ raise SyntaxError, "No suitable display mode selected"
178
+ end
179
+ end
180
+
181
+ #
182
+ # Used internally by the calls to get and post to determine if the response was successful
183
+ #
184
+ def evaluate_response (api_server, response)
185
+ if response.body
186
+ hash_from_json = JSON.parse(response.body)
187
+ end
188
+
189
+ unless hash_from_json
190
+ raise "Unable to parse API response from #{api_server}"
191
+ end
192
+
193
+ if TU.log_cmd_results?()
194
+ TU.debug("Result: #{JSON.pretty_generate(hash_from_json)}")
195
+ end
196
+
197
+ if hash_from_json && hash_from_json["returnMessage"] && hash_from_json["returnCode"] && hash_from_json["returnCode"] != '200'
198
+ return_object = {
199
+ "httpStatus" => hash_from_json["returnCode"],
200
+ "message" => hash_from_json["returnMessage"]
201
+ }
202
+ if @@return_on_call_fail == :raise
203
+ raise RuntimeError, "There was an error (#{hash_from_json["returnCode"]}) : #{hash_from_json["returnMessage"]}"
204
+ end
205
+ return return_object
206
+ end
207
+
208
+ if response.code != '200'
209
+ if @@return_on_call_fail == :raise
210
+ raise RuntimeError, "The request returned code #{response.code}"
211
+ else
212
+ return_object = {
213
+ "httpStatus" => response.code,
214
+ "message" => "unidentified error with code #{response.code}"
215
+ }
216
+ return return_object
217
+ end
218
+ end
219
+ return hash_from_json
220
+ end
221
+
222
+ #
223
+ # Runs a 'get' call, using a given api_server and service name
224
+ #
225
+ def get(api_server, service)
226
+ api_uri = URI(self.make_uri(api_server,service))
227
+ puts "GET #{api_uri}" if ENV["SHOW_INTERNALS"]
228
+ TU.debug("GET #{api_uri}")
229
+ response = Net::HTTP.get_response(api_uri)
230
+ return evaluate_response(api_server,response)
231
+ end
232
+
233
+ #
234
+ # Runs a 'post' call, using a given api_server and service name
235
+ #
236
+ def post(api_server, service, post_params = {})
237
+ api_uri = URI(self.make_uri(api_server,service))
238
+ puts "POST #{api_uri}" if ENV["SHOW_INTERNALS"]
239
+ TU.debug("POST #{api_uri}")
240
+ response = Net::HTTP.post_form(api_uri, post_params)
241
+ return evaluate_response(api_server,response)
242
+ end
243
+ end
244
+
245
+ #
246
+ # ReplicatorAPICall is a class that handles API Calls through Tungsten Replicator tools
247
+ #
248
+ # It uses the same interface as APICall, but it is initialized differently
249
+ #
250
+ # Public instance methods:
251
+ # * initialize (name, tools_path, tool, command, help, rmi_port=10000)
252
+ # * get (service)
253
+ #
254
+ class ReplicatorAPICall < APICall
255
+
256
+ attr_accessor :command
257
+
258
+ @@template = '%-15s %-10s %-25s %s'
259
+ # name
260
+ # tool
261
+ # command
262
+ # help
263
+
264
+ #
265
+ # Initializes a ReplicatorAPICall object
266
+ # name : how to identify the API call
267
+ # tool : which tool we are calling
268
+ # tools_path : where to find the tool
269
+ # rmi_port : which port should trepctl use
270
+ # help : a brief description of the API call
271
+ #
272
+ def initialize (name, tools_path, tool, command, help, rmi_port=10000)
273
+ @name = name
274
+ @tool = tool
275
+ @command = command
276
+ @tools_path = tools_path
277
+ @rmi_port = rmi_port
278
+ @help = help
279
+ end
280
+
281
+ #
282
+ # Get a JSON object from the output of a command, such as 'trepctl status -json'
283
+ # If 'service' and host are given, the command is called with -service #{service} and -host #{host}
284
+ #
285
+ def get(service, host, more_options)
286
+ service_clause=''
287
+ host_clause=''
288
+ port_clause=''
289
+ more_options = '' unless more_options
290
+ if service
291
+ service_clause = "-service #{service}"
292
+ end
293
+ if host
294
+ host_clause = "-host #{host}"
295
+ end
296
+ if @rmi_port
297
+ port_clause= "-port #{@rmi_port}"
298
+ end
299
+ full_command = "#{@tools_path}/#{@tool} #{port_clause} #{host_clause} #{service_clause} #{command} #{more_options}"
300
+ puts full_command if ENV["SHOW_INTERNALS"]
301
+ json_text = %x(#{full_command})
302
+ return JSON.parse(json_text)
303
+ end
304
+
305
+ #
306
+ # override ancestor's header
307
+ #
308
+ def self.header
309
+ return sprintf(@@template, :name.to_s, :tool.to_s, :command.to_s, :help.to_s)
310
+ end
311
+
312
+ #
313
+ # override ancestor's dashes
314
+ #
315
+ def self.dashes
316
+ return sprintf(@@template, '----', '----', '-------', '----')
317
+ end
318
+
319
+ def to_s
320
+ return sprintf(@@template, @name, @tool, @command, @help)
321
+ end
322
+
323
+ def to_hash
324
+ return { :name.to_s => @name, :tool.to_s => @tool, :command.to_s => @command , :help.to_s => @help }
325
+ end
326
+ end
327
+
328
+ #
329
+ # Container for API calls.
330
+ # It has the definition of the api calls supported through this architecture, and methods to call them easily.
331
+ #
332
+ # Public instance methods:
333
+ # * initialize(api_server)
334
+ # * list (display_mode)
335
+ # will show all the API registered with this service
336
+ # * set_server(api_server)
337
+ # * get(service,name) will return the result of a get call
338
+ # * post(service,name) will return the result of a post operation
339
+ #
340
+
341
+ class TungstenDataserviceManager
342
+
343
+ #
344
+ # Registers all the known API calls for Tungsten data service
345
+ #
346
+ def initialize(api_server)
347
+ @api_server = api_server
348
+ @api_calls = {}
349
+ #
350
+ # get
351
+ #
352
+ add_api_call( APICall.new('status', 'status', '', 'Show cluster status', :hash, :get) )
353
+ add_api_call( APICall.new('policy', 'policy', '', 'Show current policy',:hash, :get) )
354
+ add_api_call( APICall.new('routers', '', 'service/router/status', 'Shows the routers for this data service',:hash, :get, true) )
355
+ add_api_call( APICall.new('members', '', 'service/members', 'Shows the members for this data service',:hash, :get, true) )
356
+
357
+ #
358
+ # post
359
+ #
360
+ add_api_call( APICall.new('setmaintenance', 'policy', 'maintenance', 'set policy as maintenance',:hash, :post) )
361
+ add_api_call( APICall.new('setautomatic', 'policy', 'automatic', 'set policy as automatic',:hash, :post) )
362
+ add_api_call( APICall.new('setmanual', 'policy', 'manual', 'set policy as manual',:hash, :post) )
363
+
364
+ add_api_call( APICall.new('setarchive', 'control', 'setarchive', 'Sets the archve flag for a slave', :hash, :post) )
365
+ add_api_call( APICall.new('cleararchive', 'control', 'cleararchive', 'Clears the archve flag for a slave', :hash, :post) )
366
+ add_api_call( APICall.new('promote', 'control', 'promote', 'promotes a slave to master', :hash, :post) )
367
+ add_api_call( APICall.new('shun', 'control', 'shun', 'shuns a data source',:hash, :post) )
368
+ add_api_call( APICall.new('welcome', 'control', 'welcome', 'welcomes back a data source',:hash, :post) )
369
+ add_api_call( APICall.new('backup', 'control', 'backup', 'performs a datasource backup',:hash, :post) )
370
+ add_api_call( APICall.new('restore', 'control', 'restore', 'Performs a datasource restore',:hash, :post) )
371
+ add_api_call( APICall.new('online', 'control', 'online', 'puts a datasource online',:hash, :post) )
372
+ add_api_call( APICall.new('offline', 'control', 'offline', 'Puts a datasource offline',:hash, :post) )
373
+ add_api_call( APICall.new('fail', 'control', 'fail', 'fails a datasource',:hash, :post) )
374
+ add_api_call( APICall.new('recover', 'control', 'recover', 'recover a failed datasource',:hash, :post) )
375
+ add_api_call( APICall.new('heartbeat', 'control', 'heartbeat', 'Issues a heartbeat on the master',:hash, :post) )
376
+ end
377
+
378
+ #
379
+ # Changes the default api_server
380
+ #
381
+ def set_server (api_server)
382
+ @api_server = api_server
383
+ end
384
+
385
+ #
386
+ # Registers a given API call into the service
387
+ # It is safe to use in derived classes
388
+ #
389
+ def add_api_call (api_call)
390
+ @api_calls[api_call.name()] = api_call
391
+ end
392
+
393
+ #
394
+ # returns the header for the api list
395
+ # It must be overriden by derived classes
396
+ #
397
+ def header
398
+ return APICall.header
399
+ end
400
+
401
+ #
402
+ # returns the sub-header dashes for the api list
403
+ # It must be overriden by derived classes
404
+ #
405
+ def dashes
406
+ return APICall.dashes
407
+ end
408
+
409
+ #
410
+ # Display the list of registered API calls
411
+ # using a given display_mode:
412
+ # * :text (default)
413
+ # * :hash : good for further usage of the API call within the same application
414
+ # * :json : good to export to other applications
415
+ #
416
+ # Safe to use in derived classes
417
+ #
418
+ def list (display_mode=:text)
419
+ if display_mode == :text
420
+ puts header()
421
+ puts dashes()
422
+ @api_calls.sort.each do |name,api|
423
+ puts api
424
+ end
425
+ else
426
+ if display_mode == :hash
427
+ pp self.to_hash
428
+ elsif display_mode == :json
429
+ puts JSON.generate(self.to_hash)
430
+ else
431
+ raise SyntaxError, "no suitable display method selected"
432
+ end
433
+ end
434
+ end
435
+
436
+ #
437
+ # Returns a Hash with the list of API calls
438
+ #
439
+ def to_hash
440
+ display_api_calls = {}
441
+ @api_calls.each do |name,api|
442
+ display_api_calls[name] = api.to_hash
443
+ end
444
+ display_api_calls
445
+ end
446
+
447
+ #
448
+ # Runs a 'get' call with a given API
449
+ #
450
+ def get ( service, name, api_server=nil )
451
+ api_server ||= @api_server
452
+ return call(service,name,:get, api_server)
453
+ end
454
+
455
+ #
456
+ # Runs a 'post' call with a given API
457
+ #
458
+ def post (service, name, api_server = nil)
459
+ api_server ||= @api_server
460
+ return call(service,name,:post, api_server)
461
+ end
462
+
463
+ #
464
+ # Calls the API using the method for which the call was registered.
465
+ # There is no need to specify :get or :post
466
+ #
467
+ def call_default (service, name, api_server=nil )
468
+ api_server ||= @api_server
469
+ api = @api_calls[name].to_hash
470
+ if api.type == :get
471
+ return call(service,name,:get, api_server)
472
+ else
473
+ return call(service,name,:post, api_server)
474
+ end
475
+ end
476
+
477
+ #
478
+ # Calls a named service with explicit mode (:get or :post)
479
+ #
480
+ def call (service, name , type=nil, api_server=nil)
481
+ api_server ||= @api_server
482
+ api = @api_calls[name]
483
+ unless api
484
+ raise SyntaxError, "api call #{name} not found"
485
+ end
486
+ if type == nil
487
+ type = api.type
488
+ end
489
+
490
+ if type == :get
491
+ return api.get(@api_server,service)
492
+ else
493
+ return api.post(@api_server,service)
494
+ end
495
+ end
496
+ end
497
+
498
+ #
499
+ # Derived class of TungstenDataserviceManager, designed to handle calls to Replicator tools that provide json objects
500
+ # sample usage:
501
+ # cctrl = TungstenReplicator.new('/opt/continuent/cookbook_test/tungsten/tungsten-replicator/bin')
502
+ # cctrl.list()
503
+ # puts ''
504
+ # pp cctrl.get( 'status', nil)
505
+ # pp cctrl.get( 'services', nil)
506
+ # pp cctrl.get( 'tasks', 'mysvc', 'myhost')
507
+ # pp cctrl.get( 'thl_headers', nil, nil, '-low 10 -high 80')
508
+ #
509
+ class TungstenReplicator < TungstenDataserviceManager
510
+
511
+ #
512
+ # Registers all the known API calls for Tungsten data service
513
+ #
514
+ def initialize(tools_path, rmi_port=10000)
515
+ @api_calls = {}
516
+ add_api_call( ReplicatorAPICall.new('status', tools_path, 'trepctl', 'status -json', 'Show replicator status', rmi_port ) )
517
+ %w(tasks stages stores shards).each do |option|
518
+ add_api_call( ReplicatorAPICall.new(option, tools_path,'trepctl', "status -name #{option} -json", "Show replicator #{option}", rmi_port ) )
519
+ end
520
+ add_api_call( ReplicatorAPICall.new('services', tools_path, 'trepctl', 'services -json', 'Show replicator services', rmi_port ) )
521
+ add_api_call( ReplicatorAPICall.new('properties', tools_path, 'trepctl', 'properties', 'Show replicator properties', rmi_port) )
522
+ add_api_call( ReplicatorAPICall.new('thl_headers', tools_path, 'thl', 'list -headers -json', 'Show thl headers', nil) )
523
+ end
524
+
525
+ #
526
+ # Overriding ancestor's method, to make a call to 'list' safe
527
+ #
528
+ def header
529
+ return ReplicatorAPICall.header
530
+ end
531
+
532
+ #
533
+ # Overriding ancestor's method, to make a call to 'list' safe
534
+ #
535
+ def dashes
536
+ return ReplicatorAPICall.dashes
537
+ end
538
+
539
+ #
540
+ # Gets a hash from a JSON object returned from a given call
541
+ #
542
+ # name : the api being called
543
+ # service : to qualify the service, if the API requires it
544
+ # host: to run the call with a -host option
545
+ #
546
+ def get(name, service=nil, host=nil, more_options=nil)
547
+ @api_calls[name].get(service,host,more_options)
548
+ end
549
+ end
550
+
551
+
552
+ end
553
+