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.
- checksums.yaml +15 -0
- data/README.md +36 -0
- data/bin/turbot +17 -0
- data/data/cacert.pem +3988 -0
- data/lib/turbot/auth.rb +315 -0
- data/lib/turbot/cli.rb +38 -0
- data/lib/turbot/client/cisaurus.rb +25 -0
- data/lib/turbot/client/pgbackups.rb +113 -0
- data/lib/turbot/client/rendezvous.rb +111 -0
- data/lib/turbot/client/ssl_endpoint.rb +25 -0
- data/lib/turbot/client/turbot_postgresql.rb +148 -0
- data/lib/turbot/client.rb +757 -0
- data/lib/turbot/command/auth.rb +85 -0
- data/lib/turbot/command/base.rb +192 -0
- data/lib/turbot/command/bots.rb +326 -0
- data/lib/turbot/command/config.rb +123 -0
- data/lib/turbot/command/help.rb +179 -0
- data/lib/turbot/command/keys.rb +115 -0
- data/lib/turbot/command/logs.rb +34 -0
- data/lib/turbot/command/ssl.rb +43 -0
- data/lib/turbot/command/status.rb +51 -0
- data/lib/turbot/command/update.rb +47 -0
- data/lib/turbot/command/version.rb +23 -0
- data/lib/turbot/command.rb +304 -0
- data/lib/turbot/deprecated/help.rb +38 -0
- data/lib/turbot/deprecated.rb +5 -0
- data/lib/turbot/distribution.rb +9 -0
- data/lib/turbot/errors.rb +28 -0
- data/lib/turbot/excon.rb +11 -0
- data/lib/turbot/helpers/log_displayer.rb +70 -0
- data/lib/turbot/helpers/pg_dump_restore.rb +115 -0
- data/lib/turbot/helpers/turbot_postgresql.rb +213 -0
- data/lib/turbot/helpers.rb +521 -0
- data/lib/turbot/plugin.rb +165 -0
- data/lib/turbot/updater.rb +171 -0
- data/lib/turbot/version.rb +3 -0
- data/lib/turbot.rb +19 -0
- data/lib/vendor/turbot/okjson.rb +598 -0
- data/spec/helper/legacy_help.rb +16 -0
- data/spec/helper/pg_dump_restore_spec.rb +67 -0
- data/spec/schemas/dummy_schema.json +12 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +220 -0
- data/spec/support/display_message_matcher.rb +49 -0
- data/spec/support/dummy_api.rb +120 -0
- data/spec/support/openssl_mock_helper.rb +8 -0
- data/spec/support/organizations_mock_helper.rb +11 -0
- data/spec/turbot/auth_spec.rb +214 -0
- data/spec/turbot/client/pgbackups_spec.rb +43 -0
- data/spec/turbot/client/rendezvous_spec.rb +62 -0
- data/spec/turbot/client/ssl_endpoint_spec.rb +48 -0
- data/spec/turbot/client/turbot_postgresql_spec.rb +71 -0
- data/spec/turbot/client_spec.rb +548 -0
- data/spec/turbot/command/auth_spec.rb +38 -0
- data/spec/turbot/command/base_spec.rb +66 -0
- data/spec/turbot/command/bots_spec.rb +54 -0
- data/spec/turbot/command/config_spec.rb +143 -0
- data/spec/turbot/command/help_spec.rb +90 -0
- data/spec/turbot/command/keys_spec.rb +117 -0
- data/spec/turbot/command/logs_spec.rb +60 -0
- data/spec/turbot/command/status_spec.rb +48 -0
- data/spec/turbot/command/version_spec.rb +16 -0
- data/spec/turbot/command_spec.rb +131 -0
- data/spec/turbot/helpers/turbot_postgresql_spec.rb +181 -0
- data/spec/turbot/helpers_spec.rb +48 -0
- data/spec/turbot/plugin_spec.rb +172 -0
- data/spec/turbot/updater_spec.rb +44 -0
- data/templates/manifest.json +7 -0
- data/templates/scraper.py +5 -0
- data/templates/scraper.rb +6 -0
- 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
|