aspera-cli 4.27.5 → 4.28.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/CHANGELOG.md +160 -4
- data/CONTRIBUTING.md +4 -2
- data/README.md +1 -1
- data/bin/asession +1 -1
- data/docs/README.md +1086 -331
- data/docs/test-mcp-with-ai.md +5 -3
- data/lib/aspera/agent/direct.rb +15 -4
- data/lib/aspera/agent/factory.rb +2 -1
- data/lib/aspera/agent/node.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +1 -1
- data/lib/aspera/api/node.rb +6 -2
- data/lib/aspera/ascp/installation.rb +47 -11
- data/lib/aspera/ascp/management.rb +89 -43
- data/lib/aspera/cli/ascp_actions.rb +5 -19
- data/lib/aspera/cli/bootstrapper.rb +1 -2
- data/lib/aspera/cli/command_registry.rb +43 -3
- data/lib/aspera/cli/command_spec.rb +3 -0
- data/lib/aspera/cli/completion/ascli.bash +39 -5
- data/lib/aspera/cli/completion/ascli.fish +16 -8
- data/lib/aspera/cli/completion/ascli.ps1 +41 -0
- data/lib/aspera/cli/completion/ascli.zsh +20 -4
- data/lib/aspera/cli/completion_actions.rb +360 -0
- data/lib/aspera/cli/error.rb +42 -0
- data/lib/aspera/cli/extended_value.rb +30 -5
- data/lib/aspera/cli/formatter.rb +118 -56
- data/lib/aspera/cli/hints.rb +0 -1
- data/lib/aspera/cli/http.rb +6 -5
- data/lib/aspera/cli/info.rb +10 -4
- data/lib/aspera/cli/mailer.rb +1 -1
- data/lib/aspera/cli/mcp/http_servlet.rb +156 -0
- data/lib/aspera/cli/mcp/secrets.rb +143 -0
- data/lib/aspera/cli/mcp/tool.rb +231 -0
- data/lib/aspera/cli/option_value.rb +9 -1
- data/lib/aspera/cli/options.schema.yaml +390 -39
- data/lib/aspera/cli/parser.rb +14 -5
- data/lib/aspera/cli/plugins/aoc.rb +8 -11
- data/lib/aspera/cli/plugins/base.rb +36 -15
- data/lib/aspera/cli/plugins/config.rb +15 -44
- data/lib/aspera/cli/plugins/console.rb +3 -1
- data/lib/aspera/cli/plugins/factory.rb +1 -1
- data/lib/aspera/cli/plugins/faspex5.rb +9 -19
- data/lib/aspera/cli/plugins/faspio.rb +36 -5
- data/lib/aspera/cli/plugins/httpgw.rb +1 -0
- data/lib/aspera/cli/plugins/mcp.rb +177 -118
- data/lib/aspera/cli/plugins/node.rb +14 -8
- data/lib/aspera/cli/plugins/orchestrator.rb +351 -38
- data/lib/aspera/cli/plugins/server.rb +3 -1
- data/lib/aspera/cli/plugins/shares.rb +205 -35
- data/lib/aspera/cli/preset_actions.rb +15 -7
- data/lib/aspera/cli/preset_manager.rb +24 -2
- data/lib/aspera/cli/prompt.rb +3 -1
- data/lib/aspera/cli/result.rb +17 -4
- data/lib/aspera/cli/runner.rb +133 -43
- data/lib/aspera/cli/security.rb +134 -0
- data/lib/aspera/cli/table_fitter.rb +116 -0
- data/lib/aspera/cli/terminal_formatter.rb +23 -34
- data/lib/aspera/cli/transfer_agent.rb +4 -1
- data/lib/aspera/cli/vault_manager.rb +6 -5
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/web_secret.html.erb +61 -0
- data/lib/aspera/cli/web_secret.rb +220 -0
- data/lib/aspera/cli/wizard.rb +19 -9
- data/lib/aspera/command_line_builder.rb +26 -0
- data/lib/aspera/data_repository.rb +38 -24
- data/lib/aspera/duration.rb +42 -0
- data/lib/aspera/environment.rb +88 -24
- data/lib/aspera/exec_spec.rb +6 -5
- data/lib/aspera/faspex_gw.rb +98 -32
- data/lib/aspera/faspex_postproc.rb +187 -44
- data/lib/aspera/formatter_interface.rb +0 -1
- data/lib/aspera/id_generator.rb +2 -1
- data/lib/aspera/json_rpc/client.rb +51 -32
- data/lib/aspera/json_rpc/version.rb +1 -0
- data/lib/aspera/keychain/base.rb +5 -0
- data/lib/aspera/keychain/encrypted_hash.rb +6 -2
- data/lib/aspera/keychain/factory.rb +2 -3
- data/lib/aspera/keychain/hashicorp_vault.rb +7 -4
- data/lib/aspera/keychain/macos_security.rb +119 -13
- data/lib/aspera/keychain/one_password_api.rb +8 -3
- data/lib/aspera/keychain/one_password_base.rb +14 -9
- data/lib/aspera/keychain/one_password_cli.rb +4 -2
- data/lib/aspera/log.rb +3 -6
- data/lib/aspera/markdown.rb +152 -115
- data/lib/aspera/mime.rb +6 -0
- data/lib/aspera/nagios.rb +3 -1
- data/lib/aspera/node_emulator.rb +98 -101
- data/lib/aspera/oauth/{base.rb → client/base.rb} +5 -4
- data/lib/aspera/oauth/{boot.rb → client/boot.rb} +3 -2
- data/lib/aspera/oauth/{factory.rb → client/factory.rb} +7 -0
- data/lib/aspera/oauth/{generic.rb → client/generic.rb} +1 -1
- data/lib/aspera/oauth/{json_credentials.rb → client/json_credentials.rb} +1 -1
- data/lib/aspera/oauth/{jwt.rb → client/jwt.rb} +1 -1
- data/lib/aspera/oauth/{url_json.rb → client/url_json.rb} +1 -1
- data/lib/aspera/oauth/{web.rb → client/web.rb} +2 -2
- data/lib/aspera/oauth/server/authorize.html.erb +34 -0
- data/lib/aspera/oauth/server/pkce.rb +426 -0
- data/lib/aspera/orchestrator_workflow_checker.rb +561 -0
- data/lib/aspera/preview/terminal.rb +3 -2
- data/lib/aspera/rainbow.rb +27 -1
- data/lib/aspera/rest/aspera_errors.rb +51 -59
- data/lib/aspera/rest/client.rb +21 -14
- data/lib/aspera/rest/error_analyzer.rb +9 -0
- data/lib/aspera/rest/list.rb +6 -6
- data/lib/aspera/rest/util.rb +7 -1
- data/lib/aspera/rest.rb +0 -1
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -2
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1-enhanced.yaml +4001 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0-enhanced.yaml +1097 -0
- data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +35 -35
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +819 -7
- data/lib/aspera/schema/documentation.rb +50 -78
- data/lib/aspera/schema/reader.rb +39 -11
- data/lib/aspera/schema/registry.rb +8 -2
- data/lib/aspera/secret_hider.rb +94 -15
- data/lib/aspera/secret_strength.rb +114 -0
- data/lib/aspera/servlet_helpers.rb +111 -0
- data/lib/aspera/sync/args.schema.yaml +3 -1
- data/lib/aspera/sync/conf.schema.yaml +16 -6
- data/lib/aspera/sync/operations.rb +21 -1
- data/lib/aspera/temp_file_manager.rb +79 -20
- data/lib/aspera/transfer/parameters.rb +5 -0
- data/lib/aspera/transfer/spec.rb +52 -12
- data/lib/aspera/transfer/spec.schema.yaml +8 -5
- data/lib/aspera/web_auth/index.html.erb +16 -0
- data/lib/aspera/web_auth/server.rb +18 -10
- data/lib/aspera/web_page.html.erb +262 -0
- data/lib/aspera/web_page.rb +61 -0
- data/lib/aspera/web_server_simple.rb +76 -10
- data.tar.gz.sig +0 -0
- metadata +47 -32
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/mcp_tool.rb +0 -189
- data/lib/aspera/oauth.rb +0 -9
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +0 -1784
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +0 -249
- data/lib/aspera/web_auth/index.html +0 -147
data/docs/test-mcp-with-ai.md
CHANGED
|
@@ -110,7 +110,9 @@ environment is available.
|
|
|
110
110
|
### 4.1 Demo server — browse files
|
|
111
111
|
|
|
112
112
|
> "Browse the root folder of the Aspera demo server at ssh://demo.asperademo.com:33001
|
|
113
|
-
> using username `asperaweb` and password
|
|
113
|
+
> using username `asperaweb` and password `<PASSWORD>`."
|
|
114
|
+
|
|
115
|
+
`<PASSWORD>` is the public password of the demo server.
|
|
114
116
|
|
|
115
117
|
**Expected MCP calls**:
|
|
116
118
|
|
|
@@ -118,7 +120,7 @@ environment is available.
|
|
|
118
120
|
["server", "browse", "/",
|
|
119
121
|
"--url=ssh://demo.asperademo.com:33001",
|
|
120
122
|
"--username=asperaweb",
|
|
121
|
-
"--password
|
|
123
|
+
"--password=<PASSWORD>"]
|
|
122
124
|
```
|
|
123
125
|
|
|
124
126
|
**Pass criterion**: the AI returns a file/folder listing without errors.
|
|
@@ -134,7 +136,7 @@ environment is available.
|
|
|
134
136
|
["server", "download", "/aspera-test-dir-small/10KB.1",
|
|
135
137
|
"--url=ssh://demo.asperademo.com:33001",
|
|
136
138
|
"--username=asperaweb",
|
|
137
|
-
"--password
|
|
139
|
+
"--password=<PASSWORD>",
|
|
138
140
|
"--to-folder=/tmp"]
|
|
139
141
|
```
|
|
140
142
|
|
data/lib/aspera/agent/direct.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'aspera/agent/base'
|
|
|
4
4
|
require 'aspera/ascp/installation'
|
|
5
5
|
require 'aspera/ascp/management'
|
|
6
6
|
require 'aspera/exec_spec'
|
|
7
|
+
require 'aspera/temp_file_manager'
|
|
7
8
|
require 'aspera/transfer/parameters'
|
|
8
9
|
require 'aspera/transfer/error'
|
|
9
10
|
require 'aspera/transfer/spec'
|
|
@@ -14,6 +15,7 @@ require 'socket'
|
|
|
14
15
|
require 'securerandom'
|
|
15
16
|
require 'shellwords'
|
|
16
17
|
require 'English'
|
|
18
|
+
require 'time'
|
|
17
19
|
require 'aspera/rainbow'
|
|
18
20
|
using Rainbow
|
|
19
21
|
|
|
@@ -27,7 +29,7 @@ module Aspera
|
|
|
27
29
|
# agent_params must contain '_agent_ref' pointing to the live Direct instance.
|
|
28
30
|
# @param transfer_id [String] job_id returned by start_transfer / last_job_id
|
|
29
31
|
# @param agent_params [Hash] connection parameters; '_agent_ref' is the live agent
|
|
30
|
-
# @return [Hash] status hash with at least 'status' key
|
|
32
|
+
# @return [Hash] status hash with at least 'status' key, and `bytes_transferred`, and `ended_at` when all sessions ended
|
|
31
33
|
def transfer_status(transfer_id, agent_params)
|
|
32
34
|
agent = agent_params['_agent_ref']
|
|
33
35
|
return {'status' => 'unknown', 'note' => 'direct agent not available (different process?)'} if agent.nil?
|
|
@@ -35,12 +37,14 @@ module Aspera
|
|
|
35
37
|
return {'status' => 'unknown', 'note' => "no sessions found for job #{transfer_id}"} if sessions.empty?
|
|
36
38
|
errors = sessions.filter_map { |s| s[:error] }
|
|
37
39
|
running = sessions.any? { |s| s[:thread]&.alive? }
|
|
40
|
+
result = {'bytes_transferred' => sessions.sum { |s| s[:last_size].to_i }}
|
|
41
|
+
result['ended_at'] = sessions.filter_map { |s| s[:ended_at] }.max unless running
|
|
38
42
|
if errors.any?
|
|
39
|
-
|
|
43
|
+
result.merge('status' => 'failed', 'error' => errors.first.message)
|
|
40
44
|
elsif running
|
|
41
|
-
|
|
45
|
+
result.merge('status' => 'running')
|
|
42
46
|
else
|
|
43
|
-
|
|
47
|
+
result.merge('status' => 'completed')
|
|
44
48
|
end
|
|
45
49
|
end
|
|
46
50
|
end
|
|
@@ -171,6 +175,10 @@ module Aspera
|
|
|
171
175
|
exec_spec: Transfer::Parameters.new(transfer_spec, **@tr_opts).ascp_args
|
|
172
176
|
}
|
|
173
177
|
|
|
178
|
+
# Temporary files of `ascp` (file list) are deleted when the last session ends,
|
|
179
|
+
# also after the end of the command (asynchronous transfer): hold them once per session, before any session starts
|
|
180
|
+
(multi_session_info.nil? ? 1 : multi_session_info[:count]).times { TempFileManager.instance.hold(session[:exec_spec].temp_files) }
|
|
181
|
+
|
|
174
182
|
if multi_session_info.nil?
|
|
175
183
|
Log.log.debug('Starting single session thread')
|
|
176
184
|
# single session for transfer : simple
|
|
@@ -266,6 +274,9 @@ module Aspera
|
|
|
266
274
|
rescue => e
|
|
267
275
|
session[:error] = e
|
|
268
276
|
raise if Log.log.debug? || !e.is_a?(Transfer::Error)
|
|
277
|
+
ensure
|
|
278
|
+
session[:ended_at] = Time.now.utc.iso8601
|
|
279
|
+
TempFileManager.instance.release(session[:exec_spec].temp_files)
|
|
269
280
|
end
|
|
270
281
|
Log.log.debug { "EXIT (#{Thread.current[:name]})" }
|
|
271
282
|
end
|
data/lib/aspera/agent/factory.rb
CHANGED
|
@@ -26,7 +26,8 @@ module Aspera
|
|
|
26
26
|
Dir.children(File.dirname(File.expand_path(__FILE__)))
|
|
27
27
|
.select { |file| file.end_with?(Environment::RB_EXT) }
|
|
28
28
|
.map { |file| File.basename(file, Environment::RB_EXT).to_sym }
|
|
29
|
-
.reject { |item| IGNORED_ITEMS.include?(item) }
|
|
29
|
+
.reject { |item| IGNORED_ITEMS.include?(item) }
|
|
30
|
+
.sort.to_h do |agent_sym|
|
|
30
31
|
[agent_sym, {
|
|
31
32
|
long: agent_sym.to_s.capitalize,
|
|
32
33
|
short: agent_sym.eql?(:direct) ? :a : agent_sym.to_s[0].to_sym
|
data/lib/aspera/agent/node.rb
CHANGED
data/lib/aspera/api/cos_node.rb
CHANGED
|
@@ -62,7 +62,7 @@ module Aspera
|
|
|
62
62
|
ret: :resp
|
|
63
63
|
).body
|
|
64
64
|
ats_info = XmlSimple.xml_in(xml_result_text, {'ForceArray' => false})
|
|
65
|
-
Log.dump(:ats_info, ats_info
|
|
65
|
+
Log.dump(:ats_info, ats_info)
|
|
66
66
|
Aspera.assert_hash_all(ats_info, String, nil) { 'ats_info' }
|
|
67
67
|
Aspera.assert((FASP_INFO_KEYS - ats_info.keys).empty?, 'ats_info missing required keys')
|
|
68
68
|
Aspera.assert_hash_all(ats_info['AccessKey'], String, String) { 'ats_info' }
|
data/lib/aspera/api/faspex.rb
CHANGED
data/lib/aspera/api/node.rb
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
require 'aspera/link_header'
|
|
4
4
|
require 'aspera/transfer/spec'
|
|
5
5
|
require 'aspera/rest'
|
|
6
|
-
require 'aspera/oauth'
|
|
6
|
+
require 'aspera/oauth/client/factory'
|
|
7
7
|
require 'aspera/log'
|
|
8
8
|
require 'aspera/assert'
|
|
9
|
+
require 'aspera/secret_hider'
|
|
9
10
|
require 'aspera/environment'
|
|
10
11
|
require 'base64'
|
|
11
12
|
require 'openssl'
|
|
@@ -173,12 +174,15 @@ module Aspera
|
|
|
173
174
|
payload['auth_type'] ||= 'access_key'
|
|
174
175
|
payload['expires_at'] ||= (Time.now + expiration_sec).utc.strftime('%FT%TZ')
|
|
175
176
|
payload_json = JSON.generate(payload)
|
|
176
|
-
|
|
177
|
+
token = Base64.strict_encode64(Zlib::Deflate.deflate([
|
|
177
178
|
payload_json,
|
|
178
179
|
SIGNATURE_DELIMITER,
|
|
179
180
|
Base64.strict_encode64(private_key.sign(OpenSSL::Digest.new('sha512'), payload_json)).scan(/.{1,60}/).join("\n"),
|
|
180
181
|
''
|
|
181
182
|
].join("\n")))
|
|
183
|
+
# Hidden wherever it appears (logs, results)
|
|
184
|
+
SecretHider.instance.add_secret_values(token)
|
|
185
|
+
return token
|
|
182
186
|
end
|
|
183
187
|
|
|
184
188
|
# Decode an Aspera Node bearer token
|
|
@@ -51,15 +51,24 @@ module Aspera
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Loads YAML from cloud with locations of SDK archives for all platforms
|
|
54
|
-
#
|
|
54
|
+
# Tries each URL of option `locations_url` (default if empty) in order, until one provides a valid list.
|
|
55
|
+
# All but the last URL are read with a short timeout and no retry, so that fallback is quick.
|
|
56
|
+
# @return [Array<Hash>] location structure
|
|
55
57
|
def sdk_locations
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
Aspera.assert_type(@transferd_urls, Array) { 'locations_url' }
|
|
59
|
+
location_urls = @transferd_urls.empty? ? TRANSFERD_ARCHIVE_LOCATION_URLS : @transferd_urls
|
|
60
|
+
location_urls.each_with_index do |location_url, index|
|
|
61
|
+
last = index.eql?(location_urls.length - 1)
|
|
62
|
+
Log.log.debug { "Retrieving SDK locations from #{location_url}" }
|
|
63
|
+
begin
|
|
64
|
+
transferd_locations = last ? UriReader.read(location_url) : read_fail_fast(location_url)
|
|
65
|
+
locations = Yaml.safe_load(transferd_locations)
|
|
66
|
+
Aspera.assert_type(locations, Array) { "SDK locations from #{location_url}" }
|
|
67
|
+
return locations
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
raise if last
|
|
70
|
+
Log.log.warn { "Failed to retrieve SDK locations from #{location_url}: #{e.message}, trying next" }
|
|
71
|
+
end
|
|
63
72
|
end
|
|
64
73
|
end
|
|
65
74
|
|
|
@@ -415,6 +424,7 @@ module Aspera
|
|
|
415
424
|
return sdk_name, sdk_version, folder
|
|
416
425
|
end
|
|
417
426
|
|
|
427
|
+
# @return [Array<String>] URLs of SDK locations, tried in order
|
|
418
428
|
attr_accessor :transferd_urls
|
|
419
429
|
|
|
420
430
|
private
|
|
@@ -434,20 +444,46 @@ module Aspera
|
|
|
434
444
|
EXE_FILES = %i[ascp ascp4 async transferd].freeze
|
|
435
445
|
# IDs of files present in SDK
|
|
436
446
|
SDK_FILES = (EXE_FILES + %i[ssh_private_dsa ssh_private_rsa aspera_license aspera_conf fallback_certificate fallback_private_key]).freeze
|
|
437
|
-
|
|
447
|
+
# Primary URL (ibm.biz short link) with fallback to the canonical GitHub raw source
|
|
448
|
+
TRANSFERD_ARCHIVE_LOCATION_URLS = [
|
|
449
|
+
'https://ibm.biz/sdk_location',
|
|
450
|
+
'https://raw.githubusercontent.com/IBM/aspera-cli/refs/heads/main/docs/sdk_location.yaml'
|
|
451
|
+
].freeze
|
|
452
|
+
# Timeout in seconds when another location URL remains to be tried
|
|
453
|
+
LOCATION_FAIL_FAST_TIMEOUT = 10
|
|
438
454
|
# filename for ascp with optional extension (Windows)
|
|
439
|
-
private_constant :DEFAULT_ASPERA_CONF, :EXE_FILES, :SDK_FILES, :
|
|
455
|
+
private_constant :DEFAULT_ASPERA_CONF, :EXE_FILES, :SDK_FILES, :TRANSFERD_ARCHIVE_LOCATION_URLS, :LOCATION_FAIL_FAST_TIMEOUT
|
|
440
456
|
|
|
441
457
|
def initialize
|
|
442
458
|
# cache for installed products found
|
|
443
459
|
@found_products = nil
|
|
444
|
-
|
|
460
|
+
# URLs of SDK locations, tried in order
|
|
461
|
+
@transferd_urls = TRANSFERD_ARCHIVE_LOCATION_URLS
|
|
445
462
|
# product selected with option `sdk_folder`, or nil to use SDK folder
|
|
446
463
|
@ascp_product = nil
|
|
447
464
|
# cache for folder of `ascp` of selected product
|
|
448
465
|
@ascp_folder = nil
|
|
449
466
|
end
|
|
450
467
|
|
|
468
|
+
# Read URL with a short timeout and no retry (temporarily changes global HTTP parameters)
|
|
469
|
+
# @param url [String] URL to read
|
|
470
|
+
# @return [String] content
|
|
471
|
+
def read_fail_fast(url)
|
|
472
|
+
parameters = Rest::Parameters.instance
|
|
473
|
+
session_cb = parameters.session_cb
|
|
474
|
+
retry_max = parameters.retry_max
|
|
475
|
+
parameters.retry_max = 0
|
|
476
|
+
parameters.session_cb = lambda do |http_session|
|
|
477
|
+
session_cb&.call(http_session)
|
|
478
|
+
http_session.open_timeout = [http_session.open_timeout, LOCATION_FAIL_FAST_TIMEOUT].compact.min
|
|
479
|
+
http_session.read_timeout = [http_session.read_timeout, LOCATION_FAIL_FAST_TIMEOUT].compact.min
|
|
480
|
+
end
|
|
481
|
+
UriReader.read(url)
|
|
482
|
+
ensure
|
|
483
|
+
parameters.session_cb = session_cb
|
|
484
|
+
parameters.retry_max = retry_max
|
|
485
|
+
end
|
|
486
|
+
|
|
451
487
|
# @param file_type [Symbol] one of EXE_FILES
|
|
452
488
|
# @return [String] path to executable in SDK folder
|
|
453
489
|
def sdk_exe_path(file_type)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'aspera/assert'
|
|
4
|
+
require 'aspera/log'
|
|
4
5
|
require 'aspera/string_ext'
|
|
5
6
|
|
|
6
7
|
module Aspera
|
|
@@ -8,16 +9,17 @@ module Aspera
|
|
|
8
9
|
# processing of ascp management port events
|
|
9
10
|
# Reference: `mgmtmess.c`
|
|
10
11
|
class Management
|
|
12
|
+
# cspell:ignore asmgmt faspmgmt
|
|
11
13
|
# References:
|
|
12
|
-
# https://www.google.com/search?q=FASP+error+codes
|
|
13
14
|
# https://www.ibm.com/support/pages/error-code-reference-tables
|
|
14
|
-
#
|
|
15
|
+
# `asmgmt_err.h` : codes, mnemonics and messages
|
|
16
|
+
# `mgmtmess.c` : `faspmgmt_err_name_str` (additional info) and `as_mgmt_err_is_retryable`
|
|
15
17
|
# Note that the fact that an error is retry-able is not internally defined by protocol, it's client-side responsibility
|
|
16
18
|
# rubocop:disable-next Layout/FirstHashElementLineBreak
|
|
17
19
|
ERRORS = {
|
|
18
20
|
# id retry-able mnemo/code message additional info
|
|
19
21
|
0 => {r: false, c: 'UNKNOWN', m: 'unknown', a: 'unknown'},
|
|
20
|
-
1 => {r: false, c: 'FASP_PROTO', m: 'Generic fasp(tm) protocol error', a: '
|
|
22
|
+
1 => {r: false, c: 'FASP_PROTO', m: 'Generic fasp(tm) protocol error', a: 'FASP error'},
|
|
21
23
|
2 => {r: false, c: 'ASCP', m: 'Generic SCP error', a: 'ASCP error'},
|
|
22
24
|
3 => {r: false, c: 'AMBIGUOUS_TARGET', m: 'Target incorrectly specified', a: 'Ambiguous target'},
|
|
23
25
|
4 => {r: false, c: 'NO_SUCH_FILE', m: 'No such file or directory', a: 'No such file or directory'},
|
|
@@ -29,13 +31,13 @@ module Aspera
|
|
|
29
31
|
10 => {r: false, c: 'LIC_RATE_EXCEEDED', m: 'Rate exceeds the cap imposed by license', a: 'Rate exceeds cap imposed by license'},
|
|
30
32
|
11 => {r: false, c: 'INTERNAL_ERROR', m: 'Internal error (unexpected error)', a: 'Internal error'},
|
|
31
33
|
12 => {r: true, c: 'TRANSFER_ERROR', m: 'Error establishing control connection',
|
|
32
|
-
|
|
34
|
+
a: 'Error establishing SSH connection (check SSH port and firewall)'},
|
|
33
35
|
13 => {r: true, c: 'TRANSFER_TIMEOUT', m: 'Timeout establishing control connection',
|
|
34
|
-
|
|
36
|
+
a: 'Timeout establishing SSH connection (check SSH port and firewall)'},
|
|
35
37
|
14 => {r: true, c: 'CONNECTION_ERROR', m: 'Error establishing data connection',
|
|
36
|
-
|
|
38
|
+
a: 'Error establishing UDP connection (check UDP port and firewall)'},
|
|
37
39
|
15 => {r: true, c: 'CONNECTION_TIMEOUT', m: 'Timeout establishing data connection',
|
|
38
|
-
|
|
40
|
+
a: 'Timeout establishing UDP connection (check UDP port and firewall)'},
|
|
39
41
|
16 => {r: true, c: 'CONNECTION_LOST', m: 'Connection lost', a: 'Connection lost'},
|
|
40
42
|
17 => {r: true, c: 'RCVR_SEND_ERROR', m: 'Receiver fails to send feedback', a: 'Network failure (receiver can\'t send feedback)'},
|
|
41
43
|
18 => {r: true, c: 'RCVR_RECV_ERROR', m: 'Receiver fails to receive data packets', a: 'Network failure (receiver can\'t receive UDP data)'},
|
|
@@ -67,34 +69,37 @@ module Aspera
|
|
|
67
69
|
44 => {r: true, c: 'PMTU_BRTT_ERROR', m: 'UDP session initiation fatal error', a: 'UDP session initiation fatal error'},
|
|
68
70
|
45 => {r: true, c: 'BWMEAS_ERROR', m: 'Bandwidth measurement fatal error', a: 'Bandwidth measurement fatal error'},
|
|
69
71
|
46 => {r: false, c: 'VLINK_ERROR', m: 'Virtual link error', a: 'Virtual link error'},
|
|
70
|
-
47 => {r:
|
|
71
|
-
|
|
72
|
-
48 => {r: false, c: 'FILE_ENCRYPTION_ERROR', m: 'File encryption error, e.g. corrupt file',
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
50 => {r: false, c: 'BAD_CONFIGURATION', m: 'Aspera.conf contains invalid data and was rejected', a: 'Invalid configuration'},
|
|
72
|
+
47 => {r: true, c: 'CONNECTION_ERROR_HTTP', m: 'Error establishing HTTP connection',
|
|
73
|
+
a: 'Error establishing HTTP connection (check HTTP port and firewall)'},
|
|
74
|
+
48 => {r: false, c: 'FILE_ENCRYPTION_ERROR', m: 'File encryption error, e.g. corrupt file', a: 'File encryption/decryption error, e.g. corrupt file'},
|
|
75
|
+
49 => {r: false, c: 'FILE_DECRYPTION_PASS', m: 'File decryption bad passphrase', a: 'File decryption error, bad passphrase'},
|
|
76
|
+
50 => {r: false, c: 'BAD_CONFIGURATION', m: 'Aspera.conf contains invalid data and was rejected', a: 'Invalid configuration'},
|
|
76
77
|
51 => {r: false, c: 'INSECURE_CONNECTION', m: 'Remote-host key check failure', a: 'Remote host is not who we expected'},
|
|
77
78
|
52 => {r: false, c: 'START_VALIDATION_FAILED', m: 'File start validation failed', a: 'File start validation failed'},
|
|
78
79
|
53 => {r: false, c: 'STOP_VALIDATION_FAILED', m: 'File stop validation failed', a: 'File stop validation failed'},
|
|
79
80
|
54 => {r: false, c: 'THRESHOLD_VALIDATION_FAILED', m: 'File threshold validation failed', a: 'File threshold validation failed'},
|
|
80
81
|
55 => {r: false, c: 'FILEPATH_TOO_LONG', m: 'File path/name too long for underlying file system', a: 'File path exceeds underlying file system limit'},
|
|
81
82
|
56 => {r: false, c: 'ILLEGAL_CHARS_IN_PATH', m: 'Windows path contains illegal characters',
|
|
82
|
-
|
|
83
|
-
57 => {r: false, c: 'CHUNK_MUST_MATCH_ALIGNMENT', m: 'Chunk size/start must be aligned with storage',
|
|
84
|
-
|
|
83
|
+
a: 'Path being written to Windows file system contains illegal characters'},
|
|
84
|
+
57 => {r: false, c: 'CHUNK_MUST_MATCH_ALIGNMENT', m: 'Chunk size/start must be aligned with storage',
|
|
85
|
+
a: 'Data size must match storage chunk size and alignment requirements'},
|
|
86
|
+
58 => {r: false, c: 'VALIDATION_SESSION_ABORT', m: 'Session aborted due to validation error(s)', a: 'Session aborted due to validation error(s)'},
|
|
85
87
|
59 => {r: false, c: 'REMOTE_STORAGE_ERROR', m: 'Remote storage errored', a: 'Remote storage errored'},
|
|
86
|
-
60 => {r: false, c: 'LUA_SCRIPT_ABORTED_SESSION', m: 'Session aborted due to Lua script abort', a: 'Session aborted
|
|
88
|
+
60 => {r: false, c: 'LUA_SCRIPT_ABORTED_SESSION', m: 'Session aborted due to Lua script abort', a: 'Session aborted'},
|
|
87
89
|
61 => {r: true, c: 'SSEAR_RETRYABLE', m: 'Transfer failed because of a retryable Encryption at Rest error',
|
|
88
|
-
|
|
89
|
-
62 => {r: false, c: 'SSEAR_FATAL', m: 'Transfer failed because of a fatal Encryption at Rest error',
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
a: 'Server-side Encryption At Rest unavailable'},
|
|
91
|
+
62 => {r: false, c: 'SSEAR_FATAL', m: 'Transfer failed because of a fatal Encryption at Rest error', a: 'Server-side Encryption At Rest failed'},
|
|
92
|
+
63 => {r: false, c: 'LINK_LOOP', m: 'Path refers to a symbolic link loop', a: 'Symbolic link loop'},
|
|
93
|
+
64 => {r: false, c: 'CANNOT_RENAME_PARTIAL', m: 'Can\'t rename a partial file', a: 'Cannot rename partial file'},
|
|
94
|
+
65 => {r: false, c: 'CIPHER_NON_COMPAT_FIPS', m: 'Can\'t use this cipher with FIPS mode enabled', a: 'Cypher not supported in FIPS mode'},
|
|
95
|
+
66 => {r: false, c: 'PEER_REQUIRES_FIPS', m: 'Peer rejects cipher due to FIPS mode enabled on peer', a: 'Peer requires FIPS be enabled'},
|
|
96
|
+
67 => {r: false, c: 'UPLOAD_IN_PROGRESS', m: 'Conflict between two transfers', a: 'File upload already in progress'},
|
|
97
|
+
68 => {r: false, c: 'MIN_VERSION_CHECK', m: 'Minimum version check failed', a: 'Failed to start due to minimum version check'},
|
|
98
|
+
69 => {r: true, c: 'MAX_ASCP', m: 'Maximum ascp processes on a node exceeded',
|
|
99
|
+
a: 'Failed to start session maximum transfer sessions exceeded'}
|
|
96
100
|
}.freeze
|
|
97
101
|
# cspell: disable
|
|
102
|
+
# Same as `as_mgmt_cmd_name_str` (`NOTIFICATION` is twice there too)
|
|
98
103
|
OPERATIONS = %w[
|
|
99
104
|
NOP
|
|
100
105
|
START
|
|
@@ -117,8 +122,9 @@ module Aspera
|
|
|
117
122
|
CLOSE
|
|
118
123
|
SKIP
|
|
119
124
|
ARGSTOP
|
|
120
|
-
]
|
|
125
|
+
].freeze
|
|
121
126
|
|
|
127
|
+
# Same as `as_mgmt_arg_name_str`
|
|
122
128
|
PARAMETERS = %w[
|
|
123
129
|
Type
|
|
124
130
|
File
|
|
@@ -241,7 +247,7 @@ module Aspera
|
|
|
241
247
|
FilesEncrypt
|
|
242
248
|
FilesDecrypt
|
|
243
249
|
DatagramSize
|
|
244
|
-
|
|
250
|
+
Stalled
|
|
245
251
|
XoptFlags
|
|
246
252
|
VLinkVersion
|
|
247
253
|
PeerVLinkVersion
|
|
@@ -273,23 +279,36 @@ module Aspera
|
|
|
273
279
|
PostTransferValidation
|
|
274
280
|
OverwritePolicyCap
|
|
275
281
|
ExtraCreatePolicy
|
|
276
|
-
|
|
282
|
+
FipsModeEnabled
|
|
283
|
+
].freeze
|
|
277
284
|
# Management port start message
|
|
278
285
|
PROT_VERSION = '2'
|
|
279
286
|
MGT_HEADER = "FASPMGR #{PROT_VERSION}"
|
|
280
287
|
# empty line is separator to end event information
|
|
281
288
|
MGT_FRAME_SEPARATOR = ''
|
|
282
289
|
# fields description for JSON generation
|
|
283
|
-
#
|
|
284
|
-
INTEGER_FIELDS = %w[Bytescont FaspFileArgIndex StartByte Rate MinRate Port Priority RateCap MinRateCap TCPPort CreatePolicy TimePolicy
|
|
290
|
+
# Integer fields may also be a percentage (e.g. `Rate: 50%`): kept as String
|
|
291
|
+
INTEGER_FIELDS = %w[Bytescont FaspFileArgIndex StartByte EndByte Rate MinRate Port Priority RateCap MinRateCap PriorityCap TCPPort CreatePolicy TimePolicy
|
|
285
292
|
DatagramSize XoptFlags VLinkVersion PeerVLinkVersion DSPipelineDepth PeerDSPipelineDepth ReadBlockSize WriteBlockSize
|
|
286
|
-
ClusterNumNodes ClusterNodeId Size Written Loss FileBytes PreTransferBytes TransferBytes PMTU Elapsedusec
|
|
287
|
-
|
|
293
|
+
ClusterNumNodes ClusterNodeId Size Written Loss FileBytes PreTransferBytes PartialPreTransferBytes TransferBytes PMTU Elapsedusec Delay
|
|
294
|
+
Code RetryTimeout XferRetry MoveRangeLow MoveRangeHigh
|
|
295
|
+
PreTransferFiles PreTransferDirs PreTransferSpecial PreTransferFailed PreTransferExcluded
|
|
296
|
+
ArgScansAttempted ArgScansCompleted PathScansAttempted PathScansFailed PathScansIrregular PathScansExcluded
|
|
297
|
+
DirScansCompleted FileScansCompleted DirCreatesAttempted DirCreatesFailed DirCreatesPassed
|
|
298
|
+
TransfersAttempted TransfersFailed TransfersPassed TransfersSkipped
|
|
299
|
+
ArgTransfersAttempted ArgTransfersFailed ArgTransfersPassed ArgTransfersSkipped].freeze
|
|
288
300
|
BOOLEAN_FIELDS = %w[Encryption Remote RateLock MinRateLock PolicyLock FilesEncrypt FilesDecrypt VLinkLocalEnabled VLinkRemoteEnabled
|
|
289
|
-
MoveRange Keepalive TestLogin UseProxy Precalc RTTAutocorrect].freeze
|
|
301
|
+
MoveRange Keepalive TestLogin UseProxy Precalc RTTAutocorrect Stalled PostTransferValidation FipsModeEnabled].freeze
|
|
290
302
|
BOOLEAN_TRUE = 'Yes'
|
|
303
|
+
INTEGER_VALUE = /\A-?\d+\z/
|
|
304
|
+
# Fields with path values, escaped on management port (`as_mgmt_escape_path`)
|
|
305
|
+
PATH_FIELDS = %w[Source Destination File].freeze
|
|
306
|
+
# Characters escaped in path values: `ESC` followed by the replacement
|
|
307
|
+
PATH_ESCAPE = "\e"
|
|
308
|
+
PATH_ESCAPES = {"\n" => 'n', "\r" => 'r', PATH_ESCAPE => PATH_ESCAPE}.freeze
|
|
291
309
|
|
|
292
|
-
private_constant :OPERATIONS, :PARAMETERS, :MGT_HEADER, :MGT_FRAME_SEPARATOR, :INTEGER_FIELDS, :BOOLEAN_FIELDS, :BOOLEAN_TRUE
|
|
310
|
+
private_constant :OPERATIONS, :PARAMETERS, :MGT_HEADER, :MGT_FRAME_SEPARATOR, :INTEGER_FIELDS, :BOOLEAN_FIELDS, :BOOLEAN_TRUE, :INTEGER_VALUE,
|
|
311
|
+
:PATH_FIELDS, :PATH_ESCAPE, :PATH_ESCAPES
|
|
293
312
|
# cspell: enable
|
|
294
313
|
|
|
295
314
|
class << self
|
|
@@ -305,7 +324,7 @@ module Aspera
|
|
|
305
324
|
# Translate snake case event name to native
|
|
306
325
|
# @param name [String] Field name
|
|
307
326
|
def field_snake_to_native(name)
|
|
308
|
-
field = name.delete('_')
|
|
327
|
+
field = name.to_s.delete('_')
|
|
309
328
|
result = PARAMETERS.find { |w| w.casecmp?(field) }
|
|
310
329
|
Aspera.assert(!result.nil?) { "No such field: #{name}" }
|
|
311
330
|
result
|
|
@@ -315,7 +334,7 @@ module Aspera
|
|
|
315
334
|
def event_native_to_snake(event)
|
|
316
335
|
event.each_with_object({}) do |(key, value), h|
|
|
317
336
|
h[field_native_to_snake(key)] =
|
|
318
|
-
if INTEGER_FIELDS.include?(key) then value.to_i
|
|
337
|
+
if INTEGER_FIELDS.include?(key) && INTEGER_VALUE.match?(value) then value.to_i
|
|
319
338
|
elsif BOOLEAN_FIELDS.include?(key) then value.eql?(BOOLEAN_TRUE)
|
|
320
339
|
else
|
|
321
340
|
value
|
|
@@ -323,12 +342,36 @@ module Aspera
|
|
|
323
342
|
end
|
|
324
343
|
end
|
|
325
344
|
|
|
345
|
+
# Escape path value for management port: line breaks and escape char
|
|
346
|
+
# @param value [String] Path
|
|
347
|
+
# @return [String] Escaped path
|
|
348
|
+
def escape_path(value)
|
|
349
|
+
value.gsub(/[\n\r\e]/) { |char| "#{PATH_ESCAPE}#{PATH_ESCAPES[char]}" }
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Reverse of `escape_path`
|
|
353
|
+
# @param value [String] Escaped path
|
|
354
|
+
# @return [String] Path
|
|
355
|
+
def unescape_path(value)
|
|
356
|
+
value.gsub(/\e([nr\e])/) { PATH_ESCAPES.key(Regexp.last_match(1)) }
|
|
357
|
+
end
|
|
358
|
+
|
|
326
359
|
# Build command to send on management port
|
|
360
|
+
# Frame: header, one line per field, empty line (last empty element adds the final line break)
|
|
327
361
|
# @param data [Hash] keys are snake case: e.g. {'type'=>'START','source'=>_path_,'destination'=>_path_}
|
|
328
362
|
# @return [String] frame to send on management port
|
|
329
363
|
def command_to_stream(data)
|
|
330
364
|
data
|
|
331
|
-
.map
|
|
365
|
+
.map do |key, value|
|
|
366
|
+
name = field_snake_to_native(key)
|
|
367
|
+
value = value.to_s
|
|
368
|
+
if PATH_FIELDS.include?(name)
|
|
369
|
+
value = escape_path(value)
|
|
370
|
+
else
|
|
371
|
+
Aspera.assert(!value.match?(/[\r\n]/)) { "mgt port: line break in value of #{name}" }
|
|
372
|
+
end
|
|
373
|
+
"#{name}: #{value}"
|
|
374
|
+
end
|
|
332
375
|
.unshift(MGT_HEADER)
|
|
333
376
|
.push(MGT_FRAME_SEPARATOR, '')
|
|
334
377
|
.join("\n")
|
|
@@ -347,21 +390,24 @@ module Aspera
|
|
|
347
390
|
# @param line [String] line of mgt port event
|
|
348
391
|
# @return [Hash] event hash or nil if event is not yet complete
|
|
349
392
|
def process_line(line)
|
|
350
|
-
# Log.log.debug{"line=[#{line}]"}
|
|
351
393
|
case line
|
|
352
394
|
when MGT_HEADER
|
|
353
395
|
# begin event
|
|
396
|
+
Log.log.warn { "mgt port: incomplete event discarded: #{@event_build}" } unless @event_build.nil?
|
|
354
397
|
@event_build = {}
|
|
355
|
-
when /^([^:]+): (.*)$/
|
|
356
|
-
Aspera.assert_type(@event_build, Hash) { 'mgt port: unexpected line: data without header' }
|
|
357
|
-
# event field
|
|
358
|
-
@event_build[Regexp.last_match(1)] = Regexp.last_match(2)
|
|
359
398
|
when MGT_FRAME_SEPARATOR
|
|
360
399
|
Aspera.assert_type(@event_build, Hash) { 'mgt port: unexpected line: end frame without header' }
|
|
361
400
|
@last_event = @event_build
|
|
362
401
|
@event_build = nil
|
|
363
402
|
return @last_event
|
|
364
|
-
else
|
|
403
|
+
else
|
|
404
|
+
# event field: name up to first colon, optional single space (as `as_mgmt_buffered_read_msg`)
|
|
405
|
+
name, value = line.split(':', 2)
|
|
406
|
+
Aspera.error_unexpected_value(line) { 'mgt port' } if value.nil?
|
|
407
|
+
Aspera.assert_type(@event_build, Hash) { 'mgt port: unexpected line: data without header' }
|
|
408
|
+
value = value.delete_prefix(' ')
|
|
409
|
+
value = self.class.unescape_path(value) if PATH_FIELDS.include?(name)
|
|
410
|
+
@event_build[name] = value
|
|
365
411
|
end
|
|
366
412
|
return
|
|
367
413
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'aspera/agent/factory'
|
|
4
4
|
require 'aspera/schema/registry'
|
|
5
5
|
require 'aspera/schema/reader'
|
|
6
|
+
require 'aspera/schema/documentation'
|
|
6
7
|
|
|
7
8
|
module Aspera
|
|
8
9
|
module Cli
|
|
@@ -101,33 +102,18 @@ module Aspera
|
|
|
101
102
|
def action_agents_show(agent_name:, **)
|
|
102
103
|
names = Agent::Factory::ALL[agent_name]
|
|
103
104
|
schema_key = AGENT_SCHEMA_KEY[agent_name]
|
|
104
|
-
|
|
105
|
+
Result::SingleObject.new({
|
|
105
106
|
'name' => agent_name.to_s,
|
|
106
107
|
'short' => names[:short].to_s,
|
|
107
108
|
'description' => schema_key ? Schema::Registry.instance.reader(schema_key).current['description'].to_s : '(no configurable parameters)'
|
|
108
|
-
}
|
|
109
|
-
if schema_key
|
|
110
|
-
properties = Schema::Registry.instance.reader(schema_key).current['properties'] || {}
|
|
111
|
-
rows = properties.map do |pname, pdef|
|
|
112
|
-
row = {'parameter' => pname, 'type' => pdef['type'].to_s, 'description' => pdef['description'].to_s}
|
|
113
|
-
row['required'] = (Schema::Registry.instance.reader(schema_key).current['required'] || []).include?(pname) ? 'yes' : 'no'
|
|
114
|
-
row['default'] = pdef.key?('default') ? pdef['default'].inspect : ''
|
|
115
|
-
row['enum'] = pdef.key?('enum') ? pdef['enum'].join(', ') : ''
|
|
116
|
-
row
|
|
117
|
-
end
|
|
118
|
-
return Result::ObjectList.new(rows, fields: %w[parameter required type default enum description])
|
|
119
|
-
end
|
|
120
|
-
Result::SingleObject.new(agent_info)
|
|
109
|
+
})
|
|
121
110
|
end
|
|
122
111
|
|
|
123
112
|
def action_agents_parameters(agent_name:, **)
|
|
124
113
|
schema_key = AGENT_SCHEMA_KEY[agent_name]
|
|
125
114
|
return Result::Nothing.new if schema_key.nil?
|
|
126
|
-
|
|
127
|
-
rows
|
|
128
|
-
{'name' => pname, 'type' => pdef['type'].to_s, 'description' => pdef['description'].to_s}
|
|
129
|
-
end
|
|
130
|
-
Result::ObjectList.new(rows, fields: %w[name type description])
|
|
115
|
+
builder = Schema::Documentation.new(TerminalFormatter, Schema::Registry.instance.reader(schema_key)).build
|
|
116
|
+
Result::ObjectList.new(builder.rows, fields: builder.columns)
|
|
131
117
|
end
|
|
132
118
|
end
|
|
133
119
|
end
|
|
@@ -16,7 +16,7 @@ require 'aspera/persistency_folder'
|
|
|
16
16
|
require 'aspera/rest'
|
|
17
17
|
require 'aspera/log'
|
|
18
18
|
require 'aspera/assert'
|
|
19
|
-
require 'aspera/oauth'
|
|
19
|
+
require 'aspera/oauth/client/web'
|
|
20
20
|
require 'aspera/ssl'
|
|
21
21
|
require 'aspera/schema/registry'
|
|
22
22
|
|
|
@@ -174,7 +174,6 @@ module Aspera
|
|
|
174
174
|
OAuth::Web.additional_info = "#{Info::CMD_NAME} v#{Cli::VERSION}"
|
|
175
175
|
Transfer::Parameters.file_list_folder = File.join(@context.main_folder, FILE_LIST_FOLDER_NAME)
|
|
176
176
|
Rest::ErrorAnalyzer.instance.log_file = File.join(@context.main_folder, REST_EXCEPTIONS_LOG_FILENAME)
|
|
177
|
-
Rest::AsperaErrors.register_handlers
|
|
178
177
|
end
|
|
179
178
|
|
|
180
179
|
# @return [String] ~/.aspera
|