aspera-cli 4.14.0 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +300 -185
  5. data/CONTRIBUTING.md +74 -23
  6. data/README.md +2346 -1619
  7. data/bin/ascli +16 -25
  8. data/bin/asession +15 -15
  9. data/examples/dascli +2 -2
  10. data/examples/proxy.pac +1 -1
  11. data/lib/aspera/aoc.rb +216 -150
  12. data/lib/aspera/ascmd.rb +25 -18
  13. data/lib/aspera/assert.rb +45 -0
  14. data/lib/aspera/cli/basic_auth_plugin.rb +9 -6
  15. data/lib/aspera/cli/error.rb +17 -0
  16. data/lib/aspera/cli/extended_value.rb +51 -16
  17. data/lib/aspera/cli/formatter.rb +276 -174
  18. data/lib/aspera/cli/hints.rb +81 -0
  19. data/lib/aspera/cli/main.rb +114 -147
  20. data/lib/aspera/cli/manager.rb +181 -136
  21. data/lib/aspera/cli/plugin.rb +82 -64
  22. data/lib/aspera/cli/plugins/alee.rb +0 -1
  23. data/lib/aspera/cli/plugins/aoc.rb +327 -331
  24. data/lib/aspera/cli/plugins/ats.rb +12 -8
  25. data/lib/aspera/cli/plugins/bss.rb +2 -2
  26. data/lib/aspera/cli/plugins/config.rb +575 -439
  27. data/lib/aspera/cli/plugins/console.rb +40 -0
  28. data/lib/aspera/cli/plugins/cos.rb +4 -5
  29. data/lib/aspera/cli/plugins/faspex.rb +111 -92
  30. data/lib/aspera/cli/plugins/faspex5.rb +245 -182
  31. data/lib/aspera/cli/plugins/node.rb +239 -160
  32. data/lib/aspera/cli/plugins/orchestrator.rb +56 -19
  33. data/lib/aspera/cli/plugins/preview.rb +54 -38
  34. data/lib/aspera/cli/plugins/server.rb +63 -20
  35. data/lib/aspera/cli/plugins/shares.rb +64 -38
  36. data/lib/aspera/cli/sync_actions.rb +68 -0
  37. data/lib/aspera/cli/transfer_agent.rb +64 -67
  38. data/lib/aspera/cli/transfer_progress.rb +73 -0
  39. data/lib/aspera/cli/version.rb +1 -1
  40. data/lib/aspera/colors.rb +3 -1
  41. data/lib/aspera/command_line_builder.rb +27 -22
  42. data/lib/aspera/cos_node.rb +6 -4
  43. data/lib/aspera/coverage.rb +22 -0
  44. data/lib/aspera/data_repository.rb +33 -2
  45. data/lib/aspera/environment.rb +21 -8
  46. data/lib/aspera/fasp/agent_alpha.rb +116 -0
  47. data/lib/aspera/fasp/agent_base.rb +40 -76
  48. data/lib/aspera/fasp/agent_connect.rb +21 -22
  49. data/lib/aspera/fasp/agent_direct.rb +169 -179
  50. data/lib/aspera/fasp/agent_httpgw.rb +200 -195
  51. data/lib/aspera/fasp/agent_node.rb +43 -35
  52. data/lib/aspera/fasp/agent_trsdk.rb +124 -41
  53. data/lib/aspera/fasp/error_info.rb +2 -2
  54. data/lib/aspera/fasp/faux_file.rb +52 -0
  55. data/lib/aspera/fasp/installation.rb +89 -191
  56. data/lib/aspera/fasp/management.rb +249 -0
  57. data/lib/aspera/fasp/parameters.rb +86 -47
  58. data/lib/aspera/fasp/parameters.yaml +75 -8
  59. data/lib/aspera/fasp/products.rb +162 -0
  60. data/lib/aspera/fasp/resume_policy.rb +7 -5
  61. data/lib/aspera/fasp/sync.rb +273 -0
  62. data/lib/aspera/fasp/transfer_spec.rb +10 -8
  63. data/lib/aspera/fasp/uri.rb +6 -6
  64. data/lib/aspera/faspex_gw.rb +11 -8
  65. data/lib/aspera/faspex_postproc.rb +8 -7
  66. data/lib/aspera/hash_ext.rb +2 -2
  67. data/lib/aspera/id_generator.rb +3 -1
  68. data/lib/aspera/json_rpc.rb +51 -0
  69. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  70. data/lib/aspera/keychain/macos_security.rb +15 -13
  71. data/lib/aspera/line_logger.rb +23 -0
  72. data/lib/aspera/log.rb +61 -19
  73. data/lib/aspera/nagios.rb +7 -2
  74. data/lib/aspera/node.rb +105 -21
  75. data/lib/aspera/node_simulator.rb +214 -0
  76. data/lib/aspera/oauth.rb +57 -36
  77. data/lib/aspera/open_application.rb +4 -4
  78. data/lib/aspera/persistency_action_once.rb +13 -14
  79. data/lib/aspera/persistency_folder.rb +5 -4
  80. data/lib/aspera/preview/file_types.rb +56 -268
  81. data/lib/aspera/preview/generator.rb +28 -39
  82. data/lib/aspera/preview/options.rb +2 -0
  83. data/lib/aspera/preview/terminal.rb +36 -16
  84. data/lib/aspera/preview/utils.rb +23 -29
  85. data/lib/aspera/proxy_auto_config.rb +6 -3
  86. data/lib/aspera/rest.rb +127 -80
  87. data/lib/aspera/rest_call_error.rb +1 -1
  88. data/lib/aspera/rest_error_analyzer.rb +16 -14
  89. data/lib/aspera/rest_errors_aspera.rb +39 -34
  90. data/lib/aspera/secret_hider.rb +18 -17
  91. data/lib/aspera/ssh.rb +10 -5
  92. data/lib/aspera/temp_file_manager.rb +11 -4
  93. data/lib/aspera/web_auth.rb +10 -7
  94. data/lib/aspera/web_server_simple.rb +11 -5
  95. data.tar.gz.sig +0 -0
  96. metadata +108 -39
  97. metadata.gz.sig +0 -0
  98. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  99. data/lib/aspera/cli/listener/logger.rb +0 -22
  100. data/lib/aspera/cli/listener/progress.rb +0 -50
  101. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  102. data/lib/aspera/cli/plugins/sync.rb +0 -44
  103. data/lib/aspera/fasp/listener.rb +0 -13
  104. data/lib/aspera/sync.rb +0 -213
@@ -1,95 +1,193 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:ignore jsonpp
3
4
  require 'aspera/secret_hider'
5
+ require 'aspera/environment'
6
+ require 'aspera/log'
7
+ require 'aspera/assert'
4
8
  require 'terminal-table'
5
9
  require 'yaml'
6
10
  require 'pp'
7
11
 
8
12
  module Aspera
9
13
  module Cli
14
+ CONF_OVERVIEW_KEYS = %w[preset parameter value].freeze
15
+ # This class is used to transform a complex structure into a simple hash
16
+ class Flattener
17
+ def initialize
18
+ @result = nil
19
+ end
20
+
21
+ # General method
22
+ def flatten(something)
23
+ assert_type(something, Hash)
24
+ @result = {}
25
+ flatten_any(something, '')
26
+ return @result
27
+ end
28
+
29
+ # Special method for configuration overview
30
+ def config_over(something)
31
+ @result = []
32
+ something.each do |config, preset|
33
+ preset.each do |parameter, value|
34
+ @result.push(CONF_OVERVIEW_KEYS.zip([config, parameter, value]).to_h)
35
+ end
36
+ end
37
+ return @result
38
+ end
39
+
40
+ private
41
+
42
+ # Recursive function to flatten any type
43
+ # @param something [Object] to be flattened
44
+ # @param name [String] name of englobing key
45
+ def flatten_any(something, name)
46
+ if something.is_a?(Hash)
47
+ flattened_hash(something, name)
48
+ elsif something.is_a?(Array)
49
+ flatten_array(something, name)
50
+ elsif something.is_a?(String) && something.empty?
51
+ @result[name] = Formatter.special('empty string')
52
+ elsif something.nil?
53
+ @result[name] = Formatter.special('null')
54
+ # elsif something.eql?(true) || something.eql?(false)
55
+ # @result[name] = something
56
+ else
57
+ @result[name] = something
58
+ end
59
+ end
60
+
61
+ # Recursive function to flatten an array
62
+ # @param array [Array] to be flattened
63
+ # @param name [String] name of englobing key
64
+ def flatten_array(array, name)
65
+ if array.empty?
66
+ @result[name] = Formatter.special('empty list')
67
+ elsif array.all?(String)
68
+ @result[name] = array.join("\n")
69
+ elsif array.all?{|i| i.is_a?(Hash) && i.keys.eql?(%w[name])}
70
+ @result[name] = array.map(&:values).join(', ')
71
+ elsif array.all?{|i| i.is_a?(Hash) && i.keys.sort.eql?(%w[name value])}
72
+ flattened_hash(array.each_with_object({}){|i, h|h[i['name']] = i['value']}, name)
73
+ else
74
+ array.each_with_index { |item, index| flatten_any(item, "#{name}.#{index}")}
75
+ end
76
+ nil
77
+ end
78
+
79
+ # Recursive function to flatten a Hash
80
+ # @param hash [Hash] to be flattened
81
+ # @param name [String] name of englobing key
82
+ def flattened_hash(hash, name)
83
+ prefix = name.empty? ? '' : "#{name}."
84
+ hash.each do |k, v|
85
+ flatten_any(v, "#{prefix}#{k}")
86
+ end
87
+ end
88
+ end # class
89
+
10
90
  # Take care of output
11
91
  class Formatter
12
- FIELDS_ALL = 'ALL'
13
- FIELDS_DEFAULT = 'DEF'
92
+ FIELDS_LESS = '-'
14
93
  CSV_RECORD_SEPARATOR = "\n"
15
94
  CSV_FIELD_SEPARATOR = ','
16
95
  # supported output formats
17
96
  DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv].freeze
18
97
  # user output levels
19
98
  DISPLAY_LEVELS = %i[info data error].freeze
20
- CONF_OVERVIEW_KEYS = %w[config parameter value].freeze
21
- KEY_VALUE = %w[key value].freeze
99
+ RESULT_PARAMS = %i[type data total fields name].freeze
22
100
 
23
- private_constant :FIELDS_ALL, :FIELDS_DEFAULT, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR,
24
- :CONF_OVERVIEW_KEYS, :KEY_VALUE
101
+ private_constant :DISPLAY_FORMATS, :DISPLAY_LEVELS, :CSV_RECORD_SEPARATOR, :CSV_FIELD_SEPARATOR
102
+ # prefix to display error messages in user messages (terminal)
103
+ ERROR_FLASH = 'ERROR:'.bg_red.gray.blink.freeze
104
+ WARNING_FLASH = 'WARNING:'.bg_brown.black.blink.freeze
105
+ HINT_FLASH = 'HINT:'.bg_green.gray.blink.freeze
25
106
 
26
107
  class << self
27
- # special for Aspera on Cloud display node
28
- # {"param" => [{"name"=>"foo","value"=>"bar"}]} will be expanded to {"param.foo" : "bar"}
29
- def flatten_name_value_list(hash)
30
- hash.keys.each do |k| # rubocop:disable Style/HashEachMethods
31
- v = hash[k]
32
- next unless v.is_a?(Array) && v.map(&:class).uniq.eql?([Hash]) && v.map(&:keys).flatten.sort.uniq.eql?(%w[name value])
33
- v.each do |pair|
34
- hash["#{k}.#{pair['name']}"] = pair['value']
108
+ # Highlight special values
109
+ def special(what, use_colors: $stdout.isatty)
110
+ result = "<#{what}>"
111
+ if use_colors
112
+ result = if %w[null empty].any?{|s|what.include?(s)}
113
+ result.dim
114
+ else
115
+ result.reverse_color
35
116
  end
36
- hash.delete(k)
37
117
  end
118
+ return result
38
119
  end
39
120
 
40
- def flatten_config_overview(hash_array_conf)
41
- r = []
42
- hash_array_conf.each do |config, preset|
43
- preset.each do |parameter, value|
44
- r.push(CONF_OVERVIEW_KEYS.zip([config, parameter, SecretHider.deep_remove_secret(value).to_s]).to_h)
45
- end
46
- end
47
- return r
121
+ def all_but(list)
122
+ list = [list] unless list.is_a?(Array)
123
+ return list.map{|i|"#{FIELDS_LESS}#{i}"}.unshift(ExtendedValue::ALL)
48
124
  end
49
125
 
50
- def simple_hash?(h)
51
- !(h.values.any?{|v|[Hash, Array].any?{|c|v.is_a?(c)}})
126
+ def tick(yes)
127
+ result =
128
+ if Environment.use_unicode?
129
+ if yes
130
+ "\u2713"
131
+ else
132
+ "\u2717"
133
+ end
134
+ elsif yes
135
+ 'Y'
136
+ else
137
+ ' '
138
+ end
139
+ return result.green if yes
140
+ return result.red
52
141
  end
53
142
 
54
- # recursive function to modify a hash
55
- # @param source [Hash] to be modified
56
- # @param expand_last [TrueClass,FalseClass] true if last level is not
57
- # @param result [Hash] new hash flattened
58
- # @param prefix [String] true if last level is not
59
- def flattened_object(source, result: {}, prefix: '', expand_last: false)
60
- Log.log.debug{"(#{expand_last})[#{simple_hash?(source)}] -#{source.values}- \n-#{source}-"}
61
- source.each do |k, v|
62
- if v.is_a?(Hash) && !(expand_last && simple_hash?(v))
63
- flattened_object(v, result: result, prefix: prefix + k.to_s + '.', expand_last: expand_last)
64
- else
65
- result[prefix + k.to_s] = v
143
+ def auto_type(data)
144
+ result = {type: :other_struct, data: data}
145
+ result[:type] = :single_object if result[:data].is_a?(Hash)
146
+ if result[:data].is_a?(Array)
147
+ if result[:data].all?(Hash)
148
+ result[:type] = :object_list
66
149
  end
67
150
  end
68
151
  return result
69
152
  end
153
+ end # self
154
+
155
+ # initialize the formatter
156
+ def initialize
157
+ @options = {}
70
158
  end
71
159
 
72
- attr_accessor :option_flat_hash, :option_transpose_single, :option_format, :option_display, :option_fields, :option_table_style,
73
- :option_select, :option_show_secrets
160
+ def option_handler(option_symbol, operation, value=nil)
161
+ assert_values(operation, %i[set get])
162
+ case operation
163
+ when :set
164
+ @options[option_symbol] = value
165
+ if option_symbol.eql?(:output)
166
+ $stdout = if value.eql?('-')
167
+ STDOUT # rubocop:disable Style/GlobalStdStream
168
+ else
169
+ File.open(value, 'w')
170
+ end
171
+ end
172
+ when :get then return @options[option_symbol]
173
+ else error_unreachable_line
174
+ end
175
+ nil
176
+ end
74
177
 
75
- # adds options but does not parse
76
- def initialize(opt_mgr)
77
- @option_format = :table
78
- @option_display = :info
79
- @option_fields = FIELDS_DEFAULT
80
- @option_select = nil
81
- @option_table_style = ':.:'
82
- @option_flat_hash = true
83
- @option_transpose_single = true
84
- @option_show_secrets = false
85
- opt_mgr.declare(:format, 'Output format', values: DISPLAY_FORMATS, handler: {o: self, m: :option_format})
86
- opt_mgr.declare(:display, 'Output only some information', values: DISPLAY_LEVELS, handler: {o: self, m: :option_display})
87
- opt_mgr.declare(:fields, "Comma separated list of fields, or #{FIELDS_ALL}, or #{FIELDS_DEFAULT}", handler: {o: self, m: :option_fields})
88
- opt_mgr.declare(:select, 'Select only some items in lists: column, value', types: Hash, handler: {o: self, m: :option_select})
89
- opt_mgr.declare(:table_style, 'Table display style', handler: {o: self, m: :option_table_style})
90
- opt_mgr.declare(:flat_hash, 'Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_flat_hash})
91
- opt_mgr.declare(:transpose_single, 'Single object fields output vertically', values: :bool, handler: {o: self, m: :option_transpose_single})
92
- opt_mgr.declare(:show_secrets, 'Show secrets on command output', values: :bool, handler: {o: self, m: :option_show_secrets})
178
+ def declare_options(options)
179
+ options.declare(:format, 'Output format', values: DISPLAY_FORMATS, handler: {o: self, m: :option_handler}, default: :table)
180
+ options.declare(:output, 'Destination for results', types: String, handler: {o: self, m: :option_handler})
181
+ options.declare(:display, 'Output only some information', values: DISPLAY_LEVELS, handler: {o: self, m: :option_handler}, default: :info)
182
+ options.declare(
183
+ :fields, "Comma separated list of: fields, or #{ExtendedValue::ALL}, or #{ExtendedValue::DEF}", handler: {o: self, m: :option_handler},
184
+ types: [String, Array, Regexp, Proc],
185
+ default: ExtendedValue::DEF)
186
+ options.declare(:select, 'Select only some items in lists: column, value', types: [Hash, Proc], handler: {o: self, m: :option_handler})
187
+ options.declare(:table_style, 'Table display style', handler: {o: self, m: :option_handler}, default: ':.:')
188
+ options.declare(:flat_hash, 'Display deep values as additional keys', values: :bool, handler: {o: self, m: :option_handler}, default: true)
189
+ options.declare(:transpose_single, 'Single object fields output vertically', values: :bool, handler: {o: self, m: :option_handler}, default: true)
190
+ options.declare(:show_secrets, 'Show secrets on command output', values: :bool, handler: {o: self, m: :option_handler}, default: false)
93
191
  end
94
192
 
95
193
  # main output method
@@ -98,10 +196,10 @@ module Aspera
98
196
  # error: always displayed on stderr
99
197
  def display_message(message_level, message)
100
198
  case message_level
101
- when :data then $stdout.puts(message) unless @option_display.eql?(:error)
102
- when :info then $stdout.puts(message) if @option_display.eql?(:info)
199
+ when :data then $stdout.puts(message) unless @options[:display].eql?(:error)
200
+ when :info then $stdout.puts(message) if @options[:display].eql?(:info)
103
201
  when :error then $stderr.puts(message)
104
- else raise "wrong message_level:#{message_level}"
202
+ else error_unexpected_value(message_level)
105
203
  end
106
204
  end
107
205
 
@@ -110,155 +208,159 @@ module Aspera
110
208
  end
111
209
 
112
210
  def display_item_count(number, total)
211
+ number = number.to_i
212
+ total = total.to_i
213
+ return if total.eql?(0) && number.eql?(0)
113
214
  count_msg = "Items: #{number}/#{total}"
114
- count_msg = count_msg.bg_red unless number.to_i.eql?(total.to_i)
215
+ count_msg = count_msg.bg_red unless number.eql?(total)
115
216
  display_status(count_msg)
116
217
  end
117
218
 
118
- def result_default_fields(results, table_rows_hash_val)
119
- unless results[:fields].nil?
120
- raise "internal error: [fields] must be Array, not #{results[:fields].class}" unless results[:fields].is_a?(Array)
121
- if results[:fields].first.eql?(:all_but) && !table_rows_hash_val.empty?
122
- filter = results[:fields][1..-1]
123
- return table_rows_hash_val.first.keys.reject{|i|filter.include?(i)}
219
+ def all_fields(data)
220
+ data.each_with_object({}){|v, m|v.each_key{|c|m[c] = true}}.keys
221
+ end
222
+
223
+ # this method computes the list of fields to display
224
+ # data: array of hash
225
+ # default: list of fields to display by default (may contain special values)
226
+ def compute_fields(data, default)
227
+ Log.log.debug{"compute_fields: data:#{data.class} default:#{default.class} #{default}"}
228
+ request =
229
+ case @options[:fields]
230
+ when NilClass then [ExtendedValue::DEF]
231
+ when String then @options[:fields].split(',')
232
+ when Array then @options[:fields]
233
+ when Regexp then return all_fields(data).select{|i|i.match(@options[:fields])}
234
+ when Proc then return all_fields(data).select{|i|@options[:fields].call(i)}
235
+ else error_unexpected_value(@options[:fields])
236
+ end
237
+ result = []
238
+ until request.empty?
239
+ item = request.shift
240
+ removal = false
241
+ if item[0].eql?(FIELDS_LESS)
242
+ removal = true
243
+ item = item[1..-1]
244
+ end
245
+ case item
246
+ when ExtendedValue::ALL
247
+ # get the list of all column names used in all lines, not just first one, as all lines may have different columns
248
+ request.unshift(*all_fields(data))
249
+ when ExtendedValue::DEF
250
+ default = all_fields(data).select{|i|default.call(i)} if default.is_a?(Proc)
251
+ default = all_fields(data) if default.nil?
252
+ request.unshift(*default)
253
+ else
254
+ if removal
255
+ result = result.reject{|i|i.eql?(item)}
256
+ else
257
+ result.push(item)
258
+ end
124
259
  end
125
- return results[:fields]
126
260
  end
127
- return ['empty'] if table_rows_hash_val.empty?
128
- return table_rows_hash_val.first.keys
261
+ return result
129
262
  end
130
263
 
131
- def result_all_fields(_results, table_rows_hash_val)
132
- raise 'internal error: must be array' unless table_rows_hash_val.is_a?(Array)
133
- # get the list of all column names used in all lines, not just first one, as all lines may have different columns
134
- return table_rows_hash_val.each_with_object({}){|v, m|v.each_key{|c|m[c] = true}; }.keys
264
+ # this method displays a table
265
+ # object_array: array of hash
266
+ # fields: list of column names
267
+ def display_table(object_array, fields)
268
+ assert(!fields.nil?){'missing fields parameter'}
269
+ case @options[:select]
270
+ when Proc
271
+ object_array.select!{|i|@options[:select].call(i)}
272
+ when Hash
273
+ @options[:select].each{|k, v|object_array.select!{|i|i[k].eql?(v)}}
274
+ end
275
+ if object_array.empty?
276
+ # no display for csv
277
+ display_message(:info, Formatter.special('empty')) if @options[:format].eql?(:table)
278
+ return
279
+ end
280
+ if object_array.length == 1 && fields.length == 1
281
+ display_message(:data, object_array.first[fields.first])
282
+ return
283
+ end
284
+ # Special case if only one row (it could be object_list or single_object)
285
+ if @options[:transpose_single] && object_array.length == 1
286
+ new_columns = %i[key value]
287
+ single = object_array.first
288
+ object_array = fields.map { |i| new_columns.zip([i, single[i]]).to_h }
289
+ fields = new_columns
290
+ end
291
+ Log.log.debug{Log.dump(:object_array, object_array)}
292
+ # convert data to string, and keep only display fields
293
+ final_table_rows = object_array.map { |r| fields.map { |c| r[c].to_s } }
294
+ # here : fields : list of column names
295
+ case @options[:format]
296
+ when :table
297
+ style = @options[:table_style].chars
298
+ # display the table !
299
+ display_message(:data, Terminal::Table.new(
300
+ headings: fields,
301
+ rows: final_table_rows,
302
+ border_x: style[0],
303
+ border_y: style[1],
304
+ border_i: style[2]))
305
+ when :csv
306
+ display_message(:data, final_table_rows.map{|t| t.join(CSV_FIELD_SEPARATOR)}.join(CSV_RECORD_SEPARATOR))
307
+ end
135
308
  end
136
309
 
137
310
  # this method displays the results, especially the table format
138
311
  def display_results(results)
139
- raise "INTERNAL ERROR, result must be Hash (got: #{results.class}: #{results})" unless results.is_a?(Hash)
140
- raise 'INTERNAL ERROR, result must have type' unless results.key?(:type)
141
- raise 'INTERNAL ERROR, result must have data' unless results.key?(:data) || %i[empty nothing].include?(results[:type])
142
- res_data = results[:data]
143
- # for config overview, it is name and value
144
- is_config_overview = res_data.is_a?(Array) && !res_data.empty? && res_data.first.is_a?(Hash) && res_data.first.keys.sort.eql?(CONF_OVERVIEW_KEYS)
145
- SecretHider.deep_remove_secret(res_data, is_name_value: is_config_overview) unless @option_show_secrets || @option_display.eql?(:data)
146
- # comma separated list in string format
147
- user_asked_fields_list_str = @option_fields
148
- case @option_format
312
+ assert_type(results, Hash)
313
+ assert((results.keys - RESULT_PARAMS).empty?){"result unsupported key: #{results.keys - RESULT_PARAMS}"}
314
+ assert(results.key?(:type)){"result must have type (#{results})"}
315
+ assert(results.key?(:data) || %i[empty nothing].include?(results[:type])){'result must have data'}
316
+ Log.log.debug{"display_results: #{results[:data].class} #{results[:type]}"}
317
+ display_item_count(results[:data].length, results[:total]) if results.key?(:total)
318
+ SecretHider.deep_remove_secret(results[:data]) unless @options[:show_secrets] || @options[:display].eql?(:data)
319
+ case @options[:format]
149
320
  when :text
150
- display_message(:data, res_data.to_s)
321
+ display_message(:data, results[:data].to_s)
151
322
  when :nagios
152
- Nagios.process(res_data)
323
+ Nagios.process(results[:data])
153
324
  when :ruby
154
- display_message(:data, PP.pp(res_data, +''))
325
+ display_message(:data, PP.pp(results[:data], +''))
155
326
  when :json
156
- display_message(:data, JSON.generate(res_data))
327
+ display_message(:data, JSON.generate(results[:data]))
157
328
  when :jsonpp
158
- display_message(:data, JSON.pretty_generate(res_data))
329
+ display_message(:data, JSON.pretty_generate(results[:data]))
159
330
  when :yaml
160
- display_message(:data, res_data.to_yaml)
331
+ display_message(:data, results[:data].to_yaml)
161
332
  when :table, :csv
162
- if !@option_transpose_single && results[:type].eql?(:single_object)
163
- results[:type] = :object_list
164
- res_data = [res_data]
165
- end
166
333
  case results[:type]
167
- when :object_list # goes to table display
168
- raise "internal error: unexpected type: #{res_data.class}, expecting Array" unless res_data.is_a?(Array)
334
+ when :config_over
335
+ display_table(Flattener.new.config_over(results[:data]), CONF_OVERVIEW_KEYS)
336
+ when :object_list, :single_object
337
+ obj_list = results[:data]
338
+ obj_list = [obj_list] if results[:type].eql?(:single_object)
339
+ assert_type(obj_list, Array)
340
+ assert(obj_list.all?(Hash)){"expecting Array of Hash: #{obj_list.inspect}"}
169
341
  # :object_list is an array of hash tables, where key=colum name
170
- table_rows_hash_val = res_data
171
- final_table_columns = nil
172
- if @option_flat_hash
173
- table_rows_hash_val.map!{|obj|self.class.flattened_object(obj, expand_last: results[:option_expand_last])}
174
- end
175
- final_table_columns =
176
- case user_asked_fields_list_str
177
- when FIELDS_DEFAULT then result_default_fields(results, table_rows_hash_val)
178
- when FIELDS_ALL then result_all_fields(results, table_rows_hash_val)
179
- else
180
- if user_asked_fields_list_str.start_with?('+')
181
- result_default_fields(results, table_rows_hash_val).push(*user_asked_fields_list_str.gsub(/^\+/, '').split(','))
182
- elsif user_asked_fields_list_str.start_with?('-')
183
- result_default_fields(results, table_rows_hash_val).reject{|i| user_asked_fields_list_str.gsub(/^-/, '').split(',').include?(i)}
184
- else
185
- user_asked_fields_list_str.split(',')
186
- end
187
- end
188
- when :single_object # goes to table display
189
- # :single_object is a simple hash table (can be nested)
190
- raise "internal error: expecting Hash: got #{res_data.class}: #{res_data}" unless res_data.is_a?(Hash)
191
- final_table_columns = results[:columns] || KEY_VALUE
192
- if @option_flat_hash
193
- res_data = self.class.flattened_object(res_data, expand_last: results[:option_expand_last])
194
- self.class.flatten_name_value_list(res_data)
195
- end
196
- asked_fields =
197
- case user_asked_fields_list_str
198
- when FIELDS_DEFAULT then results[:fields] || res_data.keys
199
- when FIELDS_ALL then res_data.keys
200
- else user_asked_fields_list_str.split(',')
201
- end
202
- table_rows_hash_val = asked_fields.map { |i| { final_table_columns.first => i, final_table_columns.last => res_data[i] } }
203
- # if only one row, and columns are key/value, then display the value only
204
- if table_rows_hash_val.length == 1 && final_table_columns.eql?(KEY_VALUE)
205
- display_message(:data, res_data.values.first)
206
- return
207
- end
208
- when :value_list # goes to table display
342
+ obj_list = obj_list.map{|obj|Flattener.new.flatten(obj)} if @options[:flat_hash]
343
+ display_table(obj_list, compute_fields(obj_list, results[:fields]))
344
+ when :value_list
209
345
  # :value_list is a simple array of values, name of column provided in the :name
210
- final_table_columns = [results[:name]]
211
- table_rows_hash_val = res_data.map { |i| { results[:name] => i } }
346
+ display_table(results[:data].map { |i| { results[:name] => i } }, [results[:name]])
212
347
  when :empty # no table
213
- display_message(:info, 'empty')
348
+ display_message(:info, Formatter.special('empty'))
214
349
  return
215
350
  when :nothing # no result expected
216
351
  Log.log.debug('no result expected')
217
- return
218
352
  when :status # no table
219
353
  # :status displays a simple message
220
- display_message(:info, res_data)
221
- return
354
+ display_message(:info, results[:data])
222
355
  when :text # no table
223
356
  # :status displays a simple message
224
- display_message(:data, res_data)
225
- return
357
+ display_message(:data, results[:data])
226
358
  when :other_struct # no table
227
359
  # :other_struct is any other type of structure
228
- display_message(:data, PP.pp(res_data, +''))
229
- return
360
+ display_message(:data, PP.pp(results[:data], +''))
230
361
  else
231
362
  raise "unknown data type: #{results[:type]}"
232
363
  end
233
- # here we expect: table_rows_hash_val and final_table_columns
234
- raise 'no field specified' if final_table_columns.nil?
235
- if table_rows_hash_val.empty?
236
- display_message(:info, 'empty'.gray) if @option_format.eql?(:table)
237
- return
238
- end
239
- # convert to string with special function. here table_rows_hash_val is an array of hash
240
- table_rows_hash_val = results[:textify].call(table_rows_hash_val) if results.key?(:textify)
241
- unless @option_select.nil? || (@option_select.respond_to?(:empty?) && @option_select.empty?)
242
- raise CliBadArgument, "expecting hash for select, have #{@option_select.class}: #{@option_select}" unless @option_select.is_a?(Hash)
243
- @option_select.each{|k, v|table_rows_hash_val.select!{|i|i[k].eql?(v)}}
244
- end
245
- # convert data to string, and keep only display fields
246
- final_table_rows = table_rows_hash_val.map { |r| final_table_columns.map { |c| r[c].to_s } }
247
- # here : final_table_columns : list of column names
248
- # here: final_table_rows : array of list of value
249
- case @option_format
250
- when :table
251
- style = @option_table_style.chars
252
- # display the table !
253
- display_message(:data, Terminal::Table.new(
254
- headings: final_table_columns,
255
- rows: final_table_rows,
256
- border_x: style[0],
257
- border_y: style[1],
258
- border_i: style[2]))
259
- when :csv
260
- display_message(:data, final_table_rows.map{|t| t.join(CSV_FIELD_SEPARATOR)}.join(CSV_RECORD_SEPARATOR))
261
- end
262
364
  end
263
365
  end
264
366
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/fasp/error'
4
+ require 'aspera/rest'
5
+ require 'aspera/log'
6
+ require 'aspera/assert'
7
+ require 'net/ssh'
8
+ require 'openssl'
9
+
10
+ module Aspera
11
+ module Cli
12
+ # Provide hints on errors
13
+ class Hints
14
+ # Well know issues that users may get
15
+ ERROR_HINTS = [
16
+ {
17
+ exception: Fasp::Error,
18
+ match: 'Remote host is not who we expected',
19
+ remediation: [
20
+ 'For this specific error, refer to:',
21
+ "#{SRC_URL}#error-remote-host-is-not-who-we-expected",
22
+ 'Add this to arguments:',
23
+ %q{--ts=@json:'{"sshfp":null}'"}
24
+ ]
25
+ },
26
+ {
27
+ exception: Aspera::RestCallError,
28
+ match: /Signature has expired/,
29
+ remediation: [
30
+ 'There is too much time difference between your computer and the server',
31
+ 'Check your local time: is time synchronization enabled?'
32
+ ]
33
+ },
34
+ {
35
+ exception: OpenSSL::SSL::SSLError,
36
+ match: /(does not match the server certificate|certificate verify failed)/,
37
+ remediation: [
38
+ 'You can ignore SSL errors with option:',
39
+ '--insecure=yes'
40
+ ]
41
+ },
42
+ {
43
+ exception: OpenSSL::PKey::RSAError,
44
+ match: /Neither PUB key nor PRIV key/,
45
+ remediation: [
46
+ 'option: private_key expects a key PEM value, not path to file',
47
+ 'if you provide a path: prefix with @file:',
48
+ 'e.g. --private-key=@file:/path/to/key.pem'
49
+ ]
50
+ }
51
+ ]
52
+
53
+ private_constant :ERROR_HINTS
54
+
55
+ class << self
56
+ def hint_for(error, formatter)
57
+ ERROR_HINTS.each do |hint|
58
+ next unless error.is_a?(hint[:exception])
59
+ message = error.message
60
+ matches = hint[:match]
61
+ matches = [matches] unless matches.is_a?(Array)
62
+ matches.each do |m|
63
+ assert_values(m.class, [String, Regexp])
64
+ case m
65
+ when String
66
+ next unless message.eql?(m)
67
+ when Regexp
68
+ next unless message.match?(m)
69
+ else error_unexpected_value(m)
70
+ end
71
+ remediation = hint[:remediation]
72
+ remediation = [remediation] unless remediation.is_a?(Array)
73
+ remediation.each{|r|formatter.display_message(:error, "#{Formatter::HINT_FLASH} #{r}")}
74
+ break
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end