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,315 @@
1
+ require "cgi"
2
+ require "turbot"
3
+ require "turbot/client"
4
+ require "turbot/helpers"
5
+
6
+ require "netrc"
7
+
8
+ class Turbot::Auth
9
+ class << self
10
+ include Turbot::Helpers
11
+
12
+ attr_accessor :credentials
13
+
14
+ def api
15
+ @api ||= begin
16
+ Turbot::API.new(default_params.merge(:api_key => password))
17
+ end
18
+ end
19
+
20
+ def client
21
+ @client ||= begin
22
+ client = Turbot::Client.new(user, password, host)
23
+ client.on_warning { |msg| self.display("\n#{msg}\n\n") }
24
+ client
25
+ end
26
+ end
27
+
28
+ def login
29
+ delete_credentials
30
+ get_credentials
31
+ end
32
+
33
+ def logout
34
+ delete_credentials
35
+ end
36
+
37
+ # will raise if not authenticated
38
+ def check
39
+ api.get_user
40
+ end
41
+
42
+ def default_host
43
+ "http://turbot"
44
+ end
45
+
46
+ def git_host
47
+ ENV['TURBOT_GIT_HOST'] || host
48
+ end
49
+
50
+ def host
51
+ ENV['TURBOT_HOST'] || default_host
52
+ end
53
+
54
+ def reauthorize
55
+ @credentials = ask_for_and_save_credentials
56
+ end
57
+
58
+ def user # :nodoc:
59
+ get_credentials[0]
60
+ end
61
+
62
+ def password # :nodoc:
63
+ get_credentials[1]
64
+ end
65
+
66
+ def api_key
67
+ api.get_api_key
68
+ end
69
+
70
+ def api_key_for_credentials(user = get_credentials[0], password = get_credentials[1])
71
+ api = Turbot::API.new(default_params)
72
+ api.get_api_key_for_credentials(user, password)["api_key"]
73
+ end
74
+
75
+ def get_credentials # :nodoc:
76
+ @credentials ||= (read_credentials || ask_for_and_save_credentials)
77
+ end
78
+
79
+ def delete_credentials
80
+ if netrc
81
+ netrc.delete("api.#{host}")
82
+ netrc.delete("code.#{host}")
83
+ netrc.save
84
+ end
85
+ @api, @client, @credentials = nil, nil
86
+ end
87
+
88
+ def netrc_path
89
+ default = Netrc.default_path
90
+ encrypted = default + ".gpg"
91
+ if File.exists?(encrypted)
92
+ encrypted
93
+ else
94
+ default
95
+ end
96
+ end
97
+
98
+ def netrc # :nodoc:
99
+ @netrc ||= begin
100
+ File.exists?(netrc_path) && Netrc.read(netrc_path)
101
+ rescue => error
102
+ if error.message =~ /^Permission bits for/
103
+ perm = File.stat(netrc_path).mode & 0777
104
+ abort("Permissions #{perm} for '#{netrc_path}' are too open. You should run `chmod 0600 #{netrc_path}` so that your credentials are NOT accessible by others.")
105
+ else
106
+ raise error
107
+ end
108
+ end
109
+ end
110
+
111
+ def read_credentials
112
+ if ENV['TURBOT_API_KEY']
113
+ ['', ENV['TURBOT_API_KEY']]
114
+ else
115
+
116
+ # read netrc credentials if they exist
117
+ if netrc
118
+ # force migration of long api tokens (80 chars) to short ones (40)
119
+ # #write_credentials rewrites both api.* and code.*
120
+ credentials = netrc["api.#{host}"]
121
+ if credentials && credentials[1].length > 40
122
+ @credentials = [ credentials[0], credentials[1][0,40] ]
123
+ write_credentials
124
+ end
125
+
126
+ netrc["api.#{host}"]
127
+ end
128
+ end
129
+ end
130
+
131
+ def write_credentials
132
+ FileUtils.mkdir_p(File.dirname(netrc_path))
133
+ FileUtils.touch(netrc_path)
134
+ unless running_on_windows?
135
+ FileUtils.chmod(0600, netrc_path)
136
+ end
137
+ netrc["api.#{host}"] = self.credentials
138
+ netrc["code.#{host}"] = self.credentials
139
+ netrc.save
140
+ end
141
+
142
+ def echo_off
143
+ with_tty do
144
+ system "stty -echo"
145
+ end
146
+ end
147
+
148
+ def echo_on
149
+ with_tty do
150
+ system "stty echo"
151
+ end
152
+ end
153
+
154
+ def ask_for_credentials
155
+ puts "Enter your Turbot credentials."
156
+
157
+ print "Email: "
158
+ user = ask
159
+
160
+ print "Password (typing will be hidden): "
161
+ password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
162
+
163
+ [user, api_key_for_credentials(user, password)]
164
+ end
165
+
166
+ def ask_for_password_on_windows
167
+ require "Win32API"
168
+ char = nil
169
+ password = ''
170
+
171
+ while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
172
+ break if char == 10 || char == 13 # received carriage return or newline
173
+ if char == 127 || char == 8 # backspace and delete
174
+ password.slice!(-1, 1)
175
+ else
176
+ # windows might throw a -1 at us so make sure to handle RangeError
177
+ (password << char.chr) rescue RangeError
178
+ end
179
+ end
180
+ puts
181
+ return password
182
+ end
183
+
184
+ def ask_for_password
185
+ echo_off
186
+ password = ask
187
+ puts
188
+ echo_on
189
+ return password
190
+ end
191
+
192
+ def ask_for_and_save_credentials
193
+ begin
194
+ # ask for username and password, look up API key against API given these
195
+ # In looking up the API key it also attempts to log the user in
196
+ @credentials = ask_for_credentials
197
+ # write these to a hidden file
198
+ write_credentials
199
+ check
200
+ rescue Turbot::API::Errors::NotFound, Turbot::API::Errors::Unauthorized => e
201
+ delete_credentials
202
+ display "Authentication failed."
203
+ retry if retry_login?
204
+ exit 1
205
+ rescue Exception => e
206
+ delete_credentials
207
+ raise e
208
+ end
209
+ check_for_associated_ssh_key unless Turbot::Command.current_command == "keys:add"
210
+ @credentials
211
+ end
212
+
213
+ def check_for_associated_ssh_key
214
+ if api.get_ssh_keys.empty?
215
+ associate_or_generate_ssh_key
216
+ end
217
+ end
218
+
219
+ def associate_or_generate_ssh_key
220
+ public_keys = Dir.glob("#{home_directory}/.ssh/*.pub").sort
221
+
222
+ case public_keys.length
223
+ when 0 then
224
+ display "Could not find an existing public key."
225
+ display "Would you like to generate one? [Yn] ", false
226
+ unless ask.strip.downcase == "n"
227
+ display "Generating new SSH public key."
228
+ generate_ssh_key("id_rsa")
229
+ associate_key("#{home_directory}/.ssh/id_rsa.pub")
230
+ end
231
+ when 1 then
232
+ display "Found existing public key: #{public_keys.first}"
233
+ associate_key(public_keys.first)
234
+ else
235
+ display "Found the following SSH public keys:"
236
+ public_keys.each_with_index do |key, index|
237
+ display "#{index+1}) #{File.basename(key)}"
238
+ end
239
+ display "Which would you like to use with your Turbot account? ", false
240
+ choice = ask.to_i - 1
241
+ chosen = public_keys[choice]
242
+ if choice == -1 || chosen.nil?
243
+ error("Invalid choice")
244
+ end
245
+ associate_key(chosen)
246
+ end
247
+ end
248
+
249
+ def generate_ssh_key(keyfile)
250
+ ssh_dir = File.join(home_directory, ".ssh")
251
+ unless File.exists?(ssh_dir)
252
+ FileUtils.mkdir_p ssh_dir
253
+ unless running_on_windows?
254
+ File.chmod(0700, ssh_dir)
255
+ end
256
+ end
257
+ output = `ssh-keygen -t rsa -N "" -f \"#{home_directory}/.ssh/#{keyfile}\" 2>&1`
258
+ if ! $?.success?
259
+ error("Could not generate key: #{output}")
260
+ end
261
+ end
262
+
263
+ def associate_key(key)
264
+ action("Uploading SSH public key #{key}") do
265
+ if File.exists?(key)
266
+ api.post_key(File.read(key))
267
+ else
268
+ error("Could not upload SSH public key: key file '" + key + "' does not exist")
269
+ end
270
+ end
271
+ end
272
+
273
+ def retry_login?
274
+ @login_attempts ||= 0
275
+ @login_attempts += 1
276
+ @login_attempts < 3
277
+ end
278
+
279
+ def verified_hosts
280
+ %w( turbot.com turbot-shadow.com )
281
+ end
282
+
283
+ def base_host(host)
284
+ parts = URI.parse(full_host(host)).host.split(".")
285
+ return parts.first if parts.size == 1
286
+ parts[-2..-1].join(".")
287
+ end
288
+
289
+ def full_host(host)
290
+ (host =~ /^http/) ? host : "https://api.#{host}"
291
+ end
292
+
293
+ def verify_host?(host)
294
+ hostname = base_host(host)
295
+ verified = verified_hosts.include?(hostname)
296
+ verified = false if ENV["TURBOT_SSL_VERIFY"] == "disable"
297
+ verified
298
+ end
299
+
300
+ protected
301
+
302
+ def default_params
303
+ uri = URI.parse(full_host(host))
304
+ {
305
+ :headers => {
306
+ 'User-Agent' => Turbot.user_agent
307
+ },
308
+ :host => uri.host,
309
+ :port => uri.port,
310
+ :scheme => uri.scheme,
311
+ :ssl_verify_peer => verify_host?(host)
312
+ }
313
+ end
314
+ end
315
+ end
data/lib/turbot/cli.rb ADDED
@@ -0,0 +1,38 @@
1
+ load('turbot/helpers.rb') # reload helpers after possible inject_loadpath
2
+ load('turbot/updater.rb') # reload updater after possible inject_loadpath
3
+
4
+ require "turbot"
5
+ require "turbot/command"
6
+ require "turbot/helpers"
7
+
8
+ # workaround for rescue/reraise to define errors in command.rb failing in 1.8.6
9
+ if RUBY_VERSION =~ /^1.8.6/
10
+ require('turbot-api')
11
+ require('rest_client')
12
+ end
13
+
14
+ class Turbot::CLI
15
+
16
+ extend Turbot::Helpers
17
+
18
+ def self.start(*args)
19
+ begin
20
+ if $stdin.isatty
21
+ $stdin.sync = true
22
+ end
23
+ if $stdout.isatty
24
+ $stdout.sync = true
25
+ end
26
+ command = args.shift.strip rescue "help"
27
+ Turbot::Command.load
28
+ Turbot::Command.run(command, args)
29
+ rescue Interrupt
30
+ `stty icanon echo`
31
+ error("Command cancelled.")
32
+ rescue => error
33
+ styled_error(error)
34
+ exit(1)
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,25 @@
1
+ require "turbot/client"
2
+
3
+ class Turbot::Client::Cisaurus
4
+
5
+ include Turbot::Helpers
6
+
7
+ def initialize(uri)
8
+ require 'rest_client'
9
+ @uri = URI.parse(uri)
10
+ end
11
+
12
+ def authenticated_resource(path)
13
+ host = "#{@uri.scheme}://#{@uri.host}"
14
+ host += ":#{@uri.port}" if @uri.port
15
+ RestClient::Resource.new("#{host}#{path}", "", Turbot::Auth.api_key)
16
+ end
17
+
18
+ def copy_slug(from, to)
19
+ authenticated_resource("/v1/bots/#{from}/copy/#{to}").post(json_encode("description" => "Forked from #{from}"), :content_type => :json).headers[:location]
20
+ end
21
+
22
+ def job_done?(job_location)
23
+ 202 != authenticated_resource(job_location).get.code
24
+ end
25
+ end
@@ -0,0 +1,113 @@
1
+ require "turbot/client"
2
+
3
+ class Turbot::Client::Pgbackups
4
+
5
+ include Turbot::Helpers
6
+
7
+ def initialize(uri)
8
+ require 'rest_client'
9
+ @uri = URI.parse(uri)
10
+ end
11
+
12
+ def authenticated_resource(path)
13
+ host = "#{@uri.scheme}://#{@uri.host}"
14
+ host += ":#{@uri.port}" if @uri.port
15
+ RestClient::Resource.new("#{host}#{path}",
16
+ :user => @uri.user,
17
+ :password => @uri.password,
18
+ :headers => {:x_turbot_gem_version => Turbot::Client.version}
19
+ )
20
+ end
21
+
22
+ def create_transfer(from_url, from_name, to_url, to_name, opts={})
23
+ # opts[:expire] => true will delete the oldest backup if at the plan limit
24
+ resource = authenticated_resource("/client/transfers")
25
+ params = {:from_url => from_url, :from_name => from_name, :to_url => to_url, :to_name => to_name}.merge opts
26
+ json_decode post(resource, params).body
27
+ end
28
+
29
+ def get_transfers
30
+ resource = authenticated_resource("/client/transfers")
31
+ json_decode get(resource).body
32
+ end
33
+
34
+ def get_transfer(id)
35
+ resource = authenticated_resource("/client/transfers/#{id}")
36
+ json_decode get(resource).body
37
+ end
38
+
39
+ def get_backups(opts={})
40
+ resource = authenticated_resource("/client/backups")
41
+ json_decode get(resource).body
42
+ end
43
+
44
+ def get_backup(name, opts={})
45
+ name = URI.escape(name)
46
+ resource = authenticated_resource("/client/backups/#{name}")
47
+ json_decode get(resource).body
48
+ end
49
+
50
+ def get_latest_backup
51
+ resource = authenticated_resource("/client/latest_backup")
52
+ json_decode get(resource).body
53
+ end
54
+
55
+ def delete_backup(name)
56
+ name = URI.escape(name)
57
+ begin
58
+ resource = authenticated_resource("/client/backups/#{name}")
59
+ delete(resource).body
60
+ true
61
+ rescue RestClient::ResourceNotFound => e
62
+ false
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def get(resource)
69
+ check_errors do
70
+ response = resource.get
71
+ display_turbot_warning response
72
+ response
73
+ end
74
+ end
75
+
76
+ def post(resource, params)
77
+ check_errors do
78
+ response = resource.post(params)
79
+ display_turbot_warning response
80
+ response
81
+ end
82
+ end
83
+
84
+ def delete(resource)
85
+ check_errors do
86
+ response = resource.delete
87
+ display_turbot_warning response
88
+ response
89
+ end
90
+ end
91
+
92
+ def check_errors
93
+ yield
94
+ rescue RestClient::Unauthorized
95
+ error "Invalid PGBACKUPS_URL"
96
+ end
97
+
98
+ def display_turbot_warning(response)
99
+ warning = response.headers[:x_turbot_warning]
100
+ display warning if warning
101
+ response
102
+ end
103
+
104
+ end
105
+
106
+ module Pgbackups
107
+ class Client < Turbot::Client::Pgbackups
108
+ def initialize(*args)
109
+ Turbot::Helpers.deprecate "Pgbackups::Client has been deprecated. Please use Turbot::Client::Pgbackups instead."
110
+ super
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,111 @@
1
+ require "timeout"
2
+ require "socket"
3
+ require "uri"
4
+ require "turbot/auth"
5
+ require "turbot/client"
6
+ require "turbot/helpers"
7
+
8
+ class Turbot::Client::Rendezvous
9
+
10
+ include Turbot::Helpers
11
+
12
+ attr_reader :rendezvous_url, :connect_timeout, :activity_timeout, :input, :output, :on_connect
13
+
14
+ def initialize(opts)
15
+ @rendezvous_url = opts[:rendezvous_url]
16
+ @connect_timeout = opts[:connect_timeout]
17
+ @activity_timeout = opts[:activity_timeout]
18
+ @input = opts[:input]
19
+ @output = opts[:output]
20
+ end
21
+
22
+ def on_connect(&blk)
23
+ @on_connect = blk if block_given?
24
+ @on_connect
25
+ end
26
+
27
+ def start
28
+ uri = URI.parse(rendezvous_url)
29
+ host, port, secret = uri.host, uri.port, uri.path[1..-1]
30
+
31
+ ssl_socket = Timeout.timeout(connect_timeout) do
32
+ ssl_context = OpenSSL::SSL::SSLContext.new
33
+ ssl_context.ssl_version = :TLSv1
34
+
35
+ if Turbot::Auth.verify_host?(host)
36
+ ssl_context.ca_file = File.expand_path("../../../../data/cacert.pem", __FILE__)
37
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
38
+ end
39
+
40
+ tcp_socket = TCPSocket.open(host, port)
41
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
42
+ ssl_socket.connect
43
+ ssl_socket.puts(secret)
44
+ ssl_socket.readline
45
+ ssl_socket
46
+ end
47
+
48
+ on_connect.call if on_connect
49
+
50
+ readables = [input, ssl_socket].compact
51
+
52
+ begin
53
+ loop do
54
+ if o = IO.select(readables, nil, nil, activity_timeout)
55
+ if (input && (o.first.first == input))
56
+ begin
57
+ data = input.readpartial(10000)
58
+ rescue EOFError
59
+ ssl_socket.write(4.chr)
60
+ ssl_socket.flush
61
+ readables.delete(input)
62
+ next
63
+ end
64
+ if running_on_windows?
65
+ data.gsub!("\r\n", "\n") # prevent double CRs
66
+ end
67
+ ssl_socket.write(data)
68
+ ssl_socket.flush
69
+ elsif (o.first.first == ssl_socket)
70
+ begin
71
+ data = ssl_socket.readpartial(10000)
72
+ rescue EOFError
73
+ break
74
+ end
75
+ output.write(fixup(data))
76
+ end
77
+ else
78
+ raise(Timeout::Error.new)
79
+ end
80
+ end
81
+ rescue Interrupt
82
+ ssl_socket.write(3.chr)
83
+ ssl_socket.flush
84
+ retry
85
+ rescue SignalException => e
86
+ if Signal.list["QUIT"] == e.signo
87
+ ssl_socket.write(28.chr)
88
+ ssl_socket.flush
89
+ retry
90
+ end
91
+ raise
92
+ rescue Errno::EIO
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def fixup(data)
99
+ return nil if ! data
100
+ if data.respond_to?(:force_encoding)
101
+ data.force_encoding('utf-8') if data.respond_to?(:force_encoding)
102
+ end
103
+ if running_on_windows?
104
+ begin
105
+ data.gsub!(/\e\[[\d;]+m/, '')
106
+ rescue # ignore failed gsub, for instance when non-utf8
107
+ end
108
+ end
109
+ output.isatty ? data : data.gsub(/\cM/,"")
110
+ end
111
+ end
@@ -0,0 +1,25 @@
1
+ class Turbot::Client
2
+ def ssl_endpoint_add(bot, pem, key)
3
+ json_decode(post("bots/#{bot}/ssl-endpoints", :accept => :json, :pem => pem, :key => key).to_s)
4
+ end
5
+
6
+ def ssl_endpoint_info(bot, cname)
7
+ json_decode(get("bots/#{bot}/ssl-endpoints/#{escape(cname)}", :accept => :json).to_s)
8
+ end
9
+
10
+ def ssl_endpoint_list(bot)
11
+ json_decode(get("bots/#{bot}/ssl-endpoints", :accept => :json).to_s)
12
+ end
13
+
14
+ def ssl_endpoint_remove(bot, cname)
15
+ json_decode(delete("bots/#{bot}/ssl-endpoints/#{escape(cname)}", :accept => :json).to_s)
16
+ end
17
+
18
+ def ssl_endpoint_rollback(bot, cname)
19
+ json_decode(post("bots/#{bot}/ssl-endpoints/#{escape(cname)}/rollback", :accept => :json).to_s)
20
+ end
21
+
22
+ def ssl_endpoint_update(bot, cname, pem, key)
23
+ json_decode(put("bots/#{bot}/ssl-endpoints/#{escape(cname)}", :accept => :json, :pem => pem, :key => key).to_s)
24
+ end
25
+ end