aspera-cli 4.24.1 → 4.24.2

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 (87) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +15 -2
  4. data/README.md +745 -436
  5. data/bin/ascli +20 -1
  6. data/bin/asession +23 -27
  7. data/lib/aspera/agent/base.rb +10 -21
  8. data/lib/aspera/agent/connect.rb +2 -3
  9. data/lib/aspera/agent/desktop.rb +2 -2
  10. data/lib/aspera/agent/direct.rb +49 -32
  11. data/lib/aspera/agent/factory.rb +31 -0
  12. data/lib/aspera/api/aoc.rb +79 -49
  13. data/lib/aspera/api/faspex.rb +212 -0
  14. data/lib/aspera/api/node.rb +99 -84
  15. data/lib/aspera/ascp/installation.rb +22 -21
  16. data/lib/aspera/ascp/management.rb +119 -23
  17. data/lib/aspera/assert.rb +14 -8
  18. data/lib/aspera/cli/extended_value.rb +15 -15
  19. data/lib/aspera/cli/formatter.rb +7 -5
  20. data/lib/aspera/cli/hints.rb +8 -0
  21. data/lib/aspera/cli/info.rb +4 -4
  22. data/lib/aspera/cli/main.rb +55 -70
  23. data/lib/aspera/cli/manager.rb +7 -4
  24. data/lib/aspera/cli/plugins/alee.rb +2 -1
  25. data/lib/aspera/cli/plugins/aoc.rb +110 -186
  26. data/lib/aspera/cli/plugins/ats.rb +4 -4
  27. data/lib/aspera/cli/plugins/base.rb +335 -0
  28. data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
  29. data/lib/aspera/cli/plugins/config.rb +249 -220
  30. data/lib/aspera/cli/plugins/console.rb +15 -15
  31. data/lib/aspera/cli/plugins/cos.rb +2 -2
  32. data/lib/aspera/cli/plugins/factory.rb +78 -0
  33. data/lib/aspera/cli/plugins/faspex.rb +17 -20
  34. data/lib/aspera/cli/plugins/faspex5.rb +79 -193
  35. data/lib/aspera/cli/plugins/faspio.rb +14 -13
  36. data/lib/aspera/cli/plugins/httpgw.rb +13 -12
  37. data/lib/aspera/cli/plugins/node.rb +34 -32
  38. data/lib/aspera/cli/plugins/oauth.rb +48 -0
  39. data/lib/aspera/cli/plugins/orchestrator.rb +15 -13
  40. data/lib/aspera/cli/plugins/preview.rb +4 -4
  41. data/lib/aspera/cli/plugins/server.rb +15 -13
  42. data/lib/aspera/cli/plugins/shares.rb +18 -15
  43. data/lib/aspera/cli/sync_actions.rb +1 -1
  44. data/lib/aspera/cli/transfer_agent.rb +24 -20
  45. data/lib/aspera/cli/transfer_progress.rb +6 -6
  46. data/lib/aspera/cli/version.rb +3 -3
  47. data/lib/aspera/cli/wizard.rb +65 -53
  48. data/lib/aspera/colors.rb +6 -0
  49. data/lib/aspera/command_line_builder.rb +45 -50
  50. data/lib/aspera/command_line_converter.rb +2 -1
  51. data/lib/aspera/coverage.rb +1 -1
  52. data/lib/aspera/data_repository.rb +1 -1
  53. data/lib/aspera/environment.rb +10 -7
  54. data/lib/aspera/faspex_gw.rb +6 -4
  55. data/lib/aspera/faspex_postproc.rb +1 -1
  56. data/lib/aspera/keychain/macos_security.rb +1 -1
  57. data/lib/aspera/log.rb +37 -9
  58. data/lib/aspera/nagios.rb +1 -1
  59. data/lib/aspera/oauth/base.rb +17 -10
  60. data/lib/aspera/oauth/factory.rb +8 -8
  61. data/lib/aspera/oauth/web.rb +2 -2
  62. data/lib/aspera/products/connect.rb +4 -3
  63. data/lib/aspera/products/desktop.rb +1 -4
  64. data/lib/aspera/products/other.rb +9 -1
  65. data/lib/aspera/products/transferd.rb +0 -1
  66. data/lib/aspera/rest.rb +126 -83
  67. data/lib/aspera/ssh.rb +3 -3
  68. data/lib/aspera/sync/args.schema.yaml +46 -3
  69. data/lib/aspera/sync/conf.schema.yaml +130 -94
  70. data/lib/aspera/sync/operations.rb +16 -16
  71. data/lib/aspera/temp_file_manager.rb +17 -5
  72. data/lib/aspera/transfer/error.rb +16 -7
  73. data/lib/aspera/transfer/parameters.rb +34 -20
  74. data/lib/aspera/transfer/resumer.rb +74 -0
  75. data/lib/aspera/transfer/spec.rb +4 -3
  76. data/lib/aspera/transfer/spec.schema.yaml +132 -51
  77. data/lib/aspera/transfer/spec_doc.rb +41 -35
  78. data/lib/aspera/uri_reader.rb +1 -1
  79. data/lib/aspera/web_auth.rb +6 -6
  80. data.tar.gz.sig +0 -0
  81. metadata +9 -7
  82. metadata.gz.sig +0 -0
  83. data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
  84. data/lib/aspera/cli/plugin.rb +0 -333
  85. data/lib/aspera/cli/plugin_factory.rb +0 -81
  86. data/lib/aspera/resumer.rb +0 -77
  87. data/lib/aspera/transfer/error_info.rb +0 -91
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/cli/extended_value'
4
+ require 'aspera/assert'
5
+
6
+ module Aspera
7
+ module Cli
8
+ module Plugins
9
+ # Base class for command plugins
10
+ class Base
11
+ # Operations without id (create list)
12
+ GLOBAL_OPS = %i[create list].freeze
13
+ # Operations with id (modify delete show)
14
+ INSTANCE_OPS = %i[modify delete show].freeze
15
+ # All standard operations (create list modify delete show)
16
+ ALL_OPS = (GLOBAL_OPS + INSTANCE_OPS).freeze
17
+ # Special query parameter: `max`: max number of items for list command
18
+ MAX_ITEMS = 'max'
19
+ # Special query parameter: `pmax`: max number of pages for list command
20
+ MAX_PAGES = 'pmax'
21
+ # Special identifier format: look for this name to find where supported
22
+ REGEX_LOOKUP_ID_BY_FIELD = /^%([^:]+):(.*)$/
23
+
24
+ class << self
25
+ def declare_options(options)
26
+ options.declare(:query, 'Additional filter for for some commands (list/delete)', types: [Hash, Array])
27
+ options.declare(:property, 'Name of property to set (modify operation)')
28
+ options.declare(:bulk, 'Bulk operation (only some)', values: :bool, default: :no)
29
+ options.declare(:bfail, 'Bulk operation error handling', values: :bool, default: :yes)
30
+ end
31
+ end
32
+
33
+ def initialize(context:)
34
+ # Check presence in descendant of mandatory method and constant
35
+ Aspera.assert(respond_to?(:execute_action), type: InternalError){"Missing method 'execute_action' in #{self.class}"}
36
+ Aspera.assert(self.class.constants.include?(:ACTIONS), type: InternalError){"Missing constant 'ACTIONS' in #{self.class}"}
37
+ @context = context
38
+ add_manual_header if @context.man_header
39
+ end
40
+
41
+ # Global objects
42
+ attr_reader :context
43
+
44
+ def options; @context.options; end
45
+ def transfer; @context.transfer; end
46
+ def config; @context.config; end
47
+ def formatter; @context.formatter; end
48
+ def persistency; @context.persistency; end
49
+
50
+ def add_manual_header(has_options = true)
51
+ # Manual header for all plugins
52
+ options.parser.separator('')
53
+ options.parser.separator("COMMAND: #{self.class.name.split('::').last.downcase}")
54
+ options.parser.separator("SUBCOMMANDS: #{self.class.const_get(:ACTIONS).map(&:to_s).sort.join(' ')}")
55
+ options.parser.separator('OPTIONS:') if has_options
56
+ end
57
+
58
+ # Must be called AFTER the instance action:
59
+ # ... folder browse _call_instance_identifier
60
+ #
61
+ # @param description [String] description of the identifier
62
+ # @param as_option [Symbol] option name to use if identifier is an option
63
+ # @param block [Proc] block to search for identifier based on attribute value
64
+ # @return [String, Array] identifier or list of ids
65
+ def instance_identifier(description: 'identifier', as_option: nil, &block)
66
+ if as_option.nil?
67
+ res_id = options.get_next_argument(description, multiple: options.get_option(:bulk)) if res_id.nil?
68
+ else
69
+ res_id = options.get_option(as_option)
70
+ end
71
+ # Can be an Array
72
+ if res_id.is_a?(String) && (m = res_id.match(REGEX_LOOKUP_ID_BY_FIELD))
73
+ if block
74
+ res_id = yield(m[1], ExtendedValue.instance.evaluate(m[2]))
75
+ else
76
+ raise Cli::BadArgument, "Percent syntax for #{description} not supported in this context"
77
+ end
78
+ end
79
+ return res_id
80
+ end
81
+
82
+ # For create and delete operations: execute one action or multiple if bulk is yes
83
+ # @param command [Symbol] operation: :create, :delete, ...
84
+ # @param descr [String] description of the value
85
+ # @param values [Object] the value(s), or the type of value to get from user
86
+ # @param id_result [String] key in result hash to use as identifier
87
+ # @param fields [Array] fields to display
88
+ # @param &block [Proc] block to execute for each value
89
+ def do_bulk_operation(command:, descr: nil, values: Hash, id_result: 'id', fields: :default)
90
+ Aspera.assert(block_given?){'missing block'}
91
+ is_bulk = options.get_option(:bulk)
92
+ case values
93
+ when :identifier
94
+ values = instance_identifier(description: descr)
95
+ when Class
96
+ values = value_create_modify(command: command, description: descr, type: values, bulk: is_bulk)
97
+ end
98
+ # If not bulk, there is a single value
99
+ params = is_bulk ? values : [values]
100
+ Log.log.warn('Empty list given for bulk operation') if params.empty?
101
+ Log.dump(:bulk_operation, params)
102
+ result_list = []
103
+ params.each do |param|
104
+ # Init for delete
105
+ result = {id_result => param}
106
+ begin
107
+ # Execute custom code
108
+ res = yield(param)
109
+ # If block returns a hash, let's use this (create)
110
+ result = res if res.is_a?(Hash)
111
+ # TODO: remove when faspio gw api fixes this
112
+ result = res.first if res.is_a?(Array) && res.first.is_a?(Hash)
113
+ # Create -> created
114
+ result['status'] = "#{command}#{'e' unless command.to_s.end_with?('e')}d".gsub(/yed$/, 'ied')
115
+ rescue StandardError => e
116
+ raise e if options.get_option(:bfail)
117
+ result['status'] = e.to_s
118
+ end
119
+ result_list.push(result)
120
+ end
121
+ display_fields = [id_result, 'status']
122
+ if is_bulk
123
+ return Main.result_object_list(result_list, fields: display_fields)
124
+ else
125
+ display_fields = fields unless fields.eql?(:default)
126
+ return Main.result_single_object(result_list.first, fields: display_fields)
127
+ end
128
+ end
129
+
130
+ # Operations: Create, Delete, Show, List, Modify
131
+ # @param api [Rest] api to use
132
+ # @param entity [String] sub path in URL to resource relative to base url
133
+ # @param command [Symbol] command to execute: create show list modify delete
134
+ # @param display_fields [Array] fields to display by default
135
+ # @param items_key [String] result is in a sub key of the json
136
+ # @param delete_style [String] if set, the delete operation by array in payload
137
+ # @param id_as_arg [String] if set, the id is provided as url argument ?<id_as_arg>=<id>
138
+ # @param is_singleton [Boolean] if true, entity is the full path to the resource
139
+ # @param tclo [Bool] if set, :list use paging with total_count, limit, offset
140
+ # @param block [Proc] block to search for identifier based on attribute value
141
+ # @return result suitable for CLI result
142
+ def entity_execute(
143
+ api:,
144
+ entity:,
145
+ command: nil,
146
+ display_fields: nil,
147
+ items_key: nil,
148
+ delete_style: nil,
149
+ id_as_arg: false,
150
+ is_singleton: false,
151
+ list_query: nil,
152
+ tclo: false,
153
+ &block
154
+ )
155
+ command = options.get_next_command(ALL_OPS) if command.nil?
156
+ if is_singleton
157
+ one_res_path = entity
158
+ elsif INSTANCE_OPS.include?(command)
159
+ one_res_id = instance_identifier(&block)
160
+ one_res_path = "#{entity}/#{one_res_id}"
161
+ one_res_path = "#{entity}?#{id_as_arg}=#{one_res_id}" if id_as_arg
162
+ end
163
+
164
+ case command
165
+ when :create
166
+ raise BadArgument, 'cannot create singleton' if is_singleton
167
+ return do_bulk_operation(command: command, descr: 'data', fields: display_fields) do |params|
168
+ api.create(entity, params)
169
+ end
170
+ when :delete
171
+ raise BadArgument, 'cannot delete singleton' if is_singleton
172
+ if !delete_style.nil?
173
+ one_res_id = [one_res_id] unless one_res_id.is_a?(Array)
174
+ Aspera.assert_type(one_res_id, Array, type: Cli::BadArgument)
175
+ api.call(
176
+ operation: 'DELETE',
177
+ subpath: entity,
178
+ content_type: Rest::MIME_JSON,
179
+ body: {delete_style => one_res_id},
180
+ headers: {'Accept' => Rest::MIME_JSON}
181
+ )
182
+ return Main.result_status('deleted')
183
+ end
184
+ return do_bulk_operation(command: command, values: one_res_id) do |one_id|
185
+ api.delete("#{entity}/#{one_id}", query_read_delete)
186
+ {'id' => one_id}
187
+ end
188
+ when :show
189
+ return Main.result_single_object(api.read(one_res_path), fields: display_fields)
190
+ when :list
191
+ if tclo
192
+ data, total = list_entities_limit_offset_total_count(api: api, entity:, items_key: items_key, query: query_read_delete(default: list_query))
193
+ return Main.result_object_list(data, total: total, fields: display_fields)
194
+ end
195
+ resp = api.call(operation: 'GET', subpath: entity, headers: {'Accept' => Rest::MIME_JSON}, query: query_read_delete)
196
+ return Main.result_empty if resp[:http].code == '204'
197
+ data = resp[:data]
198
+ # TODO: not generic : which application is this for ?
199
+ if resp[:http]['Content-Type'].start_with?('application/vnd.api+json')
200
+ Log.log.debug('is vnd.api')
201
+ data = data[entity]
202
+ end
203
+ data = data[items_key] if items_key
204
+ case data
205
+ when Hash
206
+ return Main.result_single_object(data, fields: display_fields)
207
+ when Array
208
+ return Main.result_object_list(data, fields: display_fields) if data.empty? || data.first.is_a?(Hash)
209
+ return Main.result_value_list(data)
210
+ else
211
+ raise "An error occurred: unexpected result type for list: #{data.class}"
212
+ end
213
+ when :modify
214
+ parameters = value_create_modify(command: command)
215
+ property = options.get_option(:property)
216
+ parameters = {property => parameters} unless property.nil?
217
+ api.update(one_res_path, parameters)
218
+ return Main.result_status('modified')
219
+ else
220
+ raise "unknown action: #{command}"
221
+ end
222
+ end
223
+
224
+ # Query parameters in URL suitable for REST: list/GET and delete/DELETE
225
+ def query_read_delete(default: nil)
226
+ query = options.get_option(:query)
227
+ # Dup default, as it could be frozen
228
+ query = default.dup if query.nil?
229
+ Log.log.debug{"query_read_delete=#{query}".bg_red}
230
+ begin
231
+ # Check it is suitable
232
+ URI.encode_www_form(query) unless query.nil?
233
+ rescue StandardError => e
234
+ raise Cli::BadArgument, "Query must be an extended value (Hash, Array) which can be encoded with URI.encode_www_form. Refer to manual. (#{e.message})"
235
+ end
236
+ return query
237
+ end
238
+
239
+ # Retrieves an extended value from command line, used for creation or modification of entities
240
+ # @param command [Symbol] command name for error message
241
+ # @param type [Class] expected type of value, either a Class, an Array of Class
242
+ # @param bulk [Boolean] if true, value must be an Array of <type>
243
+ # @param default [Object] default value if not provided
244
+ def value_create_modify(command:, description: nil, type: Hash, bulk: false, default: nil)
245
+ value = options.get_next_argument(
246
+ "parameters for #{command}#{" (#{description})" unless description.nil?}", mandatory: default.nil?,
247
+ validation: bulk ? Array : type
248
+ )
249
+ value = default if value.nil?
250
+ unless type.nil?
251
+ type = [type] unless type.is_a?(Array)
252
+ Aspera.assert(type.all?(Class)){"check types must be a Class, not #{type.map(&:class).join(',')}"}
253
+ if bulk
254
+ Aspera.assert_type(value, Array, type: Cli::BadArgument)
255
+ value.each do |v|
256
+ Aspera.assert_values(v.class, type, type: Cli::BadArgument)
257
+ end
258
+ else
259
+ Aspera.assert_values(value.class, type, type: Cli::BadArgument)
260
+ end
261
+ end
262
+ return value
263
+ end
264
+
265
+ # Get a (full or partial) list of all entities of a given type with query: offset/limit
266
+ # @param `api` [Rest] the API object
267
+ # @param `entity` [String,Symbol] the API endpoint of entity to list
268
+ # @param `items_key` [String] key in the result to get the list of items
269
+ # @param `query` [Hash,nil] additional query parameters
270
+ # @return [Array] items, total_count
271
+ def list_entities_limit_offset_total_count(
272
+ api:,
273
+ entity:,
274
+ items_key: nil,
275
+ query: nil
276
+ )
277
+ entity = entity.to_s if entity.is_a?(Symbol)
278
+ items_key = entity.split('/').last if items_key.nil?
279
+ query = {} if query.nil?
280
+ Aspera.assert_type(entity, String)
281
+ Aspera.assert_type(items_key, String)
282
+ Aspera.assert_type(query, Hash)
283
+ Log.log.debug{"list_entities t=#{entity} k=#{items_key} q=#{query}"}
284
+ result = []
285
+ offset = 0
286
+ max_items = query.delete(MAX_ITEMS)
287
+ remain_pages = query.delete(MAX_PAGES)
288
+ # Merge default parameters, by default 100 per page
289
+ query = {'limit'=> PER_PAGE_DEFAULT}.merge(query)
290
+ total_count = nil
291
+ loop do
292
+ query['offset'] = offset
293
+ page_result = api.read(entity, query)
294
+ Aspera.assert_type(page_result[items_key], Array)
295
+ result.concat(page_result[items_key])
296
+ # Reach the limit set by user ?
297
+ if !max_items.nil? && (result.length >= max_items)
298
+ result = result.slice(0, max_items)
299
+ break
300
+ end
301
+ total_count ||= page_result['total_count']
302
+ break if result.length >= total_count
303
+ remain_pages -= 1 unless remain_pages.nil?
304
+ break if remain_pages == 0
305
+ offset += page_result[items_key].length
306
+ formatter.long_operation_running
307
+ end
308
+ formatter.long_operation_terminated
309
+ return result, total_count
310
+ end
311
+
312
+ # Lookup an entity id from its name
313
+ # @param entity [String] the type of entity to lookup, by default it is the path, and it is also the field name in result
314
+ # @param value [String] the value to lookup
315
+ # @param field [String] the field to match, by default it is 'name'
316
+ # @param items_key [String] key in the result to get the list of items (override entity)
317
+ # @param query [Hash] additional query parameters
318
+ def lookup_entity_by_field(api:, entity:, value:, field: 'name', items_key: nil, query: :default)
319
+ if query.eql?(:default)
320
+ Aspera.assert(field.eql?('name')){'Default query is on name only'}
321
+ query = {'q'=> value}
322
+ end
323
+ found = list_entities_limit_offset_total_count(api: api, entity: entity, items_key: items_key, query: query).first.select{ |i| i[field].eql?(value)}
324
+ case found.length
325
+ when 0 then raise "No #{entity} with #{field} = #{value}"
326
+ when 1 then return found.first
327
+ else raise "Found #{found.length} #{entity} with #{field} = #{value}"
328
+ end
329
+ end
330
+ PER_PAGE_DEFAULT = 1000
331
+ private_constant :PER_PAGE_DEFAULT
332
+ end
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/rest'
4
+ require 'aspera/cli/plugins/base'
5
+
6
+ module Aspera
7
+ module Cli
8
+ module Plugins
9
+ # base class for applications supporting basic authentication
10
+ class BasicAuth < Base
11
+ class << self
12
+ def declare_options(options)
13
+ options.declare(:url, 'URL of application, e.g. https://app.example.com/aspera/app')
14
+ options.declare(:username, "User's identifier")
15
+ options.declare(:password, "User's password")
16
+ options.parse_options!
17
+ end
18
+ end
19
+
20
+ def initialize(context:, basic_options: true)
21
+ super(context: context)
22
+ BasicAuth.declare_options(options) if basic_options
23
+ end
24
+
25
+ # returns a Rest object with basic auth
26
+ def basic_auth_params(subpath = nil)
27
+ api_url = options.get_option(:url, mandatory: true)
28
+ api_url = "#{api_url}/#{subpath}" unless subpath.nil?
29
+ return {
30
+ base_url: api_url,
31
+ auth: {
32
+ type: :basic,
33
+ username: options.get_option(:username, mandatory: true),
34
+ password: options.get_option(:password, mandatory: true)
35
+ }
36
+ }
37
+ end
38
+
39
+ def basic_auth_api(subpath = nil)
40
+ return Rest.new(**basic_auth_params(subpath))
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end