aspera-cli 4.15.0 → 4.17.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 +375 -280
- data/CONTRIBUTING.md +71 -18
- data/README.md +1978 -1656
- data/bin/ascli +13 -31
- data/bin/asession +32 -22
- data/examples/dascli +2 -2
- data/lib/aspera/agent/alpha.rb +117 -0
- data/lib/aspera/agent/base.rb +61 -0
- data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
- data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +116 -116
- data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +21 -19
- data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +21 -33
- data/lib/aspera/agent/trsdk.rb +188 -0
- data/lib/aspera/api/aoc.rb +586 -0
- data/lib/aspera/api/ats.rb +46 -0
- data/lib/aspera/api/cos_node.rb +95 -0
- data/lib/aspera/api/node.rb +344 -0
- data/lib/aspera/ascmd.rb +47 -14
- data/lib/aspera/{fasp → ascp}/installation.rb +54 -15
- data/lib/aspera/{fasp → ascp}/management.rb +14 -14
- data/lib/aspera/{fasp → ascp}/products.rb +1 -1
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
- data/lib/aspera/cli/extended_value.rb +5 -5
- data/lib/aspera/cli/formatter.rb +27 -14
- data/lib/aspera/cli/hints.rb +7 -6
- data/lib/aspera/cli/main.rb +49 -29
- data/lib/aspera/cli/manager.rb +46 -36
- data/lib/aspera/cli/plugin.rb +34 -20
- data/lib/aspera/cli/plugin_factory.rb +61 -0
- data/lib/aspera/cli/plugins/alee.rb +7 -7
- data/lib/aspera/cli/plugins/aoc.rb +168 -132
- data/lib/aspera/cli/plugins/ats.rb +33 -33
- data/lib/aspera/cli/plugins/bss.rb +3 -4
- data/lib/aspera/cli/plugins/config.rb +250 -272
- data/lib/aspera/cli/plugins/console.rb +8 -6
- data/lib/aspera/cli/plugins/cos.rb +20 -19
- data/lib/aspera/cli/plugins/faspex.rb +71 -60
- data/lib/aspera/cli/plugins/faspex5.rb +212 -133
- data/lib/aspera/cli/plugins/node.rb +83 -75
- data/lib/aspera/cli/plugins/orchestrator.rb +36 -44
- data/lib/aspera/cli/plugins/preview.rb +33 -31
- data/lib/aspera/cli/plugins/server.rb +33 -32
- data/lib/aspera/cli/plugins/shares.rb +39 -33
- data/lib/aspera/cli/sync_actions.rb +9 -9
- data/lib/aspera/cli/transfer_agent.rb +45 -25
- data/lib/aspera/cli/transfer_progress.rb +2 -3
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +5 -0
- data/lib/aspera/command_line_builder.rb +16 -14
- data/lib/aspera/coverage.rb +21 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +5 -4
- data/lib/aspera/faspex_gw.rb +13 -11
- data/lib/aspera/faspex_postproc.rb +6 -5
- data/lib/aspera/id_generator.rb +4 -2
- data/lib/aspera/json_rpc.rb +10 -8
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +29 -22
- data/lib/aspera/log.rb +5 -4
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node_simulator.rb +213 -0
- data/lib/aspera/oauth/base.rb +143 -0
- data/lib/aspera/oauth/factory.rb +124 -0
- data/lib/aspera/oauth/generic.rb +34 -0
- data/lib/aspera/oauth/jwt.rb +51 -0
- data/lib/aspera/oauth/url_json.rb +31 -0
- data/lib/aspera/oauth/web.rb +50 -0
- data/lib/aspera/oauth.rb +5 -328
- data/lib/aspera/open_application.rb +7 -7
- 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 +17 -7
- data/lib/aspera/preview/utils.rb +8 -7
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +187 -140
- data/lib/aspera/rest_error_analyzer.rb +1 -0
- data/lib/aspera/rest_errors_aspera.rb +5 -3
- data/lib/aspera/resumer.rb +77 -0
- data/lib/aspera/secret_hider.rb +5 -2
- data/lib/aspera/ssh.rb +15 -8
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/{fasp → transfer}/error.rb +3 -3
- data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
- data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
- data/lib/aspera/{fasp → transfer}/parameters.rb +95 -120
- data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +23 -19
- data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
- data/lib/aspera/transfer/sync.rb +273 -0
- data/lib/aspera/{fasp → transfer}/uri.rb +10 -9
- data/lib/aspera/web_server_simple.rb +12 -3
- data.tar.gz.sig +0 -0
- metadata +92 -68
- metadata.gz.sig +0 -0
- data/lib/aspera/aoc.rb +0 -606
- data/lib/aspera/ats_api.rb +0 -47
- data/lib/aspera/cos_node.rb +0 -93
- data/lib/aspera/fasp/agent_aspera.rb +0 -126
- data/lib/aspera/fasp/agent_base.rb +0 -48
- data/lib/aspera/fasp/agent_trsdk.rb +0 -146
- data/lib/aspera/fasp/resume_policy.rb +0 -77
- data/lib/aspera/node.rb +0 -338
- data/lib/aspera/sync.rb +0 -219
|
@@ -1,58 +1,61 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'aspera/
|
|
4
|
-
require 'aspera/
|
|
5
|
-
require 'aspera/
|
|
6
|
-
require 'aspera/
|
|
7
|
-
require 'aspera/
|
|
8
|
-
require 'aspera/
|
|
9
|
-
require 'aspera/
|
|
3
|
+
require 'aspera/agent/base'
|
|
4
|
+
require 'aspera/ascp/installation'
|
|
5
|
+
require 'aspera/ascp/management'
|
|
6
|
+
require 'aspera/transfer/parameters'
|
|
7
|
+
require 'aspera/transfer/error'
|
|
8
|
+
require 'aspera/transfer/spec'
|
|
9
|
+
require 'aspera/resumer'
|
|
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'
|
|
16
16
|
|
|
17
17
|
module Aspera
|
|
18
|
-
module
|
|
18
|
+
module Agent
|
|
19
19
|
# executes a local "ascp", connects mgt port, equivalent of "Fasp Manager"
|
|
20
|
-
class
|
|
20
|
+
class Direct < Base
|
|
21
21
|
# options for initialize (same as values in option transfer_info)
|
|
22
22
|
DEFAULT_OPTIONS = {
|
|
23
|
-
spawn_timeout_sec: 3,
|
|
24
|
-
spawn_delay_sec: 2,
|
|
25
23
|
wss: true, # true: if both SSH and wss in ts: prefer wss
|
|
24
|
+
ascp_args: [],
|
|
25
|
+
spawn_timeout_sec: 2,
|
|
26
|
+
spawn_delay_sec: 2, # optional delay to start between sessions
|
|
26
27
|
multi_incr_udp: true,
|
|
28
|
+
trusted_certs: [], # list of files with trusted certificates (stores)
|
|
27
29
|
resume: {},
|
|
28
|
-
ascp_args: [],
|
|
29
|
-
check_ignore: nil, # callback with host,port
|
|
30
30
|
quiet: true, # by default no native ascp progress bar
|
|
31
|
-
|
|
31
|
+
check_ignore_cb: nil, # callback with host,port
|
|
32
|
+
management_cb: nil # callback for management events
|
|
32
33
|
}.freeze
|
|
34
|
+
LISTEN_LOCAL_ADDRESS = '127.0.0.1'
|
|
35
|
+
ANY_AVAILABLE_PORT = 0 # 0 means any available port
|
|
33
36
|
# spellchecker: enable
|
|
34
|
-
private_constant :DEFAULT_OPTIONS
|
|
37
|
+
private_constant :DEFAULT_OPTIONS, :LISTEN_LOCAL_ADDRESS, :ANY_AVAILABLE_PORT
|
|
35
38
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
39
|
+
# method of Base
|
|
40
|
+
# start ascp transfer(s) (non blocking), single or multi-session
|
|
41
|
+
# session information added to @sessions
|
|
38
42
|
# @param transfer_spec [Hash] aspera transfer specification
|
|
43
|
+
# @param token_regenerator [Object] object with method refreshed_transfer_token
|
|
39
44
|
def start_transfer(transfer_spec, token_regenerator: nil)
|
|
40
|
-
the_job_id = SecureRandom.uuid
|
|
41
45
|
# clone transfer spec because we modify it (first level keys)
|
|
42
46
|
transfer_spec = transfer_spec.clone
|
|
43
|
-
# if there
|
|
44
|
-
if transfer_spec
|
|
47
|
+
# if there are aspera tags
|
|
48
|
+
if transfer_spec.dig('tags', Transfer::Spec::TAG_RESERVED).is_a?(Hash)
|
|
45
49
|
# TODO: what is this for ? only on local ascp ?
|
|
46
50
|
# NOTE: important: transfer id must be unique: generate random id
|
|
47
51
|
# using a non unique id results in discard of tags in AoC, and a package is never finalized
|
|
48
52
|
# all sessions in a multi-session transfer must have the same xfer_id (see admin manual)
|
|
49
|
-
transfer_spec['tags'][
|
|
53
|
+
transfer_spec['tags'][Transfer::Spec::TAG_RESERVED]['xfer_id'] ||= SecureRandom.uuid
|
|
50
54
|
Log.log.debug{"xfer id=#{transfer_spec['xfer_id']}"}
|
|
51
|
-
# TODO: useful ? node only ?
|
|
52
|
-
transfer_spec['tags'][
|
|
55
|
+
# TODO: useful ? node only ? seems to be a timeout for retry in node
|
|
56
|
+
transfer_spec['tags'][Transfer::Spec::TAG_RESERVED]['xfer_retry'] ||= 3600
|
|
53
57
|
end
|
|
54
58
|
Log.log.debug{Log.dump('ts', transfer_spec)}
|
|
55
|
-
|
|
56
59
|
# Compute this before using transfer spec because it potentially modifies the transfer spec
|
|
57
60
|
# (even if the var is not used in single session)
|
|
58
61
|
multi_session_info = nil
|
|
@@ -70,37 +73,31 @@ module Aspera
|
|
|
70
73
|
multi_session_info = nil
|
|
71
74
|
elsif @options[:multi_incr_udp] # multi_session_info[:count] > 0
|
|
72
75
|
# if option not true: keep default udp port for all sessions
|
|
73
|
-
multi_session_info[:udp_base] = transfer_spec.key?('fasp_port') ? transfer_spec['fasp_port'] :
|
|
76
|
+
multi_session_info[:udp_base] = transfer_spec.key?('fasp_port') ? transfer_spec['fasp_port'] : Transfer::Spec::UDP_PORT
|
|
74
77
|
# delete from original transfer spec, as we will increment values
|
|
75
78
|
transfer_spec.delete('fasp_port')
|
|
76
79
|
# override if specified, else use default value
|
|
77
80
|
end
|
|
78
81
|
end
|
|
79
82
|
|
|
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
83
|
# generic session information
|
|
90
84
|
session = {
|
|
85
|
+
id: nil, # SessionId from INIT message in mgt port
|
|
86
|
+
job_id: SecureRandom.uuid, # job id (regroup sessions)
|
|
87
|
+
ts: transfer_spec, # transfer spec
|
|
91
88
|
thread: nil, # Thread object monitoring management port, not nil when pushed to :sessions
|
|
92
89
|
error: nil, # exception if failed
|
|
93
90
|
io: nil, # management port server socket
|
|
94
|
-
id: nil, # SessionId from INIT message in mgt port
|
|
95
91
|
token_regenerator: token_regenerator, # regenerate bearer token with oauth
|
|
96
|
-
|
|
92
|
+
# env vars and args to ascp (from transfer spec)
|
|
93
|
+
env_args: Transfer::Parameters.new(transfer_spec, @options).ascp_args
|
|
97
94
|
}
|
|
98
95
|
|
|
99
96
|
if multi_session_info.nil?
|
|
100
97
|
Log.log.debug('Starting single session thread')
|
|
101
98
|
# single session for transfer : simple
|
|
102
99
|
session[:thread] = Thread.new(session) {|s|transfer_thread_entry(s)}
|
|
103
|
-
|
|
100
|
+
@sessions.push(session)
|
|
104
101
|
else
|
|
105
102
|
Log.log.debug('Starting multi session threads')
|
|
106
103
|
1.upto(multi_session_info[:count]) do |i|
|
|
@@ -108,22 +105,19 @@ module Aspera
|
|
|
108
105
|
sleep(@options[:spawn_delay_sec]) unless i.eql?(1)
|
|
109
106
|
# do deep copy (each thread has its own copy because it is modified here below and in thread)
|
|
110
107
|
this_session = session.clone
|
|
108
|
+
this_session[:ts] = this_session[:ts].clone
|
|
111
109
|
this_session[:env_args] = this_session[:env_args].clone
|
|
112
110
|
this_session[:env_args][:args] = this_session[:env_args][:args].clone
|
|
111
|
+
# set multi session part
|
|
113
112
|
this_session[:env_args][:args].unshift("-C#{i}:#{multi_session_info[:count]}")
|
|
114
113
|
# option: increment (default as per ascp manual) or not (cluster on other side ?)
|
|
115
114
|
this_session[:env_args][:args].unshift('-O', (multi_session_info[:udp_base] + i - 1).to_s) if @options[:multi_incr_udp]
|
|
115
|
+
# finally start the thread
|
|
116
116
|
this_session[:thread] = Thread.new(this_session) {|s|transfer_thread_entry(s)}
|
|
117
|
-
|
|
117
|
+
@sessions.push(this_session)
|
|
118
118
|
end
|
|
119
119
|
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
|
|
120
|
+
return session[:job_id]
|
|
127
121
|
end # start_transfer
|
|
128
122
|
|
|
129
123
|
# wait for completion of all jobs started
|
|
@@ -132,16 +126,14 @@ module Aspera
|
|
|
132
126
|
Log.log.debug('wait_for_transfers_completion')
|
|
133
127
|
# set to non-nil to exit loop
|
|
134
128
|
result = []
|
|
135
|
-
@
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
result.push(session[:error] || :success)
|
|
140
|
-
end
|
|
129
|
+
@sessions.each do |session|
|
|
130
|
+
Log.log.debug{"join #{session[:thread]}"}
|
|
131
|
+
session[:thread].join
|
|
132
|
+
result.push(session[:error] || :success)
|
|
141
133
|
end
|
|
142
134
|
Log.log.debug('all transfers joined')
|
|
143
135
|
# since all are finished and we return the result, clear statuses
|
|
144
|
-
@
|
|
136
|
+
@sessions.clear
|
|
145
137
|
return result
|
|
146
138
|
end
|
|
147
139
|
|
|
@@ -150,12 +142,6 @@ module Aspera
|
|
|
150
142
|
Log.log.debug('fasp local shutdown')
|
|
151
143
|
end
|
|
152
144
|
|
|
153
|
-
# cspell:disable
|
|
154
|
-
# begin 'Type' => 'NOTIFICATION', 'PreTransferBytes' => size
|
|
155
|
-
# progress 'Type' => 'STATS', 'Bytescont' => size
|
|
156
|
-
# end 'Type' => 'DONE'
|
|
157
|
-
# cspell:enable
|
|
158
|
-
|
|
159
145
|
# @param event management port event
|
|
160
146
|
def process_progress(event)
|
|
161
147
|
session_id = event['SessionId']
|
|
@@ -198,63 +184,66 @@ module Aspera
|
|
|
198
184
|
# start ascp with management port.
|
|
199
185
|
# raises FaspError on error
|
|
200
186
|
# if there is a thread info: set and broadcast session id
|
|
187
|
+
# runs in separate thread
|
|
201
188
|
# @param env_args a hash containing :args :env :ascp_version
|
|
202
189
|
# @param session this session information
|
|
203
190
|
# could be private method
|
|
204
191
|
def start_transfer_with_args_env(env_args, session)
|
|
205
|
-
|
|
206
|
-
|
|
192
|
+
Aspera.assert_type(env_args, Hash)
|
|
193
|
+
Aspera.assert_type(session, Hash)
|
|
194
|
+
Log.log.debug{"env_args=#{env_args.inspect}"}
|
|
195
|
+
notify_progress(session_id: nil, type: :pre_start, info: 'starting')
|
|
207
196
|
begin
|
|
208
|
-
|
|
197
|
+
ascp_pid = nil
|
|
198
|
+
# we use Socket directly, instead of TCPServer, as it gives access to lower level options
|
|
199
|
+
socket_class = RUBY_ENGINE.eql?('jruby') ? ServerSocket : Socket
|
|
200
|
+
mgt_server_socket = socket_class.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
|
201
|
+
# open any available (0) local TCP port for use as ascp management port
|
|
202
|
+
mgt_server_socket.bind(Addrinfo.tcp(LISTEN_LOCAL_ADDRESS, ANY_AVAILABLE_PORT))
|
|
203
|
+
# build arguments and add mgt port
|
|
204
|
+
ascp_arguments = ['-M', mgt_server_socket.local_address.ip_port.to_s].concat(env_args[:args])
|
|
209
205
|
# get location of ascp executable
|
|
210
|
-
ascp_path =
|
|
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')
|
|
216
|
-
# open an available (0) local TCP port as ascp management
|
|
217
|
-
mgt_sock = TCPServer.new('127.0.0.1', 0)
|
|
218
|
-
# clone arguments as we eed to modify with mgt port
|
|
219
|
-
ascp_arguments = env_args[:args].clone
|
|
220
|
-
# add management port on the selected local port
|
|
221
|
-
ascp_arguments.unshift('-M', mgt_sock.addr[1].to_s)
|
|
206
|
+
ascp_path = Ascp::Installation.instance.path(env_args[:ascp_version])
|
|
222
207
|
# display ascp command line
|
|
223
208
|
Log.log.debug do
|
|
224
209
|
[
|
|
225
|
-
'execute:',
|
|
210
|
+
'execute:'.red,
|
|
226
211
|
env_args[:env].map{|k, v| "#{k}=#{Shellwords.shellescape(v)}"},
|
|
227
212
|
Shellwords.shellescape(ascp_path),
|
|
228
213
|
ascp_arguments.map{|a|Shellwords.shellescape(a)}
|
|
229
214
|
].flatten.join(' ')
|
|
230
215
|
end
|
|
231
216
|
# 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
|
|
217
|
+
ascp_pid = Process.spawn(env_args[:env], [ascp_path, ascp_path], *ascp_arguments, close_others: true)
|
|
218
|
+
Log.log.debug{"spawned ascp pid #{ascp_pid}"}
|
|
237
219
|
notify_progress(session_id: nil, type: :pre_start, info: 'waiting for ascp')
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
220
|
+
mgt_server_socket.listen(1)
|
|
221
|
+
# TODO: timeout does not work when Process.spawn is used... until process exits, then it works
|
|
222
|
+
Log.log.debug{"before select, timeout: #{@options[:spawn_timeout_sec]}"}
|
|
223
|
+
readable, _, _ = IO.select([mgt_server_socket], nil, nil, @options[:spawn_timeout_sec])
|
|
224
|
+
Log.log.debug('after select, before accept')
|
|
225
|
+
Aspera.assert(readable, exception_class: Transfer::Error){'timeout waiting mgt port connect (select not readable)'}
|
|
226
|
+
# There is a connection to accept
|
|
227
|
+
client_socket, _client_addrinfo = mgt_server_socket.accept
|
|
228
|
+
Log.log.debug('after accept')
|
|
229
|
+
ascp_mgt_io = client_socket.to_io
|
|
230
|
+
# management messages include file names which may be utf8
|
|
231
|
+
# by default socket is US-ASCII
|
|
232
|
+
# TODO: use same value as Encoding.default_external
|
|
233
|
+
ascp_mgt_io.set_encoding(Encoding::UTF_8)
|
|
246
234
|
session[:io] = ascp_mgt_io
|
|
247
|
-
processor = Management.new
|
|
235
|
+
processor = Ascp::Management.new
|
|
248
236
|
# read management port, until socket is closed (gets returns nil)
|
|
249
237
|
while (line = ascp_mgt_io.gets)
|
|
250
238
|
event = processor.process_line(line.chomp)
|
|
251
239
|
next unless event
|
|
252
240
|
# event is ready
|
|
253
|
-
Log.log.
|
|
254
|
-
|
|
241
|
+
Log.log.trace1{Log.dump(:management_port, event)}
|
|
242
|
+
@options[:management_cb]&.call(event)
|
|
255
243
|
process_progress(event)
|
|
256
244
|
Log.log.error((event['Description']).to_s) if event['Type'].eql?('FILEERROR') # cspell:disable-line
|
|
257
245
|
end
|
|
246
|
+
Log.log.debug('management io closed')
|
|
258
247
|
last_event = processor.last_event
|
|
259
248
|
# check that last status was received before process exit
|
|
260
249
|
if last_event.is_a?(Hash)
|
|
@@ -264,10 +253,10 @@ module Aspera
|
|
|
264
253
|
session[:token_regenerator].respond_to?(:refreshed_transfer_token)
|
|
265
254
|
# regenerate token here, expired, or error on it
|
|
266
255
|
# Note: in multi-session, each session will have a different one.
|
|
267
|
-
Log.log.warn('Regenerating
|
|
256
|
+
Log.log.warn('Regenerating token for transfer')
|
|
268
257
|
env_args[:env]['ASPERA_SCP_TOKEN'] = session[:token_regenerator].refreshed_transfer_token
|
|
269
258
|
end
|
|
270
|
-
raise
|
|
259
|
+
raise Transfer::Error.new(last_event['Description'], last_event['Code'].to_i)
|
|
271
260
|
when 'DONE'
|
|
272
261
|
nil
|
|
273
262
|
else
|
|
@@ -275,42 +264,52 @@ module Aspera
|
|
|
275
264
|
end # case
|
|
276
265
|
end
|
|
277
266
|
rescue SystemCallError => e
|
|
278
|
-
# Process.spawn
|
|
279
|
-
raise
|
|
280
|
-
rescue Timeout::Error
|
|
281
|
-
raise Fasp::Error, 'timeout waiting mgt port connect'
|
|
267
|
+
# Process.spawn failed, or socket error
|
|
268
|
+
raise Transfer::Error, e.message
|
|
282
269
|
rescue Interrupt
|
|
283
|
-
raise
|
|
270
|
+
raise Transfer::Error, 'transfer interrupted by user'
|
|
284
271
|
ensure
|
|
285
|
-
|
|
272
|
+
mgt_server_socket.close
|
|
273
|
+
# if ascp was successfully started, check its status
|
|
286
274
|
unless ascp_pid.nil?
|
|
287
275
|
# "wait" for process to avoid zombie
|
|
288
276
|
Process.wait(ascp_pid)
|
|
289
277
|
status = $CHILD_STATUS
|
|
290
278
|
ascp_pid = nil
|
|
291
279
|
session.delete(:io)
|
|
292
|
-
if
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
raise
|
|
296
|
-
|
|
297
|
-
|
|
280
|
+
# status is nil if an exception occurred before starting ascp
|
|
281
|
+
if !status&.success?
|
|
282
|
+
message = status.nil? ? 'ascp not started' : "ascp failed (#{status})"
|
|
283
|
+
# raise error only if there was not already an exception (ERROR_INFO)
|
|
284
|
+
raise Transfer::Error, message unless $ERROR_INFO
|
|
285
|
+
# else display this message also, as main exception is already here
|
|
286
|
+
Log.log.error(message)
|
|
298
287
|
end
|
|
299
288
|
end
|
|
300
289
|
end # begin-ensure
|
|
301
290
|
end # start_transfer_with_args_env
|
|
302
291
|
|
|
303
|
-
#
|
|
292
|
+
# @return [Array] list of sessions for a job
|
|
293
|
+
def sessions_by_job(job_id)
|
|
294
|
+
@sessions.select{|s| s[:job_id].eql?(job_id)}
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# @return [Hash] session information
|
|
298
|
+
def session_by_id(id)
|
|
299
|
+
matches = @sessions.select{|s| s[:id].eql?(id)}
|
|
300
|
+
raise 'no such session' if matches.empty?
|
|
301
|
+
raise 'more than one session' if matches.length > 1
|
|
302
|
+
return matches.first
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# send command of management port to ascp session (used in `asession)
|
|
304
306
|
# @param job_id identified transfer process
|
|
305
307
|
# @param session_index index of session (for multi session)
|
|
306
308
|
# @param data command on mgt port, examples:
|
|
307
309
|
# {'type'=>'START','source'=>_path_,'destination'=>_path_}
|
|
308
310
|
# {'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?
|
|
311
|
+
def send_command(job_id, data)
|
|
312
|
+
session = session_by_id(job_id)
|
|
314
313
|
Log.log.debug{"command: #{data}"}
|
|
315
314
|
# build command
|
|
316
315
|
command = data
|
|
@@ -321,20 +320,21 @@ module Aspera
|
|
|
321
320
|
.join("\n")
|
|
322
321
|
session[:io].puts(command)
|
|
323
322
|
end
|
|
323
|
+
attr_reader :sessions
|
|
324
324
|
|
|
325
325
|
private
|
|
326
326
|
|
|
327
327
|
# @param options : keys(symbol): see DEFAULT_OPTIONS
|
|
328
328
|
def initialize(options={})
|
|
329
329
|
super(options)
|
|
330
|
+
# set default options and override if specified
|
|
331
|
+
@options = Base.options(default: DEFAULT_OPTIONS, options: options)
|
|
332
|
+
Log.log.debug{Log.dump(:agent_options, @options)}
|
|
333
|
+
@resume_policy = Resumer.new(@options[:resume].symbolize_keys)
|
|
330
334
|
# all transfer jobs, key = SecureRandom.uuid, protected by mutex, cond var on change
|
|
331
|
-
@
|
|
335
|
+
@sessions = []
|
|
332
336
|
# mutex protects global data accessed by threads
|
|
333
337
|
@mutex = Mutex.new
|
|
334
|
-
# set default options and override if specified
|
|
335
|
-
@options = AgentBase.options(default: DEFAULT_OPTIONS, options: options)
|
|
336
|
-
@resume_policy = ResumePolicy.new(@options[:resume].symbolize_keys)
|
|
337
|
-
Log.log.debug{Log.dump(:agent_options, @options)}
|
|
338
338
|
end
|
|
339
339
|
|
|
340
340
|
# transfer thread entry
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'aspera/
|
|
4
|
-
require 'aspera/
|
|
5
|
-
require 'aspera/
|
|
3
|
+
require 'aspera/agent/base'
|
|
4
|
+
require 'aspera/transfer/spec'
|
|
5
|
+
require 'aspera/transfer/faux_file'
|
|
6
6
|
require 'aspera/log'
|
|
7
|
+
require 'aspera/assert'
|
|
7
8
|
require 'aspera/rest'
|
|
8
9
|
require 'securerandom'
|
|
9
10
|
require 'websocket'
|
|
@@ -11,7 +12,7 @@ require 'base64'
|
|
|
11
12
|
require 'json'
|
|
12
13
|
|
|
13
14
|
module Aspera
|
|
14
|
-
module
|
|
15
|
+
module Agent
|
|
15
16
|
# Start a transfer using Aspera HTTP Gateway, using web socket secure for uploads
|
|
16
17
|
# ref: https://api.ibm.com/explorer/catalog/aspera/product/ibm-aspera/api/http-gateway-api/doc/guides-toc
|
|
17
18
|
# https://developer.ibm.com/apis/catalog?search=%22aspera%20http%22
|
|
@@ -25,7 +26,7 @@ module Aspera
|
|
|
25
26
|
# 1 JSON.slice_upload File start "end_slice_upload" sent_v2_delimiter
|
|
26
27
|
# 2.. Binary File binary chunks "end upload" sent_general
|
|
27
28
|
# last JSON.slice_upload File end "end_slice_upload" sent_v2_delimiter
|
|
28
|
-
class
|
|
29
|
+
class Httpgw < Base
|
|
29
30
|
MSG_SEND_TRANSFER_SPEC = 'transfer_spec'
|
|
30
31
|
MSG_SEND_SLICE_UPLOAD = 'slice_upload'
|
|
31
32
|
MSG_RECV_DATA_RECEIVED_SIGNAL = 'end upload'
|
|
@@ -159,7 +160,7 @@ module Aspera
|
|
|
159
160
|
# modify transfer spec to be suitable for GW
|
|
160
161
|
transfer_spec['paths'].each do |item|
|
|
161
162
|
# save actual file location to be able read contents later
|
|
162
|
-
file_to_add = FauxFile.open(item['source'])
|
|
163
|
+
file_to_add = Transfer::FauxFile.open(item['source'])
|
|
163
164
|
if file_to_add
|
|
164
165
|
item['source'] = file_to_add.path
|
|
165
166
|
item['file_size'] = file_to_add.size
|
|
@@ -175,7 +176,8 @@ module Aspera
|
|
|
175
176
|
files_to_read.push(file_to_add)
|
|
176
177
|
total_bytes_to_transfer += item['file_size']
|
|
177
178
|
end
|
|
178
|
-
|
|
179
|
+
# TODO: check that this is available in endpoints: @api_info['endpoints']
|
|
180
|
+
upload_url = File.join(@gw_base_url, @options[:api_version], 'upload')
|
|
179
181
|
notify_progress(session_id: nil, type: :pre_start, info: 'connecting wss')
|
|
180
182
|
# open web socket to end point (equivalent to Net::HTTP.start)
|
|
181
183
|
http_session = Rest.start_http_session(upload_url)
|
|
@@ -185,7 +187,7 @@ module Aspera
|
|
|
185
187
|
@ws_io.write(@ws_handshake.to_s)
|
|
186
188
|
sleep(0.1)
|
|
187
189
|
@ws_handshake << @ws_io.readuntil("\r\n\r\n")
|
|
188
|
-
|
|
190
|
+
Aspera.assert(@ws_handshake.finished?){'Error in websocket handshake'}
|
|
189
191
|
Log.log.debug{"#{LOG_WS_SEND}handshake success"}
|
|
190
192
|
# start read thread after handshake
|
|
191
193
|
@ws_read_thread = Thread.new {process_read_thread}
|
|
@@ -226,7 +228,7 @@ module Aspera
|
|
|
226
228
|
fileIndex: file_index
|
|
227
229
|
}
|
|
228
230
|
file = files_to_read[file_index]
|
|
229
|
-
if file.is_a?(FauxFile)
|
|
231
|
+
if file.is_a?(Transfer::FauxFile)
|
|
230
232
|
slice_info[:name] = file.path
|
|
231
233
|
else
|
|
232
234
|
file = File.open(file)
|
|
@@ -294,7 +296,7 @@ module Aspera
|
|
|
294
296
|
end
|
|
295
297
|
creation = @gw_api.create('v1/download', {'transfer_spec' => transfer_spec})[:data]
|
|
296
298
|
transfer_uuid = creation['url'].split('/').last
|
|
297
|
-
|
|
299
|
+
file_name =
|
|
298
300
|
if transfer_spec['zip_required'] || transfer_spec['paths'].length > 1
|
|
299
301
|
# it is a zip file if zip is required or there is more than 1 file
|
|
300
302
|
transfer_spec['download_name'] + '.zip'
|
|
@@ -302,8 +304,8 @@ module Aspera
|
|
|
302
304
|
# it is a plain file if we don't require zip and there is only one file
|
|
303
305
|
File.basename(transfer_spec['paths'].first['source'])
|
|
304
306
|
end
|
|
305
|
-
|
|
306
|
-
@gw_api.call(
|
|
307
|
+
file_path = File.join(transfer_spec['destination_root'], file_name)
|
|
308
|
+
@gw_api.call(operation: 'GET', subpath: "v1/download/#{transfer_uuid}", save_to_file: file_path)
|
|
307
309
|
end
|
|
308
310
|
|
|
309
311
|
# start FASP transfer based on transfer spec (hash table)
|
|
@@ -311,14 +313,14 @@ module Aspera
|
|
|
311
313
|
# HTTP download only supports file list
|
|
312
314
|
def start_transfer(transfer_spec, token_regenerator: nil)
|
|
313
315
|
raise 'GW URL must be set' if @gw_api.nil?
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
Aspera.assert_type(transfer_spec['paths'], Array){'paths'}
|
|
317
|
+
Aspera.assert_type(transfer_spec['token'], String){'only token based transfer is supported in GW'}
|
|
316
318
|
Log.log.debug{Log.dump(:user_spec, transfer_spec)}
|
|
317
319
|
transfer_spec['authentication'] ||= 'token'
|
|
318
320
|
case transfer_spec['direction']
|
|
319
|
-
when
|
|
321
|
+
when Transfer::Spec::DIRECTION_SEND
|
|
320
322
|
upload(transfer_spec)
|
|
321
|
-
when
|
|
323
|
+
when Transfer::Spec::DIRECTION_RECEIVE
|
|
322
324
|
download(transfer_spec)
|
|
323
325
|
else
|
|
324
326
|
raise "unexpected direction: [#{transfer_spec['direction']}]"
|
|
@@ -339,10 +341,10 @@ module Aspera
|
|
|
339
341
|
|
|
340
342
|
def initialize(opts)
|
|
341
343
|
super(opts)
|
|
342
|
-
@options =
|
|
344
|
+
@options = Base.options(default: DEFAULT_OPTIONS, options: opts)
|
|
343
345
|
# remove /v1 from end of user-provided GW url: we need the base url only
|
|
344
|
-
@options[:url].gsub(%r{/v1/*$}, '')
|
|
345
|
-
@gw_api = Rest.new(
|
|
346
|
+
@gw_base_url = @options[:url].gsub(%r{/v1/*$}, '')
|
|
347
|
+
@gw_api = Rest.new(base_url: @gw_base_url)
|
|
346
348
|
@api_info = @gw_api.read('v1/info')[:data]
|
|
347
349
|
Log.log.debug{Log.dump(:api_info, @api_info)}
|
|
348
350
|
# web socket endpoint: by default use v2 (newer gateways), without base64 encoding
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# cspell:ignore precalc
|
|
4
|
-
require 'aspera/
|
|
5
|
-
require 'aspera/
|
|
6
|
-
require 'aspera/node'
|
|
4
|
+
require 'aspera/agent/base'
|
|
5
|
+
require 'aspera/transfer/spec'
|
|
6
|
+
require 'aspera/api/node'
|
|
7
7
|
require 'aspera/log'
|
|
8
|
+
require 'aspera/assert'
|
|
8
9
|
require 'aspera/oauth'
|
|
9
10
|
|
|
10
11
|
module Aspera
|
|
11
|
-
module
|
|
12
|
+
module Agent
|
|
12
13
|
# this singleton class is used by the CLI to provide a common interface to start a transfer
|
|
13
14
|
# before using it, the use must set the `node_api` member.
|
|
14
|
-
class
|
|
15
|
+
class Node < Base
|
|
15
16
|
DEFAULT_OPTIONS = {
|
|
16
17
|
url: :required,
|
|
17
18
|
username: :required,
|
|
@@ -22,15 +23,15 @@ module Aspera
|
|
|
22
23
|
# attr_writer :options
|
|
23
24
|
|
|
24
25
|
def initialize(opts)
|
|
25
|
-
|
|
26
|
+
Aspera.assert_type(opts, Hash){'node agent options'}
|
|
26
27
|
super(opts)
|
|
27
|
-
options =
|
|
28
|
+
options = Base.options(default: DEFAULT_OPTIONS, options: opts)
|
|
28
29
|
# root id is required for access key
|
|
29
30
|
@root_id = options[:root_id]
|
|
30
31
|
rest_params = { base_url: options[:url]}
|
|
31
|
-
if
|
|
32
|
-
|
|
33
|
-
rest_params[:headers] =
|
|
32
|
+
if OAuth::Factory.bearer?(options[:password])
|
|
33
|
+
Aspera.assert(!@root_id.nil?){'root_id not allowed for access key'}
|
|
34
|
+
rest_params[:headers] = Api::Node.bearer_headers(options[:password], access_key: options[:username])
|
|
34
35
|
else
|
|
35
36
|
rest_params[:auth] = {
|
|
36
37
|
type: :basic,
|
|
@@ -38,7 +39,7 @@ module Aspera
|
|
|
38
39
|
password: options[:password]
|
|
39
40
|
}
|
|
40
41
|
end
|
|
41
|
-
@node_api = Rest.new(rest_params)
|
|
42
|
+
@node_api = Rest.new(**rest_params)
|
|
42
43
|
# TODO: currently only supports one transfer. This is bad shortcut. but ok for CLI.
|
|
43
44
|
@transfer_id = nil
|
|
44
45
|
# Log.log.debug{Log.dump(:agent_options, @options)}
|
|
@@ -46,11 +47,9 @@ module Aspera
|
|
|
46
47
|
|
|
47
48
|
# used internally to ensure node api is set before using.
|
|
48
49
|
def node_api_
|
|
49
|
-
|
|
50
|
+
Aspera.assert(!@node_api.nil?){'Before using this object, set the node_api attribute to a Aspera::Rest object'}
|
|
50
51
|
return @node_api
|
|
51
52
|
end
|
|
52
|
-
# use this to read the node_api end point.
|
|
53
|
-
# attr_reader :node_api
|
|
54
53
|
|
|
55
54
|
# use this to set the node_api end point before using the class.
|
|
56
55
|
def node_api=(new_value)
|
|
@@ -65,30 +64,19 @@ module Aspera
|
|
|
65
64
|
# add root id if access key
|
|
66
65
|
if !@root_id.nil?
|
|
67
66
|
case transfer_spec['direction']
|
|
68
|
-
when
|
|
69
|
-
when
|
|
70
|
-
else
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
# manage special additional parameter
|
|
74
|
-
if transfer_spec.key?('EX_ssh_key_paths') && transfer_spec['EX_ssh_key_paths'].is_a?(Array) && !transfer_spec['EX_ssh_key_paths'].empty?
|
|
75
|
-
# not standard, so place standard field
|
|
76
|
-
if transfer_spec.key?('ssh_private_key')
|
|
77
|
-
Log.log.warn('Both ssh_private_key and EX_ssh_key_paths are present, using ssh_private_key')
|
|
78
|
-
else
|
|
79
|
-
Log.log.warn('EX_ssh_key_paths has multiple keys, using first one only') unless transfer_spec['EX_ssh_key_paths'].length.eql?(1)
|
|
80
|
-
transfer_spec['ssh_private_key'] = File.read(transfer_spec['EX_ssh_key_paths'].first)
|
|
81
|
-
transfer_spec.delete('EX_ssh_key_paths')
|
|
67
|
+
when Transfer::Spec::DIRECTION_SEND then transfer_spec['source_root_id'] = @root_id
|
|
68
|
+
when Transfer::Spec::DIRECTION_RECEIVE then transfer_spec['destination_root_id'] = @root_id
|
|
69
|
+
else Aspera.error_unexpected_value(transfer_spec['direction'])
|
|
82
70
|
end
|
|
83
71
|
end
|
|
84
72
|
# add mandatory retry parameter for node api
|
|
85
73
|
ts_tags = transfer_spec['tags']
|
|
86
|
-
if ts_tags.is_a?(Hash) && ts_tags[
|
|
87
|
-
ts_tags[
|
|
74
|
+
if ts_tags.is_a?(Hash) && ts_tags[Transfer::Spec::TAG_RESERVED].is_a?(Hash)
|
|
75
|
+
ts_tags[Transfer::Spec::TAG_RESERVED]['xfer_retry'] ||= 150
|
|
88
76
|
end
|
|
89
77
|
# Optimization in case of sending to the same node
|
|
90
78
|
# TODO: probably remove this, as /etc/hosts shall be used for that
|
|
91
|
-
if !transfer_spec['wss_enabled'] && transfer_spec['remote_host'].eql?(URI.parse(node_api_.
|
|
79
|
+
if !transfer_spec['wss_enabled'] && transfer_spec['remote_host'].eql?(URI.parse(node_api_.base_url).host)
|
|
92
80
|
transfer_spec['remote_host'] = '127.0.0.1'
|
|
93
81
|
end
|
|
94
82
|
resp = node_api_.create('ops/transfers', transfer_spec)[:data]
|
|
@@ -132,10 +120,10 @@ module Aspera
|
|
|
132
120
|
notify_progress(type: :end, session_id: @transfer_id)
|
|
133
121
|
# Bug in HSTS ? transfer is marked failed, but there is no reason
|
|
134
122
|
break if transfer_data['error_code'].eql?(0) && transfer_data['error_desc'].empty?
|
|
135
|
-
raise
|
|
123
|
+
raise Transfer::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
|
|
136
124
|
else
|
|
137
125
|
Log.log.warn{"transfer_data -> #{transfer_data}"}
|
|
138
|
-
raise
|
|
126
|
+
raise Transfer::Error, "status: #{transfer_data['status']}. code: #{transfer_data['error_code']}. description: #{transfer_data['error_desc']}"
|
|
139
127
|
end
|
|
140
128
|
sleep(1.0)
|
|
141
129
|
end
|