aspera-cli 4.15.0 → 4.16.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.
- 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}")
|