aspera-cli 4.21.1 → 4.22.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 +1 -1
- data/CHANGELOG.md +52 -22
- data/CONTRIBUTING.md +69 -148
- data/README.md +929 -668
- data/bin/ascli +5 -14
- data/bin/asession +1 -3
- data/examples/get_proto_file.rb +4 -3
- data/examples/proxy.pac +20 -20
- data/lib/aspera/agent/base.rb +11 -5
- data/lib/aspera/agent/connect.rb +30 -28
- data/lib/aspera/agent/{alpha.rb → desktop.rb} +35 -31
- data/lib/aspera/agent/direct.rb +141 -121
- data/lib/aspera/agent/httpgw.rb +22 -26
- data/lib/aspera/agent/node.rb +14 -11
- data/lib/aspera/agent/transferd.rb +30 -19
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +6 -6
- data/lib/aspera/api/cos_node.rb +2 -2
- data/lib/aspera/api/httpgw.rb +7 -3
- data/lib/aspera/api/node.rb +10 -8
- data/lib/aspera/ascmd.rb +3 -3
- data/lib/aspera/ascp/installation.rb +53 -72
- data/lib/aspera/ascp/management.rb +1 -1
- data/lib/aspera/assert.rb +11 -2
- data/lib/aspera/cli/error.rb +2 -2
- data/lib/aspera/cli/extended_value.rb +46 -21
- data/lib/aspera/cli/formatter.rb +55 -48
- data/lib/aspera/cli/hints.rb +1 -1
- data/lib/aspera/cli/info.rb +1 -0
- data/lib/aspera/cli/main.rb +192 -170
- data/lib/aspera/cli/manager.rb +18 -18
- data/lib/aspera/cli/plugin.rb +23 -20
- data/lib/aspera/cli/plugin_factory.rb +1 -1
- data/lib/aspera/cli/plugins/alee.rb +1 -1
- data/lib/aspera/cli/plugins/aoc.rb +247 -159
- data/lib/aspera/cli/plugins/ats.rb +19 -17
- data/lib/aspera/cli/plugins/config.rb +76 -113
- data/lib/aspera/cli/plugins/console.rb +5 -3
- data/lib/aspera/cli/plugins/faspex.rb +39 -35
- data/lib/aspera/cli/plugins/faspex5.rb +111 -84
- data/lib/aspera/cli/plugins/faspio.rb +13 -1
- data/lib/aspera/cli/plugins/httpgw.rb +13 -1
- data/lib/aspera/cli/plugins/node.rb +312 -182
- data/lib/aspera/cli/plugins/orchestrator.rb +34 -40
- data/lib/aspera/cli/plugins/preview.rb +3 -3
- data/lib/aspera/cli/plugins/server.rb +6 -6
- data/lib/aspera/cli/plugins/shares.rb +5 -5
- data/lib/aspera/cli/sync_actions.rb +19 -18
- data/lib/aspera/cli/transfer_agent.rb +5 -5
- data/lib/aspera/cli/transfer_progress.rb +2 -2
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +116 -95
- data/lib/aspera/coverage.rb +8 -5
- data/lib/aspera/environment.rb +26 -17
- data/lib/aspera/faspex_gw.rb +14 -14
- data/lib/aspera/faspex_postproc.rb +10 -11
- data/lib/aspera/hash_ext.rb +4 -14
- data/lib/aspera/json_rpc.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +47 -34
- data/lib/aspera/keychain/factory.rb +41 -0
- data/lib/aspera/keychain/hashicorp_vault.rb +71 -0
- data/lib/aspera/keychain/macos_security.rb +19 -11
- data/lib/aspera/log.rb +28 -34
- data/lib/aspera/nagios.rb +6 -6
- data/lib/aspera/node_simulator.rb +8 -8
- data/lib/aspera/oauth/base.rb +14 -7
- data/lib/aspera/oauth/factory.rb +5 -6
- data/lib/aspera/oauth/url_json.rb +6 -6
- data/lib/aspera/persistency_action_once.rb +6 -4
- data/lib/aspera/persistency_folder.rb +2 -2
- data/lib/aspera/preview/generator.rb +13 -10
- data/lib/aspera/preview/options.rb +16 -16
- data/lib/aspera/preview/terminal.rb +4 -4
- data/lib/aspera/preview/utils.rb +15 -17
- data/lib/aspera/products/connect.rb +35 -1
- data/lib/aspera/products/{alpha.rb → desktop.rb} +3 -3
- data/lib/aspera/products/transferd.rb +9 -2
- data/lib/aspera/proxy_auto_config.rb +2 -2
- data/lib/aspera/rest.rb +56 -47
- data/lib/aspera/rest_errors_aspera.rb +1 -1
- data/lib/aspera/secret_hider.rb +12 -5
- data/lib/aspera/ssh.rb +4 -4
- data/lib/aspera/temp_file_manager.rb +5 -4
- data/lib/aspera/transfer/convert.rb +29 -0
- data/lib/aspera/transfer/error_info.rb +66 -66
- data/lib/aspera/transfer/parameters.rb +13 -68
- data/lib/aspera/transfer/spec.rb +5 -6
- data/lib/aspera/transfer/spec.schema.yaml +753 -0
- data/lib/aspera/transfer/spec_doc.rb +62 -0
- data/lib/aspera/transfer/sync.rb +23 -72
- data/lib/aspera/transfer/sync_instance.schema.yaml +13 -0
- data/lib/aspera/transfer/sync_session.schema.yaml +79 -0
- data/lib/aspera/transfer/uri.rb +6 -6
- data/lib/aspera/uri_reader.rb +18 -1
- data/lib/aspera/web_auth.rb +1 -1
- data/lib/aspera/web_server_simple.rb +53 -44
- data.tar.gz.sig +0 -0
- metadata +28 -165
- metadata.gz.sig +0 -0
- data/examples/build_exec +0 -74
- data/examples/build_exec_rubyc +0 -40
- data/examples/build_package.sh +0 -28
- data/lib/aspera/transfer/spec.yaml +0 -718
data/lib/aspera/cli/main.rb
CHANGED
@@ -15,14 +15,36 @@ require 'aspera/assert'
|
|
15
15
|
|
16
16
|
module Aspera
|
17
17
|
module Cli
|
18
|
+
# Global objects shared with plugins
|
19
|
+
class Objects
|
20
|
+
MEMBERS = %i[options transfer config formatter persistency].freeze
|
21
|
+
attr_accessor(*MEMBERS)
|
22
|
+
|
23
|
+
def validate
|
24
|
+
MEMBERS.each do |i|
|
25
|
+
Aspera.assert(instance_variable_defined?(:"@#{i}"))
|
26
|
+
Aspera.assert(!instance_variable_get(:"@#{i}").nil?)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def only_manual?
|
31
|
+
transfer.eql?(:only_manual)
|
32
|
+
end
|
33
|
+
|
34
|
+
def only_manual
|
35
|
+
@transfer = :only_manual
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
18
39
|
# The main CLI class
|
19
40
|
class Main
|
20
41
|
# Plugins store transfer result using this key and use result_transfer_multiple()
|
21
42
|
STATUS_FIELD = 'status'
|
22
43
|
COMMAND_CONFIG = :config
|
23
44
|
COMMAND_HELP = :help
|
45
|
+
SCALAR_TYPES = [String, Integer, Symbol].freeze
|
24
46
|
|
25
|
-
private_constant :COMMAND_CONFIG, :COMMAND_HELP
|
47
|
+
private_constant :COMMAND_CONFIG, :COMMAND_HELP, :SCALAR_TYPES
|
26
48
|
|
27
49
|
class << self
|
28
50
|
# expect some list, but nothing to display
|
@@ -33,6 +55,8 @@ module Aspera
|
|
33
55
|
|
34
56
|
def result_status(status); return {type: :status, data: status}; end
|
35
57
|
|
58
|
+
def result_text(data); return {type: :text, data: data}; end
|
59
|
+
|
36
60
|
def result_success; return result_status('complete'); end
|
37
61
|
|
38
62
|
# Process statuses of finished transfer sessions
|
@@ -57,80 +81,189 @@ module Aspera
|
|
57
81
|
item[STATUS_FIELD] = item[STATUS_FIELD].map(&:to_s).join(',')
|
58
82
|
end
|
59
83
|
raise global_status unless global_status.eql?(:success)
|
60
|
-
return
|
84
|
+
return result_object_list(status_table)
|
61
85
|
end
|
62
86
|
|
63
87
|
def result_image(blob, formatter:)
|
64
88
|
return Main.result_status(formatter.status_image(blob))
|
65
89
|
end
|
66
90
|
|
67
|
-
def result_single_object(data)
|
68
|
-
return {type: :single_object, data: data}
|
91
|
+
def result_single_object(data, fields: nil)
|
92
|
+
return {type: :single_object, data: data, fields: fields}
|
69
93
|
end
|
70
94
|
|
71
95
|
def result_object_list(data, fields: nil, total: nil)
|
72
|
-
return {type: :object_list, data: data, fields: fields, total:
|
96
|
+
return {type: :object_list, data: data, fields: fields, total: total}
|
73
97
|
end
|
74
98
|
|
75
99
|
def result_value_list(data, name)
|
76
100
|
return {type: :value_list, data: data, name: name}
|
77
101
|
end
|
78
|
-
end
|
79
102
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
103
|
+
# Determines type of result based on data
|
104
|
+
def result_auto(data)
|
105
|
+
case data
|
106
|
+
when Hash
|
107
|
+
return result_single_object(data)
|
108
|
+
when Array
|
109
|
+
all_types = data.map(&:class).uniq
|
110
|
+
return result_object_list(data) if all_types.eql?([Hash])
|
111
|
+
unsupported_types = all_types - SCALAR_TYPES
|
112
|
+
return result_value_list(data, 'list') if unsupported_types.empty?
|
113
|
+
Aspera.error_unexpected_value(unsupported_types){'list item types'}
|
114
|
+
when *SCALAR_TYPES
|
115
|
+
return result_text(data)
|
116
|
+
else Aspera.error_unexpected_value(data.class.name){'result type'}
|
117
|
+
end
|
118
|
+
end
|
85
119
|
end
|
86
120
|
|
87
|
-
# =============================================================
|
88
|
-
# Parameter handlers
|
89
|
-
#
|
90
|
-
|
91
121
|
# minimum initialization, no exception raised
|
92
122
|
def initialize(argv)
|
93
123
|
@argv = argv
|
94
|
-
|
95
|
-
|
124
|
+
early_debug_setup
|
125
|
+
Log.log.trace2{Log.dump(:argv, @argv)}
|
96
126
|
@option_help = false
|
97
127
|
@option_show_config = false
|
98
128
|
@bash_completion = false
|
99
|
-
|
100
|
-
Log.log.trace2{Log.dump(:argv, @argv)}
|
129
|
+
@env = Objects.new
|
101
130
|
end
|
102
131
|
|
132
|
+
# this is the main function called by initial script just after constructor
|
133
|
+
def process_command_line
|
134
|
+
# catch exception information , if any
|
135
|
+
exception_info = nil
|
136
|
+
# false if command shall not be executed (e.g. --show-config)
|
137
|
+
execute_command = true
|
138
|
+
# catch exceptions
|
139
|
+
begin
|
140
|
+
init_agents_and_options
|
141
|
+
# find plugins, shall be after parse! ?
|
142
|
+
PluginFactory.instance.add_plugins_from_lookup_folders
|
143
|
+
# help requested without command ? (plugins must be known here)
|
144
|
+
exit_with_usage(true) if @option_help && @env.options.command_or_arg_empty?
|
145
|
+
generate_bash_completion if @bash_completion
|
146
|
+
@env.config.periodic_check_newer_gem_version
|
147
|
+
command_sym =
|
148
|
+
if @option_show_config && @env.options.command_or_arg_empty?
|
149
|
+
COMMAND_CONFIG
|
150
|
+
else
|
151
|
+
@env.options.get_next_command(PluginFactory.instance.plugin_list.unshift(COMMAND_HELP))
|
152
|
+
end
|
153
|
+
# command will not be executed, but we need manual
|
154
|
+
@env.options.fail_on_missing_mandatory = false if @option_help || @option_show_config
|
155
|
+
# main plugin is not dynamically instantiated
|
156
|
+
case command_sym
|
157
|
+
when COMMAND_HELP
|
158
|
+
exit_with_usage(true)
|
159
|
+
when COMMAND_CONFIG
|
160
|
+
command_plugin = @env.config
|
161
|
+
else
|
162
|
+
# get plugin, set options, etc
|
163
|
+
command_plugin = get_plugin_instance_with_options(command_sym)
|
164
|
+
# parse plugin specific options
|
165
|
+
@env.options.parse_options!
|
166
|
+
end
|
167
|
+
# help requested for current plugin
|
168
|
+
exit_with_usage(false) if @option_help
|
169
|
+
if @option_show_config
|
170
|
+
@env.formatter.display_results(type: :single_object, data: @env.options.known_options(only_defined: true).stringify_keys)
|
171
|
+
execute_command = false
|
172
|
+
end
|
173
|
+
# locking for single execution (only after "per plugin" option, in case lock port is there)
|
174
|
+
lock_port = @env.options.get_option(:lock_port)
|
175
|
+
if !lock_port.nil?
|
176
|
+
begin
|
177
|
+
# no need to close later, will be freed on process exit. must save in member else it is garbage collected
|
178
|
+
Log.log.debug{"Opening lock port #{lock_port}"}
|
179
|
+
# loopback address, could also be 'localhost'
|
180
|
+
@tcp_server = TCPServer.new('127.0.0.1', lock_port)
|
181
|
+
rescue StandardError => e
|
182
|
+
execute_command = false
|
183
|
+
Log.log.warn{"Another instance is already running (#{e.message})."}
|
184
|
+
end
|
185
|
+
end
|
186
|
+
pid_file = @env.options.get_option(:pid_file)
|
187
|
+
if !pid_file.nil?
|
188
|
+
File.write(pid_file, Process.pid)
|
189
|
+
Log.log.debug{"Wrote pid #{Process.pid} to #{pid_file}"}
|
190
|
+
at_exit{File.delete(pid_file)}
|
191
|
+
end
|
192
|
+
# execute and display (if not exclusive execution)
|
193
|
+
@env.formatter.display_results(**command_plugin.execute_action) if execute_command
|
194
|
+
# save config file if command modified it
|
195
|
+
@env.config.save_config_file_if_needed
|
196
|
+
# finish
|
197
|
+
@env.transfer.shutdown
|
198
|
+
rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
|
199
|
+
rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL'}
|
200
|
+
rescue Cli::BadArgument => e; exception_info = {e: e, t: 'Argument', usage: true}
|
201
|
+
rescue Cli::BadIdentifier => e; exception_info = {e: e, t: 'Identifier'}
|
202
|
+
rescue Cli::Error => e; exception_info = {e: e, t: 'Tool', usage: true}
|
203
|
+
rescue Transfer::Error => e; exception_info = {e: e, t: 'Transfer'}
|
204
|
+
rescue RestCallError => e; exception_info = {e: e, t: 'Rest'}
|
205
|
+
rescue SocketError => e; exception_info = {e: e, t: 'Network'}
|
206
|
+
rescue StandardError => e; exception_info = {e: e, t: "Other(#{e.class.name})", debug: true}
|
207
|
+
rescue Interrupt => e; exception_info = {e: e, t: 'Interruption', debug: true}
|
208
|
+
end
|
209
|
+
# cleanup file list files
|
210
|
+
TempFileManager.instance.cleanup
|
211
|
+
# 1- processing of error condition
|
212
|
+
unless exception_info.nil?
|
213
|
+
Log.log.warn(exception_info[:e].message) if Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
|
214
|
+
@env.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} #{exception_info[:t]}: #{exception_info[:e].message}")
|
215
|
+
@env.formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
|
216
|
+
# Is that a known error condition with proposal for remediation ?
|
217
|
+
Hints.hint_for(exception_info[:e], @env.formatter)
|
218
|
+
end
|
219
|
+
# 2- processing of command not processed (due to exception or bad command line)
|
220
|
+
if execute_command || @option_show_config
|
221
|
+
@env.options.final_errors.each do |msg|
|
222
|
+
@env.formatter.display_message(:error, "#{Formatter::ERROR_FLASH} Argument: #{msg}")
|
223
|
+
# add code as exception if there is not already an error
|
224
|
+
exception_info = {e: Exception.new(msg), t: 'UnusedArg'} if exception_info.nil?
|
225
|
+
end
|
226
|
+
end
|
227
|
+
# 3- in case of error, fail the process status
|
228
|
+
unless exception_info.nil?
|
229
|
+
# show stack trace in debug mode
|
230
|
+
raise exception_info[:e] if Log.log.debug?
|
231
|
+
# else give hint and exit
|
232
|
+
@env.formatter.display_message(:error, 'Use --log-level=debug to get more details.') if exception_info[:debug]
|
233
|
+
Process.exit(1)
|
234
|
+
end
|
235
|
+
return nil
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
103
240
|
# This can throw exception if there is a problem with the environment, needs to be caught by execute method
|
104
241
|
def init_agents_and_options
|
105
|
-
@plug_init[:only_manual] = false
|
106
242
|
# create formatter, in case there is an exception, it is used to display.
|
107
|
-
@
|
243
|
+
@env.formatter = Formatter.new
|
108
244
|
# create command line manager with arguments
|
109
|
-
@
|
245
|
+
@env.options = Manager.new(Info::CMD_NAME, @argv)
|
110
246
|
# formatter adds options
|
111
|
-
@
|
112
|
-
ExtendedValue.instance.default_decoder = options.get_option(:struct_parser)
|
247
|
+
@env.formatter.declare_options(@env.options)
|
248
|
+
ExtendedValue.instance.default_decoder = @env.options.get_option(:struct_parser)
|
113
249
|
# compare $0 with expected name
|
114
250
|
current_prog_name = File.basename($PROGRAM_NAME)
|
115
|
-
formatter.display_message(
|
251
|
+
@env.formatter.display_message(
|
116
252
|
:error,
|
117
253
|
"#{Formatter::WARNING_FLASH} Please use '#{Info::CMD_NAME}' instead of '#{current_prog_name}'") unless current_prog_name.eql?(Info::CMD_NAME)
|
118
254
|
# declare and parse global options
|
119
255
|
declare_global_options
|
120
256
|
# the Config plugin adds the @preset parser, so declare before TransferAgent which may use it
|
121
|
-
@
|
122
|
-
|
123
|
-
|
124
|
-
Aspera.assert(@plug_init[:persistency]){'missing persistency object'}
|
257
|
+
@env.config = Plugins::Config.new(broker: @env, man_header: false)
|
258
|
+
# data persistency is set in config
|
259
|
+
Aspera.assert(@env.persistency){'missing persistency object'}
|
125
260
|
# the TransferAgent plugin may use the @preset parser
|
126
|
-
@
|
261
|
+
@env.transfer = TransferAgent.new(@env.options, @env.config)
|
127
262
|
# add commands for config plugin after all options have been added
|
128
|
-
@
|
129
|
-
|
130
|
-
Aspera.assert(nil_keys.empty?){"nil : #{nil_keys}"}
|
131
|
-
Log.log.debug('plugin env created'.red)
|
263
|
+
@env.config.add_manual_header(false)
|
264
|
+
@env.validate
|
132
265
|
# set banner when all environment is created so that additional extended value modifiers are known, e.g. @preset
|
133
|
-
options.parser.banner = app_banner
|
266
|
+
@env.options.parser.banner = app_banner
|
134
267
|
end
|
135
268
|
|
136
269
|
def app_banner
|
@@ -170,41 +303,40 @@ module Aspera
|
|
170
303
|
# define header for manual
|
171
304
|
def declare_global_options
|
172
305
|
Log.log.debug('declare_global_options')
|
173
|
-
options.declare(:help, 'Show this message', values: :none, short: 'h')
|
174
|
-
options.declare(:bash_comp, 'Generate bash completion for command', values: :none)
|
175
|
-
options.declare(:show_config, 'Display parameters used for the provided action', values: :none)
|
176
|
-
options.declare(:version, 'Display version', values: :none, short: 'v')
|
177
|
-
options.declare(
|
306
|
+
@env.options.declare(:help, 'Show this message', values: :none, short: 'h'){@option_help = true}
|
307
|
+
@env.options.declare(:bash_comp, 'Generate bash completion for command', values: :none){@bash_completion = true}
|
308
|
+
@env.options.declare(:show_config, 'Display parameters used for the provided action', values: :none){@option_show_config = true}
|
309
|
+
@env.options.declare(:version, 'Display version', values: :none, short: 'v'){@env.formatter.display_message(:data, Cli::VERSION); Process.exit(0)} # rubocop:disable Style/Semicolon, Layout/LineLength
|
310
|
+
@env.options.declare(
|
178
311
|
:ui, 'Method to start browser',
|
179
312
|
values: Environment::USER_INTERFACES,
|
180
313
|
handler: {o: Environment.instance, m: :url_method},
|
181
314
|
default: Environment.default_gui_mode)
|
182
|
-
options.declare(:log_level, 'Log level', values: Log.levels, handler: {o: Log.instance, m: :level})
|
183
|
-
options.declare(:logger, 'Logging method', values: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
|
184
|
-
options.declare(:lock_port, 'Prevent dual execution of a command, e.g. in cron', coerce: Integer, types: Integer)
|
185
|
-
options.declare(:once_only, 'Process only new items (some commands)', values: :bool, default: false)
|
186
|
-
options.declare(:log_secrets, 'Show passwords in logs', values: :bool, handler: {o: SecretHider, m: :log_secrets})
|
187
|
-
options.declare(:clean_temp, 'Cleanup temporary files on exit', values: :bool, handler: {o: TempFileManager.instance, m: :cleanup_on_exit})
|
188
|
-
options.declare(:pid_file, 'Write process identifier to file, delete on exit', types: String)
|
315
|
+
@env.options.declare(:log_level, 'Log level', values: Log.levels, handler: {o: Log.instance, m: :level})
|
316
|
+
@env.options.declare(:logger, 'Logging method', values: Log::LOG_TYPES, handler: {o: Log.instance, m: :logger_type})
|
317
|
+
@env.options.declare(:lock_port, 'Prevent dual execution of a command, e.g. in cron', coerce: Integer, types: Integer)
|
318
|
+
@env.options.declare(:once_only, 'Process only new items (some commands)', values: :bool, default: false)
|
319
|
+
@env.options.declare(:log_secrets, 'Show passwords in logs', values: :bool, handler: {o: SecretHider, m: :log_secrets})
|
320
|
+
@env.options.declare(:clean_temp, 'Cleanup temporary files on exit', values: :bool, handler: {o: TempFileManager.instance, m: :cleanup_on_exit})
|
321
|
+
@env.options.declare(:pid_file, 'Write process identifier to file, delete on exit', types: String)
|
189
322
|
# parse declared options
|
190
|
-
options.parse_options!
|
323
|
+
@env.options.parse_options!
|
191
324
|
end
|
192
325
|
|
193
326
|
# @return the plugin instance, based on name
|
194
327
|
# also loads the plugin options, and default values from conf file
|
195
328
|
# @param plugin_name_sym : symbol for plugin name
|
196
|
-
def get_plugin_instance_with_options(plugin_name_sym
|
197
|
-
env ||= @plug_init
|
329
|
+
def get_plugin_instance_with_options(plugin_name_sym)
|
198
330
|
Log.log.debug{"get_plugin_instance_with_options(#{plugin_name_sym})"}
|
199
331
|
# load default params only if no param already loaded before plugin instantiation
|
200
|
-
env
|
201
|
-
command_plugin = PluginFactory.instance.create(plugin_name_sym,
|
332
|
+
@env.config.add_plugin_default_preset(plugin_name_sym)
|
333
|
+
command_plugin = PluginFactory.instance.create(plugin_name_sym, broker: @env)
|
202
334
|
return command_plugin
|
203
335
|
end
|
204
336
|
|
205
337
|
def generate_bash_completion
|
206
|
-
if options.get_next_argument('', multiple: true, mandatory: false).nil?
|
207
|
-
PluginFactory.instance.plugin_list.each{|p|puts p}
|
338
|
+
if @env.options.get_next_argument('', multiple: true, mandatory: false).nil?
|
339
|
+
PluginFactory.instance.plugin_list.each{ |p| puts p}
|
208
340
|
else
|
209
341
|
Log.log.warn('only first level completion so far')
|
210
342
|
end
|
@@ -214,27 +346,24 @@ module Aspera
|
|
214
346
|
def exit_with_usage(include_all_plugins)
|
215
347
|
Log.log.debug{"exit_with_usage(#{include_all_plugins})".bg_red}
|
216
348
|
# display main plugin options (+config)
|
217
|
-
formatter.display_message(:error, options.parser)
|
349
|
+
@env.formatter.display_message(:error, @env.options.parser)
|
218
350
|
if include_all_plugins
|
351
|
+
@env.only_manual
|
219
352
|
# list plugins that have a "require" field, i.e. all but main plugin
|
220
353
|
PluginFactory.instance.plugin_list.each do |plugin_name_sym|
|
221
354
|
# config was already included in the global options
|
222
355
|
next if plugin_name_sym.eql?(COMMAND_CONFIG)
|
223
356
|
# override main option parser with a brand new, to avoid having global options
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
plugin_env[:options].parser.banner = '' # remove default banner
|
228
|
-
get_plugin_instance_with_options(plugin_name_sym, plugin_env)
|
357
|
+
@env.options = Manager.new(Info::CMD_NAME)
|
358
|
+
@env.options.parser.banner = '' # remove default banner
|
359
|
+
get_plugin_instance_with_options(plugin_name_sym)
|
229
360
|
# display generated help for plugin options
|
230
|
-
formatter.display_message(:error,
|
361
|
+
@env.formatter.display_message(:error, @env.options.parser.help)
|
231
362
|
end
|
232
363
|
end
|
233
364
|
Process.exit(0)
|
234
365
|
end
|
235
366
|
|
236
|
-
protected
|
237
|
-
|
238
367
|
# early debug for parser
|
239
368
|
# Note: does not accept shortcuts
|
240
369
|
def early_debug_setup
|
@@ -249,113 +378,6 @@ module Aspera
|
|
249
378
|
$stderr.puts("Error: #{e}")
|
250
379
|
end
|
251
380
|
end
|
252
|
-
|
253
|
-
public
|
254
|
-
|
255
|
-
# this is the main function called by initial script just after constructor
|
256
|
-
def process_command_line
|
257
|
-
# catch exception information , if any
|
258
|
-
exception_info = nil
|
259
|
-
# false if command shall not be executed (e.g. --show-config)
|
260
|
-
execute_command = true
|
261
|
-
# catch exceptions
|
262
|
-
begin
|
263
|
-
init_agents_and_options
|
264
|
-
# find plugins, shall be after parse! ?
|
265
|
-
PluginFactory.instance.add_plugins_from_lookup_folders
|
266
|
-
# help requested without command ? (plugins must be known here)
|
267
|
-
exit_with_usage(true) if @option_help && options.command_or_arg_empty?
|
268
|
-
generate_bash_completion if @bash_completion
|
269
|
-
config.periodic_check_newer_gem_version
|
270
|
-
command_sym =
|
271
|
-
if @option_show_config && options.command_or_arg_empty?
|
272
|
-
COMMAND_CONFIG
|
273
|
-
else
|
274
|
-
options.get_next_command(PluginFactory.instance.plugin_list.unshift(COMMAND_HELP))
|
275
|
-
end
|
276
|
-
# command will not be executed, but we need manual
|
277
|
-
options.fail_on_missing_mandatory = false if @option_help || @option_show_config
|
278
|
-
# main plugin is not dynamically instantiated
|
279
|
-
case command_sym
|
280
|
-
when COMMAND_HELP
|
281
|
-
exit_with_usage(true)
|
282
|
-
when COMMAND_CONFIG
|
283
|
-
command_plugin = config
|
284
|
-
else
|
285
|
-
# get plugin, set options, etc
|
286
|
-
command_plugin = get_plugin_instance_with_options(command_sym)
|
287
|
-
# parse plugin specific options
|
288
|
-
options.parse_options!
|
289
|
-
end
|
290
|
-
# help requested for current plugin
|
291
|
-
exit_with_usage(false) if @option_help
|
292
|
-
if @option_show_config
|
293
|
-
formatter.display_results(type: :single_object, data: options.known_options(only_defined: true).stringify_keys)
|
294
|
-
execute_command = false
|
295
|
-
end
|
296
|
-
# locking for single execution (only after "per plugin" option, in case lock port is there)
|
297
|
-
lock_port = options.get_option(:lock_port)
|
298
|
-
if !lock_port.nil?
|
299
|
-
begin
|
300
|
-
# no need to close later, will be freed on process exit. must save in member else it is garbage collected
|
301
|
-
Log.log.debug{"Opening lock port #{lock_port}"}
|
302
|
-
@tcp_server = TCPServer.new('127.0.0.1', lock_port)
|
303
|
-
rescue StandardError => e
|
304
|
-
execute_command = false
|
305
|
-
Log.log.warn{"Another instance is already running (#{e.message})."}
|
306
|
-
end
|
307
|
-
end
|
308
|
-
pid_file = options.get_option(:pid_file)
|
309
|
-
if !pid_file.nil?
|
310
|
-
File.write(pid_file, Process.pid)
|
311
|
-
Log.log.debug{"Wrote pid #{Process.pid} to #{pid_file}"}
|
312
|
-
at_exit{File.delete(pid_file)}
|
313
|
-
end
|
314
|
-
# execute and display (if not exclusive execution)
|
315
|
-
formatter.display_results(**command_plugin.execute_action) if execute_command
|
316
|
-
# save config file if command modified it
|
317
|
-
config.save_config_file_if_needed
|
318
|
-
# finish
|
319
|
-
transfer.shutdown
|
320
|
-
rescue Net::SSH::AuthenticationFailed => e; exception_info = {e: e, t: 'SSH', security: true}
|
321
|
-
rescue OpenSSL::SSL::SSLError => e; exception_info = {e: e, t: 'SSL'}
|
322
|
-
rescue Cli::BadArgument => e; exception_info = {e: e, t: 'Argument', usage: true}
|
323
|
-
rescue Cli::NoSuchIdentifier => e; exception_info = {e: e, t: 'Identifier'}
|
324
|
-
rescue Cli::Error => e; exception_info = {e: e, t: 'Tool', usage: true}
|
325
|
-
rescue Transfer::Error => e; exception_info = {e: e, t: 'Transfer'}
|
326
|
-
rescue RestCallError => e; exception_info = {e: e, t: 'Rest'}
|
327
|
-
rescue SocketError => e; exception_info = {e: e, t: 'Network'}
|
328
|
-
rescue StandardError => e; exception_info = {e: e, t: "Other(#{e.class.name})", debug: true}
|
329
|
-
rescue Interrupt => e; exception_info = {e: e, t: 'Interruption', debug: true}
|
330
|
-
end
|
331
|
-
# cleanup file list files
|
332
|
-
TempFileManager.instance.cleanup
|
333
|
-
# 1- processing of error condition
|
334
|
-
unless exception_info.nil?
|
335
|
-
Log.log.warn(exception_info[:e].message) if Log.instance.logger_type.eql?(:syslog) && exception_info[:security]
|
336
|
-
formatter.display_message(:error, "#{Formatter::ERROR_FLASH} #{exception_info[:t]}: #{exception_info[:e].message}")
|
337
|
-
formatter.display_message(:error, 'Use option -h to get help.') if exception_info[:usage]
|
338
|
-
# Is that a known error condition with proposal for remediation ?
|
339
|
-
Hints.hint_for(exception_info[:e], formatter)
|
340
|
-
end
|
341
|
-
# 2- processing of command not processed (due to exception or bad command line)
|
342
|
-
if execute_command || @option_show_config
|
343
|
-
options.final_errors.each do |msg|
|
344
|
-
formatter.display_message(:error, "#{Formatter::ERROR_FLASH} Argument: #{msg}")
|
345
|
-
# add code as exception if there is not already an error
|
346
|
-
exception_info = {e: Exception.new(msg), t: 'UnusedArg'} if exception_info.nil?
|
347
|
-
end
|
348
|
-
end
|
349
|
-
# 3- in case of error, fail the process status
|
350
|
-
unless exception_info.nil?
|
351
|
-
# show stack trace in debug mode
|
352
|
-
raise exception_info[:e] if Log.log.debug?
|
353
|
-
# else give hint and exit
|
354
|
-
formatter.display_message(:error, 'Use --log-level=debug to get more details.') if exception_info[:debug]
|
355
|
-
Process.exit(1)
|
356
|
-
end
|
357
|
-
return nil
|
358
|
-
end
|
359
381
|
end
|
360
382
|
end
|
361
383
|
end
|
data/lib/aspera/cli/manager.rb
CHANGED
@@ -21,7 +21,7 @@ module Aspera
|
|
21
21
|
@option_name = option_name
|
22
22
|
@has_writer = @object.respond_to?(writer_method)
|
23
23
|
Log.log.trace1{"AttrAccessor: #{@option_name}: #{@object.class}.#{@method}: writer=#{@has_writer}"}
|
24
|
-
Aspera.assert(@object.respond_to?(@method))
|
24
|
+
Aspera.assert(@object.respond_to?(@method)){"#{object} does not respond to #{method_name}"}
|
25
25
|
end
|
26
26
|
|
27
27
|
def value
|
@@ -48,7 +48,7 @@ module Aspera
|
|
48
48
|
BOOLEAN_SIMPLE = %i[no yes].freeze
|
49
49
|
FALSE_VALUES = [BOOLEAN_SIMPLE.first, false].freeze
|
50
50
|
TRUE_VALUES = [BOOLEAN_SIMPLE.last, true].freeze
|
51
|
-
BOOLEAN_VALUES =
|
51
|
+
BOOLEAN_VALUES = (TRUE_VALUES + FALSE_VALUES).freeze
|
52
52
|
|
53
53
|
# option name separator on command line
|
54
54
|
OPTION_SEP_LINE = '-'
|
@@ -76,9 +76,9 @@ module Aspera
|
|
76
76
|
# find shortened string value in allowed symbol list
|
77
77
|
def get_from_list(short_value, descr, allowed_values)
|
78
78
|
# we accept shortcuts
|
79
|
-
matching_exact = allowed_values.select{|i| i.to_s.eql?(short_value)}
|
79
|
+
matching_exact = allowed_values.select{ |i| i.to_s.eql?(short_value)}
|
80
80
|
return matching_exact.first if matching_exact.length == 1
|
81
|
-
matching = allowed_values.select{|i| i.to_s.start_with?(short_value)}
|
81
|
+
matching = allowed_values.select{ |i| i.to_s.start_with?(short_value)}
|
82
82
|
multi_choice_assert(!matching.empty?, "unknown value for #{descr}: #{short_value}", allowed_values)
|
83
83
|
multi_choice_assert(matching.length.eql?(1), "ambiguous shortcut for #{descr}: #{short_value}", matching)
|
84
84
|
return enum_to_bool(matching.first) if allowed_values.eql?(BOOLEAN_VALUES)
|
@@ -89,7 +89,7 @@ module Aspera
|
|
89
89
|
# @param error_msg [String] error message
|
90
90
|
# @param accept_list [Array] list of allowed values
|
91
91
|
def multi_choice_assert(assertion, error_msg, accept_list)
|
92
|
-
raise Cli::BadArgument, [error_msg, 'Use:'].concat(accept_list.map{|c|"- #{c}"}.sort).join("\n") unless assertion
|
92
|
+
raise Cli::BadArgument, [error_msg, 'Use:'].concat(accept_list.map{ |c| "- #{c}"}.sort).join("\n") unless assertion
|
93
93
|
end
|
94
94
|
|
95
95
|
# change option name with dash to name with underscore
|
@@ -112,8 +112,8 @@ module Aspera
|
|
112
112
|
value_list = check_array ? to_check : [to_check]
|
113
113
|
value_list.each do |value|
|
114
114
|
raise Cli::BadArgument,
|
115
|
-
"#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{type_list.length > 1 ? 'one of ' : ''}#{type_list.map(&:name).join(',')}" unless
|
116
|
-
type_list.any?{|t|value.is_a?(t)}
|
115
|
+
"#{what.to_s.capitalize} #{descr} is a #{value.class} but must be #{type_list.length > 1 ? 'one of: ' : ''}#{type_list.map(&:name).join(', ')}" unless
|
116
|
+
type_list.any?{ |t| value.is_a?(t)}
|
117
117
|
end
|
118
118
|
end
|
119
119
|
end
|
@@ -182,7 +182,7 @@ module Aspera
|
|
182
182
|
|
183
183
|
# @param descr [String] description for help
|
184
184
|
# @param mandatory [Boolean] if true, raise error if option not set
|
185
|
-
# @param multiple [Boolean] if true, return remaining arguments
|
185
|
+
# @param multiple [Boolean] if true, return remaining arguments (Array)
|
186
186
|
# @param accept_list [Array] list of allowed values (Symbol)
|
187
187
|
# @param validation [Class, Array] accepted value type(s) or list of Symbols
|
188
188
|
# @param aliases [Hash] map of aliases: key = alias, value = real value
|
@@ -202,13 +202,13 @@ module Aspera
|
|
202
202
|
if !@unprocessed_cmd_line_arguments.empty?
|
203
203
|
how_many = multiple ? @unprocessed_cmd_line_arguments.length : 1
|
204
204
|
values = @unprocessed_cmd_line_arguments.shift(how_many)
|
205
|
-
values = values.map{|v|evaluate_extended_value(v, allowed_types)}
|
205
|
+
values = values.map{ |v| evaluate_extended_value(v, allowed_types)}
|
206
206
|
# if expecting list and only one arg of type array : it is the list
|
207
|
-
values = values.first if values.length.eql?(1) && values.first.is_a?(Array)
|
207
|
+
values = values.first if multiple && values.length.eql?(1) && values.first.is_a?(Array)
|
208
208
|
if accept_list
|
209
209
|
allowed_values = [].concat(accept_list)
|
210
210
|
allowed_values.concat(aliases.keys) unless aliases.nil?
|
211
|
-
values = values.map{|v|self.class.get_from_list(v, descr, allowed_values)}
|
211
|
+
values = values.map{ |v| self.class.get_from_list(v, descr, allowed_values)}
|
212
212
|
end
|
213
213
|
multiple ? values : values.first
|
214
214
|
elsif !default.nil? then default
|
@@ -309,7 +309,7 @@ module Aspera
|
|
309
309
|
Aspera.assert(!@declared_options.key?(option_symbol)){"#{option_symbol} already declared"}
|
310
310
|
Aspera.assert(description[-1] != '.'){"#{option_symbol} ends with dot"}
|
311
311
|
Aspera.assert(description[0] == description[0].upcase){"#{option_symbol} description does not start with capital"}
|
312
|
-
Aspera.assert(!['hash', 'extended value'].any?{|s|description.downcase.include?(s)
|
312
|
+
Aspera.assert(!['hash', 'extended value'].any?{ |s| description.downcase.include?(s)}){"#{option_symbol} shall use :types"}
|
313
313
|
opt = @declared_options[option_symbol] = {
|
314
314
|
read_write: handler.nil? ? :value : :accessor,
|
315
315
|
# by default passwords and secrets are sensitive, else specify when declaring the option
|
@@ -339,7 +339,7 @@ module Aspera
|
|
339
339
|
on_args.push(symbol_to_option(option_symbol, 'VALUE'))
|
340
340
|
on_args.push("-#{short}VALUE") unless short.nil?
|
341
341
|
on_args.push(coerce) unless coerce.nil?
|
342
|
-
@parser.on(*on_args)
|
342
|
+
@parser.on(*on_args){ |v| set_option(option_symbol, v, where: SOURCE_USER)}
|
343
343
|
when Array, :bool
|
344
344
|
if values.eql?(:bool)
|
345
345
|
values = BOOLEAN_VALUES
|
@@ -348,14 +348,14 @@ module Aspera
|
|
348
348
|
# this option value must be a symbol
|
349
349
|
opt[:values] = values
|
350
350
|
value = get_option(option_symbol)
|
351
|
-
help_values = values.map{|i|i.eql?(value) ? highlight_current(i) : i}.join(', ')
|
351
|
+
help_values = values.map{ |i| i.eql?(value) ? highlight_current(i) : i}.join(', ')
|
352
352
|
if values.eql?(BOOLEAN_VALUES)
|
353
|
-
help_values = BOOLEAN_SIMPLE.map{|i|(i.eql?(:yes) && value) || (i.eql?(:no) && !value) ? highlight_current(i) : i}.join(', ')
|
353
|
+
help_values = BOOLEAN_SIMPLE.map{ |i| (i.eql?(:yes) && value) || (i.eql?(:no) && !value) ? highlight_current(i) : i}.join(', ')
|
354
354
|
end
|
355
355
|
on_args[0] = "#{description}: #{help_values}"
|
356
356
|
on_args.push(symbol_to_option(option_symbol, 'ENUM'))
|
357
357
|
on_args.push(values)
|
358
|
-
@parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), where: SOURCE_USER)}
|
358
|
+
@parser.on(*on_args){ |v| set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), where: SOURCE_USER)}
|
359
359
|
when :date
|
360
360
|
on_args.push(symbol_to_option(option_symbol, 'DATE'))
|
361
361
|
@parser.on(*on_args) do |v|
|
@@ -478,7 +478,7 @@ module Aspera
|
|
478
478
|
def prompt_user_input_in_list(prompt, sym_list)
|
479
479
|
loop do
|
480
480
|
input = prompt_user_input(prompt).to_sym
|
481
|
-
if sym_list.any?{|a|a.eql?(input)}
|
481
|
+
if sym_list.any?{ |a| a.eql?(input)}
|
482
482
|
return input
|
483
483
|
else
|
484
484
|
$stderr.puts("No such #{prompt}: #{input}, select one of: #{sym_list.join(', ')}")
|
@@ -522,7 +522,7 @@ module Aspera
|
|
522
522
|
private
|
523
523
|
|
524
524
|
def evaluate_extended_value(value, types)
|
525
|
-
if DEFAULT_PARSER_TYPES.include?(types) || (types.is_a?(Array) && types.all?{|t|DEFAULT_PARSER_TYPES.include?(t)})
|
525
|
+
if DEFAULT_PARSER_TYPES.include?(types) || (types.is_a?(Array) && types.all?{ |t| DEFAULT_PARSER_TYPES.include?(t)})
|
526
526
|
return ExtendedValue.instance.evaluate_with_default(value)
|
527
527
|
end
|
528
528
|
return ExtendedValue.instance.evaluate(value)
|