aspera-cli 4.27.3 → 4.27.5
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 +118 -0
- data/CONTRIBUTING.md +5 -2
- data/bin/ascli +1 -0
- data/docs/README.md +224 -42
- data/lib/aspera/agent/base.rb +7 -2
- data/lib/aspera/agent/connect.rb +0 -4
- data/lib/aspera/agent/desktop.rb +0 -4
- data/lib/aspera/agent/direct.rb +49 -21
- data/lib/aspera/agent/node.rb +6 -5
- data/lib/aspera/agent/transferd.rb +2 -2
- data/lib/aspera/api/faspex.rb +2 -0
- data/lib/aspera/api/httpgw.rb +1 -2
- data/lib/aspera/api/node.rb +3 -3
- data/lib/aspera/ascp/installation.rb +1 -1
- data/lib/aspera/cli/async_transfer_store.rb +10 -9
- data/lib/aspera/cli/bootstrapper.rb +3 -1
- data/lib/aspera/cli/command_registry.rb +69 -7
- data/lib/aspera/cli/command_spec.rb +1 -1
- data/lib/aspera/cli/extended_value.rb +4 -3
- data/lib/aspera/cli/formatter.rb +10 -8
- data/lib/aspera/cli/http.rb +8 -20
- data/lib/aspera/cli/option_types.rb +3 -1
- data/lib/aspera/cli/option_value.rb +16 -19
- data/lib/aspera/cli/options.schema.yaml +86 -10
- data/lib/aspera/cli/parser.rb +30 -18
- data/lib/aspera/cli/plugins/aoc.rb +133 -156
- data/lib/aspera/cli/plugins/ats.rb +1 -7
- data/lib/aspera/cli/plugins/base.rb +40 -34
- data/lib/aspera/cli/plugins/config.rb +24 -11
- data/lib/aspera/cli/plugins/console.rb +1 -1
- data/lib/aspera/cli/plugins/faspex5.rb +30 -11
- data/lib/aspera/cli/plugins/faspio.rb +1 -1
- data/lib/aspera/cli/plugins/node.rb +26 -19
- data/lib/aspera/cli/plugins/orchestrator.rb +73 -44
- data/lib/aspera/cli/plugins/preview.rb +21 -19
- data/lib/aspera/cli/plugins/server.rb +3 -4
- data/lib/aspera/cli/plugins/shares.rb +21 -24
- data/lib/aspera/cli/preset_actions.rb +28 -18
- data/lib/aspera/cli/preset_manager.rb +34 -19
- data/lib/aspera/cli/prompt.rb +2 -1
- data/lib/aspera/cli/result.rb +33 -22
- data/lib/aspera/cli/runner.rb +1 -5
- data/lib/aspera/cli/sync_actions.rb +17 -16
- data/lib/aspera/cli/transfer_actions.rb +14 -3
- data/lib/aspera/cli/transfer_agent.rb +6 -4
- data/lib/aspera/cli/transfer_progress.rb +290 -55
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +1 -1
- data/lib/aspera/coverage.rb +0 -1
- data/lib/aspera/environment.rb +29 -5
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/factory.rb +2 -1
- data/lib/aspera/log.rb +25 -2
- data/lib/aspera/node_emulator.rb +759 -0
- data/lib/aspera/oauth/base.rb +2 -1
- data/lib/aspera/oauth/factory.rb +6 -3
- data/lib/aspera/oauth/json_credentials.rb +34 -0
- data/lib/aspera/oauth/jwt.rb +3 -4
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_folder.rb +1 -3
- data/lib/aspera/preview/generator.rb +4 -1
- data/lib/aspera/preview/options.schema.yaml +119 -0
- data/lib/aspera/rest/aspera_errors.rb +12 -0
- data/lib/aspera/rest/client.rb +28 -19
- data/lib/aspera/rest/list.rb +14 -8
- data/lib/aspera/rest/parameters.rb +2 -2
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
- data/lib/aspera/schema/reader.rb +12 -18
- data/lib/aspera/schema/registry.rb +6 -1
- data.tar.gz.sig +0 -0
- metadata +5 -3
- metadata.gz.sig +0 -0
- data/lib/aspera/node_simulator.rb +0 -345
- data/lib/aspera/preview/options.rb +0 -45
|
@@ -72,7 +72,7 @@ module Aspera
|
|
|
72
72
|
username: options.get_option(:username, mandatory: true),
|
|
73
73
|
password: options.get_option(:password, mandatory: true)
|
|
74
74
|
},
|
|
75
|
-
test_args: '
|
|
75
|
+
test_args: 'browse /'
|
|
76
76
|
}
|
|
77
77
|
end
|
|
78
78
|
|
|
@@ -159,7 +159,7 @@ module Aspera
|
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
Sync::Operations::DIRECTIONS.each do |dir|
|
|
162
|
-
|
|
162
|
+
define_action_method([:sync, dir]) { |path:, sync_info: {}, **| run_sync_transfer(dir, path: path, sync_info: sync_info) { server_transfer_spec } }
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
# --- DSL ---
|
|
@@ -187,8 +187,7 @@ module Aspera
|
|
|
187
187
|
Sync::Operations::DIRECTIONS.each do |dir|
|
|
188
188
|
command dir, description: "#{dir.capitalize}-sync with server", transfer_paths: :send, arguments: SyncActions::PATH_AND_INFO_ARGS
|
|
189
189
|
end
|
|
190
|
-
|
|
191
|
-
SyncActions.register_sync_admin_commands(self, :admin)
|
|
190
|
+
sync_admin_commands :admin
|
|
192
191
|
end
|
|
193
192
|
|
|
194
193
|
commands_under :health do
|
|
@@ -247,19 +247,22 @@ module Aspera
|
|
|
247
247
|
lookup: :lookup_share_id
|
|
248
248
|
command :user_permissions,
|
|
249
249
|
description: 'Manage user permissions on a share',
|
|
250
|
-
arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}]
|
|
250
|
+
arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}],
|
|
251
|
+
setup: :setup_share_permissions
|
|
251
252
|
command :group_permissions,
|
|
252
253
|
description: 'Manage group permissions on a share',
|
|
253
|
-
arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}]
|
|
254
|
+
arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}],
|
|
255
|
+
setup: :setup_share_permissions
|
|
254
256
|
end
|
|
255
257
|
|
|
256
|
-
# share user_permissions and group_permissions:
|
|
257
|
-
|
|
258
|
+
# share user_permissions and group_permissions: full CRUD, identified by the user or group id
|
|
259
|
+
{user_permissions: :user, group_permissions: :group}.each do |perm_type, entity_type|
|
|
258
260
|
commands_under [:admin, :share, perm_type] do
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
crud_commands entity: :"share_#{perm_type}",
|
|
262
|
+
api: :@api_shares_admin,
|
|
263
|
+
name: entity_noun(perm_type),
|
|
264
|
+
id_name: :"#{entity_type}_id",
|
|
265
|
+
lookup: :"lookup_shares_#{entity_type}_all_id"
|
|
263
266
|
end
|
|
264
267
|
end
|
|
265
268
|
|
|
@@ -286,6 +289,12 @@ module Aspera
|
|
|
286
289
|
{}
|
|
287
290
|
end
|
|
288
291
|
|
|
292
|
+
# share_id: resolved by Phase A via arguments:(:identifier, lookup: :lookup_share_id) on the perm_type node
|
|
293
|
+
# @return [Hash] ctx with API path of user and group permissions of the share
|
|
294
|
+
def setup_share_permissions(share_id:, **)
|
|
295
|
+
%i[user_permissions group_permissions].to_h { |perm_type| [:"share_#{perm_type}", "data/shares/#{share_id}/#{perm_type}"] }
|
|
296
|
+
end
|
|
297
|
+
|
|
289
298
|
# Lookup a share id by field/value using the admin API.
|
|
290
299
|
def lookup_share_id(field, value, **)
|
|
291
300
|
Rest::List.lookup_entity_generic(entity: 'share', field: field, value: value) { @api_shares_admin.read('data/shares') }['id']
|
|
@@ -328,7 +337,7 @@ module Aspera
|
|
|
328
337
|
# @param op [Symbol] CRUD operation
|
|
329
338
|
# entity_id: resolved via arguments:(:identifier) on the setting node
|
|
330
339
|
# permission_id: (share_permissions show) and data: (modify) resolved via arguments: on the leaf
|
|
331
|
-
def
|
|
340
|
+
def admin_entity_setting(entity_type, location, setting, op, entity_id:, permission_id: nil, data: nil)
|
|
332
341
|
entity_path = "#{admin_entity_path(entity_type, location)}/#{entity_id}/#{setting}"
|
|
333
342
|
send(:"entity_#{op}", api: @api_shares_admin, entity: entity_path, id: permission_id, is_singleton: !setting.eql?(:share_permissions), data: data)
|
|
334
343
|
end
|
|
@@ -336,7 +345,7 @@ module Aspera
|
|
|
336
345
|
# Shared handler for :users (group only)
|
|
337
346
|
# group_id: resolved by Phase A via arguments:(:identifier) on the :users node
|
|
338
347
|
# user_id: and data: resolved via arguments: on the leaf (GROUP_USERS_ARGS)
|
|
339
|
-
def
|
|
348
|
+
def admin_entity_users(entity_type, location, op, group_id:, user_id: nil, data: nil)
|
|
340
349
|
path = admin_entity_path(entity_type, location)
|
|
341
350
|
prefix = location.eql?(:all) ? '' : "#{location}_"
|
|
342
351
|
send(:"entity_#{op}", api: @api_shares_admin, entity: "#{path}/#{group_id}/#{prefix}users", id: user_id, data: data)
|
|
@@ -353,7 +362,7 @@ module Aspera
|
|
|
353
362
|
setting_ops = setting.eql?(:share_permissions) ? SHARE_PERMISSIONS_OPS : %i[show modify]
|
|
354
363
|
setting_ops.each do |op|
|
|
355
364
|
define_action_method([:admin, entity_type, location, setting, op]) do |**kwargs|
|
|
356
|
-
|
|
365
|
+
admin_entity_setting(entity_type, location, setting, op, entity_id: kwargs[:"#{entity_type}_id"], permission_id: kwargs[:permission_id], data: kwargs[setting])
|
|
357
366
|
end
|
|
358
367
|
end
|
|
359
368
|
end
|
|
@@ -362,7 +371,7 @@ module Aspera
|
|
|
362
371
|
# group users: no create route (Rails only exposes index+show+update+destroy)
|
|
363
372
|
GROUP_USERS_OPS.each do |op|
|
|
364
373
|
define_action_method([:admin, entity_type, location, :users, op]) do |group_id:, user_id: nil, user: nil, **|
|
|
365
|
-
|
|
374
|
+
admin_entity_users(entity_type, location, op, group_id: group_id, user_id: user_id, data: user)
|
|
366
375
|
end
|
|
367
376
|
end
|
|
368
377
|
end
|
|
@@ -389,18 +398,6 @@ module Aspera
|
|
|
389
398
|
end
|
|
390
399
|
end
|
|
391
400
|
end
|
|
392
|
-
|
|
393
|
-
# Handlers for admin > share > user_permissions|group_permissions > op
|
|
394
|
-
# share_id: resolved by Phase A via arguments:(:identifier, lookup: :lookup_share_id) on the perm_type node
|
|
395
|
-
# permission_id: resolved via arguments: on the show leaf
|
|
396
|
-
# Rails only exposes index+show for share permissions (read-only)
|
|
397
|
-
%i[user_permissions group_permissions].each do |perm_type|
|
|
398
|
-
SHARE_PERMISSIONS_OPS.each do |op|
|
|
399
|
-
define_action_method([:admin, :share, perm_type, op]) do |share_id:, permission_id: nil, **|
|
|
400
|
-
send(:"entity_#{op}", api: @api_shares_admin, entity: "data/shares/#{share_id}/#{perm_type}", id: permission_id)
|
|
401
|
-
end
|
|
402
|
-
end
|
|
403
|
-
end
|
|
404
401
|
end
|
|
405
402
|
end
|
|
406
403
|
end
|
|
@@ -52,6 +52,14 @@ module Aspera
|
|
|
52
52
|
|
|
53
53
|
SECRET_KEYWORDS = %w[password secret].freeze
|
|
54
54
|
|
|
55
|
+
# @param name [String] preset name, or `GLOBAL` for the global default preset
|
|
56
|
+
# @param create [Boolean] `true` for an action writing in the preset: the global default preset is declared and created if needed
|
|
57
|
+
# @return [String] preset name
|
|
58
|
+
def preset_name(name, create: false)
|
|
59
|
+
return name unless name.eql?(GLOBAL_DEFAULT_KEYWORD)
|
|
60
|
+
presets.global_default_preset(create: create)
|
|
61
|
+
end
|
|
62
|
+
|
|
55
63
|
# If `option_name` ends with a secret keyword and the vault is configured,
|
|
56
64
|
# move the clear-text value into the vault and replace it with a @vault: reference.
|
|
57
65
|
# @param preset [Hash] the preset hash (modified in place)
|
|
@@ -82,16 +90,16 @@ module Aspera
|
|
|
82
90
|
public
|
|
83
91
|
|
|
84
92
|
def action_preset_show(name:, **)
|
|
85
|
-
name =
|
|
93
|
+
name = preset_name(name)
|
|
86
94
|
cp = presets.config_presets
|
|
87
|
-
raise "no such preset: #{name}" unless cp.key?(name)
|
|
95
|
+
raise Cli::BadArgument, "no such preset: #{name}" unless cp.key?(name)
|
|
88
96
|
Result::SingleObject.new(PresetManager.deep_clone(cp[name]))
|
|
89
97
|
end
|
|
90
98
|
|
|
91
99
|
def action_preset_delete(name:, **)
|
|
92
|
-
name =
|
|
100
|
+
name = preset_name(name)
|
|
93
101
|
cp = presets.config_presets
|
|
94
|
-
raise "no such preset: #{name}" unless cp.key?(name)
|
|
102
|
+
raise Cli::BadArgument, "no such preset: #{name}" unless cp.key?(name)
|
|
95
103
|
cp.delete(name)
|
|
96
104
|
# Remove default declarations referring to the deleted preset
|
|
97
105
|
defaults = cp[PresetManager::Key::DEFAULTS]
|
|
@@ -103,11 +111,11 @@ module Aspera
|
|
|
103
111
|
end
|
|
104
112
|
|
|
105
113
|
def action_preset_get(name:, param_name:, **)
|
|
106
|
-
name =
|
|
114
|
+
name = preset_name(name)
|
|
107
115
|
cp = presets.config_presets
|
|
108
|
-
raise "no such preset: #{name}" unless cp.key?(name)
|
|
116
|
+
raise Cli::BadArgument, "no such preset: #{name}" unless cp.key?(name)
|
|
109
117
|
value = cp[name][param_name]
|
|
110
|
-
raise "no such option in preset #{name}
|
|
118
|
+
raise Cli::BadArgument, "no such option in preset #{name}: #{param_name}" if value.nil?
|
|
111
119
|
case value
|
|
112
120
|
when Numeric, String then return Result::Text.new(ExtendedValue.instance.evaluate(value.to_s, context: 'preset'))
|
|
113
121
|
end
|
|
@@ -115,15 +123,15 @@ module Aspera
|
|
|
115
123
|
end
|
|
116
124
|
|
|
117
125
|
def action_preset_unset(name:, param_name:, **)
|
|
118
|
-
name =
|
|
126
|
+
name = preset_name(name)
|
|
119
127
|
cp = presets.config_presets
|
|
120
|
-
raise "no such preset: #{name}" unless cp.key?(name)
|
|
128
|
+
raise Cli::BadArgument, "no such preset: #{name}" unless cp.key?(name)
|
|
121
129
|
cp[name].delete(param_name)
|
|
122
130
|
Result::Status.new("Removed: #{name}: #{param_name}")
|
|
123
131
|
end
|
|
124
132
|
|
|
125
133
|
def action_preset_set(name:, param_name:, param_value:, **)
|
|
126
|
-
name =
|
|
134
|
+
name = preset_name(name, create: true)
|
|
127
135
|
param_name = Parser.option_line_to_name(param_name)
|
|
128
136
|
presets.set_key(name, param_name, Parser.smart_convert(param_value))
|
|
129
137
|
secure_preset_option(presets.config_presets[name], name, param_name)
|
|
@@ -131,15 +139,15 @@ module Aspera
|
|
|
131
139
|
end
|
|
132
140
|
|
|
133
141
|
def action_preset_initialize(name:, preset:, **)
|
|
134
|
-
name =
|
|
142
|
+
name = preset_name(name, create: true)
|
|
135
143
|
cp = presets.config_presets
|
|
136
144
|
Log.log.warn { "configuration already exists: #{name}, overwriting" } if cp.key?(name)
|
|
137
145
|
cp[name] = preset
|
|
138
|
-
Result::Status.new("
|
|
146
|
+
Result::Status.new("Initialized: #{name}")
|
|
139
147
|
end
|
|
140
148
|
|
|
141
149
|
def action_preset_update(name:, **)
|
|
142
|
-
name =
|
|
150
|
+
name = preset_name(name, create: true)
|
|
143
151
|
unprocessed_options = options.unprocessed_options_with_value
|
|
144
152
|
Log.dump(:opts, unprocessed_options)
|
|
145
153
|
cp = presets.config_presets
|
|
@@ -150,14 +158,16 @@ module Aspera
|
|
|
150
158
|
end
|
|
151
159
|
|
|
152
160
|
def action_preset_ask(name:, option_names:, **)
|
|
153
|
-
name =
|
|
161
|
+
name = preset_name(name, create: true)
|
|
154
162
|
cp = presets.config_presets
|
|
155
163
|
cp[name] ||= {}
|
|
156
164
|
# Option names are only known at runtime: cannot be declared as interactive arguments
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
165
|
+
options.with_interactive do
|
|
166
|
+
option_names.each do |option_name|
|
|
167
|
+
option_value = options.get_interactive(option_name)
|
|
168
|
+
cp[name][option_name] = option_value
|
|
169
|
+
secure_preset_option(cp[name], name, option_name)
|
|
170
|
+
end
|
|
161
171
|
end
|
|
162
172
|
Result::Status.new("Updated: #{name}")
|
|
163
173
|
end
|
|
@@ -59,30 +59,38 @@ module Aspera
|
|
|
59
59
|
|
|
60
60
|
# Read and validate the YAML config file.
|
|
61
61
|
# Sets @config_presets and @checksum_on_disk.
|
|
62
|
+
# A file that cannot be read (YAML error, duplicate keys, access) is left untouched: it shall be fixed.
|
|
63
|
+
# A file with an unexpected structure is renamed for manual conversion, and a new one is created next time.
|
|
62
64
|
def read_config_file
|
|
63
65
|
Log.log.debug { "config file is: #{@config_file}".red }
|
|
64
66
|
if File.exist?(@config_file)
|
|
65
67
|
Log.log.debug { "loading #{@config_file}" }
|
|
66
|
-
|
|
68
|
+
begin
|
|
69
|
+
@config_presets = Yaml.safe_load(File.read(@config_file))
|
|
70
|
+
rescue Psych::SyntaxError
|
|
71
|
+
Log.log.error { "YAML error in config file: #{@config_file}" }
|
|
72
|
+
raise
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
raise Cli::Error, "Config file #{@config_file}: #{e.message}"
|
|
75
|
+
end
|
|
67
76
|
@checksum_on_disk = checksum
|
|
68
77
|
else
|
|
69
78
|
Log.log.warn { "No config file found. New configuration file: #{@config_file}" }
|
|
70
79
|
@config_presets = {Key::CONFIG => {Key::VERSION => 'new file'}}
|
|
71
80
|
# @checksum_on_disk remains nil: will be saved on first write
|
|
72
81
|
end
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
begin
|
|
83
|
+
validate_config_presets!
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
Log.log.debug { "-> #{e.class.name} : #{e}" }
|
|
86
|
+
if File.exist?(@config_file)
|
|
87
|
+
new_name = "#{@config_file}.pre#{Cli::VERSION}.manual_conversion_needed"
|
|
88
|
+
File.rename(@config_file, new_name)
|
|
89
|
+
Log.log.warn { "Renamed config file to #{new_name}." }
|
|
90
|
+
Log.log.warn('Manual Conversion is required. Next time, a new empty file will be created.')
|
|
91
|
+
end
|
|
92
|
+
raise Cli::Error, e.to_s
|
|
84
93
|
end
|
|
85
|
-
raise Cli::Error, e.to_s
|
|
86
94
|
end
|
|
87
95
|
|
|
88
96
|
# Save to disk only if content changed since last load/save.
|
|
@@ -132,7 +140,11 @@ module Aspera
|
|
|
132
140
|
Aspera.assert(!include_path.include?(config_name), type: Cli::Error) { 'loop in include' }
|
|
133
141
|
include_path = include_path.clone
|
|
134
142
|
current = @config_presets
|
|
135
|
-
|
|
143
|
+
# A preset name may contain a dot (e.g. a host name): the longest name of an existing preset is used
|
|
144
|
+
parts = config_name.split(PRESET_DIG_SEPARATOR)
|
|
145
|
+
count = parts.length.downto(1).find { |n| @config_presets.key?(parts.first(n).join(PRESET_DIG_SEPARATOR)) } || 1
|
|
146
|
+
names = [parts.first(count).join(PRESET_DIG_SEPARATOR)] + parts.drop(count)
|
|
147
|
+
names.each do |name|
|
|
136
148
|
Aspera.assert_type(current, Hash, type: Cli::Error) { "sub key: #{include_path}" }
|
|
137
149
|
include_path.push(name)
|
|
138
150
|
current = current[name]
|
|
@@ -188,14 +200,17 @@ module Aspera
|
|
|
188
200
|
nil
|
|
189
201
|
end
|
|
190
202
|
|
|
191
|
-
# @
|
|
192
|
-
|
|
203
|
+
# @param create [Boolean] `true`: declare and create the global default preset if needed (to write in it)
|
|
204
|
+
# @return [String] name of the global default preset
|
|
205
|
+
def global_default_preset(create: true)
|
|
193
206
|
result = plugin_default_name(CONF_GLOBAL_SYM)
|
|
194
207
|
if result.nil?
|
|
195
208
|
result = Key::GLOBAL
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
if create
|
|
210
|
+
set_key(Key::DEFAULTS, CONF_GLOBAL_SYM, result)
|
|
211
|
+
# A default declaration requires the preset to exist
|
|
212
|
+
@config_presets[result] ||= {}
|
|
213
|
+
end
|
|
199
214
|
end
|
|
200
215
|
result
|
|
201
216
|
end
|
data/lib/aspera/cli/prompt.rb
CHANGED
|
@@ -8,12 +8,13 @@ module Aspera
|
|
|
8
8
|
# Console input
|
|
9
9
|
module Prompt
|
|
10
10
|
# Prompt user for console input
|
|
11
|
+
# The prompt is displayed on `stderr`, like for sensitive input, so that `stdout` contains only results.
|
|
11
12
|
# @param prompt [String] prompt string to display
|
|
12
13
|
# @param sensitive [Boolean] whether to hide typed input
|
|
13
14
|
# @return [String] user input stripped of trailing newline
|
|
14
15
|
def prompt_user_input(prompt, sensitive: false)
|
|
15
16
|
return $stdin.getpass("#{prompt}> ") if sensitive
|
|
16
|
-
print("#{prompt}> ")
|
|
17
|
+
$stderr.print("#{prompt}> ")
|
|
17
18
|
line = $stdin.gets
|
|
18
19
|
Aspera.assert_type(line, String) { 'Unexpected end of standard input' }
|
|
19
20
|
line.chomp
|
data/lib/aspera/cli/result.rb
CHANGED
|
@@ -43,10 +43,7 @@ module Aspera
|
|
|
43
43
|
case formatter.format_type
|
|
44
44
|
when :nagios then Nagios.process(@data)
|
|
45
45
|
when :text then formatter.display_message(:data, @data.to_s)
|
|
46
|
-
when
|
|
47
|
-
when :json then formatter.display_message(:data, JSON.generate(filtered_data))
|
|
48
|
-
when :jsonpp then formatter.display_message(:data, JSON.pretty_generate(filtered_data))
|
|
49
|
-
when :yaml then formatter.display_message(:data, YAML.dump(filtered_data))
|
|
46
|
+
when *STRUCTURED_FORMATS then display_structured(formatter, filtered_data)
|
|
50
47
|
when :image
|
|
51
48
|
if @data.nil?
|
|
52
49
|
formatter.display_message(:data, formatter.special_format('null (no image)'))
|
|
@@ -81,6 +78,27 @@ module Aspera
|
|
|
81
78
|
else Aspera.error_unexpected_value(formatter.format_type) { 'format' }
|
|
82
79
|
end
|
|
83
80
|
end
|
|
81
|
+
|
|
82
|
+
# Formats rendering the data structure itself
|
|
83
|
+
STRUCTURED_FORMATS = %i[ruby json jsonpp yaml].freeze
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Display a value in one of STRUCTURED_FORMATS
|
|
88
|
+
# @param formatter [Formatter] The formatter to use
|
|
89
|
+
# @param value [Object] Value to render
|
|
90
|
+
# @return [nil]
|
|
91
|
+
def display_structured(formatter, value)
|
|
92
|
+
text =
|
|
93
|
+
case formatter.format_type
|
|
94
|
+
when :ruby then PP.pp(value, +'')
|
|
95
|
+
when :json then JSON.generate(value)
|
|
96
|
+
when :jsonpp then JSON.pretty_generate(value)
|
|
97
|
+
when :yaml then YAML.dump(value)
|
|
98
|
+
else Aspera.error_unexpected_value(formatter.format_type) { 'structured format' }
|
|
99
|
+
end
|
|
100
|
+
formatter.display_message(:data, text)
|
|
101
|
+
end
|
|
84
102
|
end
|
|
85
103
|
|
|
86
104
|
# Special result types - each has its own class
|
|
@@ -96,8 +114,11 @@ module Aspera
|
|
|
96
114
|
|
|
97
115
|
def format(formatter)
|
|
98
116
|
case formatter.format_type
|
|
99
|
-
when :text, :nagios
|
|
117
|
+
when :text, :nagios
|
|
100
118
|
formatter.display_message(:data, @data.to_s)
|
|
119
|
+
when *STRUCTURED_FORMATS
|
|
120
|
+
# An empty list, or no value
|
|
121
|
+
display_structured(formatter, @data.eql?(:empty) ? [] : nil)
|
|
101
122
|
when :table, :csv
|
|
102
123
|
if @data.eql?(:nothing)
|
|
103
124
|
Log.log.debug('no result expected')
|
|
@@ -132,8 +153,10 @@ module Aspera
|
|
|
132
153
|
def format(formatter)
|
|
133
154
|
data = formatter.hide_secrets? ? formatter.hide_secrets_in_string(@data) : @data
|
|
134
155
|
case formatter.format_type
|
|
135
|
-
when :text, :nagios
|
|
156
|
+
when :text, :nagios
|
|
136
157
|
formatter.display_message(:data, data)
|
|
158
|
+
when *STRUCTURED_FORMATS
|
|
159
|
+
display_structured(formatter, data)
|
|
137
160
|
when :table, :csv
|
|
138
161
|
formatter.display_message(:info, data)
|
|
139
162
|
else
|
|
@@ -161,14 +184,8 @@ module Aspera
|
|
|
161
184
|
case formatter.format_type
|
|
162
185
|
when :text, :nagios
|
|
163
186
|
formatter.display_message(:data, data.to_s)
|
|
164
|
-
when
|
|
165
|
-
formatter
|
|
166
|
-
when :json
|
|
167
|
-
formatter.display_message(:data, JSON.generate(data))
|
|
168
|
-
when :jsonpp
|
|
169
|
-
formatter.display_message(:data, JSON.pretty_generate(data))
|
|
170
|
-
when :yaml
|
|
171
|
-
formatter.display_message(:data, YAML.dump(data))
|
|
187
|
+
when *STRUCTURED_FORMATS
|
|
188
|
+
display_structured(formatter, data)
|
|
172
189
|
when :table, :csv
|
|
173
190
|
formatter.display_message(:data, data)
|
|
174
191
|
else
|
|
@@ -271,14 +288,8 @@ module Aspera
|
|
|
271
288
|
formatter.display_table(@data.map { |i| {@name => i} }, [@name])
|
|
272
289
|
when :text
|
|
273
290
|
formatter.display_message(:data, @data.join("\n"))
|
|
274
|
-
when
|
|
275
|
-
formatter
|
|
276
|
-
when :json
|
|
277
|
-
formatter.display_message(:data, JSON.generate(@data))
|
|
278
|
-
when :jsonpp
|
|
279
|
-
formatter.display_message(:data, JSON.pretty_generate(@data))
|
|
280
|
-
when :yaml
|
|
281
|
-
formatter.display_message(:data, YAML.dump(@data))
|
|
291
|
+
when *STRUCTURED_FORMATS
|
|
292
|
+
display_structured(formatter, @data)
|
|
282
293
|
else
|
|
283
294
|
super
|
|
284
295
|
end
|
data/lib/aspera/cli/runner.rb
CHANGED
|
@@ -348,9 +348,7 @@ module Aspera
|
|
|
348
348
|
# Create command line manager with arguments
|
|
349
349
|
@context.options = Parser.new(Info::CMD_NAME, @argv)
|
|
350
350
|
ExtendedValue.instance.on(EXTEND_ARGS) { |v| @context.options.args_as_extended(v) }
|
|
351
|
-
|
|
352
|
-
Formatter.declare_options(@context.options)
|
|
353
|
-
@context.formatter.bind_options(@context.options)
|
|
351
|
+
@context.formatter.declare_options(@context.options)
|
|
354
352
|
# Compare $0 with expected name
|
|
355
353
|
current_prog_name = File.basename($PROGRAM_NAME)
|
|
356
354
|
Aspera.assert(current_prog_name.eql?(Info::CMD_NAME), type: :warn) { "Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'" }
|
|
@@ -378,8 +376,6 @@ module Aspera
|
|
|
378
376
|
@context.secret_finder = SecretFinder.new(@context.options, @context.presets)
|
|
379
377
|
# The TransferAgent plugin may use the @preset parser
|
|
380
378
|
@context.transfer = TransferAgent.new(@context)
|
|
381
|
-
# Add commands for config plugin after all options have been added
|
|
382
|
-
@context.config.add_manual_header(false)
|
|
383
379
|
@context.validate
|
|
384
380
|
# Set banner when all environment is created so that additional extended value modifiers are known, e.g. @preset
|
|
385
381
|
end
|
|
@@ -17,55 +17,56 @@ module Aspera
|
|
|
17
17
|
(23..24).map { |i| "P(#{i})" }).freeze
|
|
18
18
|
# Positional arguments shared by sync transfer commands (push/pull/bidi) and sync admin commands.
|
|
19
19
|
PATH_AND_INFO_ARGS = [{name: :path}, {name: :sync_info, type: Hash, mandatory: false, default: {}}].freeze
|
|
20
|
-
# Names of the leaf commands registered under any `sync admin` node.
|
|
21
|
-
ADMIN_COMMANDS = %i[find status meta counters file_info overview query].freeze
|
|
22
20
|
# When a plugin class includes SyncActions, register the :sql option
|
|
23
|
-
# in that class's DSL registry so Base#initialize auto-declares it
|
|
21
|
+
# in that class's DSL registry so Base#initialize auto-declares it,
|
|
22
|
+
# and add the class-level DSL methods of ClassMethods.
|
|
24
23
|
class << self
|
|
25
24
|
def included(base)
|
|
26
25
|
base.option(:sql, description: 'SQL suffix appended to sqlite3 queries for admin subcommands (e.g. WHERE clause)')
|
|
26
|
+
base.extend(ClassMethods)
|
|
27
27
|
end
|
|
28
|
+
end
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
# @param
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
# Class-level DSL methods added to the plugin class that includes SyncActions.
|
|
31
|
+
module ClassMethods
|
|
32
|
+
# DSL class method: declare the `sync admin` node and its 7 leaf commands.
|
|
33
|
+
# @param parent [Symbol, Array<Symbol>] path relative to current scope, e.g. :admin
|
|
34
|
+
def sync_admin_commands(parent)
|
|
35
|
+
commands_under(parent, description: 'Manage sync database (admin operations)') do
|
|
36
|
+
command(
|
|
36
37
|
:find, description: 'Find sync database files', arguments: [{name: :path}],
|
|
37
38
|
action: lambda do |path:, **|
|
|
38
39
|
dbs = Sync::Operations.list_db_files(path)
|
|
39
40
|
Result::ObjectList.new(dbs.keys.map { |n| {name: n, path: dbs[n]} })
|
|
40
41
|
end
|
|
41
42
|
)
|
|
42
|
-
|
|
43
|
+
command(
|
|
43
44
|
:status, description: 'Show sync session status', arguments: PATH_AND_INFO_ARGS,
|
|
44
45
|
action: ->(path:, sync_info: {}, **) { Result::SingleObject.new(Sync::Operations.admin_status(async_info_from_args(path: path, sync_info: sync_info))) }
|
|
45
46
|
)
|
|
46
|
-
|
|
47
|
+
command(
|
|
47
48
|
:meta, description: 'Show sync session metadata', arguments: PATH_AND_INFO_ARGS,
|
|
48
49
|
action: lambda do |path:, sync_info: {}, **|
|
|
49
50
|
require 'aspera/sync/database'
|
|
50
51
|
Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).meta(options.get_option(:sql)))
|
|
51
52
|
end
|
|
52
53
|
)
|
|
53
|
-
|
|
54
|
+
command(
|
|
54
55
|
:counters, description: 'Show sync counters', arguments: PATH_AND_INFO_ARGS,
|
|
55
56
|
action: lambda do |path:, sync_info: {}, **|
|
|
56
57
|
require 'aspera/sync/database'
|
|
57
58
|
Result::SingleObject.new(db_from_args(path: path, sync_info: sync_info).counters(options.get_option(:sql)))
|
|
58
59
|
end
|
|
59
60
|
)
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
command(:file_info, description: 'Show per-file sync state', arguments: PATH_AND_INFO_ARGS, action: :action_sync_admin_file_info)
|
|
62
|
+
command(
|
|
62
63
|
:overview, description: 'Show sync database overview', arguments: PATH_AND_INFO_ARGS,
|
|
63
64
|
action: lambda do |path:, sync_info: {}, **|
|
|
64
65
|
require 'aspera/sync/database'
|
|
65
66
|
Result::ObjectList.new(db_from_args(path: path, sync_info: sync_info).overview, fields: %w[table name type])
|
|
66
67
|
end
|
|
67
68
|
)
|
|
68
|
-
|
|
69
|
+
command(
|
|
69
70
|
:query, description: 'Execute a raw SQL query', arguments: PATH_AND_INFO_ARGS,
|
|
70
71
|
action: lambda do |path:, sync_info: {}, **|
|
|
71
72
|
require 'aspera/sync/database'
|
|
@@ -15,7 +15,8 @@ module Aspera
|
|
|
15
15
|
#
|
|
16
16
|
# Architecture:
|
|
17
17
|
# - remote-daemon agents (desktop, node, connect, transferd): transfer_id + agent_params
|
|
18
|
-
# are persisted in AsyncTransferStore
|
|
18
|
+
# are persisted in AsyncTransferStore (without secrets, taken from current options);
|
|
19
|
+
# status is re-queried via Agent::Xxx.transfer_status
|
|
19
20
|
# - direct agent: transfers live in Ruby threads - store is the agent's @sessions;
|
|
20
21
|
# status is not persistable across process restarts (returns an informational message)
|
|
21
22
|
module TransferActions
|
|
@@ -25,10 +26,12 @@ module Aspera
|
|
|
25
26
|
store = async_transfer_store
|
|
26
27
|
entry = store.read(job_id)
|
|
27
28
|
Aspera.assert(!entry.nil?, type: Cli::BadArgument) { "Unknown job_id: #{job_id}" }
|
|
29
|
+
# Secrets are not persisted: take them from current transfer options (e.g. `--transfer`), for the same agent type
|
|
30
|
+
agent_params = current_agent_params(entry['agent_type']).merge(entry['agent_params'] || {})
|
|
28
31
|
# Inject the in-process agent reference (direct/httpgw) if still alive in this process.
|
|
29
32
|
ref = store.agent_ref(job_id)
|
|
30
|
-
|
|
31
|
-
live = query_live_status(entry)
|
|
33
|
+
agent_params['_agent_ref'] = ref if ref
|
|
34
|
+
live = query_live_status(entry.merge('agent_params' => agent_params))
|
|
32
35
|
if live
|
|
33
36
|
entry.merge!(live)
|
|
34
37
|
store.write(job_id, entry)
|
|
@@ -53,6 +56,14 @@ module Aspera
|
|
|
53
56
|
|
|
54
57
|
private
|
|
55
58
|
|
|
59
|
+
# @param agent_type [String] agent of the async transfer
|
|
60
|
+
# @return [Hash] agent parameters from current transfer options (e.g. `--transfer`), if for the same agent type
|
|
61
|
+
def current_agent_params(agent_type)
|
|
62
|
+
current = context.transfer.transfer_options
|
|
63
|
+
return {} unless (current['agent'] || 'direct').to_s.eql?(agent_type)
|
|
64
|
+
current.except('agent', 'asynchronous')
|
|
65
|
+
end
|
|
66
|
+
|
|
56
67
|
# Lazy accessor - requires context.persistency (available in all Base sub-classes).
|
|
57
68
|
# Delegate to the TransferAgent's store so that the in-memory agent-ref
|
|
58
69
|
# registry is shared between the two components in the same process.
|
|
@@ -44,7 +44,7 @@ module Aspera
|
|
|
44
44
|
|
|
45
45
|
extend OptionDeclarator
|
|
46
46
|
|
|
47
|
-
option :ts, description: 'Override transfer spec values', schema: Schema::Registry::TRANSFER_SPEC
|
|
47
|
+
option :ts, description: 'Override transfer spec values', schema: Schema::Registry::TRANSFER_SPEC, on_set: :user_transfer_spec=
|
|
48
48
|
option :to_folder, description: 'Destination folder for transferred files'
|
|
49
49
|
option :sources, description: "How list of transferred files is provided (#{FILE_LIST_OPTIONS.join(',')})", default: FILE_LIST_FROM_ARGS
|
|
50
50
|
option :src_type, description: 'Type of file list', allowed: %i[list pair], default: :list
|
|
@@ -64,8 +64,8 @@ module Aspera
|
|
|
64
64
|
@transfer_paths = nil
|
|
65
65
|
# HTTPGW URL provided by webapp
|
|
66
66
|
@httpgw_url_lambda = nil
|
|
67
|
-
|
|
68
|
-
@context.options
|
|
67
|
+
# Instance variables used by `on_set` callbacks are set above
|
|
68
|
+
self.class.declare_options(@context.options, target: self)
|
|
69
69
|
@context.options.parse_options!
|
|
70
70
|
@notification_cb = nil
|
|
71
71
|
if !@context.options.get_option(:notify_to).nil?
|
|
@@ -115,8 +115,10 @@ module Aspera
|
|
|
115
115
|
agent_options.merge!(@context.presets.by_name(param_set_name).symbolize_keys)
|
|
116
116
|
end
|
|
117
117
|
when :direct
|
|
118
|
-
#
|
|
118
|
+
# By default, no native `ascp` progress bar
|
|
119
119
|
agent_options[:quiet] = true unless agent_options.key?(:quiet)
|
|
120
|
+
# Native `ascp` progress bar requested: no other progress bar, unless option `progress_bar` is set
|
|
121
|
+
agent_options[:progress] = nil if !agent_options[:quiet] && @context.options.option_def(:progress_bar).source.eql?(:default)
|
|
120
122
|
agent_options[:check_ignore_cb] = ->(host, port) { @context.http_config.ignore_cert?(host, port) }
|
|
121
123
|
# JRuby
|
|
122
124
|
agent_options[:trusted_certs] = @context.http_config.trusted_cert_locations unless agent_options.key?(:trusted_certs)
|