turbot 0.0.2

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.
Files changed (71) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +36 -0
  3. data/bin/turbot +17 -0
  4. data/data/cacert.pem +3988 -0
  5. data/lib/turbot/auth.rb +315 -0
  6. data/lib/turbot/cli.rb +38 -0
  7. data/lib/turbot/client/cisaurus.rb +25 -0
  8. data/lib/turbot/client/pgbackups.rb +113 -0
  9. data/lib/turbot/client/rendezvous.rb +111 -0
  10. data/lib/turbot/client/ssl_endpoint.rb +25 -0
  11. data/lib/turbot/client/turbot_postgresql.rb +148 -0
  12. data/lib/turbot/client.rb +757 -0
  13. data/lib/turbot/command/auth.rb +85 -0
  14. data/lib/turbot/command/base.rb +192 -0
  15. data/lib/turbot/command/bots.rb +326 -0
  16. data/lib/turbot/command/config.rb +123 -0
  17. data/lib/turbot/command/help.rb +179 -0
  18. data/lib/turbot/command/keys.rb +115 -0
  19. data/lib/turbot/command/logs.rb +34 -0
  20. data/lib/turbot/command/ssl.rb +43 -0
  21. data/lib/turbot/command/status.rb +51 -0
  22. data/lib/turbot/command/update.rb +47 -0
  23. data/lib/turbot/command/version.rb +23 -0
  24. data/lib/turbot/command.rb +304 -0
  25. data/lib/turbot/deprecated/help.rb +38 -0
  26. data/lib/turbot/deprecated.rb +5 -0
  27. data/lib/turbot/distribution.rb +9 -0
  28. data/lib/turbot/errors.rb +28 -0
  29. data/lib/turbot/excon.rb +11 -0
  30. data/lib/turbot/helpers/log_displayer.rb +70 -0
  31. data/lib/turbot/helpers/pg_dump_restore.rb +115 -0
  32. data/lib/turbot/helpers/turbot_postgresql.rb +213 -0
  33. data/lib/turbot/helpers.rb +521 -0
  34. data/lib/turbot/plugin.rb +165 -0
  35. data/lib/turbot/updater.rb +171 -0
  36. data/lib/turbot/version.rb +3 -0
  37. data/lib/turbot.rb +19 -0
  38. data/lib/vendor/turbot/okjson.rb +598 -0
  39. data/spec/helper/legacy_help.rb +16 -0
  40. data/spec/helper/pg_dump_restore_spec.rb +67 -0
  41. data/spec/schemas/dummy_schema.json +12 -0
  42. data/spec/spec.opts +1 -0
  43. data/spec/spec_helper.rb +220 -0
  44. data/spec/support/display_message_matcher.rb +49 -0
  45. data/spec/support/dummy_api.rb +120 -0
  46. data/spec/support/openssl_mock_helper.rb +8 -0
  47. data/spec/support/organizations_mock_helper.rb +11 -0
  48. data/spec/turbot/auth_spec.rb +214 -0
  49. data/spec/turbot/client/pgbackups_spec.rb +43 -0
  50. data/spec/turbot/client/rendezvous_spec.rb +62 -0
  51. data/spec/turbot/client/ssl_endpoint_spec.rb +48 -0
  52. data/spec/turbot/client/turbot_postgresql_spec.rb +71 -0
  53. data/spec/turbot/client_spec.rb +548 -0
  54. data/spec/turbot/command/auth_spec.rb +38 -0
  55. data/spec/turbot/command/base_spec.rb +66 -0
  56. data/spec/turbot/command/bots_spec.rb +54 -0
  57. data/spec/turbot/command/config_spec.rb +143 -0
  58. data/spec/turbot/command/help_spec.rb +90 -0
  59. data/spec/turbot/command/keys_spec.rb +117 -0
  60. data/spec/turbot/command/logs_spec.rb +60 -0
  61. data/spec/turbot/command/status_spec.rb +48 -0
  62. data/spec/turbot/command/version_spec.rb +16 -0
  63. data/spec/turbot/command_spec.rb +131 -0
  64. data/spec/turbot/helpers/turbot_postgresql_spec.rb +181 -0
  65. data/spec/turbot/helpers_spec.rb +48 -0
  66. data/spec/turbot/plugin_spec.rb +172 -0
  67. data/spec/turbot/updater_spec.rb +44 -0
  68. data/templates/manifest.json +7 -0
  69. data/templates/scraper.py +5 -0
  70. data/templates/scraper.rb +6 -0
  71. metadata +199 -0
@@ -0,0 +1,757 @@
1
+ require 'rexml/document'
2
+ require 'uri'
3
+ require 'time'
4
+ require 'turbot/auth'
5
+ require 'turbot/command'
6
+ require 'turbot/helpers'
7
+ require 'turbot/version'
8
+ require 'turbot/client/ssl_endpoint'
9
+
10
+ # A Ruby class to call the Turbot REST API. You might use this if you want to
11
+ # manage your Turbot bots from within a Ruby program, such as Capistrano.
12
+ #
13
+ # Example:
14
+ #
15
+ # require 'turbot'
16
+ # turbot = Turbot::Client.new('me@example.com', 'mypass')
17
+ # turbot.create()
18
+ #
19
+ class Turbot::Client
20
+
21
+ include Turbot::Helpers
22
+ extend Turbot::Helpers
23
+
24
+ def self.version
25
+ Turbot::VERSION
26
+ end
27
+
28
+ def self.gem_version_string
29
+ "turbot-gem/#{version}"
30
+ end
31
+
32
+ attr_accessor :host, :user, :password
33
+
34
+ def initialize(user, password, host=Turbot::Auth.host)
35
+ require 'rest_client'
36
+ @user = user
37
+ @password = password
38
+ @host = host
39
+ end
40
+
41
+ def self.deprecate
42
+ method = caller.first.split('`').last[0...-1]
43
+ source = caller[1].split(' ').first[0...-3]
44
+ $stderr.puts(" ! DEPRECATED: Turbot::Client##{method} is deprecated, please use the turbot-api gem.")
45
+ $stderr.puts(" ! DEPRECATED: More information available at https://github.com/openc/turbot-client")
46
+ $stderr.puts(" ! DEPRECATED: Deprecated method called from #{source}.")
47
+ end
48
+
49
+ def deprecate
50
+ self.class.deprecate
51
+ end
52
+
53
+ def self.auth(user, password, host=Turbot::Auth.host)
54
+ deprecate # 08/01/2012
55
+ client = new(user, password, host)
56
+ json_decode client.post('/login', { :username => user, :password => password }, :accept => 'json').to_s
57
+ end
58
+
59
+ # Show a list of bots which you are a collaborator on.
60
+ def list
61
+ deprecate # 07/26/2012
62
+ doc = xml(get('/bots').to_s)
63
+ doc.elements.to_a("//bots/bot").map do |a|
64
+ name = a.elements.to_a("name").first
65
+ owner = a.elements.to_a("owner").first
66
+ [name.text, owner.text]
67
+ end
68
+ end
69
+
70
+ # Show info such as mode, custom domain, and collaborators on an bot.
71
+ def info(name_or_domain)
72
+ deprecate # 07/26/2012
73
+ raise ArgumentError.new("name_or_domain is required for info") unless name_or_domain
74
+ name_or_domain = name_or_domain.gsub(/^(http:\/\/)?(www\.)?/, '')
75
+ doc = xml(get("/bots/#{name_or_domain}").to_s)
76
+ attrs = hash_from_xml_doc(doc)[:bot]
77
+ attrs.merge!(:collaborators => list_collaborators(attrs[:name]))
78
+ attrs.merge!(:addons => installed_addons(attrs[:name]))
79
+ end
80
+
81
+ # Create a new bot, with an optional name.
82
+ def create(name=nil, options={})
83
+ deprecate # 07/26/2012
84
+ name = create_request(name, options)
85
+ loop do
86
+ break if create_complete?(name)
87
+ sleep 1
88
+ end
89
+ name
90
+ end
91
+
92
+ def create_bot(name=nil, options={})
93
+ deprecate # 07/26/2012
94
+ options[:name] = name if name
95
+ json_decode(post("/bots", { :bot => options }, :accept => "application/json").to_s)
96
+ end
97
+
98
+ def create_request(name=nil, options={})
99
+ deprecate # 07/26/2012
100
+ options[:name] = name if name
101
+ xml(post('/bots', :bot => options).to_s).elements["//bot/name"].text
102
+ end
103
+
104
+ def create_complete?(name)
105
+ deprecate # 07/26/2012
106
+ put("/bots/#{name}/status", {}).code == 201
107
+ end
108
+
109
+ # Update an bot. Available attributes:
110
+ # :name => rename the bot (changes http and git urls)
111
+ def update(name, attributes)
112
+ deprecate # 07/26/2012
113
+ put("/bots/#{name}", :bot => attributes).to_s
114
+ end
115
+
116
+ # Destroy the bot permanently.
117
+ def destroy(name)
118
+ deprecate # 07/26/2012
119
+ delete("/bots/#{name}").to_s
120
+ end
121
+
122
+ def maintenance(bot_name, mode)
123
+ deprecate # 07/31/2012
124
+ mode = mode == :on ? '1' : '0'
125
+ post("/bots/#{bot_name}/server/maintenance", :maintenance_mode => mode).to_s
126
+ end
127
+
128
+ def config_vars(bot_name)
129
+ deprecate # 07/27/2012
130
+ json_decode get("/bots/#{bot_name}/config_vars", :accept => :json).to_s
131
+ end
132
+
133
+ def add_config_vars(bot_name, new_vars)
134
+ deprecate # 07/27/2012
135
+ put("/bots/#{bot_name}/config_vars", json_encode(new_vars), :accept => :json).to_s
136
+ end
137
+
138
+ def remove_config_var(bot_name, key)
139
+ deprecate # 07/27/2012
140
+ delete("/bots/#{bot_name}/config_vars/#{escape(key)}", :accept => :json).to_s
141
+ end
142
+
143
+ def clear_config_vars(bot_name)
144
+ deprecate # 07/27/2012
145
+ delete("/bots/#{bot_name}/config_vars").to_s
146
+ end
147
+
148
+ # Get a list of collaborators on the bot, returns an array of hashes each with :email
149
+ def list_collaborators(bot_name)
150
+ deprecate # 07/31/2012
151
+ doc = xml(get("/bots/#{bot_name}/collaborators").to_s)
152
+ doc.elements.to_a("//collaborators/collaborator").map do |a|
153
+ { :email => a.elements['email'].text }
154
+ end
155
+ end
156
+
157
+ # Invite a person by email address to collaborate on the bot.
158
+ def add_collaborator(bot_name, email)
159
+ deprecate # 07/31/2012
160
+ xml(post("/bots/#{bot_name}/collaborators", { 'collaborator[email]' => email }).to_s)
161
+ end
162
+
163
+ # Remove a collaborator.
164
+ def remove_collaborator(bot_name, email)
165
+ deprecate # 07/31/2012
166
+ delete("/bots/#{bot_name}/collaborators/#{escape(email)}").to_s
167
+ end
168
+
169
+ def list_domains(bot_name)
170
+ deprecate # 08/02/2012
171
+ doc = xml(get("/bots/#{bot_name}/domains").to_s)
172
+ doc.elements.to_a("//domains/*").map do |d|
173
+ attrs = { :domain => d.elements['domain'].text }
174
+ if cert = d.elements['cert']
175
+ attrs[:cert] = {
176
+ :expires_at => Time.parse(cert.elements['expires-at'].text),
177
+ :subject => cert.elements['subject'].text,
178
+ :issuer => cert.elements['issuer'].text,
179
+ }
180
+ end
181
+ attrs
182
+ end
183
+ end
184
+
185
+ def add_domain(bot_name, domain)
186
+ deprecate # 07/31/2012
187
+ post("/bots/#{bot_name}/domains", domain).to_s
188
+ end
189
+
190
+ def remove_domain(bot_name, domain)
191
+ deprecate # 07/31/2012
192
+ raise ArgumentError.new("invalid domain: #{domain.inspect}") if domain.to_s.strip == ""
193
+ delete("/bots/#{bot_name}/domains/#{domain}").to_s
194
+ end
195
+
196
+ def remove_domains(bot_name)
197
+ deprecate # 07/31/2012
198
+ delete("/bots/#{bot_name}/domains").to_s
199
+ end
200
+
201
+ # Get the list of ssh public keys for the current user.
202
+ def keys
203
+ deprecate # 07/31/2012
204
+ doc = xml get('/user/keys').to_s
205
+ doc.elements.to_a('//keys/key').map do |key|
206
+ key.elements['contents'].text
207
+ end
208
+ end
209
+
210
+ # Add an ssh public key to the current user.
211
+ def add_key(key)
212
+ deprecate # 07/31/2012
213
+ post("/user/keys", key, { 'Content-Type' => 'text/ssh-authkey' }).to_s
214
+ end
215
+
216
+ # Remove an existing ssh public key from the current user.
217
+ def remove_key(key)
218
+ deprecate # 07/31/2012
219
+ delete("/user/keys/#{escape(key)}").to_s
220
+ end
221
+
222
+ # Clear all keys on the current user.
223
+ def remove_all_keys
224
+ deprecate # 07/31/2012
225
+ delete("/user/keys").to_s
226
+ end
227
+
228
+ # Retreive ps list for the given bot name.
229
+ def ps(bot_name)
230
+ deprecate # 07/31/2012
231
+ json_decode get("/bots/#{bot_name}/ps", :accept => 'application/json').to_s
232
+ end
233
+
234
+ # Restart the bot servers.
235
+ def restart(bot_name)
236
+ deprecate # 07/31/2012
237
+ delete("/bots/#{bot_name}/server").to_s
238
+ end
239
+
240
+ def dynos(bot_name)
241
+ deprecate # 07/31/2012
242
+ doc = xml(get("/bots/#{bot_name}").to_s)
243
+ doc.elements["//bot/dynos"].text.to_i
244
+ end
245
+
246
+ def workers(bot_name)
247
+ deprecate # 07/31/2012
248
+ doc = xml(get("/bots/#{bot_name}").to_s)
249
+ doc.elements["//bot/workers"].text.to_i
250
+ end
251
+
252
+ # Scales the web dynos.
253
+ def set_dynos(bot_name, qty)
254
+ deprecate # 07/31/2012
255
+ put("/bots/#{bot_name}/dynos", :dynos => qty).to_s
256
+ end
257
+
258
+ # Scales the background dynos.
259
+ def set_workers(bot_name, qty)
260
+ deprecate # 07/31/2012
261
+ put("/bots/#{bot_name}/workers", :workers => qty).to_s
262
+ end
263
+
264
+ def ps_run(bot, opts={})
265
+ deprecate # 07/31/2012
266
+ json_decode post("/bots/#{bot}/ps", opts, :accept => :json).to_s
267
+ end
268
+
269
+ def ps_scale(bot, opts={})
270
+ deprecate # 07/31/2012
271
+ Integer(post("/bots/#{bot}/ps/scale", opts).to_s)
272
+ end
273
+
274
+ def ps_restart(bot, opts={})
275
+ deprecate # 07/31/2012
276
+ post("/bots/#{bot}/ps/restart", opts)
277
+ end
278
+
279
+ def ps_stop(bot, opts={})
280
+ deprecate # 07/31/2012
281
+ post("/bots/#{bot}/ps/stop", opts)
282
+ end
283
+
284
+ def releases(bot)
285
+ deprecate # 07/31/2012
286
+ json_decode get("/bots/#{bot}/releases", :accept => :json).to_s
287
+ end
288
+
289
+ def release(bot, release)
290
+ deprecate # 07/31/2012
291
+ json_decode get("/bots/#{bot}/releases/#{release}", :accept => :json).to_s
292
+ end
293
+
294
+ def rollback(bot, release=nil)
295
+ deprecate # 07/31/2012
296
+ post("/bots/#{bot}/releases", :rollback => release)
297
+ end
298
+
299
+ # Fetch recent logs from the bot server.
300
+ def logs(bot_name)
301
+ deprecate # 07/31/2012
302
+ get("/bots/#{bot_name}/logs").to_s
303
+ end
304
+
305
+ def list_features(bot)
306
+ deprecate # 07/31/2012
307
+ json_decode(get("features?bot=#{bot}", :accept => :json).to_s)
308
+ end
309
+
310
+ def get_feature(bot, name)
311
+ deprecate # 07/31/2012
312
+ json_decode get("features/#{name}?bot=#{bot}", :accept => :json).to_s
313
+ end
314
+
315
+ def enable_feature(bot, name)
316
+ deprecate # 07/31/2012
317
+ json_decode post("/features/#{name}?bot=#{bot}", :accept => :json).to_s
318
+ end
319
+
320
+ def disable_feature(bot, name)
321
+ deprecate # 07/31/2012
322
+ json_decode delete("/features/#{name}?bot=#{bot}", :accept => :json).to_s
323
+ end
324
+
325
+ # Get a list of stacks available to the bot, with the current one marked.
326
+ def list_stacks(bot_name, options={})
327
+ deprecate # 07/31/2012
328
+ include_deprecated = options.delete(:include_deprecated) || false
329
+
330
+ json_decode get("/bots/#{bot_name}/stack",
331
+ :params => { :include_deprecated => include_deprecated },
332
+ :accept => 'application/json'
333
+ ).to_s
334
+ end
335
+
336
+ # Request a stack migration.
337
+ def migrate_to_stack(bot_name, stack)
338
+ deprecate # 07/31/2012
339
+ put("/bots/#{bot_name}/stack", stack, :accept => 'text/plain').to_s
340
+ end
341
+
342
+ # Run a rake command on the Turbot bot and return output as a string
343
+ def rake(bot_name, cmd)
344
+ # deprecated by virtue of start deprecation 08/02/2012
345
+ start(bot_name, "rake #{cmd}", :attached).to_s
346
+ end
347
+
348
+ class Service
349
+ attr_accessor :attached
350
+
351
+ def initialize(client, bot)
352
+ require 'rest_client'
353
+ @client = client
354
+ @bot = bot
355
+ end
356
+
357
+ # start the service
358
+ def start(command, attached=false)
359
+ @attached = attached
360
+ @response = @client.post(
361
+ "/bots/#{@bot}/services",
362
+ command,
363
+ :content_type => 'text/plain'
364
+ )
365
+ @next_chunk = @response.to_s
366
+ @interval = 0
367
+ self
368
+ rescue RestClient::RequestFailed => e
369
+ raise AppCrashed, e.http_body if e.http_code == 502
370
+ raise
371
+ end
372
+
373
+ # Does the service have any remaining output?
374
+ def end_of_stream?
375
+ @next_chunk.nil?
376
+ end
377
+
378
+ # Read the next chunk of output.
379
+ def read
380
+ chunk = @client.get(@next_chunk)
381
+ if chunk.headers[:location].nil? && chunk.code != 204
382
+ # no more chunks
383
+ @next_chunk = nil
384
+ chunk.to_s
385
+ elsif chunk.to_s == ''
386
+ # assume no content and back off
387
+ @interval = 2
388
+ ''
389
+ elsif location = chunk.headers[:location]
390
+ # some data read and next chunk available
391
+ @next_chunk = location
392
+ @interval = 0
393
+ chunk.to_s
394
+ end
395
+ end
396
+
397
+ # Iterate over all output chunks until EOF is reached.
398
+ def each
399
+ until end_of_stream?
400
+ sleep(@interval)
401
+ output = read
402
+ yield output unless output.empty?
403
+ end
404
+ end
405
+
406
+ # All output as a string
407
+ def to_s
408
+ buf = []
409
+ each { |part| buf << part }
410
+ buf.join
411
+ end
412
+ end
413
+
414
+ # Run a service. If Responds to #each and yields output as it's received.
415
+ def start(bot_name, command, attached=false)
416
+ deprecate # 08/02/2012
417
+ service = Service.new(self, bot_name)
418
+ service.start(command, attached)
419
+ end
420
+
421
+ def add_ssl(bot_name, pem, key)
422
+ json_decode(post("/bots/#{bot_name}/ssl", :pem => pem, :key => key).to_s)
423
+ end
424
+
425
+ def remove_ssl(bot_name, domain)
426
+ delete("/bots/#{bot_name}/domains/#{domain}/ssl").to_s
427
+ end
428
+
429
+ def clear_ssl(bot_name)
430
+ delete("/bots/#{bot_name}/ssl")
431
+ end
432
+
433
+ class AppCrashed < RuntimeError; end
434
+
435
+ # support for console sessions
436
+ class ConsoleSession
437
+ def initialize(id, bot, client)
438
+ require 'rest_client'
439
+ @id = id; @bot = bot; @client = client
440
+ end
441
+ def run(cmd)
442
+ @client.run_console_command("/bots/#{@bot}/consoles/#{@id}/command", cmd, "=> ")
443
+ end
444
+ end
445
+
446
+ # Execute a one-off console command, or start a new console tty session if
447
+ # cmd is nil.
448
+ def console(bot_name, cmd=nil)
449
+ if block_given?
450
+ id = post("/bots/#{bot_name}/consoles").to_s
451
+ yield ConsoleSession.new(id, bot_name, self)
452
+ delete("/bots/#{bot_name}/consoles/#{id}").to_s
453
+ else
454
+ run_console_command("/bots/#{bot_name}/console", cmd)
455
+ end
456
+ rescue RestClient::BadGateway => e
457
+ raise(AppCrashed, <<-ERROR)
458
+ Unable to attach to a dyno to open a console session.
459
+ Your application may have crashed.
460
+ Check the output of "turbot ps" and "turbot logs" for more information.
461
+ ERROR
462
+ end
463
+
464
+ # internal method to run console commands formatting the output
465
+ def run_console_command(url, command, prefix=nil)
466
+ output = post(url, { :command => command }, :accept => "text/plain").to_s
467
+ return output unless prefix
468
+ if output.include?("\n")
469
+ lines = output.split("\n")
470
+ (lines[0..-2] << "#{prefix}#{lines.last}").join("\n")
471
+ else
472
+ prefix + output
473
+ end
474
+ rescue RestClient::RequestFailed => e
475
+ if e.http_code == 422
476
+ Turbot::Command.extract_error(e.http_body, :raw => true)
477
+ else
478
+ raise e
479
+ end
480
+ end
481
+
482
+ def read_logs(bot_name, options=[])
483
+ query = "&" + options.join("&") unless options.empty?
484
+ url = get("/bots/#{bot_name}/logs?logplex=true#{query}").to_s
485
+ if url == 'Use old logs'
486
+ puts get("/bots/#{bot_name}/logs").to_s
487
+ else
488
+ uri = URI.parse(url);
489
+
490
+ if uri.scheme == 'https'
491
+ proxy = https_proxy
492
+ else
493
+ proxy = http_proxy
494
+ end
495
+
496
+ if proxy
497
+ proxy_uri = URI.parse(proxy)
498
+ http = Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
499
+ else
500
+ http = Net::HTTP.new(uri.host, uri.port)
501
+ end
502
+
503
+ if uri.scheme == 'https'
504
+ http.use_ssl = true
505
+ if ENV["TURBOT_SSL_VERIFY"] == "disable"
506
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
507
+ else
508
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
509
+ http.ca_file = local_ca_file
510
+ http.verify_callback = lambda do |preverify_ok, ssl_context|
511
+ if (!preverify_ok) || ssl_context.error != 0
512
+ error "WARNING: Unable to verify SSL certificate for #{host}\nTo disable SSL verification, run with TURBOT_SSL_VERIFY=disable"
513
+ end
514
+ true
515
+ end
516
+ end
517
+ end
518
+
519
+ http.read_timeout = 60 * 60 * 24
520
+
521
+ begin
522
+ http.start do
523
+ http.request_get(uri.path + (uri.query ? "?" + uri.query : "")) do |response|
524
+ error(response.message) if response.is_a? Net::HTTPServerError
525
+ response["Tail-warning"] && $stderr.puts(response["X-Turbot-Warning"])
526
+ response.read_body do |chunk|
527
+ yield chunk
528
+ end
529
+ end
530
+ end
531
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
532
+ error("Could not connect to logging service")
533
+ rescue Timeout::Error, EOFError
534
+ error("\nRequest timed out")
535
+ end
536
+ end
537
+ end
538
+
539
+ def list_drains(bot_name)
540
+ get("/bots/#{bot_name}/logs/drains").to_s
541
+ end
542
+
543
+ def add_drain(bot_name, url)
544
+ post("/bots/#{bot_name}/logs/drains", "url=#{CGI.escape(url)}").to_s
545
+ end
546
+
547
+ def remove_drain(bot_name, url)
548
+ delete("/bots/#{bot_name}/logs/drains?url=#{CGI.escape(url)}").to_s
549
+ end
550
+
551
+ def addons(filters = {})
552
+ url = "/addons"
553
+ params = filters.map{|k,v| "#{k}=#{v}"}.join("&")
554
+ params = nil if params.empty?
555
+ json_decode get([url,params].compact.join("?"), :accept => 'application/json').to_s
556
+ end
557
+
558
+ def installed_addons(bot_name)
559
+ json_decode get("/bots/#{bot_name}/addons", :accept => 'application/json').to_s
560
+ end
561
+
562
+ def install_addon(bot_name, addon, config={})
563
+ configure_addon :install, bot_name, addon, config
564
+ end
565
+
566
+ def upgrade_addon(bot_name, addon, config={})
567
+ configure_addon :upgrade, bot_name, addon, config
568
+ end
569
+ alias_method :downgrade_addon, :upgrade_addon
570
+
571
+ def uninstall_addon(bot_name, addon, options={})
572
+ configure_addon :uninstall, bot_name, addon, options
573
+ end
574
+
575
+ def httpcache_purge(bot_name)
576
+ delete("/bots/#{bot_name}/httpcache").to_s
577
+ end
578
+
579
+ def on_warning(&blk)
580
+ @warning_callback = blk
581
+ end
582
+
583
+ ##################
584
+
585
+ def resource(uri, options={})
586
+ RestClient.proxy = case URI.parse(realize_full_uri(uri)).scheme
587
+ when "http"
588
+ http_proxy
589
+ when "https"
590
+ https_proxy
591
+ end
592
+ RestClient::Resource.new(realize_full_uri(uri), options.merge(:user => user, :password => password))
593
+ end
594
+
595
+ def get(uri, extra_headers={}) # :nodoc:
596
+ process(:get, uri, extra_headers)
597
+ end
598
+
599
+ def post(uri, payload="", extra_headers={}) # :nodoc:
600
+ process(:post, uri, extra_headers, payload)
601
+ end
602
+
603
+ def put(uri, payload, extra_headers={}) # :nodoc:
604
+ process(:put, uri, extra_headers, payload)
605
+ end
606
+
607
+ def delete(uri, extra_headers={}) # :nodoc:
608
+ process(:delete, uri, extra_headers)
609
+ end
610
+
611
+ def process(method, uri, extra_headers={}, payload=nil)
612
+ headers = turbot_headers.merge(extra_headers)
613
+ args = [method, payload, headers].compact
614
+
615
+ resource_options = default_resource_options_for_uri(uri)
616
+
617
+ begin
618
+ response = resource(uri, resource_options).send(*args)
619
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
620
+ host = URI.parse(realize_full_uri(uri)).host
621
+ error "Unable to connect to #{host}"
622
+ rescue RestClient::SSLCertificateNotVerified => ex
623
+ host = URI.parse(realize_full_uri(uri)).host
624
+ error "WARNING: Unable to verify SSL certificate for #{host}\nTo disable SSL verification, run with TURBOT_SSL_VERIFY=disable"
625
+ end
626
+
627
+ extract_warning(response)
628
+ response
629
+ end
630
+
631
+ def extract_warning(response)
632
+ return unless response
633
+ if response.headers[:x_turbot_warning] && @warning_callback
634
+ warning = response.headers[:x_turbot_warning]
635
+ @displayed_warnings ||= {}
636
+ unless @displayed_warnings[warning]
637
+ @warning_callback.call(warning)
638
+ @displayed_warnings[warning] = true
639
+ end
640
+ end
641
+ end
642
+
643
+ def turbot_headers # :nodoc:
644
+ {
645
+ 'X-Turbot-API-Version' => '2',
646
+ 'User-Agent' => Turbot.user_agent,
647
+ 'X-Ruby-Version' => RUBY_VERSION,
648
+ 'X-Ruby-Platform' => RUBY_PLATFORM
649
+ }
650
+ end
651
+
652
+ def xml(raw) # :nodoc:
653
+ REXML::Document.new(raw)
654
+ end
655
+
656
+ def escape(value) # :nodoc:
657
+ escaped = URI.escape(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
658
+ escaped.gsub('.', '%2E') # not covered by the previous URI.escape
659
+ end
660
+
661
+ module JSON
662
+ def self.parse(json)
663
+ json_decode(json)
664
+ end
665
+ end
666
+
667
+ private
668
+
669
+ def configure_addon(action, bot_name, addon, config = {})
670
+ response = update_addon action,
671
+ addon_path(bot_name, addon),
672
+ config
673
+
674
+ json_decode(response.to_s) unless response.to_s.empty?
675
+ end
676
+
677
+ def addon_path(bot_name, addon)
678
+ "/bots/#{bot_name}/addons/#{escape(addon)}"
679
+ end
680
+
681
+ def update_addon(action, path, config)
682
+ params = { :config => config }
683
+ bot = params[:config].delete(:confirm)
684
+ headers = { :accept => 'application/json' }
685
+ params.merge!(:confirm => bot) if bot
686
+
687
+ case action
688
+ when :install
689
+ post path, params, headers
690
+ when :upgrade
691
+ put path, params, headers
692
+ when :uninstall
693
+ confirm = bot ? "confirm=#{bot}" : ''
694
+ delete "#{path}?#{confirm}", headers
695
+ end
696
+ end
697
+
698
+ def realize_full_uri(given)
699
+ full_host = (host =~ /^http/) ? host : "https://api.#{host}"
700
+ host = URI.parse(full_host)
701
+ uri = URI.parse(given)
702
+ uri.host ||= host.host
703
+ uri.scheme ||= host.scheme || "https"
704
+ uri.path = (uri.path[0..0] == "/") ? uri.path : "/#{uri.path}"
705
+ uri.port = host.port if full_host =~ /\:\d+/
706
+ uri.to_s
707
+ end
708
+
709
+ def default_resource_options_for_uri(uri)
710
+ if ENV["TURBOT_SSL_VERIFY"] == "disable"
711
+ {}
712
+ elsif realize_full_uri(uri) =~ %r|^https://api.turbot.com|
713
+ { :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => local_ca_file }
714
+ else
715
+ {}
716
+ end
717
+ end
718
+
719
+ def local_ca_file
720
+ File.expand_path("../../../data/cacert.pem", __FILE__)
721
+ end
722
+
723
+ def hash_from_xml_doc(elements)
724
+ elements.inject({}) do |hash, e|
725
+ next(hash) unless e.respond_to?(:children)
726
+ hash.update(e.name.gsub("-","_").to_sym => case e.children.length
727
+ when 0 then nil
728
+ when 1 then e.text
729
+ else hash_from_xml_doc(e.children)
730
+ end)
731
+ end
732
+ end
733
+
734
+ def http_proxy
735
+ proxy = ENV['HTTP_PROXY'] || ENV['http_proxy']
736
+ if proxy && !proxy.empty?
737
+ unless /^[^:]+:\/\// =~ proxy
738
+ proxy = "http://" + proxy
739
+ end
740
+ proxy
741
+ else
742
+ nil
743
+ end
744
+ end
745
+
746
+ def https_proxy
747
+ proxy = ENV['HTTPS_PROXY'] || ENV['https_proxy']
748
+ if proxy && !proxy.empty?
749
+ unless /^[^:]+:\/\// =~ proxy
750
+ proxy = "https://" + proxy
751
+ end
752
+ proxy
753
+ else
754
+ nil
755
+ end
756
+ end
757
+ end