turbot 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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