aspera-cli 4.14.0 → 4.16.0

Sign up to get free protection for your applications and to get access to all the features.
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