aspera-cli 4.15.0 → 4.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/BUGS.md +29 -3
- data/CHANGELOG.md +292 -228
- data/CONTRIBUTING.md +69 -18
- data/README.md +1102 -952
- data/bin/ascli +13 -31
- data/bin/asession +3 -1
- data/examples/dascli +2 -2
- data/lib/aspera/aoc.rb +28 -33
- data/lib/aspera/ascmd.rb +3 -6
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/extended_value.rb +5 -5
- data/lib/aspera/cli/formatter.rb +26 -13
- data/lib/aspera/cli/hints.rb +4 -3
- data/lib/aspera/cli/main.rb +16 -3
- data/lib/aspera/cli/manager.rb +45 -36
- data/lib/aspera/cli/plugin.rb +20 -13
- data/lib/aspera/cli/plugins/aoc.rb +103 -73
- data/lib/aspera/cli/plugins/ats.rb +4 -3
- data/lib/aspera/cli/plugins/config.rb +114 -119
- data/lib/aspera/cli/plugins/cos.rb +2 -2
- data/lib/aspera/cli/plugins/faspex.rb +23 -19
- data/lib/aspera/cli/plugins/faspex5.rb +75 -43
- data/lib/aspera/cli/plugins/node.rb +28 -15
- data/lib/aspera/cli/plugins/orchestrator.rb +4 -2
- data/lib/aspera/cli/plugins/preview.rb +9 -7
- data/lib/aspera/cli/plugins/server.rb +6 -3
- data/lib/aspera/cli/plugins/shares.rb +30 -26
- data/lib/aspera/cli/sync_actions.rb +9 -9
- data/lib/aspera/cli/transfer_agent.rb +21 -14
- data/lib/aspera/cli/transfer_progress.rb +2 -3
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +13 -11
- data/lib/aspera/cos_node.rb +3 -2
- data/lib/aspera/coverage.rb +22 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +4 -2
- data/lib/aspera/fasp/{agent_aspera.rb → agent_alpha.rb} +29 -39
- data/lib/aspera/fasp/agent_base.rb +17 -7
- data/lib/aspera/fasp/agent_direct.rb +88 -84
- data/lib/aspera/fasp/agent_httpgw.rb +4 -3
- data/lib/aspera/fasp/agent_node.rb +3 -2
- data/lib/aspera/fasp/agent_trsdk.rb +79 -37
- data/lib/aspera/fasp/installation.rb +51 -12
- data/lib/aspera/fasp/management.rb +11 -6
- data/lib/aspera/fasp/parameters.rb +53 -47
- data/lib/aspera/fasp/resume_policy.rb +7 -5
- data/lib/aspera/fasp/sync.rb +273 -0
- data/lib/aspera/fasp/transfer_spec.rb +10 -8
- data/lib/aspera/fasp/uri.rb +2 -2
- data/lib/aspera/faspex_gw.rb +11 -8
- data/lib/aspera/faspex_postproc.rb +6 -5
- data/lib/aspera/id_generator.rb +3 -1
- data/lib/aspera/json_rpc.rb +10 -8
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +15 -13
- data/lib/aspera/log.rb +4 -3
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node.rb +17 -16
- data/lib/aspera/node_simulator.rb +214 -0
- data/lib/aspera/oauth.rb +22 -19
- data/lib/aspera/persistency_action_once.rb +13 -14
- data/lib/aspera/persistency_folder.rb +3 -2
- data/lib/aspera/preview/file_types.rb +53 -267
- data/lib/aspera/preview/generator.rb +7 -5
- data/lib/aspera/preview/terminal.rb +14 -5
- data/lib/aspera/preview/utils.rb +8 -7
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +29 -13
- data/lib/aspera/rest_error_analyzer.rb +1 -0
- data/lib/aspera/rest_errors_aspera.rb +2 -0
- data/lib/aspera/secret_hider.rb +5 -2
- data/lib/aspera/ssh.rb +10 -8
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/web_server_simple.rb +2 -1
- data.tar.gz.sig +0 -0
- metadata +96 -45
- metadata.gz.sig +0 -0
- data/lib/aspera/sync.rb +0 -219
@@ -8,8 +8,8 @@ require 'aspera/fasp/resume_policy'
|
|
8
8
|
require 'aspera/fasp/transfer_spec'
|
9
9
|
require 'aspera/fasp/management'
|
10
10
|
require 'aspera/log'
|
11
|
+
require 'aspera/assert'
|
11
12
|
require 'socket'
|
12
|
-
require 'timeout'
|
13
13
|
require 'securerandom'
|
14
14
|
require 'shellwords'
|
15
15
|
require 'English'
|
@@ -20,8 +20,8 @@ module Aspera
|
|
20
20
|
class AgentDirect < Aspera::Fasp::AgentBase
|
21
21
|
# options for initialize (same as values in option transfer_info)
|
22
22
|
DEFAULT_OPTIONS = {
|
23
|
-
spawn_timeout_sec:
|
24
|
-
spawn_delay_sec: 2,
|
23
|
+
spawn_timeout_sec: 2,
|
24
|
+
spawn_delay_sec: 2, # optional delay to start between sessions
|
25
25
|
wss: true, # true: if both SSH and wss in ts: prefer wss
|
26
26
|
multi_incr_udp: true,
|
27
27
|
resume: {},
|
@@ -30,17 +30,20 @@ module Aspera
|
|
30
30
|
quiet: true, # by default no native ascp progress bar
|
31
31
|
trusted_certs: [] # list of files with trusted certificates (stores)
|
32
32
|
}.freeze
|
33
|
+
LISTEN_ADDRESS = '127.0.0.1'
|
34
|
+
LISTEN_AVAILABLE_PORT = 0 # 0 means any available port
|
33
35
|
# spellchecker: enable
|
34
|
-
private_constant :DEFAULT_OPTIONS
|
36
|
+
private_constant :DEFAULT_OPTIONS, :LISTEN_ADDRESS
|
35
37
|
|
36
|
-
#
|
37
|
-
#
|
38
|
+
# method of Aspera::Fasp::AgentBase
|
39
|
+
# start ascp transfer(s) (non blocking), single or multi-session
|
40
|
+
# session information added to @sessions
|
38
41
|
# @param transfer_spec [Hash] aspera transfer specification
|
42
|
+
# @param token_regenerator [Object] object with method refreshed_transfer_token
|
39
43
|
def start_transfer(transfer_spec, token_regenerator: nil)
|
40
|
-
the_job_id = SecureRandom.uuid
|
41
44
|
# clone transfer spec because we modify it (first level keys)
|
42
45
|
transfer_spec = transfer_spec.clone
|
43
|
-
# if there
|
46
|
+
# if there are aspera tags
|
44
47
|
if transfer_spec['tags'].is_a?(Hash) && transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED].is_a?(Hash)
|
45
48
|
# TODO: what is this for ? only on local ascp ?
|
46
49
|
# NOTE: important: transfer id must be unique: generate random id
|
@@ -48,11 +51,10 @@ module Aspera
|
|
48
51
|
# all sessions in a multi-session transfer must have the same xfer_id (see admin manual)
|
49
52
|
transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['xfer_id'] ||= SecureRandom.uuid
|
50
53
|
Log.log.debug{"xfer id=#{transfer_spec['xfer_id']}"}
|
51
|
-
# TODO: useful ? node only ?
|
54
|
+
# TODO: useful ? node only ? seems to be a timeout for retry in node
|
52
55
|
transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['xfer_retry'] ||= 3600
|
53
56
|
end
|
54
57
|
Log.log.debug{Log.dump('ts', transfer_spec)}
|
55
|
-
|
56
58
|
# Compute this before using transfer spec because it potentially modifies the transfer spec
|
57
59
|
# (even if the var is not used in single session)
|
58
60
|
multi_session_info = nil
|
@@ -77,30 +79,24 @@ module Aspera
|
|
77
79
|
end
|
78
80
|
end
|
79
81
|
|
80
|
-
# compute known arguments and environment variables
|
81
|
-
env_args = Parameters.new(transfer_spec, @options).ascp_args
|
82
|
-
|
83
|
-
# transfer job can be multi session
|
84
|
-
xfer_job = {
|
85
|
-
id: the_job_id,
|
86
|
-
sessions: [] # all sessions as below
|
87
|
-
}
|
88
|
-
|
89
82
|
# generic session information
|
90
83
|
session = {
|
84
|
+
id: nil, # SessionId from INIT message in mgt port
|
85
|
+
job_id: SecureRandom.uuid, # job id (regroup sessions)
|
86
|
+
ts: transfer_spec, # transfer spec
|
91
87
|
thread: nil, # Thread object monitoring management port, not nil when pushed to :sessions
|
92
88
|
error: nil, # exception if failed
|
93
89
|
io: nil, # management port server socket
|
94
|
-
id: nil, # SessionId from INIT message in mgt port
|
95
90
|
token_regenerator: token_regenerator, # regenerate bearer token with oauth
|
96
|
-
|
91
|
+
# env vars and args to ascp (from transfer spec)
|
92
|
+
env_args: Parameters.new(transfer_spec, @options).ascp_args
|
97
93
|
}
|
98
94
|
|
99
95
|
if multi_session_info.nil?
|
100
96
|
Log.log.debug('Starting single session thread')
|
101
97
|
# single session for transfer : simple
|
102
98
|
session[:thread] = Thread.new(session) {|s|transfer_thread_entry(s)}
|
103
|
-
|
99
|
+
@sessions.push(session)
|
104
100
|
else
|
105
101
|
Log.log.debug('Starting multi session threads')
|
106
102
|
1.upto(multi_session_info[:count]) do |i|
|
@@ -108,22 +104,19 @@ module Aspera
|
|
108
104
|
sleep(@options[:spawn_delay_sec]) unless i.eql?(1)
|
109
105
|
# do deep copy (each thread has its own copy because it is modified here below and in thread)
|
110
106
|
this_session = session.clone
|
107
|
+
this_session[:ts] = this_session[:ts].clone
|
111
108
|
this_session[:env_args] = this_session[:env_args].clone
|
112
109
|
this_session[:env_args][:args] = this_session[:env_args][:args].clone
|
110
|
+
# set multi session part
|
113
111
|
this_session[:env_args][:args].unshift("-C#{i}:#{multi_session_info[:count]}")
|
114
112
|
# option: increment (default as per ascp manual) or not (cluster on other side ?)
|
115
113
|
this_session[:env_args][:args].unshift('-O', (multi_session_info[:udp_base] + i - 1).to_s) if @options[:multi_incr_udp]
|
114
|
+
# finally start the thread
|
116
115
|
this_session[:thread] = Thread.new(this_session) {|s|transfer_thread_entry(s)}
|
117
|
-
|
116
|
+
@sessions.push(this_session)
|
118
117
|
end
|
119
118
|
end
|
120
|
-
|
121
|
-
|
122
|
-
# add job to list of jobs
|
123
|
-
@jobs[the_job_id] = xfer_job
|
124
|
-
Log.log.debug{"jobs: #{@jobs.keys.count}"}
|
125
|
-
|
126
|
-
return the_job_id
|
119
|
+
return session[:job_id]
|
127
120
|
end # start_transfer
|
128
121
|
|
129
122
|
# wait for completion of all jobs started
|
@@ -132,16 +125,14 @@ module Aspera
|
|
132
125
|
Log.log.debug('wait_for_transfers_completion')
|
133
126
|
# set to non-nil to exit loop
|
134
127
|
result = []
|
135
|
-
@
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
result.push(session[:error] || :success)
|
140
|
-
end
|
128
|
+
@sessions.each do |session|
|
129
|
+
Log.log.debug{"join #{session[:thread]}"}
|
130
|
+
session[:thread].join
|
131
|
+
result.push(session[:error] || :success)
|
141
132
|
end
|
142
133
|
Log.log.debug('all transfers joined')
|
143
134
|
# since all are finished and we return the result, clear statuses
|
144
|
-
@
|
135
|
+
@sessions.clear
|
145
136
|
return result
|
146
137
|
end
|
147
138
|
|
@@ -198,27 +189,26 @@ module Aspera
|
|
198
189
|
# start ascp with management port.
|
199
190
|
# raises FaspError on error
|
200
191
|
# if there is a thread info: set and broadcast session id
|
192
|
+
# runs in separate thread
|
201
193
|
# @param env_args a hash containing :args :env :ascp_version
|
202
194
|
# @param session this session information
|
203
195
|
# could be private method
|
204
196
|
def start_transfer_with_args_env(env_args, session)
|
205
|
-
|
206
|
-
|
197
|
+
assert_type(env_args, Hash)
|
198
|
+
assert_type(session, Hash)
|
199
|
+
Log.log.debug{"env_args=#{env_args.inspect}"}
|
200
|
+
notify_progress(session_id: nil, type: :pre_start, info: 'starting')
|
207
201
|
begin
|
208
|
-
|
209
|
-
|
210
|
-
ascp_path = @mutex.synchronize do
|
211
|
-
Fasp::Installation.instance.path(env_args[:ascp_version])
|
212
|
-
end
|
213
|
-
# (optional) check it exists
|
214
|
-
raise Fasp::Error, "no such file: #{ascp_path}" unless File.exist?(ascp_path)
|
215
|
-
notify_progress(session_id: nil, type: :pre_start, info: 'starting ascp')
|
202
|
+
# we use Socket directly, instead of TCPServer, s it gives access to lower level options
|
203
|
+
mgt_server_socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
216
204
|
# open an available (0) local TCP port as ascp management
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
205
|
+
# Socket.pack_sockaddr_in(LISTEN_AVAILABLE_PORT, LISTEN_ADDRESS)
|
206
|
+
mgt_server_socket.bind(Addrinfo.tcp(LISTEN_ADDRESS, LISTEN_AVAILABLE_PORT))
|
207
|
+
# clone arguments and add mgt port
|
208
|
+
ascp_arguments = ['-M', mgt_server_socket.local_address.ip_port.to_s].concat(env_args[:args])
|
209
|
+
# mgt_server_socket.addr[1]
|
210
|
+
# get location of ascp executable
|
211
|
+
ascp_path = Fasp::Installation.instance.path(env_args[:ascp_version])
|
222
212
|
# display ascp command line
|
223
213
|
Log.log.debug do
|
224
214
|
[
|
@@ -229,20 +219,23 @@ module Aspera
|
|
229
219
|
].flatten.join(' ')
|
230
220
|
end
|
231
221
|
# start ascp in separate process
|
232
|
-
ascp_pid = Process.spawn(env_args[:env], [ascp_path, ascp_path], *ascp_arguments)
|
233
|
-
|
234
|
-
Log.log.debug{"before accept for pid (#{ascp_pid})"}
|
235
|
-
# init management socket
|
236
|
-
ascp_mgt_io = nil
|
222
|
+
ascp_pid = Process.spawn(env_args[:env], [ascp_path, ascp_path], *ascp_arguments, close_others: true)
|
223
|
+
Log.log.debug{"spawned pid #{ascp_pid}"}
|
237
224
|
notify_progress(session_id: nil, type: :pre_start, info: 'waiting for ascp')
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
225
|
+
mgt_server_socket.listen(1)
|
226
|
+
# TODO: timeout does not work when Process.spawn is used... until process exits, then it works
|
227
|
+
Log.log.debug{"before select, timeout: #{@options[:spawn_timeout_sec]}"}
|
228
|
+
readable, _, _ = IO.select([mgt_server_socket], nil, nil, @options[:spawn_timeout_sec])
|
229
|
+
Log.log.debug('after select, before accept')
|
230
|
+
assert(readable, exception_class: Fasp::Error){'timeout waiting mgt port connect (select not readable)'}
|
231
|
+
# There is a connection to accept
|
232
|
+
client_socket, _client_addrinfo = mgt_server_socket.accept
|
233
|
+
Log.log.debug('after accept')
|
234
|
+
ascp_mgt_io = client_socket.to_io
|
235
|
+
# management messages include file names which may be utf8
|
236
|
+
# by default socket is US-ASCII
|
237
|
+
# TODO: use same value as Encoding.default_external
|
238
|
+
ascp_mgt_io.set_encoding(Encoding::UTF_8)
|
246
239
|
session[:io] = ascp_mgt_io
|
247
240
|
processor = Management.new
|
248
241
|
# read management port, until socket is closed (gets returns nil)
|
@@ -250,11 +243,12 @@ module Aspera
|
|
250
243
|
event = processor.process_line(line.chomp)
|
251
244
|
next unless event
|
252
245
|
# event is ready
|
253
|
-
Log.log.
|
246
|
+
Log.log.trace1{Log.dump(:management_port, event)}
|
254
247
|
# Log.log.trace1{"event: #{JSON.generate(Management.enhanced_event_format(event))}"}
|
255
248
|
process_progress(event)
|
256
249
|
Log.log.error((event['Description']).to_s) if event['Type'].eql?('FILEERROR') # cspell:disable-line
|
257
250
|
end
|
251
|
+
Log.log.debug('management io closed')
|
258
252
|
last_event = processor.last_event
|
259
253
|
# check that last status was received before process exit
|
260
254
|
if last_event.is_a?(Hash)
|
@@ -264,7 +258,7 @@ module Aspera
|
|
264
258
|
session[:token_regenerator].respond_to?(:refreshed_transfer_token)
|
265
259
|
# regenerate token here, expired, or error on it
|
266
260
|
# Note: in multi-session, each session will have a different one.
|
267
|
-
Log.log.warn('Regenerating
|
261
|
+
Log.log.warn('Regenerating token for transfer')
|
268
262
|
env_args[:env]['ASPERA_SCP_TOKEN'] = session[:token_regenerator].refreshed_transfer_token
|
269
263
|
end
|
270
264
|
raise Fasp::Error.new(last_event['Description'], last_event['Code'].to_i)
|
@@ -277,11 +271,10 @@ module Aspera
|
|
277
271
|
rescue SystemCallError => e
|
278
272
|
# Process.spawn
|
279
273
|
raise Fasp::Error, e.message
|
280
|
-
rescue Timeout::Error
|
281
|
-
raise Fasp::Error, 'timeout waiting mgt port connect'
|
282
274
|
rescue Interrupt
|
283
275
|
raise Fasp::Error, 'transfer interrupted by user'
|
284
276
|
ensure
|
277
|
+
mgt_server_socket.close
|
285
278
|
# if ascp was successfully started
|
286
279
|
unless ascp_pid.nil?
|
287
280
|
# "wait" for process to avoid zombie
|
@@ -290,27 +283,37 @@ module Aspera
|
|
290
283
|
ascp_pid = nil
|
291
284
|
session.delete(:io)
|
292
285
|
if !status.success?
|
293
|
-
message = "ascp failed
|
294
|
-
# raise error only if there was not already an exception
|
286
|
+
message = "ascp failed: #{status}"
|
287
|
+
# raise error only if there was not already an exception (ERROR_INFO)
|
295
288
|
raise Fasp::Error, message unless $ERROR_INFO
|
296
|
-
# else
|
297
|
-
Log.log.
|
289
|
+
# else display this message also, as main exception is already here
|
290
|
+
Log.log.error(message)
|
298
291
|
end
|
299
292
|
end
|
300
293
|
end # begin-ensure
|
301
294
|
end # start_transfer_with_args_env
|
302
295
|
|
303
|
-
#
|
296
|
+
# @return [Array] list of sessions for a job
|
297
|
+
def sessions_by_job(job_id)
|
298
|
+
@sessions.select{|s| s[:job_id].eql?(job_id)}
|
299
|
+
end
|
300
|
+
|
301
|
+
# @return [Hash] session information
|
302
|
+
def session_by_id(id)
|
303
|
+
matches = @sessions.select{|s| s[:id].eql?(id)}
|
304
|
+
raise 'no such session' if matches.empty?
|
305
|
+
raise 'more than one session' if matches.length > 1
|
306
|
+
return matches.first
|
307
|
+
end
|
308
|
+
|
309
|
+
# send command of management port to ascp session (used in `asession)
|
304
310
|
# @param job_id identified transfer process
|
305
311
|
# @param session_index index of session (for multi session)
|
306
312
|
# @param data command on mgt port, examples:
|
307
313
|
# {'type'=>'START','source'=>_path_,'destination'=>_path_}
|
308
314
|
# {'type'=>'DONE'}
|
309
|
-
def send_command(job_id,
|
310
|
-
|
311
|
-
raise 'no such job' if job.nil?
|
312
|
-
session = job[:sessions][session_index]
|
313
|
-
raise 'no such session' if session.nil?
|
315
|
+
def send_command(job_id, data)
|
316
|
+
session = session_by_id(job_id)
|
314
317
|
Log.log.debug{"command: #{data}"}
|
315
318
|
# build command
|
316
319
|
command = data
|
@@ -321,20 +324,21 @@ module Aspera
|
|
321
324
|
.join("\n")
|
322
325
|
session[:io].puts(command)
|
323
326
|
end
|
327
|
+
attr_reader :sessions
|
324
328
|
|
325
329
|
private
|
326
330
|
|
327
331
|
# @param options : keys(symbol): see DEFAULT_OPTIONS
|
328
332
|
def initialize(options={})
|
329
333
|
super(options)
|
330
|
-
# all transfer jobs, key = SecureRandom.uuid, protected by mutex, cond var on change
|
331
|
-
@jobs = {}
|
332
|
-
# mutex protects global data accessed by threads
|
333
|
-
@mutex = Mutex.new
|
334
334
|
# set default options and override if specified
|
335
335
|
@options = AgentBase.options(default: DEFAULT_OPTIONS, options: options)
|
336
|
-
@resume_policy = ResumePolicy.new(@options[:resume].symbolize_keys)
|
337
336
|
Log.log.debug{Log.dump(:agent_options, @options)}
|
337
|
+
@resume_policy = ResumePolicy.new(@options[:resume].symbolize_keys)
|
338
|
+
# all transfer jobs, key = SecureRandom.uuid, protected by mutex, cond var on change
|
339
|
+
@sessions = []
|
340
|
+
# mutex protects global data accessed by threads
|
341
|
+
@mutex = Mutex.new
|
338
342
|
end
|
339
343
|
|
340
344
|
# transfer thread entry
|
@@ -4,6 +4,7 @@ require 'aspera/fasp/agent_base'
|
|
4
4
|
require 'aspera/fasp/transfer_spec'
|
5
5
|
require 'aspera/fasp/faux_file'
|
6
6
|
require 'aspera/log'
|
7
|
+
require 'aspera/assert'
|
7
8
|
require 'aspera/rest'
|
8
9
|
require 'securerandom'
|
9
10
|
require 'websocket'
|
@@ -185,7 +186,7 @@ module Aspera
|
|
185
186
|
@ws_io.write(@ws_handshake.to_s)
|
186
187
|
sleep(0.1)
|
187
188
|
@ws_handshake << @ws_io.readuntil("\r\n\r\n")
|
188
|
-
|
189
|
+
assert(@ws_handshake.finished?){'Error in websocket handshake'}
|
189
190
|
Log.log.debug{"#{LOG_WS_SEND}handshake success"}
|
190
191
|
# start read thread after handshake
|
191
192
|
@ws_read_thread = Thread.new {process_read_thread}
|
@@ -311,8 +312,8 @@ module Aspera
|
|
311
312
|
# HTTP download only supports file list
|
312
313
|
def start_transfer(transfer_spec, token_regenerator: nil)
|
313
314
|
raise 'GW URL must be set' if @gw_api.nil?
|
314
|
-
|
315
|
-
|
315
|
+
assert_type(transfer_spec['paths'], Array){'paths'}
|
316
|
+
assert_type(transfer_spec['token'], String){'only token based transfer is supported in GW'}
|
316
317
|
Log.log.debug{Log.dump(:user_spec, transfer_spec)}
|
317
318
|
transfer_spec['authentication'] ||= 'token'
|
318
319
|
case transfer_spec['direction']
|
@@ -5,6 +5,7 @@ require 'aspera/fasp/agent_base'
|
|
5
5
|
require 'aspera/fasp/transfer_spec'
|
6
6
|
require 'aspera/node'
|
7
7
|
require 'aspera/log'
|
8
|
+
require 'aspera/assert'
|
8
9
|
require 'aspera/oauth'
|
9
10
|
|
10
11
|
module Aspera
|
@@ -22,7 +23,7 @@ module Aspera
|
|
22
23
|
# attr_writer :options
|
23
24
|
|
24
25
|
def initialize(opts)
|
25
|
-
|
26
|
+
assert_type(opts, Hash){'node agent options'}
|
26
27
|
super(opts)
|
27
28
|
options = AgentBase.options(default: DEFAULT_OPTIONS, options: opts)
|
28
29
|
# root id is required for access key
|
@@ -67,7 +68,7 @@ module Aspera
|
|
67
68
|
case transfer_spec['direction']
|
68
69
|
when Fasp::TransferSpec::DIRECTION_SEND then transfer_spec['source_root_id'] = @root_id
|
69
70
|
when Fasp::TransferSpec::DIRECTION_RECEIVE then transfer_spec['destination_root_id'] = @root_id
|
70
|
-
else
|
71
|
+
else error_unexpected_value(transfer_spec['direction'])
|
71
72
|
end
|
72
73
|
end
|
73
74
|
# manage special additional parameter
|
@@ -2,46 +2,84 @@
|
|
2
2
|
|
3
3
|
require 'aspera/fasp/agent_base'
|
4
4
|
require 'aspera/fasp/installation'
|
5
|
+
require 'aspera/temp_file_manager'
|
6
|
+
require 'aspera/log'
|
7
|
+
require 'aspera/assert'
|
5
8
|
require 'json'
|
6
9
|
require 'uri'
|
7
10
|
|
8
11
|
module Aspera
|
9
12
|
module Fasp
|
10
13
|
class AgentTrsdk < Aspera::Fasp::AgentBase
|
14
|
+
# see https://github.com/grpc/grpc/blob/master/doc/naming.md
|
15
|
+
# https://grpc.io/docs/guides/custom-name-resolution/
|
16
|
+
LOCAL_SOCKET_ADDR = '127.0.0.1'
|
17
|
+
PORT_SEP = ':'
|
18
|
+
# port zero means select a random available high port
|
19
|
+
AUTO_LOCAL_TCP_PORT = "#{PORT_SEP}0"
|
11
20
|
DEFAULT_OPTIONS = {
|
12
|
-
url:
|
13
|
-
external: false,
|
14
|
-
keep: false
|
21
|
+
url: AUTO_LOCAL_TCP_PORT,
|
22
|
+
external: false, # expect that an external daemon is already running
|
23
|
+
keep: false # do not shutdown daemon on exit
|
15
24
|
}.freeze
|
16
25
|
private_constant :DEFAULT_OPTIONS
|
17
26
|
|
27
|
+
class << self
|
28
|
+
# Well, the port number is only in log file
|
29
|
+
def daemon_port_from_log(log_file)
|
30
|
+
result = nil
|
31
|
+
# if port is zero, a dynamic port was created, get it
|
32
|
+
File.open(log_file, 'r') do |file|
|
33
|
+
file.each_line do |line|
|
34
|
+
# Well, it's tricky to depend on log
|
35
|
+
if (m = line.match(/Info: API Server: Listening on ([^:]+):(\d+) /))
|
36
|
+
result = m[2].to_i
|
37
|
+
# no "break" , need to read last matching log line
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
raise 'Port not found in daemon logs' if result.nil?
|
42
|
+
Log.log.debug{"Got port #{result} from log"}
|
43
|
+
return result
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
18
47
|
# options come from transfer_info
|
19
48
|
def initialize(user_opts={})
|
20
49
|
super(user_opts)
|
21
50
|
@options = AgentBase.options(default: DEFAULT_OPTIONS, options: user_opts)
|
22
|
-
|
23
|
-
raise
|
51
|
+
is_local_auto_port = @options[:url].eql?(AUTO_LOCAL_TCP_PORT)
|
52
|
+
raise 'Cannot use options `keep` or `external` with port zero' if is_local_auto_port && (@options[:keep] || @options[:external])
|
24
53
|
Log.log.debug{Log.dump(:agent_options, @options)}
|
25
|
-
# load
|
54
|
+
# load SDK stub class on demand, as it's an optional gem
|
26
55
|
$LOAD_PATH.unshift(Installation.instance.sdk_ruby_folder)
|
27
56
|
require 'transfer_services_pb'
|
28
|
-
#
|
57
|
+
# keep PID for optional shutdown
|
29
58
|
@daemon_pid = nil
|
59
|
+
daemon_endpoint = @options[:url]
|
60
|
+
Log.log.debug{Log.dump(:daemon_endpoint, daemon_endpoint)}
|
61
|
+
# retry loop
|
30
62
|
begin
|
31
|
-
|
63
|
+
# no address: local bind
|
64
|
+
daemon_endpoint = "#{LOCAL_SOCKET_ADDR}#{daemon_endpoint}" if daemon_endpoint.match?(/^#{PORT_SEP}[0-9]+$/o)
|
65
|
+
# Create stub (without credentials)
|
66
|
+
@transfer_client = Transfersdk::TransferService::Stub.new(daemon_endpoint, :this_channel_is_insecure)
|
67
|
+
# Initiate actual connection
|
32
68
|
get_info_response = @transfer_client.get_info(Transfersdk::InstanceInfoRequest.new)
|
33
|
-
Log.log.debug{"
|
34
|
-
Log.log.warn{'
|
69
|
+
Log.log.debug{"Daemon info: #{get_info_response}"}
|
70
|
+
Log.log.warn{'Attached to existing daemon'} unless @daemon_pid || @options[:external] || @options[:keep]
|
35
71
|
at_exit{shutdown}
|
36
|
-
rescue GRPC::Unavailable
|
37
|
-
|
38
|
-
raise
|
72
|
+
rescue GRPC::Unavailable => e
|
73
|
+
# if transferd is external: do not start it, or other error
|
74
|
+
raise if @options[:external] || !e.message.include?('failed to connect')
|
75
|
+
# we already tried to start a daemon, but it failed
|
76
|
+
assert(@daemon_pid.nil?){"Daemon started with PID #{@daemon_pid}, but connection failed to #{daemon_endpoint}}"}
|
39
77
|
Log.log.warn('no daemon present, starting daemon...') if @options[:external]
|
40
78
|
# location of daemon binary
|
41
|
-
|
42
|
-
#
|
43
|
-
|
44
|
-
|
79
|
+
sdk_folder = File.realpath(File.join(Installation.instance.sdk_ruby_folder, '..'))
|
80
|
+
# transferd only supports local ip and port
|
81
|
+
daemon_uri = URI.parse("ipv4://#{daemon_endpoint}")
|
82
|
+
assert(daemon_uri.scheme.eql?('ipv4')){"Invalid scheme daemon URI #{daemon_endpoint}"}
|
45
83
|
# create a config file for daemon
|
46
84
|
config = {
|
47
85
|
address: daemon_uri.host,
|
@@ -49,35 +87,35 @@ module Aspera
|
|
49
87
|
fasp_runtime: {
|
50
88
|
use_embedded: false,
|
51
89
|
user_defined: {
|
52
|
-
bin:
|
53
|
-
etc:
|
90
|
+
bin: sdk_folder,
|
91
|
+
etc: sdk_folder
|
54
92
|
}
|
55
93
|
}
|
56
94
|
}
|
57
|
-
|
58
|
-
|
95
|
+
# config file and logs are created in same folder
|
96
|
+
transferd_base_tmp = TempFileManager.instance.new_file_path_global('transferd')
|
97
|
+
Log.log.debug{"transferd base tmp #{transferd_base_tmp}"}
|
98
|
+
conf_file = "#{transferd_base_tmp}.conf"
|
99
|
+
log_stdout = "#{transferd_base_tmp}.out"
|
100
|
+
log_stderr = "#{transferd_base_tmp}.err"
|
101
|
+
File.write(conf_file, config.to_json)
|
102
|
+
@daemon_pid = Process.spawn(Installation.instance.path(:transferd), '--config', conf_file, out: log_stdout, err: log_stderr)
|
59
103
|
begin
|
60
|
-
# wait for process to initialize
|
104
|
+
# wait for process to initialize, max 2 seconds
|
61
105
|
Timeout.timeout(2.0) do
|
106
|
+
# this returns if process dies (within 2 seconds)
|
62
107
|
_, status = Process.wait2(@daemon_pid)
|
63
|
-
raise "
|
108
|
+
raise "Transfer daemon exited with status #{status.exitstatus}. Check files: #{log_stdout} and #{log_stderr}"
|
64
109
|
end
|
65
110
|
rescue Timeout::Error
|
66
111
|
nil
|
67
112
|
end
|
68
|
-
Log.log.debug{"
|
113
|
+
Log.log.debug{"Daemon started with pid #{@daemon_pid}"}
|
69
114
|
Process.detach(@daemon_pid) if @options[:keep]
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
if (m = line.match(/Info: API Server: Listening on ([^:]+):(\d+) /))
|
75
|
-
daemon_uri.port = m[2].to_i
|
76
|
-
# no "break" , need to keep last one
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
115
|
+
at_exit {shutdown}
|
116
|
+
# update port for next connection attempt (if auto high port was requested)
|
117
|
+
daemon_endpoint = "#{LOCAL_SOCKET_ADDR}#{PORT_SEP}#{self.class.daemon_port_from_log(log_stdout)}" if is_local_auto_port
|
118
|
+
# local daemon started, try again
|
81
119
|
retry
|
82
120
|
end
|
83
121
|
end
|
@@ -133,8 +171,12 @@ module Aspera
|
|
133
171
|
end
|
134
172
|
|
135
173
|
def shutdown
|
136
|
-
|
137
|
-
|
174
|
+
stop_daemon unless @options[:keep]
|
175
|
+
end
|
176
|
+
|
177
|
+
def stop_daemon
|
178
|
+
if !@daemon_pid.nil?
|
179
|
+
Log.log.debug("Stopping daemon #{@daemon_pid}")
|
138
180
|
Process.kill('INT', @daemon_pid)
|
139
181
|
_, status = Process.wait2(@daemon_pid)
|
140
182
|
Log.log.debug("daemon stopped #{status}")
|