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