aspera-cli 4.21.2 → 4.23.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 (105) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +1 -1
  4. data/CHANGELOG.md +402 -374
  5. data/CONTRIBUTING.md +6 -10
  6. data/README.md +1018 -687
  7. data/lib/aspera/agent/base.rb +9 -5
  8. data/lib/aspera/agent/connect.rb +30 -28
  9. data/lib/aspera/agent/desktop.rb +29 -25
  10. data/lib/aspera/agent/direct.rb +137 -125
  11. data/lib/aspera/agent/httpgw.rb +22 -26
  12. data/lib/aspera/agent/node.rb +14 -11
  13. data/lib/aspera/agent/transferd.rb +6 -2
  14. data/lib/aspera/api/aoc.rb +15 -18
  15. data/lib/aspera/api/cos_node.rb +1 -1
  16. data/lib/aspera/api/httpgw.rb +15 -7
  17. data/lib/aspera/api/node.rb +6 -4
  18. data/lib/aspera/ascmd.rb +17 -9
  19. data/lib/aspera/ascp/installation.rb +21 -19
  20. data/lib/aspera/ascp/management.rb +1 -1
  21. data/lib/aspera/assert.rb +14 -5
  22. data/lib/aspera/cli/error.rb +2 -2
  23. data/lib/aspera/cli/extended_value.rb +38 -19
  24. data/lib/aspera/cli/formatter.rb +48 -48
  25. data/lib/aspera/cli/hints.rb +10 -2
  26. data/lib/aspera/cli/main.rb +190 -168
  27. data/lib/aspera/cli/manager.rb +16 -16
  28. data/lib/aspera/cli/plugin.rb +24 -21
  29. data/lib/aspera/cli/plugin_factory.rb +1 -1
  30. data/lib/aspera/cli/plugins/alee.rb +1 -1
  31. data/lib/aspera/cli/plugins/aoc.rb +173 -126
  32. data/lib/aspera/cli/plugins/ats.rb +19 -17
  33. data/lib/aspera/cli/plugins/config.rb +87 -98
  34. data/lib/aspera/cli/plugins/console.rb +5 -3
  35. data/lib/aspera/cli/plugins/faspex.rb +39 -35
  36. data/lib/aspera/cli/plugins/faspex5.rb +104 -80
  37. data/lib/aspera/cli/plugins/faspio.rb +13 -1
  38. data/lib/aspera/cli/plugins/httpgw.rb +13 -1
  39. data/lib/aspera/cli/plugins/node.rb +336 -205
  40. data/lib/aspera/cli/plugins/orchestrator.rb +34 -40
  41. data/lib/aspera/cli/plugins/preview.rb +3 -3
  42. data/lib/aspera/cli/plugins/server.rb +7 -6
  43. data/lib/aspera/cli/plugins/shares.rb +5 -5
  44. data/lib/aspera/cli/sync_actions.rb +19 -18
  45. data/lib/aspera/cli/transfer_agent.rb +11 -15
  46. data/lib/aspera/cli/transfer_progress.rb +2 -2
  47. data/lib/aspera/cli/version.rb +1 -1
  48. data/lib/aspera/command_line_builder.rb +116 -95
  49. data/lib/aspera/coverage.rb +4 -3
  50. data/lib/aspera/data_repository.rb +1 -0
  51. data/lib/aspera/environment.rb +7 -6
  52. data/lib/aspera/faspex_gw.rb +14 -14
  53. data/lib/aspera/faspex_postproc.rb +7 -6
  54. data/lib/aspera/hash_ext.rb +2 -2
  55. data/lib/aspera/json_rpc.rb +1 -1
  56. data/lib/aspera/keychain/encrypted_hash.rb +47 -34
  57. data/lib/aspera/keychain/factory.rb +41 -0
  58. data/lib/aspera/keychain/hashicorp_vault.rb +71 -0
  59. data/lib/aspera/keychain/macos_security.rb +19 -11
  60. data/lib/aspera/log.rb +29 -34
  61. data/lib/aspera/nagios.rb +6 -6
  62. data/lib/aspera/node_simulator.rb +8 -8
  63. data/lib/aspera/oauth/base.rb +10 -6
  64. data/lib/aspera/oauth/factory.rb +6 -6
  65. data/lib/aspera/oauth/url_json.rb +6 -6
  66. data/lib/aspera/persistency_action_once.rb +6 -4
  67. data/lib/aspera/persistency_folder.rb +2 -2
  68. data/lib/aspera/preview/file_types.rb +40 -33
  69. data/lib/aspera/preview/generator.rb +1 -1
  70. data/lib/aspera/preview/options.rb +16 -16
  71. data/lib/aspera/preview/terminal.rb +3 -3
  72. data/lib/aspera/preview/utils.rb +11 -13
  73. data/lib/aspera/products/connect.rb +2 -1
  74. data/lib/aspera/products/desktop.rb +1 -1
  75. data/lib/aspera/products/transferd.rb +1 -1
  76. data/lib/aspera/proxy_auto_config.rb +2 -2
  77. data/lib/aspera/rest.rb +70 -50
  78. data/lib/aspera/rest_error_analyzer.rb +1 -0
  79. data/lib/aspera/rest_errors_aspera.rb +1 -1
  80. data/lib/aspera/secret_hider.rb +5 -5
  81. data/lib/aspera/ssh.rb +5 -5
  82. data/lib/aspera/temp_file_manager.rb +1 -0
  83. data/lib/aspera/timer_limiter.rb +7 -5
  84. data/lib/aspera/transfer/async_conf.schema.yaml +716 -0
  85. data/lib/aspera/transfer/convert.rb +29 -0
  86. data/lib/aspera/transfer/error_info.rb +66 -66
  87. data/lib/aspera/transfer/parameters.rb +13 -68
  88. data/lib/aspera/transfer/spec.rb +5 -6
  89. data/lib/aspera/transfer/spec.schema.yaml +753 -0
  90. data/lib/aspera/transfer/spec_doc.rb +62 -0
  91. data/lib/aspera/transfer/sync.rb +37 -76
  92. data/lib/aspera/transfer/sync_instance.schema.yaml +20 -0
  93. data/lib/aspera/transfer/sync_session.schema.yaml +86 -0
  94. data/lib/aspera/transfer/uri.rb +6 -6
  95. data/lib/aspera/uri_reader.rb +1 -1
  96. data/lib/aspera/web_auth.rb +1 -1
  97. data/lib/aspera/web_server_simple.rb +53 -44
  98. data.tar.gz.sig +0 -0
  99. metadata +38 -7
  100. metadata.gz.sig +0 -0
  101. data/examples/build_package.sh +0 -28
  102. data/examples/dascli +0 -30
  103. data/examples/get_proto_file.rb +0 -8
  104. data/examples/proxy.pac +0 -60
  105. data/lib/aspera/transfer/spec.yaml +0 -718
@@ -21,6 +21,7 @@ module Aspera
21
21
  module Plugins
22
22
  class Node < Cli::BasicAuthPlugin
23
23
  include SyncActions
24
+
24
25
  class << self
25
26
  # directory: node, container: shares
26
27
  FOLDER_TYPES = %w[directory container].freeze
@@ -47,6 +48,7 @@ module Aspera
47
48
  test_endpoint = 'ping'
48
49
  result = api.call(operation: 'GET', subpath: test_endpoint)
49
50
  next unless result[:http].body.eql?('')
51
+ # also remove "/"
50
52
  url_end = -2 - test_endpoint.length
51
53
  return {
52
54
  url: result[:http].uri.to_s[0..url_end],
@@ -107,7 +109,7 @@ module Aspera
107
109
  SEARCH_REMOVE_FIELDS = %w[basename permissions].freeze
108
110
 
109
111
  # actions in execute_command_gen3
110
- COMMANDS_GEN3 = %i[search space mkdir mklink mkfile rename delete browse upload download http_node_download sync transport]
112
+ COMMANDS_GEN3 = %i[search space mkdir mklink mkfile rename delete browse upload download cat sync transport]
111
113
 
112
114
  BASE_ACTIONS = %i[api_details].concat(COMMANDS_GEN3).freeze
113
115
 
@@ -125,7 +127,7 @@ module Aspera
125
127
  NODE4_READ_ACTIONS = %i[bearer_token_node node_info browse find].freeze
126
128
 
127
129
  # commands for execute_command_gen4
128
- COMMANDS_GEN4 = %i[mkdir rename delete upload download sync http_node_download show modify permission thumbnail v3].concat(NODE4_READ_ACTIONS).freeze
130
+ COMMANDS_GEN4 = %i[mkdir mklink mkfile rename delete upload download sync cat show modify permission thumbnail v3].concat(NODE4_READ_ACTIONS).freeze
129
131
 
130
132
  # commands supported in ATS for COS
131
133
  COMMANDS_COS = %i[upload download info access_keys api_details transfer].freeze
@@ -134,13 +136,16 @@ module Aspera
134
136
 
135
137
  GEN4_LS_FIELDS = %w[name type recursive_size size modified_time access_level].freeze
136
138
 
137
- def initialize(api: nil, **env)
139
+ # @param api [Rest] an existing API object for the Node API
140
+ # @param prefix_path [String,nil] for Faspex 4, allows browsing a package
141
+ def initialize(api: nil, prefix_path: nil, **env)
142
+ @prefix_path = prefix_path
138
143
  super(**env, basic_options: api.nil?)
139
- Node.declare_options(options) if api.nil?
140
- return if only_manual
144
+ Node.declare_options(options)
145
+ return if env[:broker].only_manual?
141
146
  @api_node =
142
147
  if !api.nil?
143
- # this can be Api::Node or Rest (shares)
148
+ # this can be Api::Node or Rest (Shares)
144
149
  api
145
150
  elsif OAuth::Factory.bearer?(options.get_option(:password, mandatory: true))
146
151
  # info is provided like node_info of aoc
@@ -161,23 +166,22 @@ module Aspera
161
166
  end
162
167
 
163
168
  # reduce the path from a result on given named column
164
- def c_result_remove_prefix_path(result, column, path_prefix)
165
- if !path_prefix.nil?
166
- case result[:type]
167
- when :object_list
168
- result[:data].each do |item|
169
- item[column] = item[column][path_prefix.length..-1] if item[column].start_with?(path_prefix)
170
- end
171
- when :single_object
172
- item = result[:data]
173
- item[column] = item[column][path_prefix.length..-1] if item[column].start_with?(path_prefix)
169
+ def c_result_remove_prefix_path(result, column)
170
+ return result if @prefix_path.nil?
171
+ case result[:type]
172
+ when :object_list
173
+ result[:data].each do |item|
174
+ item[column] = item[column][@prefix_path.length..-1] if item[column].start_with?(@prefix_path)
174
175
  end
176
+ when :single_object
177
+ item = result[:data]
178
+ item[column] = item[column][@prefix_path.length..-1] if item[column].start_with?(@prefix_path)
175
179
  end
176
180
  return result
177
181
  end
178
182
 
179
183
  # translates paths results into CLI result, and removes prefix
180
- def c_result_translate_rem_prefix(response, type, success_msg, path_prefix)
184
+ def c_result_translate_rem_prefix(response, type, success_msg)
181
185
  errors = []
182
186
  final_result = {type: :object_list, data: [], fields: [type, 'result']}
183
187
  response['paths'].each do |p|
@@ -191,16 +195,16 @@ module Aspera
191
195
  end
192
196
  # one error make all fail
193
197
  unless errors.empty?
194
- raise errors.map{|i|"#{i.first}: #{i.last}"}.join(', ')
198
+ raise errors.map{ |i| "#{i.first}: #{i.last}"}.join(', ')
195
199
  end
196
- return c_result_remove_prefix_path(final_result, type, path_prefix)
200
+ return c_result_remove_prefix_path(final_result, type)
197
201
  end
198
202
 
199
- def browse_gen3(prefix_path)
200
- folders_to_process = [get_one_argument_with_prefix(prefix_path, 'path')]
203
+ def browse_gen3
204
+ folders_to_process = [get_one_argument_with_prefix('path')]
201
205
  query = options.get_option(:query, default: {})
202
206
  # special parameter: max number of entries in result
203
- max_items = query.delete('max')
207
+ max_items = query.delete(MAX_ITEMS)
204
208
  # special parameter: recursive browsing
205
209
  recursive = query.delete('recursive')
206
210
  # special parameter: only return one entry for the path, even if folder
@@ -209,7 +213,7 @@ module Aspera
209
213
  single_call = query.key?('skip')
210
214
  # API default is 100, so use 1000 for default
211
215
  query['count'] ||= 1000
212
- raise Cli::BadArgument, 'options recursive and skip cannot be used together' if recursive && single_call
216
+ raise Cli::BadArgument, 'options `recursive` and `skip` cannot be used together' if recursive && single_call
213
217
  all_items = []
214
218
  until folders_to_process.empty?
215
219
  path = folders_to_process.shift
@@ -222,7 +226,7 @@ module Aspera
222
226
  response = @api_node.create('files/browse', query)
223
227
  # 'file','symbolic_link'
224
228
  if !Node.gen3_entry_folder?(response['self']) || only_path
225
- result = { type: :single_object, data: response['self']}
229
+ result = {type: :single_object, data: response['self']}
226
230
  break
227
231
  end
228
232
  items = response['items']
@@ -233,7 +237,7 @@ module Aspera
233
237
  break
234
238
  end
235
239
  if recursive
236
- folders_to_process.concat(items.select{|i|Node.gen3_entry_folder?(i)}.map{|i|i['path']})
240
+ folders_to_process.concat(items.select{ |i| Node.gen3_entry_folder?(i)}.map{ |i| i['path']})
237
241
  end
238
242
  if !max_items.nil? && (all_items.count >= max_items)
239
243
  all_items = all_items.slice(0, max_items) if all_items.count > max_items
@@ -248,73 +252,94 @@ module Aspera
248
252
  end
249
253
  result ||= {type: :object_list, data: all_items}
250
254
  formatter.long_operation_terminated
251
- return c_result_remove_prefix_path(result, 'path', prefix_path)
255
+ return c_result_remove_prefix_path(result, 'path')
256
+ end
257
+
258
+ # Create async transfer spec request from direction and folders
259
+ # @param sync_direction one of push pull bidi
260
+ # @param local_path local folder to sync
261
+ # @param remote_path remote folder to sync
262
+ def sync_spec_request(sync_direction, local_path, remote_path)
263
+ case sync_direction
264
+ when :push then {
265
+ type: :sync_upload,
266
+ paths: [{
267
+ source: local_path,
268
+ destination: remote_path
269
+ }]
270
+ }
271
+ when :pull then {
272
+ type: :sync_download,
273
+ paths: [{
274
+ source: remote_path,
275
+ destination: local_path
276
+ }]
277
+ }
278
+ when :bidi then {
279
+ type: :sync,
280
+ paths: [{
281
+ source: local_path,
282
+ destination: remote_path
283
+ }]
284
+ }
285
+ else Aspera.error_unexpected_value(sync_direction)
286
+ end
252
287
  end
253
288
 
254
- # file and folder related commands
255
- def execute_command_gen3(command, prefix_path)
289
+ # Commands based on Gen3 API for file and folder
290
+ def execute_command_gen3(command)
256
291
  case command
257
292
  when :delete
258
293
  # TODO: add query for recursive
259
- paths_to_delete = get_all_arguments_with_prefix(prefix_path, 'file list')
260
- resp = @api_node.create('files/delete', { paths: paths_to_delete.map{|i| {'path' => i.start_with?('/') ? i : "/#{i}"} }})
261
- return c_result_translate_rem_prefix(resp, 'file', 'deleted', prefix_path)
294
+ paths_to_delete = get_all_arguments_with_prefix('file list')
295
+ resp = @api_node.create('files/delete', {paths: paths_to_delete.map{ |i| {'path' => i.start_with?('/') ? i : "/#{i}"}}})
296
+ return c_result_translate_rem_prefix(resp, 'file', 'deleted')
262
297
  when :search
263
- search_root = get_one_argument_with_prefix(prefix_path, 'search root')
298
+ search_root = get_one_argument_with_prefix('search root')
264
299
  parameters = {'path' => search_root}
265
300
  other_options = options.get_option(:query)
266
301
  parameters.merge!(other_options) unless other_options.nil?
267
302
  resp = @api_node.create('files/search', parameters)
268
- result = { type: :object_list, data: resp['items']}
303
+ result = {type: :object_list, data: resp['items']}
269
304
  return Main.result_empty if result[:data].empty?
270
- result[:fields] = result[:data].first.keys.reject{|i|SEARCH_REMOVE_FIELDS.include?(i)}
305
+ result[:fields] = result[:data].first.keys.reject{ |i| SEARCH_REMOVE_FIELDS.include?(i)}
271
306
  formatter.display_item_count(resp['item_count'], resp['total_count'])
272
- formatter.display_status("params: #{resp['parameters'].keys.map{|k|"#{k}:#{resp['parameters'][k]}"}.join(',')}")
273
- return c_result_remove_prefix_path(result, 'path', prefix_path)
307
+ formatter.display_status("params: #{resp['parameters'].keys.map{ |k| "#{k}:#{resp['parameters'][k]}"}.join(',')}")
308
+ return c_result_remove_prefix_path(result, 'path')
274
309
  when :space
275
- path_list = get_all_arguments_with_prefix(prefix_path, 'folder path or ext.val. list')
276
- resp = @api_node.create('space', { 'paths' => path_list.map {|i| { path: i} } })
277
- result = { type: :object_list, data: resp['paths']}
278
- # return c_result_translate_rem_prefix(resp,'folder','created',prefix_path)
279
- return c_result_remove_prefix_path(result, 'path', prefix_path)
310
+ path_list = get_all_arguments_with_prefix('folder path or ext.val. list')
311
+ resp = @api_node.create('space', {'paths' => path_list.map{ |i| {path: i}}})
312
+ result = {type: :object_list, data: resp['paths']}
313
+ # return c_result_translate_rem_prefix(resp,'folder','created',@prefix_path)
314
+ return c_result_remove_prefix_path(result, 'path')
280
315
  when :mkdir
281
- path_list = get_all_arguments_with_prefix(prefix_path, 'folder path or ext.val. list')
282
- resp = @api_node.create('files/create', { 'paths' => path_list.map{|i|{ type: :directory, path: i }}})
283
- return c_result_translate_rem_prefix(resp, 'folder', 'created', prefix_path)
316
+ path_list = get_all_arguments_with_prefix('folder path or ext.val. list')
317
+ resp = @api_node.create('files/create', {'paths' => path_list.map{ |i| {type: :directory, path: i}}})
318
+ return c_result_translate_rem_prefix(resp, 'folder', 'created')
284
319
  when :mklink
285
- target = get_one_argument_with_prefix(prefix_path, 'target')
286
- one_path = get_one_argument_with_prefix(prefix_path, 'link path')
287
- resp = @api_node.create('files/create', { 'paths' => [{ type: :symbolic_link, path: one_path, target: { path: target} }] })
288
- return c_result_translate_rem_prefix(resp, 'folder', 'created', prefix_path)
320
+ target = get_one_argument_with_prefix('target')
321
+ one_path = get_one_argument_with_prefix('link path')
322
+ resp = @api_node.create('files/create', {'paths' => [{type: :symbolic_link, path: one_path, target: {path: target}}]})
323
+ return c_result_translate_rem_prefix(resp, 'folder', 'created')
289
324
  when :mkfile
290
- one_path = get_one_argument_with_prefix(prefix_path, 'file path')
325
+ one_path = get_one_argument_with_prefix('file path')
291
326
  contents64 = Base64.strict_encode64(options.get_next_argument('contents'))
292
- resp = @api_node.create('files/create', { 'paths' => [{ type: :file, path: one_path, contents: contents64 }] })
293
- return c_result_translate_rem_prefix(resp, 'folder', 'created', prefix_path)
327
+ resp = @api_node.create('files/create', {'paths' => [{type: :file, path: one_path, contents: contents64}]})
328
+ return c_result_translate_rem_prefix(resp, 'folder', 'created')
294
329
  when :rename
295
330
  # TODO: multiple ?
296
- path_base = get_one_argument_with_prefix(prefix_path, 'path_base')
297
- path_src = get_one_argument_with_prefix(prefix_path, 'path_src')
298
- path_dst = get_one_argument_with_prefix(prefix_path, 'path_dst')
299
- resp = @api_node.create('files/rename', { 'paths' => [{ 'path' => path_base, 'source' => path_src, 'destination' => path_dst }] })
300
- return c_result_translate_rem_prefix(resp, 'entry', 'moved', prefix_path)
331
+ path_base = get_one_argument_with_prefix('path_base')
332
+ path_src = get_one_argument_with_prefix('path_src')
333
+ path_dst = get_one_argument_with_prefix('path_dst')
334
+ resp = @api_node.create('files/rename', {'paths' => [{'path' => path_base, 'source' => path_src, 'destination' => path_dst}]})
335
+ return c_result_translate_rem_prefix(resp, 'entry', 'moved')
301
336
  when :browse
302
- return browse_gen3(prefix_path)
337
+ return browse_gen3
303
338
  when :sync
304
339
  return execute_sync_action do |sync_direction, local_path, remote_path|
305
340
  # Gen3 API
306
341
  # empty transfer spec for authorization request
307
- request_transfer_spec = {
308
- type: case sync_direction
309
- when :push then :sync_upload
310
- when :pull then :sync_download
311
- when :bidi then :sync
312
- end,
313
- paths: [{
314
- source: remote_path,
315
- destination: local_path
316
- }]
317
- }
342
+ request_transfer_spec = sync_spec_request(sync_direction, local_path, remote_path)
318
343
  # add fixed parameters if any (for COS)
319
344
  @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
320
345
  # prepare payload for single request
@@ -322,7 +347,7 @@ module Aspera
322
347
  # only one request, so only one answer
323
348
  transfer_spec = @api_node.create('files/sync_setup', setup_payload)['transfer_specs'].first['transfer_spec']
324
349
  # API returns null tag... but async does not like it
325
- transfer_spec.delete_if{ |_k, v| v.nil? }
350
+ transfer_spec.delete_if{ |_k, v| v.nil?}
326
351
  # delete this part, as the returned value contains only destination, and not sources
327
352
  # transfer_spec.delete('paths') if command.eql?(:upload)
328
353
  Log.log.debug{Log.dump(:ts, transfer_spec)}
@@ -335,7 +360,7 @@ module Aspera
335
360
  request_transfer_spec[:paths] = if command.eql?(:download)
336
361
  transfer.ts_source_paths
337
362
  else
338
- [{ destination: transfer.destination_folder(Transfer::Spec::DIRECTION_SEND) }]
363
+ [{destination: transfer.destination_folder(Transfer::Spec::DIRECTION_SEND)}]
339
364
  end
340
365
  # add fixed parameters if any (for COS)
341
366
  @api_node.add_tspec_info(request_transfer_spec) if @api_node.respond_to?(:add_tspec_info)
@@ -346,32 +371,30 @@ module Aspera
346
371
  # delete this part, as the returned value contains only destination, and not sources
347
372
  transfer_spec.delete('paths') if command.eql?(:upload)
348
373
  return Main.result_transfer(transfer.start(transfer_spec))
349
- when :http_node_download
350
- remote_path = get_one_argument_with_prefix(prefix_path, 'remote path')
351
- file_name = File.basename(remote_path)
352
- @api_node.call(
374
+ when :cat
375
+ remote_path = get_one_argument_with_prefix('remote path')
376
+ File.basename(remote_path)
377
+ result = @api_node.call(
353
378
  operation: 'GET',
354
- subpath: "files/#{URI.encode_www_form_component(remote_path)}/contents",
355
- save_to_file: File.join(transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE), file_name))
356
- return Main.result_status("downloaded: #{file_name}")
379
+ subpath: "files/#{URI.encode_www_form_component(remote_path)}/contents")
380
+ return Main.result_text(result[:http].body)
357
381
  when :transport
358
- return {type: :single_object, data: @api_node.transport_params}
382
+ return Main.result_single_object(@api_node.transport_params)
359
383
  end
360
384
  Aspera.error_unreachable_line
361
385
  end
362
386
 
363
387
  # common API to node and Shares
364
- # prefix_path is used to list remote sources in Faspex
365
- def execute_simple_common(command, prefix_path)
388
+ def execute_simple_common(command)
366
389
  case command
367
390
  when *COMMANDS_GEN3
368
- execute_command_gen3(command, prefix_path)
391
+ execute_command_gen3(command)
369
392
  when :access_keys
370
393
  ak_command = options.get_next_command(%i[do set_bearer_key].concat(Plugin::ALL_OPS))
371
394
  case ak_command
372
395
  when *Plugin::ALL_OPS
373
396
  return entity_command(ak_command, @api_node, 'access_keys') do |field, value|
374
- raise 'only selector: %id:self' unless field.eql?('id') && value.eql?('self')
397
+ raise Cli::BadIdentifier, 'only selector: %id:self' unless field.eql?('id') && value.eql?('self')
375
398
  @api_node.read('access_keys/self')['id']
376
399
  end
377
400
  when :do
@@ -410,11 +433,12 @@ module Aspera
410
433
  end
411
434
  begin
412
435
  @api_node.call(
413
- operation: 'POST',
414
- subpath: 'services/soap/Transfer-201210',
415
- headers: {'Content-Type' => 'text/xml;charset=UTF-8', 'SOAPAction' => 'FASPSessionNET-200911#GetSessionInfo'},
416
- body: CENTRAL_SOAP_API_TEST,
417
- body_type: :text)[:http].body
436
+ operation: 'POST',
437
+ subpath: 'services/soap/Transfer-201210',
438
+ content_type: Rest::MIME_TEXT,
439
+ body: CENTRAL_SOAP_API_TEST,
440
+ headers: {'Content-Type' => 'text/xml;charset=UTF-8', 'SOAPAction' => 'FASPSessionNET-200911#GetSessionInfo'}
441
+ )[:http].body
418
442
  nagios.add_ok('central', 'accessible by node')
419
443
  rescue StandardError => e
420
444
  nagios.add_critical('central', e.to_s)
@@ -422,22 +446,22 @@ module Aspera
422
446
  return nagios.result
423
447
  when :events
424
448
  events = @api_node.read('events', query_read_delete)
425
- return { type: :object_list, data: events, fields: ->(f){!f.start_with?('data')} }
449
+ return Main.result_object_list(events, fields: ->(f){!f.start_with?('data')})
426
450
  when :info
427
451
  nd_info = @api_node.read('info')
428
- return { type: :single_object, data: nd_info}
452
+ return Main.result_single_object(nd_info)
429
453
  when :slash
430
454
  nd_info = @api_node.read('')
431
- return { type: :single_object, data: nd_info}
455
+ return Main.result_single_object(nd_info)
432
456
  when :license
433
457
  # requires: asnodeadmin -mu <node user> --acl-add=internal --internal
434
458
  node_license = @api_node.read('license')
435
459
  if node_license['failure'].is_a?(String) && node_license['failure'].include?('ACL')
436
460
  Log.log.error('server must have: asnodeadmin -mu <node user> --acl-add=internal --internal')
437
461
  end
438
- return {type: :single_object, data: node_license}
462
+ return Main.result_single_object(node_license)
439
463
  when :api_details
440
- return {type: :single_object, data: {base_url: @api_node.base_url}.merge(@api_node.params)}
464
+ return Main.result_single_object({base_url: @api_node.base_url}.merge(@api_node.params))
441
465
  end
442
466
  end
443
467
 
@@ -445,7 +469,7 @@ module Aspera
445
469
  # @return [Hash] api and main file id for given path or id in next argument
446
470
  def apifid_from_next_arg(top_file_id)
447
471
  file_path = instance_identifier(description: 'path or %id:<id>') do |attribute, value|
448
- raise 'Only selection "id" is supported (file id)' unless attribute.eql?('id')
472
+ raise Cli::BadIdentifier, 'Only selection "id" is supported (file id)' unless attribute.eql?('id')
449
473
  # directly return result for method
450
474
  return {api: @api_node, file_id: value}
451
475
  end
@@ -456,7 +480,7 @@ module Aspera
456
480
  def execute_command_gen4(command_repo, top_file_id)
457
481
  override_file_id = options.get_option(:root_id)
458
482
  top_file_id = override_file_id unless override_file_id.nil?
459
- raise 'Specify root file id with option root_id' if top_file_id.nil?
483
+ raise Cli::Error, 'Specify root file id with option root_id' if top_file_id.nil?
460
484
  case command_repo
461
485
  when :v3
462
486
  # NOTE: other common actions are unauthorized with user scope
@@ -480,7 +504,7 @@ module Aspera
480
504
  result[:password] = apifid[:api].oauth.authorization
481
505
  else Aspera.error_unreachable_line
482
506
  end
483
- return {type: :single_object, data: result} if command_repo.eql?(:node_info)
507
+ return Main.result_single_object(result) if command_repo.eql?(:node_info)
484
508
  # check format of bearer token
485
509
  OAuth::Factory.bearer_extract(result[:password])
486
510
  return Main.result_status(result[:password])
@@ -489,20 +513,48 @@ module Aspera
489
513
  file_info = apifid[:api].read_with_cache("files/#{apifid[:file_id]}")
490
514
  unless file_info['type'].eql?('folder')
491
515
  # a single file
492
- return {type: :object_list, data: [file_info], fields: GEN4_LS_FIELDS}
516
+ return Main.result_object_list([file_info], fields: GEN4_LS_FIELDS)
493
517
  end
494
- return {type: :object_list, data: apifid[:api].list_files(apifid[:file_id]), fields: GEN4_LS_FIELDS}
518
+ return Main.result_object_list(apifid[:api].list_files(apifid[:file_id]), fields: GEN4_LS_FIELDS)
495
519
  when :find
496
520
  apifid = apifid_from_next_arg(top_file_id)
497
521
  find_lambda = Api::Node.file_matcher_from_argument(options)
498
- return {type: :object_list, data: @api_node.find_files(apifid[:file_id], find_lambda), fields: ['path']}
499
- when :mkdir
522
+ return Main.result_object_list(@api_node.find_files(apifid[:file_id], find_lambda), fields: ['path'])
523
+ when :mkdir, :mklink, :mkfile
500
524
  containing_folder_path = options.get_next_argument('path').split(Api::Node::PATH_SEPARATOR)
501
- new_folder = containing_folder_path.pop
502
- # add trailing slash to force last link to be resolved
525
+ new_item = containing_folder_path.pop
503
526
  apifid = @api_node.resolve_api_fid(top_file_id, containing_folder_path.join(Api::Node::PATH_SEPARATOR), true)
504
- result = apifid[:api].create("files/#{apifid[:file_id]}/files", {name: new_folder, type: :folder})
505
- return Main.result_status("created: #{result['name']} (id=#{result['id']})")
527
+ query = options.get_option(:query, mandatory: false)
528
+ check_exists = true
529
+ payload = {name: new_item}
530
+ if query
531
+ check_exists = !query.delete('check').eql?(false)
532
+ target = query.delete('target')
533
+ if target
534
+ target_apifid = @api_node.resolve_api_fid(top_file_id, target, true)
535
+ payload[:target_id] = target_apifid[:file_id]
536
+ end
537
+ payload.merge!(query.symbolize_keys)
538
+ end
539
+ if check_exists
540
+ folder_content = apifid[:api].read("files/#{apifid[:file_id]}/files")
541
+ link_name = ".#{new_item}.asp-lnk"
542
+ found = folder_content.find{ |i| i['name'].eql?(new_item) || i['name'].eql?(link_name)}
543
+ raise "A #{found['type']} already exists with name #{new_item}" if found
544
+ end
545
+ case command_repo
546
+ when :mkdir
547
+ payload[:type] = :folder
548
+ when :mklink
549
+ payload[:type] = :link
550
+ Aspera.assert(payload[:target_id]){'Missing target_id'}
551
+ Aspera.assert(payload[:target_node_id]){'Missing target_node_id'}
552
+ when :mkfile
553
+ payload[:type] = :file
554
+ payload[:contents] = Base64.strict_encode64(options.get_next_argument('contents'))
555
+ end
556
+ result = apifid[:api].create("files/#{apifid[:file_id]}/files", payload)
557
+ return Main.result_single_object(result)
506
558
  when :rename
507
559
  file_path = options.get_next_argument('source path')
508
560
  apifid = @api_node.resolve_api_fid(top_file_id, file_path)
@@ -562,11 +614,11 @@ module Aspera
562
614
  # TODO: add this ? , 'destination'=>file_info['name']
563
615
  source_paths = [{'source' => '.'}]
564
616
  else
565
- raise "Unknown source type: #{file_info['type']}"
617
+ raise BadArgument, "Unknown source type: #{file_info['type']}"
566
618
  end
567
619
  end
568
620
  return Main.result_transfer(transfer.start(apifid[:api].transfer_spec_gen4(apifid[:file_id], Transfer::Spec::DIRECTION_RECEIVE, {'paths'=>source_paths})))
569
- when :http_node_download
621
+ when :cat
570
622
  source_paths = transfer.ts_source_paths
571
623
  source_folder = source_paths.shift['source']
572
624
  if source_paths.empty?
@@ -577,15 +629,14 @@ module Aspera
577
629
  raise Cli::BadArgument, 'one file at a time only in HTTP mode' if source_paths.length > 1
578
630
  file_name = source_paths.first['source']
579
631
  apifid = @api_node.resolve_api_fid(top_file_id, File.join(source_folder, file_name))
580
- apifid[:api].call(
632
+ result = apifid[:api].call(
581
633
  operation: 'GET',
582
- subpath: "files/#{apifid[:file_id]}/content",
583
- save_to_file: File.join(transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE), file_name))
584
- return Main.result_status("downloaded: #{file_name}")
634
+ subpath: "files/#{apifid[:file_id]}/content")
635
+ return Main.result_text(result[:http].body)
585
636
  when :show
586
637
  apifid = apifid_from_next_arg(top_file_id)
587
638
  items = apifid[:api].read("files/#{apifid[:file_id]}")
588
- return {type: :single_object, data: items}
639
+ return Main.result_single_object(items)
589
640
  when :modify
590
641
  apifid = apifid_from_next_arg(top_file_id)
591
642
  update_param = options.get_next_argument('update data', validation: Hash)
@@ -610,7 +661,7 @@ module Aspera
610
661
  list_query['inherited'] = false if list_query.key?('file_id') && !list_query.key?('inherited')
611
662
  # NOTE: supports per_page and page and header X-Total-Count
612
663
  items = apifid[:api].read('permissions', list_query)
613
- return {type: :object_list, data: items}
664
+ return Main.result_object_list(items)
614
665
  when :show
615
666
  perm_id = instance_identifier
616
667
  return Main.result_single_object(apifid[:api].read("permissions/#{perm_id}"))
@@ -624,7 +675,7 @@ module Aspera
624
675
  end
625
676
  when :create
626
677
  create_param = options.get_next_argument('creation data', validation: Hash)
627
- raise 'no file_id' if create_param.key?('file_id')
678
+ raise Cli::BadArgument, 'no file_id' if create_param.key?('file_id')
628
679
  create_param['file_id'] = apifid[:file_id]
629
680
  create_param['access_levels'] = Api::Node::ACCESS_LEVELS unless create_param.key?('access_levels')
630
681
  # add application specific tags (AoC)
@@ -634,7 +685,7 @@ module Aspera
634
685
  created_data = apifid[:api].create('permissions', create_param)
635
686
  # notify application of creation
636
687
  the_app&.[](:api)&.permissions_send_event(event_data: created_data, app_info: the_app)
637
- return { type: :single_object, data: created_data}
688
+ return Main.result_single_object(created_data)
638
689
  else Aspera.error_unreachable_line
639
690
  end
640
691
  else Aspera.error_unreachable_line
@@ -658,8 +709,8 @@ module Aspera
658
709
  else
659
710
  async_ids = @api_node.read('async/list')['sync_ids']
660
711
  summaries = @api_node.create('async/summary', {'syncs' => async_ids})['sync_summaries']
661
- selected = summaries.find{|s|s['name'].eql?(async_name)}
662
- raise "no such sync: #{async_name}" if selected.nil?
712
+ selected = summaries.find{ |s| s['name'].eql?(async_name)}
713
+ raise Cli::BadIdentifier, "no such sync: #{async_name}" if selected.nil?
663
714
  async_id = selected['snid']
664
715
  async_ids = [async_id]
665
716
  end
@@ -668,22 +719,22 @@ module Aspera
668
719
  case command
669
720
  when :list
670
721
  resp = @api_node.read('async/list')['sync_ids']
671
- return { type: :value_list, data: resp, name: 'id' }
722
+ return Main.result_value_list(resp, name: 'id')
672
723
  when :show
673
724
  resp = @api_node.create('async/summary', post_data)['sync_summaries']
674
725
  return Main.result_empty if resp.empty?
675
- return { type: :object_list, data: resp, fields: %w[snid name local_dir remote_dir] } if async_id.eql?(SpecialValues::ALL)
676
- return { type: :single_object, data: resp.first }
726
+ return Main.result_object_list(resp, fields: %w[snid name local_dir remote_dir]) if async_id.eql?(SpecialValues::ALL)
727
+ return Main.result_single_object(resp.first)
677
728
  when :delete
678
729
  resp = @api_node.create('async/delete', post_data)
679
- return { type: :single_object, data: resp, name: 'id' }
730
+ return Main.result_single_object(resp)
680
731
  when :bandwidth
681
732
  post_data['seconds'] = 100 # TODO: as parameter with --value
682
733
  resp = @api_node.create('async/bandwidth', post_data)
683
734
  data = resp['bandwidth_data']
684
735
  return Main.result_empty if data.empty?
685
736
  data = data.first[async_id]['data']
686
- return { type: :object_list, data: data, name: 'id' }
737
+ return Main.result_object_list(data)
687
738
  when :files
688
739
  # count int
689
740
  # filename str
@@ -706,17 +757,17 @@ module Aspera
706
757
  options.get_option(:username, mandatory: true),
707
758
  async_id]))
708
759
  unless iteration_data.first.nil?
709
- data.select!{|l| l['fnid'].to_i > iteration_data.first}
760
+ data.select!{ |l| l['fnid'].to_i > iteration_data.first}
710
761
  end
711
762
  iteration_data[0] = data.last['fnid'].to_i unless data.empty?
712
763
  end
713
764
  return Main.result_empty if data.empty?
714
765
  skip_ids_persistency&.save
715
- return { type: :object_list, data: data, name: 'id' }
766
+ return Main.result_object_list(data)
716
767
  when :counters
717
768
  resp = @api_node.create('async/counters', post_data)['sync_counters'].first[async_id].last
718
769
  return Main.result_empty if resp.nil?
719
- return { type: :single_object, data: resp }
770
+ return Main.result_single_object(resp)
720
771
  end
721
772
  end
722
773
 
@@ -731,7 +782,7 @@ module Aspera
731
782
  # name is unique, so we can return
732
783
  return id if sync_info[field].eql?(value)
733
784
  end
734
- raise Cli::BadArgument, "no such sync: #{field}=#{value}"
785
+ raise Cli::BadIdentifier, "no such sync: #{field}=#{value}"
735
786
  end
736
787
 
737
788
  ACTIONS = %i[
@@ -745,64 +796,60 @@ module Aspera
745
796
  asperabrowser
746
797
  basic_token
747
798
  bearer_token
748
- simulator].concat(COMMON_ACTIONS).freeze
799
+ simulator
800
+ telemetry
801
+ ].concat(COMMON_ACTIONS).freeze
749
802
 
750
- def execute_action(command=nil, prefix_path=nil)
803
+ def execute_action(command=nil)
751
804
  command ||= options.get_next_command(ACTIONS)
752
805
  case command
753
- when *COMMON_ACTIONS then return execute_simple_common(command, prefix_path)
806
+ when *COMMON_ACTIONS then return execute_simple_common(command)
754
807
  when :async then return execute_async # former API
755
808
  when :ssync
756
809
  # newer API
757
810
  sync_command = options.get_next_command(%i[start stop bandwidth counters files state summary].concat(Plugin::ALL_OPS) - %i[modify])
758
811
  case sync_command
759
- when *Plugin::ALL_OPS then return entity_command(sync_command, @api_node, 'asyncs', item_list_key: 'ids'){|field, value|ssync_lookup(field, value)}
812
+ when *Plugin::ALL_OPS then return entity_command(sync_command, @api_node, 'asyncs', item_list_key: 'ids'){ |field, value| ssync_lookup(field, value)}
760
813
  else
761
- asyncs_id = instance_identifier {|field, value|ssync_lookup(field, value)}
814
+ asyncs_id = instance_identifier{ |field, value| ssync_lookup(field, value)}
762
815
  if %i[start stop].include?(sync_command)
763
816
  @api_node.call(
764
- operation: 'POST',
765
- subpath: "asyncs/#{asyncs_id}/#{sync_command}",
766
- body: '',
767
- body_type: :text)[:http].body
817
+ operation: 'POST',
818
+ subpath: "asyncs/#{asyncs_id}/#{sync_command}",
819
+ content_type: Rest::MIME_TEXT,
820
+ body: ''
821
+ )[:http].body
768
822
  return Main.result_status('Done')
769
823
  end
770
824
  parameters = nil
771
825
  parameters = options.get_option(:query, default: {}) if %i[bandwidth counters files].include?(sync_command)
772
- return { type: :single_object, data: @api_node.read("asyncs/#{asyncs_id}/#{sync_command}", parameters) }
826
+ return Main.result_single_object(@api_node.read("asyncs/#{asyncs_id}/#{sync_command}", parameters))
773
827
  end
774
828
  when :stream
775
829
  command = options.get_next_command(%i[list create show modify cancel])
776
830
  case command
777
831
  when :list
778
832
  resp = @api_node.read('ops/transfers', query_read_delete)
779
- return { type: :object_list, data: resp, fields: %w[id status] } # TODO: useful?
833
+ return Main.result_object_list(resp, fields: %w[id status]) # TODO: useful?
780
834
  when :create
781
835
  resp = @api_node.create('streams', value_create_modify(command: command))
782
- return { type: :single_object, data: resp }
836
+ return Main.result_single_object(resp)
783
837
  when :show
784
838
  resp = @api_node.read("ops/transfers/#{options.get_next_argument('transfer id')}")
785
- return { type: :other_struct, data: resp }
839
+ return Main.result_single_object(resp)
786
840
  when :modify
787
841
  resp = @api_node.update("streams/#{options.get_next_argument('transfer id')}", value_create_modify(command: command))
788
- return { type: :other_struct, data: resp }
842
+ return Main.result_single_object(resp)
789
843
  when :cancel
790
844
  resp = @api_node.cancel("streams/#{options.get_next_argument('transfer id')}")
791
- return { type: :other_struct, data: resp }
792
- else
793
- raise 'error'
845
+ return Main.result_single_object(resp)
846
+ else Aspera.error_unexpected_value(command)
794
847
  end
795
848
  when :transfer
796
849
  command = options.get_next_command(%i[list cancel show modify bandwidth_average sessions])
797
- res_class_path = 'ops/transfers'
798
- if %i[cancel show modify].include?(command)
799
- one_res_id = instance_identifier
800
- one_res_path = "#{res_class_path}/#{one_res_id}"
801
- end
802
850
  case command
803
851
  when :list
804
852
  transfer_filter = query_read_delete(default: {})
805
- last_iteration_token = nil
806
853
  iteration_persistency = nil
807
854
  if options.get_option(:once_only, mandatory: true)
808
855
  iteration_persistency = PersistencyActionOnce.new(
@@ -818,36 +865,10 @@ module Aspera
818
865
  iteration_persistency.save
819
866
  return Main.result_status('Persistency reset')
820
867
  end
821
- last_iteration_token = iteration_persistency.data.first
822
868
  end
823
- raise 'reset only with once_only' if transfer_filter.key?('reset') && iteration_persistency.nil?
869
+ raise Cli::BadArgument, 'reset only with once_only' if transfer_filter.key?('reset') && iteration_persistency.nil?
824
870
  max_items = transfer_filter.delete(MAX_ITEMS)
825
- transfers_data = []
826
- loop do
827
- transfer_filter['iteration_token'] = last_iteration_token unless last_iteration_token.nil?
828
- result = @api_node.call(operation: 'GET', subpath: res_class_path, query: transfer_filter)
829
- # no data
830
- break if result[:data].empty?
831
- # get next iteration token from link
832
- next_iteration_token = nil
833
- link_info = result[:http]['Link']
834
- unless link_info.nil?
835
- m = link_info.match(/<([^>]+)>/)
836
- raise "Cannot parse iteration in Link: #{link_info}" if m.nil?
837
- next_iteration_token = CGI.parse(URI.parse(m[1]).query)['iteration_token']&.first
838
- end
839
- # same as last iteration: stop
840
- break if next_iteration_token&.eql?(last_iteration_token)
841
- last_iteration_token = next_iteration_token
842
- transfers_data.concat(result[:data])
843
- if max_items&.<=(transfers_data.length)
844
- # if !max_items.nil? && (transfers_data.length >= max_items)
845
- transfers_data = transfers_data.slice(0, max_items)
846
- break
847
- end
848
- break if last_iteration_token.nil?
849
- end
850
- iteration_persistency&.data&.[]=(0, last_iteration_token)
871
+ transfers_data = call_with_iteration(api: @api_node, operation: 'GET', subpath: 'ops/transfers', max: max_items, query: transfer_filter, iteration: iteration_persistency&.data)
851
872
  iteration_persistency&.save
852
873
  return {
853
874
  type: :object_list,
@@ -855,8 +876,8 @@ module Aspera
855
876
  fields: %w[id status start_spec.direction start_spec.remote_user start_spec.remote_host start_spec.destination_path]
856
877
  }
857
878
  when :sessions
858
- transfers_data = @api_node.read(res_class_path, query_read_delete)
859
- sessions = transfers_data.map{|t|t['sessions']}.flatten
879
+ transfers_data = @api_node.read('ops/transfers', query_read_delete)
880
+ sessions = transfers_data.map{ |t| t['sessions']}.flatten
860
881
  sessions.each do |session|
861
882
  session['start_time'] = Time.at(session['start_time_usec'] / 1_000_000.0).utc.iso8601(0)
862
883
  session['end_time'] = Time.at(session['end_time_usec'] / 1_000_000.0).utc.iso8601(0)
@@ -867,16 +888,16 @@ module Aspera
867
888
  fields: %w[id status start_time end_time target_rate_kbps]
868
889
  }
869
890
  when :cancel
870
- resp = @api_node.cancel(one_res_path)
871
- return { type: :other_struct, data: resp }
891
+ resp = @api_node.cancel("ops/transfers/#{instance_identifier}")
892
+ return Main.result_single_object(resp)
872
893
  when :show
873
- resp = @api_node.read(one_res_path)
874
- return { type: :other_struct, data: resp }
894
+ resp = @api_node.read("ops/transfers/#{instance_identifier}")
895
+ return Main.result_single_object(resp)
875
896
  when :modify
876
- resp = @api_node.update(one_res_path, options.get_next_argument('update value', validation: Hash))
877
- return { type: :other_struct, data: resp }
897
+ resp = @api_node.update("ops/transfers/#{instance_identifier}", options.get_next_argument('update value', validation: Hash))
898
+ return Main.result_single_object(resp)
878
899
  when :bandwidth_average
879
- transfers_data = @api_node.read(res_class_path, query_read_delete)
900
+ transfers_data = @api_node.read('ops/transfers', query_read_delete)
880
901
  # collect all key dates
881
902
  bandwidth_period = {}
882
903
  dir_info = %i[avg_kbps sessions].freeze
@@ -919,9 +940,8 @@ module Aspera
919
940
  end
920
941
  result.push({start: Time.at(start_date / 1_000_000), end: Time.at(end_date / 1_000_000)}.merge(period_bandwidth))
921
942
  end
922
- return { type: :object_list, data: result }
923
- else
924
- raise 'error'
943
+ return Main.result_object_list(result)
944
+ else Aspera.error_unexpected_value(command)
925
945
  end
926
946
  when :service
927
947
  command = options.get_next_command(%i[list create delete])
@@ -931,7 +951,7 @@ module Aspera
931
951
  case command
932
952
  when :list
933
953
  resp = @api_node.read('rund/services')
934
- return { type: :object_list, data: resp['services'] }
954
+ return Main.result_object_list(resp['services'])
935
955
  when :create
936
956
  # @json:'{"type":"WATCHFOLDERD","run_as":{"user":"user1"}}'
937
957
  params = options.get_next_argument('creation data', validation: Hash)
@@ -957,9 +977,9 @@ module Aspera
957
977
  return Main.result_status("#{resp['id']} created")
958
978
  when :list
959
979
  resp = @api_node.read(res_class_path, query_read_delete)
960
- return { type: :value_list, data: resp['ids'], name: 'id' }
980
+ return Main.result_value_list(resp['ids'], name: 'id')
961
981
  when :show
962
- return { type: :single_object, data: @api_node.read(one_res_path)}
982
+ return Main.result_single_object(@api_node.read(one_res_path))
963
983
  when :modify
964
984
  @api_node.update(one_res_path, options.get_option(:query, mandatory: true))
965
985
  return Main.result_status("#{one_res_id} updated")
@@ -967,7 +987,7 @@ module Aspera
967
987
  @api_node.delete(one_res_path)
968
988
  return Main.result_status("#{one_res_id} deleted")
969
989
  when :state
970
- return { type: :single_object, data: @api_node.read("#{one_res_path}/state") }
990
+ return Main.result_single_object(@api_node.read("#{one_res_path}/state"))
971
991
  end
972
992
  when :central
973
993
  command = options.get_next_command(%i[session file])
@@ -997,7 +1017,7 @@ module Aspera
997
1017
  resp = @api_node.create('services/rest/transfers/v1/files', request_data)
998
1018
  resp = JSON.parse(resp) if resp.is_a?(String)
999
1019
  Log.log.debug{Log.dump(:resp, resp)}
1000
- return { type: :object_list, data: resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path]}
1020
+ return Main.result_object_list(resp['file_transfer_info_result']['file_transfer_info'], fields: %w[session_uuid file_id status path])
1001
1021
  when :modify
1002
1022
  request_data = options.get_next_argument('request data', mandatory: false, validation: Hash, default: {})
1003
1023
  request_data.deep_merge!(validation) unless validation.nil?
@@ -1024,32 +1044,143 @@ module Aspera
1024
1044
  return Main.result_status(Api::Node.bearer_token(payload: token_info, access_key: access_key, private_key: private_key))
1025
1045
  when :simulator
1026
1046
  require 'aspera/node_simulator'
1027
- parameters = value_create_modify(command: command)
1028
- parameters = parameters.symbolize_keys
1029
- raise 'Missing key: url' unless parameters.key?(:url)
1030
- uri = URI.parse(parameters[:url])
1031
- server = WebServerSimple.new(uri, certificate: parameters[:certificate])
1032
- server.mount(uri.path, NodeSimulatorServlet, parameters[:credentials], NodeSimulator.new)
1047
+ parameters = value_create_modify(command: command, default: {}).symbolize_keys
1048
+ uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
1049
+ server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
1050
+ server.mount(uri.path, NodeSimulatorServlet, parameters.except(*WebServerSimple::PARAMS), NodeSimulator.new)
1033
1051
  server.start
1034
1052
  return Main.result_status('Simulator terminated')
1053
+ when :telemetry
1054
+ parameters = value_create_modify(command: command, default: {}).symbolize_keys
1055
+ %i[url key].each do |psym|
1056
+ raise Cli::BadArgument, "Missing parameter: #{psym}" unless parameters.key?(psym)
1057
+ end
1058
+ require 'socket'
1059
+ parameters[:interval] = 10 unless parameters.key?(:interval)
1060
+ parameters[:hostname] = Socket.gethostname unless parameters.key?(:hostname)
1061
+ interval = parameters[:interval].to_f
1062
+ raise Cli::BadArgument, 'Interval must be a positive number in seconds' if interval <= 0
1063
+ otel_api = Rest.new(
1064
+ base_url: "#{parameters[:url]}/v1",
1065
+ headers: {
1066
+ # 'Authorization' => "apiToken #{parameters[:key]}",
1067
+ 'x-instana-key' => parameters[:key],
1068
+ 'x-instana-host' => parameters[:hostname]
1069
+ }
1070
+ )
1071
+ datapoint = {
1072
+ attributes: [
1073
+ {
1074
+ key: 'server.name',
1075
+ value: {
1076
+ stringValue: 'HSTS1'
1077
+ }
1078
+ }
1079
+ ],
1080
+ asInt: nil,
1081
+ timeUnixNano: nil
1082
+ }
1083
+ # https://opentelemetry.io/docs/specs/otel/metrics/data-model/#gauge
1084
+ metrics = {
1085
+ resourceMetrics: [
1086
+ {
1087
+ resource: {
1088
+ attributes: [
1089
+ {
1090
+ key: 'service.name',
1091
+ value: {
1092
+ stringValue: 'IBMAspera'
1093
+ }
1094
+ }
1095
+ ]
1096
+ },
1097
+ scopeMetrics: [
1098
+ {
1099
+ metrics: [
1100
+ {
1101
+ name: 'active.transfers',
1102
+ description: 'Number of active transfers',
1103
+ unit: '1',
1104
+ gauge: {
1105
+ dataPoints: [
1106
+ datapoint
1107
+ ]
1108
+ }
1109
+ }
1110
+ ]
1111
+ }
1112
+ ]
1113
+ }
1114
+ ]
1115
+ }
1116
+ loop do
1117
+ timestamp = Time.now
1118
+ transfers_data = call_with_iteration(api: @api_node, operation: 'GET', subpath: 'ops/transfers', query: {active_only: true})
1119
+ datapoint[:asInt] = transfers_data.length
1120
+ datapoint[:timeUnixNano] = timestamp.to_i * 1_000_000_000 + timestamp.nsec
1121
+ Log.log.info("#{datapoint[:asInt]} active transfers")
1122
+ # https://www.ibm.com/docs/en/instana-observability/current?topic=instana-backend
1123
+ otel_api.create('metrics', metrics)
1124
+ break if interval.eql?(0.0)
1125
+ sleep([0.0, interval - (Time.now - timestamp)].max)
1126
+ end
1035
1127
  end
1036
- raise 'ERROR: shall not reach this line'
1128
+ Aspera.error_unreachable_line
1037
1129
  end
1038
1130
 
1039
1131
  private
1040
1132
 
1041
1133
  # get remaining path arguments from command line, and add prefix
1042
- def get_all_arguments_with_prefix(path_prefix, name)
1134
+ def get_all_arguments_with_prefix(name)
1043
1135
  path_args = options.get_next_argument(name, multiple: true)
1044
- return path_args if path_prefix.nil?
1045
- return path_args.map {|p| File.join(path_prefix, p)}
1136
+ return path_args if @prefix_path.nil?
1137
+ return path_args.map{ |p| File.join(@prefix_path, p)}
1046
1138
  end
1047
1139
 
1048
1140
  # get next path argument from command line, and add prefix
1049
- def get_one_argument_with_prefix(path_prefix, name)
1141
+ def get_one_argument_with_prefix(name)
1050
1142
  path_arg = options.get_next_argument(name, validation: String)
1051
- return path_arg if path_prefix.nil?
1052
- return File.join(path_prefix, path_arg)
1143
+ return path_arg if @prefix_path.nil?
1144
+ return File.join(@prefix_path, path_arg)
1145
+ end
1146
+
1147
+ # Executes the provided API call in loop
1148
+ # @param api [Rest] the API to call
1149
+ # @param iteration [Array] a single element array with the iteration token or nil
1150
+ # @param max [Integer] maximum number of items to return, or nil for no limit
1151
+ # @param query [Hash] query parameters to use for the API call
1152
+ # @param call_args [Hash] additional arguments to pass to the API call
1153
+ # @return [Array] list of items returned by the API call
1154
+ def call_with_iteration(api:, iteration: nil, max: nil, query: nil, **call_args)
1155
+ query_token = query.clone || {}
1156
+ item_list = []
1157
+ query_token[:iteration_token] = iteration.first if iteration.is_a?(Array)
1158
+ loop do
1159
+ result = api.call(**call_args, query: query_token)
1160
+ Aspera.assert_type(result[:data], Array){"Expected data to be an Array, got: #{result[:data].class}"}
1161
+ # no data
1162
+ break if result[:data].empty?
1163
+ # get next iteration token from link
1164
+ next_iteration_token = nil
1165
+ link_info = result[:http]['Link']
1166
+ unless link_info.nil?
1167
+ m = link_info.match(/<([^>]+)>/)
1168
+ Aspera.assert(m){"Cannot parse iteration in Link: #{link_info}"}
1169
+ next_iteration_token = CGI.parse(URI.parse(m[1]).query)['iteration_token']&.first
1170
+ end
1171
+ # same as last iteration: stop
1172
+ break if next_iteration_token&.eql?(query_token[:iteration_token])
1173
+ query_token[:iteration_token] = next_iteration_token
1174
+ item_list.concat(result[:data])
1175
+ if max&.<=(item_list.length)
1176
+ item_list = item_list.slice(0, max)
1177
+ break
1178
+ end
1179
+ break if next_iteration_token.nil?
1180
+ end
1181
+ # save iteration token if needed
1182
+ iteration[0] = query_token[:iteration_token] unless iteration.nil?
1183
+ item_list
1053
1184
  end
1054
1185
  end
1055
1186
  end