aspera-cli 4.13.0 → 4.14.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 (64) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +28 -5
  4. data/CONTRIBUTING.md +17 -1
  5. data/README.md +782 -401
  6. data/examples/dascli +1 -1
  7. data/examples/rubyc +24 -0
  8. data/lib/aspera/aoc.rb +21 -32
  9. data/lib/aspera/ascmd.rb +1 -0
  10. data/lib/aspera/cli/basic_auth_plugin.rb +6 -6
  11. data/lib/aspera/cli/formatter.rb +17 -25
  12. data/lib/aspera/cli/main.rb +21 -27
  13. data/lib/aspera/cli/manager.rb +128 -114
  14. data/lib/aspera/cli/plugin.rb +87 -38
  15. data/lib/aspera/cli/plugins/alee.rb +2 -2
  16. data/lib/aspera/cli/plugins/aoc.rb +216 -102
  17. data/lib/aspera/cli/plugins/ats.rb +16 -18
  18. data/lib/aspera/cli/plugins/bss.rb +3 -3
  19. data/lib/aspera/cli/plugins/config.rb +177 -367
  20. data/lib/aspera/cli/plugins/console.rb +4 -6
  21. data/lib/aspera/cli/plugins/cos.rb +12 -13
  22. data/lib/aspera/cli/plugins/faspex.rb +17 -18
  23. data/lib/aspera/cli/plugins/faspex5.rb +332 -216
  24. data/lib/aspera/cli/plugins/node.rb +171 -142
  25. data/lib/aspera/cli/plugins/orchestrator.rb +15 -18
  26. data/lib/aspera/cli/plugins/preview.rb +38 -60
  27. data/lib/aspera/cli/plugins/server.rb +22 -15
  28. data/lib/aspera/cli/plugins/shares.rb +24 -33
  29. data/lib/aspera/cli/plugins/sync.rb +3 -3
  30. data/lib/aspera/cli/transfer_agent.rb +29 -26
  31. data/lib/aspera/cli/version.rb +1 -1
  32. data/lib/aspera/colors.rb +9 -7
  33. data/lib/aspera/data/6 +0 -0
  34. data/lib/aspera/environment.rb +7 -3
  35. data/lib/aspera/fasp/agent_connect.rb +5 -0
  36. data/lib/aspera/fasp/agent_direct.rb +5 -5
  37. data/lib/aspera/fasp/agent_httpgw.rb +138 -60
  38. data/lib/aspera/fasp/agent_trsdk.rb +2 -0
  39. data/lib/aspera/fasp/error_info.rb +2 -0
  40. data/lib/aspera/fasp/installation.rb +18 -19
  41. data/lib/aspera/fasp/parameters.rb +18 -17
  42. data/lib/aspera/fasp/parameters.yaml +2 -1
  43. data/lib/aspera/fasp/resume_policy.rb +3 -3
  44. data/lib/aspera/fasp/transfer_spec.rb +6 -5
  45. data/lib/aspera/fasp/uri.rb +23 -21
  46. data/lib/aspera/faspex_postproc.rb +1 -1
  47. data/lib/aspera/hash_ext.rb +12 -2
  48. data/lib/aspera/keychain/macos_security.rb +13 -13
  49. data/lib/aspera/log.rb +1 -0
  50. data/lib/aspera/node.rb +62 -80
  51. data/lib/aspera/oauth.rb +1 -1
  52. data/lib/aspera/persistency_action_once.rb +1 -1
  53. data/lib/aspera/preview/terminal.rb +61 -15
  54. data/lib/aspera/preview/utils.rb +3 -3
  55. data/lib/aspera/proxy_auto_config.js +2 -2
  56. data/lib/aspera/rest.rb +37 -0
  57. data/lib/aspera/secret_hider.rb +6 -1
  58. data/lib/aspera/ssh.rb +1 -1
  59. data/lib/aspera/sync.rb +2 -0
  60. data.tar.gz.sig +0 -0
  61. metadata +3 -4
  62. metadata.gz.sig +0 -0
  63. data/docs/test_env.conf +0 -186
  64. data/lib/aspera/data/7 +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'aspera/colors'
4
4
  require 'aspera/log'
5
+ require 'aspera/secret_hider'
5
6
  require 'aspera/cli/extended_value'
6
7
  require 'optparse'
7
8
  require 'io/console'
@@ -103,7 +104,7 @@ module Aspera
103
104
  @unprocessed_cmd_line_options = []
104
105
  # a copy of all initial options
105
106
  @initial_cli_options = []
106
- # option description: key = option symbol, value=hash, :type, :accessor, :value, :accepted
107
+ # option description: key = option symbol, value=hash, :read_write, :accessor, :value, :accepted
107
108
  @declared_options = {}
108
109
  # do we ask missing options and arguments to user ?
109
110
  @ask_missing_mandatory = false # STDIN.isatty
@@ -130,10 +131,8 @@ module Aspera
130
131
  unless argv.nil?
131
132
  @parser.separator('')
132
133
  @parser.separator('OPTIONS: global')
133
- set_obj_attr(:interactive, self, :ask_missing_mandatory)
134
- set_obj_attr(:ask_options, self, :ask_missing_optional)
135
- add_opt_boolean(:interactive, 'use interactive input of missing params')
136
- add_opt_boolean(:ask_options, 'ask even optional options')
134
+ declare(:interactive, 'Use interactive input of missing params', values: :bool, handler: {o: self, m: :ask_missing_mandatory})
135
+ declare(:ask_options, 'Ask even optional options', values: :bool, handler: {o: self, m: :ask_missing_optional})
137
136
  parse_options!
138
137
  process_options = true
139
138
  until argv.empty?
@@ -153,21 +152,20 @@ module Aspera
153
152
  Log.log.debug{"add_cmd_line_options:commands/args=#{@unprocessed_cmd_line_arguments},options=#{@unprocessed_cmd_line_options}".red}
154
153
  end
155
154
 
156
- def get_next_command(command_list); return get_next_argument('command', expected: command_list); end
157
-
158
155
  # @param expected is
159
156
  # - Array of allowed value (single value)
160
157
  # - :multiple for remaining values
161
158
  # - :single for a single unconstrained value
162
159
  # @param mandatory true/false
163
160
  # @param type expected class for result
161
+ # @param aliases list of aliases for the value
164
162
  # @return value, list or nil
165
- def get_next_argument(descr, expected: :single, mandatory: true, type: nil)
163
+ def get_next_argument(descr, expected: :single, mandatory: true, type: nil, aliases: nil, default: nil)
166
164
  unless type.nil?
167
165
  raise 'internal: type must be a Class' unless type.is_a?(Class)
168
166
  descr = "#{descr} (#{type})"
169
167
  end
170
- result = nil
168
+ result = default
171
169
  if !@unprocessed_cmd_line_arguments.empty?
172
170
  # there are values
173
171
  case expected
@@ -179,66 +177,37 @@ module Aspera
179
177
  if result.length.eql?(1) && result.first.is_a?(Array)
180
178
  result = result.first
181
179
  end
180
+ when Array
181
+ allowed_values = [].concat(expected)
182
+ allowed_values.concat(aliases.keys) unless aliases.nil?
183
+ raise "internal error: only symbols allowed: #{allowed_values}" unless allowed_values.all?(Symbol)
184
+ result = self.class.get_from_list(@unprocessed_cmd_line_arguments.shift, descr, allowed_values)
182
185
  else
183
- result = self.class.get_from_list(@unprocessed_cmd_line_arguments.shift, descr, expected)
186
+ raise 'internal error'
184
187
  end
185
188
  elsif mandatory
186
189
  # no value provided
187
190
  result = get_interactive(:argument, descr, expected: expected)
188
191
  end
189
192
  Log.log.debug{"#{descr}=#{result}"}
193
+ result = aliases[result] if !aliases.nil? && aliases.key?(result)
190
194
  raise "argument shall be #{type.name}" unless type.nil? || result.is_a?(type)
191
195
  return result
192
196
  end
193
197
 
194
- # declare option of type :accessor, or :value
195
- def declare_option(option_symbol, type)
196
- Log.log.debug{"declare_option: #{option_symbol}: #{type}: skip=#{@declared_options.key?(option_symbol)}".green}
197
- if @declared_options.key?(option_symbol)
198
- raise "INTERNAL ERROR: option #{option_symbol} already declared. only accessor can be re-declared and ignored" \
199
- unless @declared_options[option_symbol][:type].eql?(:accessor)
200
- return
201
- end
202
- @declared_options[option_symbol] = {type: type}
203
- # by default passwords and secrets are sensitive, else specify when declaring the option
204
- @declared_options[option_symbol][:sensitive] = true if !%w[password secret key].select{|i| option_symbol.to_s.end_with?(i)}.empty?
205
- end
206
-
207
- # define option with handler
208
- def set_obj_attr(option_symbol, object, attr_symb, default_value=nil)
209
- Log.log.debug{"set attr obj #{option_symbol} (#{object},#{attr_symb})"}
210
- declare_option(option_symbol, :accessor)
211
- @declared_options[option_symbol][:accessor] = AttrAccessor.new(object, attr_symb)
212
- set_option(option_symbol, default_value, 'default obj attr') if !default_value.nil?
213
- end
214
-
215
- # set an option value by name, either store value or call handler
216
- def set_option(option_symbol, value, where='default')
217
- if !@declared_options.key?(option_symbol)
218
- Log.log.debug{"set unknown option: #{option_symbol}"}
219
- raise 'ERROR: cannot set undeclared option'
220
- # declare_option(option_symbol)
221
- end
222
- value = ExtendedValue.instance.evaluate(value)
223
- value = Manager.enum_to_bool(value) if @declared_options[option_symbol][:values].eql?(BOOLEAN_VALUES)
224
- Log.log.debug{"(#{@declared_options[option_symbol][:type]}/#{where}) set #{option_symbol}=#{value}"}
225
- case @declared_options[option_symbol][:type]
226
- when :accessor
227
- @declared_options[option_symbol][:accessor].value = value
228
- when :value
229
- @declared_options[option_symbol][:value] = value
230
- else # nil or other
231
- raise 'error'
232
- end
233
- end
198
+ def get_next_command(command_list, aliases: nil); return get_next_argument('command', expected: command_list, aliases: aliases); end
234
199
 
235
200
  # Get an option value by name
236
- # either return value or call handler, can return nil
201
+ # either return value or calls handler, can return nil
237
202
  # ask interactively if requested/required
238
- def get_option(option_symbol, is_type: :optional)
203
+ # @param mandatory [Boolean] if true, raise error if option not set
204
+ # @param allowed_types [Array] list of allowed types
205
+ def get_option(option_symbol, mandatory: false, allowed_types: nil)
206
+ allowed_types = [allowed_types] if allowed_types.is_a?(Class)
207
+ raise 'Internal Error: allowed_types must be an Array of Class or a Class' unless allowed_types.nil? || allowed_types.is_a?(Array)
239
208
  result = nil
240
209
  if @declared_options.key?(option_symbol)
241
- case @declared_options[option_symbol][:type]
210
+ case @declared_options[option_symbol][:read_write]
242
211
  when :accessor
243
212
  result = @declared_options[option_symbol][:accessor].value
244
213
  when :value
@@ -246,15 +215,15 @@ module Aspera
246
215
  else
247
216
  raise 'unknown type'
248
217
  end
249
- Log.log.debug{"(#{@declared_options[option_symbol][:type]}) get #{option_symbol}=#{result}"}
218
+ Log.log.debug{"(#{@declared_options[option_symbol][:read_write]}) get #{option_symbol}=#{result}"}
250
219
  end
251
220
  # do not fail for manual generation if option mandatory but not set
252
- result = '' if result.nil? && is_type.eql?(:mandatory) && !@fail_on_missing_mandatory
221
+ result = '' if result.nil? && mandatory && !@fail_on_missing_mandatory
253
222
  # Log.log.debug{"interactive=#{@ask_missing_mandatory}"}
254
223
  if result.nil?
255
224
  if !@ask_missing_mandatory
256
- raise CliBadArgument, "Missing mandatory option: #{option_symbol}" if is_type.eql?(:mandatory)
257
- elsif @ask_missing_optional || is_type.eql?(:mandatory)
225
+ raise CliBadArgument, "Missing mandatory option: #{option_symbol}" if mandatory
226
+ elsif @ask_missing_optional || mandatory
258
227
  # ask_missing_mandatory
259
228
  expected = :single
260
229
  # print "please enter: #{option_symbol.to_s}"
@@ -265,72 +234,117 @@ module Aspera
265
234
  set_option(option_symbol, result, 'interactive')
266
235
  end
267
236
  end
237
+ raise "option #{option_symbol} is #{result.class} but must be one of #{allowed_types}" unless \
238
+ !mandatory || allowed_types.nil? || allowed_types.any? { |t|result.is_a?(t)}
268
239
  return result
269
240
  end
270
241
 
271
- # param must be hash
272
- def add_option_preset(preset_hash, op: :push)
273
- Log.log.debug{"add_option_preset=#{preset_hash}"}
274
- raise "internal error: setting default with no hash: #{preset_hash.class}" if !preset_hash.is_a?(Hash)
275
- # incremental override
276
- preset_hash.each{|k, v|@unprocessed_defaults.send(op, [k.to_sym, v])}
277
- end
278
-
279
- # define an option with restricted values
280
- def add_opt_list(option_symbol, values, help, *on_args)
281
- declare_option(option_symbol, :value)
282
- Log.log.debug{"add_opt_list #{option_symbol}"}
283
- on_args.unshift(symbol_to_option(option_symbol, 'ENUM'))
284
- # this option value must be a symbol
285
- @declared_options[option_symbol][:values] = values
286
- value = get_option(option_symbol)
287
- help_values = values.map{|i|i.eql?(value) ? highlight_current(i) : i}.join(', ')
288
- if values.eql?(BOOLEAN_VALUES)
289
- help_values = BOOLEAN_SIMPLE.map{|i|(i.eql?(:yes) && value) || (i.eql?(:no) && !value) ? highlight_current(i) : i}.join(', ')
242
+ # set an option value by name, either store value or call handler
243
+ def set_option(option_symbol, value, where='code override')
244
+ raise CliBadArgument, "Unknown option: #{option_symbol}" unless @declared_options.key?(option_symbol)
245
+ attributes = @declared_options[option_symbol]
246
+ Log.log.warn("#{option_symbol}: Option is deprecated: #{attributes[:deprecation]}") if attributes[:deprecation]
247
+ value = ExtendedValue.instance.evaluate(value)
248
+ value = Manager.enum_to_bool(value) if attributes[:values].eql?(BOOLEAN_VALUES)
249
+ Log.log.debug{"(#{attributes[:read_write]}/#{where}) set #{option_symbol}=#{value}"}
250
+ case attributes[:read_write]
251
+ when :accessor
252
+ attributes[:accessor].value = value
253
+ when :value
254
+ attributes[:value] = value
255
+ else # nil or other
256
+ raise 'error'
290
257
  end
291
- on_args.push(values)
292
- on_args.push("#{help}: #{help_values}")
293
- Log.log.debug{"on_args=#{on_args}"}
294
- @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, help, values), 'cmdline')}
295
258
  end
296
259
 
297
- def add_opt_boolean(option_symbol, help, *on_args)
298
- add_opt_list(option_symbol, BOOLEAN_VALUES, help, *on_args)
299
- # if default was defined for obj, it may still be enum (yes/no) instead of boolean
300
- default_value = get_option(option_symbol)
301
- set_option(option_symbol, default_value, 'opt boolean') unless default_value.nil?
302
- end
303
-
304
- # define an option with open values
305
- def add_opt_simple(option_symbol, *on_args)
306
- declare_option(option_symbol, :value)
307
- Log.log.debug{"add_opt_simple #{option_symbol}"}
308
- on_args.unshift(symbol_to_option(option_symbol, 'VALUE'))
309
- Log.log.debug{"on_args=#{on_args}"}
310
- @parser.on(*on_args) { |v| set_option(option_symbol, v, 'cmdline') }
311
- end
312
-
313
- # define an option with date format
314
- def add_opt_date(option_symbol, *on_args)
315
- declare_option(option_symbol, :value)
316
- Log.log.debug{"add_opt_date #{option_symbol}"}
317
- on_args.unshift(symbol_to_option(option_symbol, 'DATE'))
318
- Log.log.debug{"on_args=#{on_args}"}
319
- @parser.on(*on_args) do |v|
320
- case v
321
- when 'now' then set_option(option_symbol, Manager.time_to_string(Time.now), 'cmdline')
322
- when /^-([0-9]+)h/ then set_option(option_symbol, Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600)), 'cmdline')
323
- else set_option(option_symbol, v, 'cmdline')
260
+ # declare an option
261
+ # @param option_symbol [Symbol] option name
262
+ # @param description [String] description for help
263
+ # @param handler [Hash] handler for option value: keys: o (object) and m (method)
264
+ # @param default [Object] default value
265
+ # @param values [nil, Array, :bool, :date, :none] list of allowed values, :bool for true/false, :date for dates, :none for on/off switch
266
+ # @param short [String] short option name
267
+ # @param coerce [Class] one of the coerce types accepted par option parser
268
+ # @param types [Class, Array] accepted value type(s)
269
+ # @param block [Proc] block to execute when option is found
270
+ def declare(option_symbol, description, handler: nil, default: nil, values: nil, short: nil, coerce: nil, types: nil, deprecation: nil, &block)
271
+ raise "INTERNAL ERROR: #{option_symbol} already declared" if @declared_options.key?(option_symbol)
272
+ raise "INTERNAL ERROR: #{option_symbol} ends with dot" unless description[-1] != '.'
273
+ raise "INTERNAL ERROR: #{option_symbol} does not start with capital" unless description[0] == description[0].upcase
274
+ raise "INTERNAL ERROR: #{option_symbol} shall use :types" if description.downcase.include?('hash') || description.downcase.include?('extended value')
275
+ opt = @declared_options[option_symbol] = {
276
+ read_write: handler.nil? ? :value : :accessor,
277
+ # by default passwords and secrets are sensitive, else specify when declaring the option
278
+ sensitive: SecretHider.secret?(option_symbol, '')
279
+ }
280
+ if !types.nil?
281
+ types = [types] unless types.is_a?(Array)
282
+ raise "INTERNAL ERROR: types must be classes: #{types}" unless types.all?(Class)
283
+ opt[:types] = types
284
+ description = "#{description} (#{types.map(&:name).join(', ')})"
285
+ end
286
+ if deprecation
287
+ opt[:deprecation] = deprecation
288
+ description = "#{description} (#{'deprecated'.blue}: #{deprecation})"
289
+ end
290
+ Log.log.debug{"declare: #{option_symbol}: #{opt[:read_write]}".green}
291
+ if opt[:read_write].eql?(:accessor)
292
+ raise 'internal error' unless handler.is_a?(Hash)
293
+ raise 'internal error' unless handler.keys.sort.eql?(%i[m o])
294
+ Log.log.debug{"set attr obj #{option_symbol} (#{handler[:o]},#{handler[:m]})"}
295
+ opt[:accessor] = AttrAccessor.new(handler[:o], handler[:m])
296
+ end
297
+ set_option(option_symbol, default, 'default') unless default.nil?
298
+ on_args = [description]
299
+ case values
300
+ when nil
301
+ on_args.push(symbol_to_option(option_symbol, 'VALUE'))
302
+ on_args.push("-#{short}VALUE") unless short.nil?
303
+ on_args.push(coerce) unless coerce.nil?
304
+ @parser.on(*on_args) { |v| set_option(option_symbol, v, 'cmdline') }
305
+ when Array, :bool
306
+ if values.eql?(:bool)
307
+ values = BOOLEAN_VALUES
308
+ set_option(option_symbol, Manager.enum_to_bool(default), 'default') unless default.nil?
309
+ end
310
+ # this option value must be a symbol
311
+ opt[:values] = values
312
+ value = get_option(option_symbol)
313
+ help_values = values.map{|i|i.eql?(value) ? highlight_current(i) : i}.join(', ')
314
+ if values.eql?(BOOLEAN_VALUES)
315
+ help_values = BOOLEAN_SIMPLE.map{|i|(i.eql?(:yes) && value) || (i.eql?(:no) && !value) ? highlight_current(i) : i}.join(', ')
316
+ end
317
+ on_args[0] = "#{description}: #{help_values}"
318
+ on_args.push(symbol_to_option(option_symbol, 'ENUM'))
319
+ on_args.push(values)
320
+ @parser.on(*on_args){|v|set_option(option_symbol, self.class.get_from_list(v.to_s, description, values), 'cmdline')}
321
+ when :date
322
+ on_args.push(symbol_to_option(option_symbol, 'DATE'))
323
+ @parser.on(*on_args) do |v|
324
+ time_string = case v
325
+ when 'now' then Manager.time_to_string(Time.now)
326
+ when /^-([0-9]+)h/ then Manager.time_to_string(Time.now - (Regexp.last_match(1).to_i * 3600))
327
+ else v
328
+ end
329
+ set_option(option_symbol, time_string, 'cmdline')
324
330
  end
331
+ when :none
332
+ raise "internal error: missing block for #{option_symbol}" if block.nil?
333
+ on_args.push(symbol_to_option(option_symbol, nil))
334
+ on_args.push("-#{short}") if short.is_a?(String)
335
+ @parser.on(*on_args, &block)
336
+ else raise 'internal error'
325
337
  end
338
+ Log.log.debug{"on_args=#{on_args}"}
326
339
  end
327
340
 
328
- # define an option without value
329
- def add_opt_switch(option_symbol, *on_args, &block)
330
- Log.log.debug{"add_opt_on #{option_symbol}"}
331
- on_args.unshift(symbol_to_option(option_symbol, nil))
332
- Log.log.debug{"on_args=#{on_args}"}
333
- @parser.on(*on_args, &block)
341
+ # Adds each of the keys of specified hash as an option
342
+ # @param preset_hash [Hash] hash of options to add
343
+ def add_option_preset(preset_hash, op: :push)
344
+ Log.log.debug{"add_option_preset=#{preset_hash}"}
345
+ raise "internal error: default expects Hash: #{preset_hash.class}" unless preset_hash.is_a?(Hash)
346
+ # incremental override
347
+ preset_hash.each{|k, v|@unprocessed_defaults.send(op, [k.to_sym, v])}
334
348
  end
335
349
 
336
350
  # check if there were unprocessed values to generate error
@@ -346,7 +360,7 @@ module Aspera
346
360
  return result
347
361
  end
348
362
 
349
- # get all original options on command line used to generate a config in config file
363
+ # get all original options on command line used to generate a config in config file
350
364
  def get_options_table(remove_from_remaining: true)
351
365
  result = {}
352
366
  @initial_cli_options.each do |optionval|
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/cli/extended_value'
4
+
3
5
  module Aspera
4
6
  module Cli
5
7
  # base class for plugins modules
@@ -8,13 +10,16 @@ module Aspera
8
10
  GLOBAL_OPS = %i[create list].freeze
9
11
  # operations with id
10
12
  INSTANCE_OPS = %i[modify delete show].freeze
13
+ # all standard operations
11
14
  ALL_OPS = [GLOBAL_OPS, INSTANCE_OPS].flatten.freeze
12
- # max number of items for list command
15
+ # special query parameter: max number of items for list command
13
16
  MAX_ITEMS = 'max'
14
- # max number of pages for list command
17
+ # special query parameter: max number of pages for list command
15
18
  MAX_PAGES = 'pmax'
16
19
  # used when all resources are selected
17
20
  VAL_ALL = 'ALL'
21
+ # special identifier format: look for this name to find where supported
22
+ REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/.freeze
18
23
 
19
24
  # global for inherited classes
20
25
  @@options_created = false # rubocop:disable Style/ClassVars
@@ -24,46 +29,54 @@ module Aspera
24
29
  # env.each_key {|k| raise "wrong agent key #{k}" unless AGENTS.include?(k)}
25
30
  @agents = env
26
31
  # check presence in descendant of mandatory method and constant
27
- raise StandardError, "missing method 'execute_action' in #{self.class}" unless respond_to?(:execute_action)
32
+ raise StandardError, "Missing method 'execute_action' in #{self.class}" unless respond_to?(:execute_action)
28
33
  raise StandardError, 'ACTIONS shall be redefined by subclass' unless self.class.constants.include?(:ACTIONS)
29
34
  options.parser.separator('')
30
35
  options.parser.separator("COMMAND: #{self.class.name.split('::').last.downcase}")
31
36
  options.parser.separator("SUBCOMMANDS: #{self.class.const_get(:ACTIONS).map(&:to_s).sort.join(' ')}")
32
37
  options.parser.separator('OPTIONS:')
33
38
  return if @@options_created
34
- options.add_opt_simple(:query, 'additional filter for API calls (extended value) (some commands)')
35
- options.add_opt_simple(:value, 'extended value for create, update, list filter')
36
- options.add_opt_simple(:property, 'name of property to set')
37
- options.add_opt_simple(:id, "resource identifier (#{INSTANCE_OPS.join(',')})")
38
- options.add_opt_boolean(:bulk, 'Bulk operation (only some)')
39
- options.add_opt_boolean(:bfail, 'Bulk operation error handling')
40
- options.set_option(:bulk, :no)
41
- options.set_option(:bfail, :yes)
39
+ options.declare(:query, 'Additional filter for for some commands (list/delete)', types: Hash)
40
+ options.declare(
41
+ :value, 'Value for create, update, list filter', types: Hash,
42
+ deprecation: 'Use positional value for create/modify or option: query for list/delete')
43
+ options.declare(:property, 'Name of property to set (modify operation)')
44
+ options.declare(:id, 'Resource identifier', deprecation: "Use identifier after verb (#{INSTANCE_OPS.join(',')})")
45
+ options.declare(:bulk, 'Bulk operation (only some)', values: :bool, default: :no)
46
+ options.declare(:bfail, 'Bulk operation error handling', values: :bool, default: :yes)
42
47
  options.parse_options!
43
48
  @@options_created = true # rubocop:disable Style/ClassVars
44
49
  end
45
50
 
46
- # must be called AFTER the instance action
47
- def instance_identifier
51
+ # must be called AFTER the instance action, ... folder browse <call instance_identifier>
52
+ # @param description [String] description of the identifier
53
+ # @param block [Proc] block to search for identifier based on attribute value
54
+ # @return [String] identifier
55
+ def instance_identifier(description: 'identifier', &block)
48
56
  res_id = options.get_option(:id)
49
- res_id = options.get_next_argument('identifier') if res_id.nil?
57
+ res_id = options.get_next_argument(description) if res_id.nil?
58
+ # cab be an Array
59
+ if res_id.is_a?(String) && (m = res_id.match(REGEX_LOOKUP_ID_BY_FIELD))
60
+ if block
61
+ res_id = yield(m[1], ExtendedValue.instance.evaluate(m[2]))
62
+ else
63
+ raise CliBadArgument, "Percent syntax for #{description} not supported in this context"
64
+ end
65
+ end
50
66
  return res_id
51
67
  end
52
68
 
53
- # TODO
54
- # def get_next_id_command(instance_ops: INSTANCE_OPS,global_ops: GLOBAL_OPS)
55
- # return get_next_argument('command',expected: command_list)
56
- # end
57
-
58
69
  # For create and delete operations: execute one actin or multiple if bulk is yes
59
- # @param params either single id or hash, or array for bulk
70
+ # @param single_or_array [Object] single hash, or array of hash for bulk
60
71
  # @param success_msg deleted or created
72
+ # @param id_result [String] key in result hash to use as identifier
73
+ # @param fields [Array] fields to display
61
74
  def do_bulk_operation(single_or_array, success_msg, id_result: 'id', fields: :default)
62
75
  raise 'programming error: missing block' unless block_given?
63
76
  params = options.get_option(:bulk) ? single_or_array : [single_or_array]
64
77
  raise 'expecting Array for bulk operation' unless params.is_a?(Array)
65
78
  Log.log.warn('Empty list given for bulk operation') if params.empty?
66
- Log.dump(:bulk_create, params)
79
+ Log.dump(:bulk_operation, params)
67
80
  result_list = []
68
81
  params.each do |param|
69
82
  # init for delete
@@ -96,11 +109,14 @@ module Aspera
96
109
  # @param id_default [String] default identifier to use for existing entity commands (show, modify)
97
110
  # @param item_list_key [String] result is in a sub key of the json
98
111
  # @param id_as_arg [String] if set, the id is provided as url argument ?<id_as_arg>=<id>
112
+ # @param is_singleton [Boolean] if true, res_class_path is the full path to the resource
99
113
  # @return result suitable for CLI result
100
- def entity_command(command, rest_api, res_class_path, display_fields: nil, id_default: nil, item_list_key: false, id_as_arg: false)
101
- if INSTANCE_OPS.include?(command)
114
+ def entity_command(command, rest_api, res_class_path, display_fields: nil, id_default: nil, item_list_key: false, id_as_arg: false, is_singleton: false, &block)
115
+ if is_singleton
116
+ one_res_path = res_class_path
117
+ elsif INSTANCE_OPS.include?(command)
102
118
  begin
103
- one_res_id = instance_identifier
119
+ one_res_id = instance_identifier(&block)
104
120
  rescue StandardError => e
105
121
  raise e if id_default.nil?
106
122
  one_res_id = id_default
@@ -108,29 +124,24 @@ module Aspera
108
124
  one_res_path = "#{res_class_path}/#{one_res_id}"
109
125
  one_res_path = "#{res_class_path}?#{id_as_arg}=#{one_res_id}" if id_as_arg
110
126
  end
111
- # parameters mandatory for create/modify
112
- if %i[create modify].include?(command)
113
- parameters = options.get_option(:value, is_type: :mandatory)
114
- end
115
- # parameters optional for list
116
- if %i[list delete].include?(command)
117
- parameters = options.get_option(:value)
118
- end
127
+
119
128
  case command
120
129
  when :create
121
- return do_bulk_operation(parameters, 'created', fields: display_fields) do |params|
130
+ raise 'cannot create singleton' if is_singleton
131
+ return do_bulk_operation(value_create_modify(command: command, type: :bulk_hash), 'created', fields: display_fields) do |params|
122
132
  raise 'expecting Hash' unless params.is_a?(Hash)
123
133
  rest_api.create(res_class_path, params)[:data]
124
134
  end
125
135
  when :delete
136
+ raise 'cannot delete singleton' if is_singleton
126
137
  return do_bulk_operation(one_res_id, 'deleted') do |one_id|
127
- rest_api.delete("#{res_class_path}/#{one_id}", parameters)
138
+ rest_api.delete("#{res_class_path}/#{one_id}", old_query_read_delete)
128
139
  {'id' => one_id}
129
140
  end
130
141
  when :show
131
142
  return {type: :single_object, data: rest_api.read(one_res_path)[:data], fields: display_fields}
132
143
  when :list
133
- resp = rest_api.read(res_class_path, parameters)
144
+ resp = rest_api.read(res_class_path, old_query_read_delete)
134
145
  data = resp[:data]
135
146
  # TODO: not generic : which application is this for ?
136
147
  if resp[:http]['Content-Type'].start_with?('application/vnd.api+json')
@@ -153,6 +164,7 @@ module Aspera
153
164
  raise "An error occurred: unexpected result type for list: #{data.class}"
154
165
  end
155
166
  when :modify
167
+ parameters = value_create_modify(command: command, type: Hash)
156
168
  property = options.get_option(:property)
157
169
  parameters = {property => parameters} unless property.nil?
158
170
  rest_api.update(one_res_path, parameters)
@@ -169,8 +181,8 @@ module Aspera
169
181
  return entity_command(command, rest_api, res_class_path, **opts)
170
182
  end
171
183
 
172
- # query for list operation
173
- def option_url_query(default)
184
+ # query parameters in URL suitable for REST list/GET and delete/DELETE
185
+ def query_read_delete(default: nil)
174
186
  query = options.get_option(:query)
175
187
  # dup default, as it could be frozen
176
188
  query = default.dup if query.nil?
@@ -179,11 +191,48 @@ module Aspera
179
191
  # check it is suitable
180
192
  URI.encode_www_form(query) unless query.nil?
181
193
  rescue StandardError => e
182
- raise CliBadArgument, "query must be an extended value which can be encoded with URI.encode_www_form. Refer to manual. (#{e.message})"
194
+ raise CliBadArgument, "Query must be an extended value which can be encoded with URI.encode_www_form. Refer to manual. (#{e.message})"
183
195
  end
184
196
  return query
185
197
  end
186
198
 
199
+ # TODO: when deprecation of `value` is completed: remove this method, replace with query_read_delete
200
+ def old_query_read_delete
201
+ query = options.get_option(:value) # legacy, deprecated, remove, one day...
202
+ query = query_read_delete if query.nil?
203
+ return query
204
+ end
205
+
206
+ # TODO: when deprecation of `value` is completed: remove this method, replace with options.get_option(:query)
207
+ def value_or_query(mandatory: false, allowed_types: nil)
208
+ value = options.get_option(:value, mandatory: false, allowed_types: allowed_types)
209
+ value = options.get_option(:query, mandatory: mandatory, allowed_types: allowed_types) if value.nil?
210
+ return value
211
+ end
212
+
213
+ # Retrieves an extended value from command line, used for creation or modification of entities
214
+ # @param default [Object] default value if not provided
215
+ # @param command [String] command name for error message
216
+ # @param type [Class] expected type of value
217
+ # TODO: when deprecation of `value` is completed: remove line with :value
218
+ def value_create_modify(default: nil, command: 'command', type: nil)
219
+ value = options.get_option(:value)
220
+ value = options.get_next_argument("parameters for #{command}", mandatory: default.nil?) if value.nil?
221
+ value = default if value.nil?
222
+ if type.nil?
223
+ # nothing to do
224
+ elsif type.is_a?(Class)
225
+ raise CliBadArgument, "Value must be a #{type}" unless value.is_a?(type)
226
+ elsif type.is_a?(Array)
227
+ raise CliBadArgument, "Value must be one of #{type.join(', ')}" unless type.any?{|t| value.is_a?(t)}
228
+ elsif type.eql?(:bulk_hash)
229
+ raise CliBadArgument, 'Value must be a Hash or Array of Hash' unless value.is_a?(Hash) || (value.is_a?(Array) && value.all?(Hash))
230
+ else
231
+ raise "Internal error: #{type}"
232
+ end
233
+ return value
234
+ end
235
+
187
236
  # shortcuts helpers for plugin environment
188
237
  %i[options transfer config formatter persistency].each do |name|
189
238
  define_method(name){@agents[name]}
@@ -13,8 +13,8 @@ module Aspera
13
13
  command = options.get_next_command(ACTIONS)
14
14
  case command
15
15
  when :entitlement
16
- entitlement_id = options.get_option(:username, is_type: :mandatory)
17
- customer_id = options.get_option(:password, is_type: :mandatory)
16
+ entitlement_id = options.get_option(:username, mandatory: true)
17
+ customer_id = options.get_option(:password, mandatory: true)
18
18
  api_metering = AoC.metering_api(entitlement_id, customer_id)
19
19
  return {type: :single_object, data: api_metering.read('entitlement')[:data]}
20
20
  end