transcriptic 0.1.0
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.
- data/bin/transcriptic +12 -0
- data/lib/transcriptic.rb +3 -0
- data/lib/transcriptic/api.rb +73 -0
- data/lib/transcriptic/api/errors.rb +24 -0
- data/lib/transcriptic/api/sequences.rb +33 -0
- data/lib/transcriptic/api/version.rb +5 -0
- data/lib/transcriptic/auth.rb +223 -0
- data/lib/transcriptic/cli.rb +12 -0
- data/lib/transcriptic/client.rb +332 -0
- data/lib/transcriptic/command.rb +233 -0
- data/lib/transcriptic/command/analyze.rb +20 -0
- data/lib/transcriptic/command/base.rb +195 -0
- data/lib/transcriptic/command/console.rb +10 -0
- data/lib/transcriptic/command/data.rb +8 -0
- data/lib/transcriptic/command/help.rb +124 -0
- data/lib/transcriptic/command/history.rb +8 -0
- data/lib/transcriptic/command/login.rb +35 -0
- data/lib/transcriptic/command/run.rb +60 -0
- data/lib/transcriptic/command/sequences.rb +45 -0
- data/lib/transcriptic/command/status.rb +17 -0
- data/lib/transcriptic/helpers.rb +311 -0
- data/lib/transcriptic/version.rb +3 -0
- data/lib/vendor/transcriptic/okjson.rb +557 -0
- metadata +161 -0
data/bin/transcriptic
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# resolve bin path, ignoring symlinks
|
4
|
+
require "pathname"
|
5
|
+
bin_file = Pathname.new(__FILE__).realpath
|
6
|
+
|
7
|
+
# add self to libpath
|
8
|
+
$:.unshift File.expand_path("../../lib", bin_file)
|
9
|
+
|
10
|
+
# start up the CLI
|
11
|
+
require "transcriptic/cli"
|
12
|
+
Transcriptic::CLI.start(*ARGV)
|
data/lib/transcriptic.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require "transcriptic/api/errors"
|
2
|
+
require "transcriptic/api/version"
|
3
|
+
|
4
|
+
require "transcriptic/api/sequences"
|
5
|
+
|
6
|
+
srand
|
7
|
+
|
8
|
+
module Transcriptic
|
9
|
+
class API
|
10
|
+
|
11
|
+
def initialize(options={})
|
12
|
+
@api_key = options.delete(:api_key) || ENV['TRANSCRIPTIC_API_KEY']
|
13
|
+
user_pass = ":#{@api_key}"
|
14
|
+
options = {
|
15
|
+
:headers => {},
|
16
|
+
:host => 'www.transcriptic.com',
|
17
|
+
:scheme => 'https'
|
18
|
+
}.merge(options)
|
19
|
+
options[:headers] = {
|
20
|
+
'Accept' => 'application/json',
|
21
|
+
'Accept-Encoding' => 'gzip',
|
22
|
+
#'Accept-Language' => 'en-US, en;q=0.8',
|
23
|
+
'Authorization' => "Basic #{Base64.encode64(user_pass).gsub("\n", '')}",
|
24
|
+
'User-Agent' => "transcriptic-rb/#{Transcriptic::API::VERSION}",
|
25
|
+
'X-Transcriptic-API-Version' => '3',
|
26
|
+
'X-Ruby-Version' => RUBY_VERSION,
|
27
|
+
'X-Ruby-Platform' => RUBY_PLATFORM
|
28
|
+
}.merge(options[:headers])
|
29
|
+
@connection = Excon.new("#{options[:scheme]}://#{options[:host]}", options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def request(params, &block)
|
33
|
+
begin
|
34
|
+
response = @connection.request(params, &block)
|
35
|
+
rescue Excon::Errors::SocketError => error
|
36
|
+
raise error
|
37
|
+
rescue Excon::Errors::Error => error
|
38
|
+
klass = case error.response.status
|
39
|
+
when 401 then Transcriptic::API::Errors::Unauthorized
|
40
|
+
when 402 then Transcriptic::API::Errors::VerificationRequired
|
41
|
+
when 403 then Transcriptic::API::Errors::Forbidden
|
42
|
+
when 404 then Transcriptic::API::Errors::NotFound
|
43
|
+
when 408 then Transcriptic::API::Errors::Timeout
|
44
|
+
when 422 then Transcriptic::API::Errors::RequestFailed
|
45
|
+
when 423 then Transcriptic::API::Errors::Locked
|
46
|
+
when /50./ then Transcriptic::API::Errors::RequestFailed
|
47
|
+
else Transcriptic::API::Errors::ErrorWithResponse
|
48
|
+
end
|
49
|
+
|
50
|
+
reerror = klass.new(error.message, error.response)
|
51
|
+
reerror.set_backtrace(error.backtrace)
|
52
|
+
raise(reerror)
|
53
|
+
end
|
54
|
+
|
55
|
+
if response.body && !response.body.empty?
|
56
|
+
if response.headers['Content-Encoding'] == 'gzip'
|
57
|
+
response.body = Zlib::GzipReader.new(StringIO.new(response.body)).read
|
58
|
+
end
|
59
|
+
begin
|
60
|
+
response.body = Transcriptic::API::OkJson.decode(response.body)
|
61
|
+
rescue
|
62
|
+
# leave non-JSON body as is
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# reset (non-persistent) connection
|
67
|
+
@connection.reset
|
68
|
+
|
69
|
+
response
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Transcriptic
|
2
|
+
class API
|
3
|
+
module Errors
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ErrorWithResponse < Error
|
7
|
+
attr_reader :response
|
8
|
+
|
9
|
+
def initialize(message, response)
|
10
|
+
super message
|
11
|
+
@response = response
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Unauthorized < ErrorWithResponse; end
|
16
|
+
class VerificationRequired < ErrorWithResponse; end
|
17
|
+
class Forbidden < ErrorWithResponse; end
|
18
|
+
class NotFound < ErrorWithResponse; end
|
19
|
+
class Timeout < ErrorWithResponse; end
|
20
|
+
class Locked < ErrorWithResponse; end
|
21
|
+
class RequestFailed < ErrorWithResponse; end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Transcriptic
|
2
|
+
class API
|
3
|
+
|
4
|
+
# DELETE /:organization/sequences/:sequence
|
5
|
+
def delete_sequence(organization, sequence)
|
6
|
+
request(
|
7
|
+
:expects => 200,
|
8
|
+
:method => :delete,
|
9
|
+
:path => "/#{organization}/sequences/#{sequence}"
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
# GET /:organization/sequences
|
14
|
+
def get_sequences(organization)
|
15
|
+
request(
|
16
|
+
:expects => 200,
|
17
|
+
:method => :get,
|
18
|
+
:path => "/#{organization}/sequences"
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
# POST /:organization/sequences
|
23
|
+
def post_domain(organization, sequence)
|
24
|
+
request(
|
25
|
+
:expects => 201,
|
26
|
+
:method => :post,
|
27
|
+
:path => "/#{organization}/sequences",
|
28
|
+
:query => {'sequence' => sequence}
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require "transcriptic"
|
2
|
+
require "transcriptic/client"
|
3
|
+
require "transcriptic/helpers"
|
4
|
+
|
5
|
+
class Transcriptic::Auth
|
6
|
+
class << self
|
7
|
+
attr_accessor :credentials
|
8
|
+
|
9
|
+
def client
|
10
|
+
@client ||= begin
|
11
|
+
client = Transcriptic::Client.new(user, password, host)
|
12
|
+
client.on_warning { |msg| self.display("\n#{msg}\n\n") }
|
13
|
+
client
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def login
|
18
|
+
delete_credentials
|
19
|
+
get_credentials
|
20
|
+
end
|
21
|
+
|
22
|
+
def logout
|
23
|
+
delete_credentials
|
24
|
+
end
|
25
|
+
|
26
|
+
def clear
|
27
|
+
@credentials = nil
|
28
|
+
@client = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
include Transcriptic::Helpers
|
32
|
+
|
33
|
+
# just a stub; will raise if not authenticated
|
34
|
+
def check
|
35
|
+
client.list
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_host
|
39
|
+
"transcriptic.com"
|
40
|
+
end
|
41
|
+
|
42
|
+
def host
|
43
|
+
ENV['TRANSCRIPTIC_HOST'] || default_host
|
44
|
+
end
|
45
|
+
|
46
|
+
def reauthorize
|
47
|
+
@credentials = ask_for_and_save_credentials
|
48
|
+
end
|
49
|
+
|
50
|
+
def user # :nodoc:
|
51
|
+
get_credentials
|
52
|
+
@credentials[0]
|
53
|
+
end
|
54
|
+
|
55
|
+
def password # :nodoc:
|
56
|
+
get_credentials
|
57
|
+
@credentials[1]
|
58
|
+
end
|
59
|
+
|
60
|
+
def credentials_file
|
61
|
+
if host == default_host
|
62
|
+
"#{home_directory}/.transcriptic/credentials"
|
63
|
+
else
|
64
|
+
"#{home_directory}/.transcriptic/credentials.#{host}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_credentials # :nodoc:
|
69
|
+
return if @credentials
|
70
|
+
unless @credentials = read_credentials
|
71
|
+
ask_for_and_save_credentials
|
72
|
+
end
|
73
|
+
@credentials
|
74
|
+
end
|
75
|
+
|
76
|
+
def read_credentials
|
77
|
+
File.exists?(credentials_file) and File.read(credentials_file).split("\n")
|
78
|
+
end
|
79
|
+
|
80
|
+
def echo_off
|
81
|
+
system "stty -echo"
|
82
|
+
end
|
83
|
+
|
84
|
+
def echo_on
|
85
|
+
system "stty echo"
|
86
|
+
end
|
87
|
+
|
88
|
+
def ask_for_credentials
|
89
|
+
puts "Enter your Transcriptic credentials."
|
90
|
+
|
91
|
+
print "Email: "
|
92
|
+
user = ask
|
93
|
+
|
94
|
+
print "Password: "
|
95
|
+
password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
|
96
|
+
api_key = Transcriptic::Client.auth(user, password, host)['api_key']
|
97
|
+
|
98
|
+
[user, api_key]
|
99
|
+
end
|
100
|
+
|
101
|
+
def ask_for_password_on_windows
|
102
|
+
require "Win32API"
|
103
|
+
char = nil
|
104
|
+
password = ''
|
105
|
+
|
106
|
+
while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
|
107
|
+
break if char == 10 || char == 13 # received carriage return or newline
|
108
|
+
if char == 127 || char == 8 # backspace and delete
|
109
|
+
password.slice!(-1, 1)
|
110
|
+
else
|
111
|
+
# windows might throw a -1 at us so make sure to handle RangeError
|
112
|
+
(password << char.chr) rescue RangeError
|
113
|
+
end
|
114
|
+
end
|
115
|
+
puts
|
116
|
+
return password
|
117
|
+
end
|
118
|
+
|
119
|
+
def ask_for_password
|
120
|
+
echo_off
|
121
|
+
password = ask
|
122
|
+
puts
|
123
|
+
echo_on
|
124
|
+
return password
|
125
|
+
end
|
126
|
+
|
127
|
+
def ask_for_and_save_credentials
|
128
|
+
begin
|
129
|
+
@credentials = ask_for_credentials
|
130
|
+
write_credentials
|
131
|
+
check
|
132
|
+
rescue ::RestClient::Unauthorized, ::RestClient::ResourceNotFound => e
|
133
|
+
delete_credentials
|
134
|
+
clear
|
135
|
+
display "Authentication failed."
|
136
|
+
retry if retry_login?
|
137
|
+
exit 1
|
138
|
+
rescue Exception => e
|
139
|
+
delete_credentials
|
140
|
+
raise e
|
141
|
+
end
|
142
|
+
check_for_associated_ssh_key unless Transcriptic::Command.current_command == "keys:add"
|
143
|
+
end
|
144
|
+
|
145
|
+
def check_for_associated_ssh_key
|
146
|
+
return unless client.keys.length.zero?
|
147
|
+
associate_or_generate_ssh_key
|
148
|
+
end
|
149
|
+
|
150
|
+
def associate_or_generate_ssh_key
|
151
|
+
public_keys = available_ssh_public_keys.sort
|
152
|
+
|
153
|
+
case public_keys.length
|
154
|
+
when 0 then
|
155
|
+
display "Could not find an existing public key."
|
156
|
+
display "Would you like to generate one? [Yn] ", false
|
157
|
+
unless ask.strip.downcase == "n"
|
158
|
+
display "Generating new SSH public key."
|
159
|
+
generate_ssh_key("id_rsa")
|
160
|
+
associate_key("#{home_directory}/.ssh/id_rsa.pub")
|
161
|
+
end
|
162
|
+
when 1 then
|
163
|
+
display "Found existing public key: #{public_keys.first}"
|
164
|
+
associate_key(public_keys.first)
|
165
|
+
else
|
166
|
+
display "Found the following SSH public keys:"
|
167
|
+
public_keys.each_with_index do |key, index|
|
168
|
+
display "#{index+1}) #{File.basename(key)}"
|
169
|
+
end
|
170
|
+
display "Which would you like to use with your Transcriptic account? ", false
|
171
|
+
chosen = public_keys[ask.to_i-1] rescue error("Invalid choice")
|
172
|
+
associate_key(chosen)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def generate_ssh_key(keyfile)
|
177
|
+
ssh_dir = File.join(home_directory, ".ssh")
|
178
|
+
unless File.exists?(ssh_dir)
|
179
|
+
FileUtils.mkdir_p ssh_dir
|
180
|
+
File.chmod(0700, ssh_dir)
|
181
|
+
end
|
182
|
+
`ssh-keygen -t rsa -N "" -f \"#{home_directory}/.ssh/#{keyfile}\" 2>&1`
|
183
|
+
end
|
184
|
+
|
185
|
+
def associate_key(key)
|
186
|
+
display "Uploading ssh public key #{key}"
|
187
|
+
client.add_key(File.read(key))
|
188
|
+
end
|
189
|
+
|
190
|
+
def available_ssh_public_keys
|
191
|
+
keys = [
|
192
|
+
"#{home_directory}/.ssh/id_rsa.pub",
|
193
|
+
"#{home_directory}/.ssh/id_dsa.pub"
|
194
|
+
]
|
195
|
+
keys.concat(Dir["#{home_directory}/.ssh/*.pub"])
|
196
|
+
keys.select { |d| File.exists?(d) }.uniq
|
197
|
+
end
|
198
|
+
|
199
|
+
def retry_login?
|
200
|
+
@login_attempts ||= 0
|
201
|
+
@login_attempts += 1
|
202
|
+
@login_attempts < 3
|
203
|
+
end
|
204
|
+
|
205
|
+
def write_credentials
|
206
|
+
FileUtils.mkdir_p(File.dirname(credentials_file))
|
207
|
+
f = File.open(credentials_file, 'w')
|
208
|
+
f.puts self.credentials
|
209
|
+
f.close
|
210
|
+
set_credentials_permissions
|
211
|
+
end
|
212
|
+
|
213
|
+
def set_credentials_permissions
|
214
|
+
FileUtils.chmod 0700, File.dirname(credentials_file)
|
215
|
+
FileUtils.chmod 0600, credentials_file
|
216
|
+
end
|
217
|
+
|
218
|
+
def delete_credentials
|
219
|
+
FileUtils.rm_f(credentials_file)
|
220
|
+
clear
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
@@ -0,0 +1,332 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'rest-client'
|
3
|
+
require 'uri'
|
4
|
+
require 'time'
|
5
|
+
require 'transcriptic/auth'
|
6
|
+
require 'transcriptic/helpers'
|
7
|
+
require 'transcriptic/version'
|
8
|
+
|
9
|
+
# A Ruby class to call the Transcriptic REST API. You might use this if you want to
|
10
|
+
# manage your Transcriptic apps from within a Ruby program, such as Capistrano.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# require 'transcriptic'
|
15
|
+
# transcriptic = Transcriptic::Client.new('me@example.com', 'mypass')
|
16
|
+
# transcriptic.create('myapp')
|
17
|
+
#
|
18
|
+
class Transcriptic::Client
|
19
|
+
|
20
|
+
include Transcriptic::Helpers
|
21
|
+
extend Transcriptic::Helpers
|
22
|
+
|
23
|
+
def self.version
|
24
|
+
Transcriptic::VERSION
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.gem_version_string
|
28
|
+
"transcriptic-gem/#{version}"
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_accessor :host, :user, :password
|
32
|
+
|
33
|
+
def self.auth(user, password, host = Transcriptic::Auth.default_host)
|
34
|
+
client = new(user, password, host)
|
35
|
+
json_decode client.post('/users/sign_in', { 'user[email]' => user, 'user[password]' => password }, :accept => 'json').to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(user, password, host=Transcriptic::Auth.default_host)
|
39
|
+
@user = user
|
40
|
+
@password = password
|
41
|
+
@host = host
|
42
|
+
end
|
43
|
+
|
44
|
+
# Show info such as mode, custom domain, and collaborators on an app.
|
45
|
+
def status(run_id)
|
46
|
+
doc = xml(get("/apps/#{name_or_domain}").to_s)
|
47
|
+
attrs = hash_from_xml_doc(doc)[:app]
|
48
|
+
attrs.merge!(:collaborators => list_collaborators(attrs[:name]))
|
49
|
+
attrs.merge!(:addons => installed_addons(attrs[:name]))
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add an ssh public key to the current user.
|
53
|
+
def add_key(key)
|
54
|
+
post("/user/keys", key, { 'Content-Type' => 'text/ssh-authkey' }).to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
# Remove an existing ssh public key from the current user.
|
58
|
+
def remove_key(key)
|
59
|
+
delete("/user/keys/#{escape(key)}").to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
# Clear all keys on the current user.
|
63
|
+
def remove_all_keys
|
64
|
+
delete("/user/keys").to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get a list of stacks available to the app, with the current one marked.
|
68
|
+
def list_stacks(app_name, options={})
|
69
|
+
include_deprecated = options.delete(:include_deprecated) || false
|
70
|
+
|
71
|
+
json_decode resource("/apps/#{app_name}/stack").get(
|
72
|
+
:params => { :include_deprecated => include_deprecated },
|
73
|
+
:accept => 'application/json'
|
74
|
+
).to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
class AppCrashed < RuntimeError; end
|
78
|
+
|
79
|
+
# Show a list of projects which you are a collaborator on.
|
80
|
+
def list
|
81
|
+
doc = xml(get('/apps').to_s)
|
82
|
+
doc.elements.to_a("//apps/app").map do |a|
|
83
|
+
name = a.elements.to_a("name").first
|
84
|
+
owner = a.elements.to_a("owner").first
|
85
|
+
[name.text, owner.text]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Show info such as mode, custom domain, and collaborators on an app.
|
90
|
+
def info(name_or_domain)
|
91
|
+
name_or_domain = name_or_domain.gsub(/^(http:\/\/)?(www\.)?/, '')
|
92
|
+
end
|
93
|
+
|
94
|
+
def organizations
|
95
|
+
json_decode resource("/organizations").get(:accept => 'application/json').to_s
|
96
|
+
end
|
97
|
+
|
98
|
+
# Add an ssh public key to the current user.
|
99
|
+
def add_sequence(organization, sequence)
|
100
|
+
post("/#{organization}/sequences", sequence, { 'Content-Type' => 'text/ssh-authkey' }).to_s
|
101
|
+
end
|
102
|
+
|
103
|
+
# Remove an existing ssh public key from the current user.
|
104
|
+
def remove_sequence(organization, sequence)
|
105
|
+
delete("/#{organization}/sequences/#{escape(key)}").to_s
|
106
|
+
end
|
107
|
+
|
108
|
+
class Protocol
|
109
|
+
attr_accessor :attached, :upid
|
110
|
+
|
111
|
+
def initialize(client, protocol, upid=nil)
|
112
|
+
@client = client
|
113
|
+
@protocol = protocol
|
114
|
+
@upid = upid
|
115
|
+
end
|
116
|
+
|
117
|
+
# launch the protocol
|
118
|
+
def launch(command, attached=false)
|
119
|
+
@attached = attached
|
120
|
+
@response = @client.post(
|
121
|
+
"/runs/#{@app}/services",
|
122
|
+
command,
|
123
|
+
:content_type => 'text/plain'
|
124
|
+
)
|
125
|
+
@next_chunk = @response.to_s
|
126
|
+
@interval = 0
|
127
|
+
self
|
128
|
+
rescue RestClient::RequestFailed => e
|
129
|
+
raise AppCrashed, e.http_body if e.http_code == 502
|
130
|
+
raise
|
131
|
+
end
|
132
|
+
|
133
|
+
# Does the service have any remaining output?
|
134
|
+
def end_of_stream?
|
135
|
+
@next_chunk.nil?
|
136
|
+
end
|
137
|
+
|
138
|
+
# Read the next chunk of output.
|
139
|
+
def read
|
140
|
+
chunk = @client.get(@next_chunk)
|
141
|
+
if chunk.headers[:location].nil? && chunk.code != 204
|
142
|
+
# no more chunks
|
143
|
+
@next_chunk = nil
|
144
|
+
chunk.to_s
|
145
|
+
elsif chunk.to_s == ''
|
146
|
+
# assume no content and back off
|
147
|
+
@interval = 2
|
148
|
+
''
|
149
|
+
elsif location = chunk.headers[:location]
|
150
|
+
# some data read and next chunk available
|
151
|
+
@next_chunk = location
|
152
|
+
@interval = 0
|
153
|
+
chunk.to_s
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Iterate over all output chunks until EOF is reached.
|
158
|
+
def each
|
159
|
+
until end_of_stream?
|
160
|
+
sleep(@interval)
|
161
|
+
output = read
|
162
|
+
yield output unless output.empty?
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# All output as a string
|
167
|
+
def to_s
|
168
|
+
buf = []
|
169
|
+
each { |part| buf << part }
|
170
|
+
buf.join
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def status(run_id)
|
175
|
+
json_decode resource("/runs/#{run_id}/status").get(:accept => 'application/json').to_s
|
176
|
+
end
|
177
|
+
|
178
|
+
# Get a Protocol instance to execute commands against.
|
179
|
+
def protocol(run_id, upid)
|
180
|
+
Protocol.new(self, run_id, upid)
|
181
|
+
end
|
182
|
+
|
183
|
+
def read_logs(run_id, options=[])
|
184
|
+
query = "&" + options.join("&") unless options.empty?
|
185
|
+
url = get("/runs/#{run_id}/logs?#{query}").to_s
|
186
|
+
uri = URI.parse(url);
|
187
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
188
|
+
|
189
|
+
if uri.scheme == 'https'
|
190
|
+
http.use_ssl = true
|
191
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
192
|
+
end
|
193
|
+
|
194
|
+
http.read_timeout = 60 * 60 * 24
|
195
|
+
|
196
|
+
begin
|
197
|
+
http.start do
|
198
|
+
http.request_get(uri.path + (uri.query ? "?" + uri.query : "")) do |request|
|
199
|
+
request.read_body do |chunk|
|
200
|
+
yield chunk
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
|
205
|
+
abort(" ! Could not connect to logging service")
|
206
|
+
rescue Timeout::Error, EOFError
|
207
|
+
abort("\n ! Request timed out")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def on_warning(&blk)
|
212
|
+
@warning_callback = blk
|
213
|
+
end
|
214
|
+
|
215
|
+
##################
|
216
|
+
|
217
|
+
def resource(uri, options={})
|
218
|
+
RestClient.proxy = ENV['HTTP_PROXY'] || ENV['http_proxy']
|
219
|
+
resource = RestClient::Resource.new(realize_full_uri(uri), options.merge(:user => user, :password => password))
|
220
|
+
resource
|
221
|
+
end
|
222
|
+
|
223
|
+
def get(uri, extra_headers={}) # :nodoc:
|
224
|
+
process(:get, uri, extra_headers)
|
225
|
+
end
|
226
|
+
|
227
|
+
def post(uri, payload="", extra_headers={}) # :nodoc:
|
228
|
+
process(:post, uri, extra_headers, payload)
|
229
|
+
end
|
230
|
+
|
231
|
+
def put(uri, payload, extra_headers={}) # :nodoc:
|
232
|
+
process(:put, uri, extra_headers, payload)
|
233
|
+
end
|
234
|
+
|
235
|
+
def delete(uri, extra_headers={}) # :nodoc:
|
236
|
+
process(:delete, uri, extra_headers)
|
237
|
+
end
|
238
|
+
|
239
|
+
def process(method, uri, extra_headers={}, payload=nil)
|
240
|
+
headers = transcriptic_headers.merge(extra_headers)
|
241
|
+
args = [method, payload, headers].compact
|
242
|
+
|
243
|
+
resource_options = default_resource_options_for_uri(uri)
|
244
|
+
|
245
|
+
begin
|
246
|
+
response = resource(uri, resource_options).send(*args)
|
247
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
|
248
|
+
host = URI.parse(realize_full_uri(uri)).host
|
249
|
+
error " ! Unable to connect to #{host}"
|
250
|
+
rescue RestClient::SSLCertificateNotVerified => ex
|
251
|
+
host = URI.parse(realize_full_uri(uri)).host
|
252
|
+
#error "WARNING: Unable to verify SSL certificate for #{host}\nTo disable SSL verification, run with TRANSCRIPTIC_SSL_VERIFY=disable"
|
253
|
+
end
|
254
|
+
|
255
|
+
extract_warning(response)
|
256
|
+
response
|
257
|
+
end
|
258
|
+
|
259
|
+
def extract_warning(response)
|
260
|
+
return unless response
|
261
|
+
if response.headers[:x_transcriptic_warning] && @warning_callback
|
262
|
+
warning = response.headers[:x_transcriptic_warning]
|
263
|
+
@displayed_warnings ||= {}
|
264
|
+
unless @displayed_warnings[warning]
|
265
|
+
@warning_callback.call(warning)
|
266
|
+
@displayed_warnings[warning] = true
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def transcriptic_headers # :nodoc:
|
272
|
+
{
|
273
|
+
'X-Transcriptic-API-Version' => '1',
|
274
|
+
'User-Agent' => self.class.gem_version_string,
|
275
|
+
'X-Ruby-Version' => RUBY_VERSION,
|
276
|
+
'X-Ruby-Platform' => RUBY_PLATFORM
|
277
|
+
}
|
278
|
+
end
|
279
|
+
|
280
|
+
def xml(raw) # :nodoc:
|
281
|
+
REXML::Document.new(raw)
|
282
|
+
end
|
283
|
+
|
284
|
+
def escape(value) # :nodoc:
|
285
|
+
escaped = URI.escape(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
286
|
+
escaped.gsub('.', '%2E') # not covered by the previous URI.escape
|
287
|
+
end
|
288
|
+
|
289
|
+
module JSON
|
290
|
+
def self.parse(json)
|
291
|
+
json_decode(json)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
private
|
296
|
+
|
297
|
+
def realize_full_uri(given)
|
298
|
+
full_host = (host =~ /^http/) ? host : "https://www.#{host}"
|
299
|
+
host = URI.parse(full_host)
|
300
|
+
uri = URI.parse(given)
|
301
|
+
uri.host ||= host.host
|
302
|
+
uri.scheme ||= host.scheme || "https"
|
303
|
+
uri.path = (uri.path[0..0] == "/") ? uri.path : "/#{uri.path}"
|
304
|
+
uri.port = host.port if full_host =~ /\:\d+/
|
305
|
+
uri.to_s
|
306
|
+
end
|
307
|
+
|
308
|
+
def default_resource_options_for_uri(uri)
|
309
|
+
if ENV["TRANSCRIPTIC_SSL_VERIFY"] == "disable"
|
310
|
+
{}
|
311
|
+
elsif realize_full_uri(uri) =~ %r|^https://www.transcriptic.com|
|
312
|
+
{ :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => local_ca_file }
|
313
|
+
else
|
314
|
+
{}
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def local_ca_file
|
319
|
+
File.expand_path("../../data/cacert.pem", __FILE__)
|
320
|
+
end
|
321
|
+
|
322
|
+
def hash_from_xml_doc(elements)
|
323
|
+
elements.inject({}) do |hash, e|
|
324
|
+
next(hash) unless e.respond_to?(:children)
|
325
|
+
hash.update(e.name.gsub("-","_").to_sym => case e.children.length
|
326
|
+
when 0 then nil
|
327
|
+
when 1 then e.text
|
328
|
+
else hash_from_xml_doc(e.children)
|
329
|
+
end)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|