aspera-cli 4.27.2 → 4.27.4

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 (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +133 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +3 -1
  6. data/docs/README.md +1006 -766
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +6 -8
  9. data/lib/aspera/agent/desktop.rb +2 -6
  10. data/lib/aspera/agent/direct.rb +52 -22
  11. data/lib/aspera/agent/node.rb +9 -8
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/alee.rb +1 -1
  14. data/lib/aspera/api/aoc.rb +14 -12
  15. data/lib/aspera/api/ats.rb +1 -1
  16. data/lib/aspera/api/cos_node.rb +2 -2
  17. data/lib/aspera/api/faspex.rb +11 -7
  18. data/lib/aspera/api/httpgw.rb +38 -35
  19. data/lib/aspera/api/node.rb +39 -34
  20. data/lib/aspera/ascmd.rb +3 -1
  21. data/lib/aspera/ascp/installation.rb +63 -28
  22. data/lib/aspera/ascp/management.rb +1 -0
  23. data/lib/aspera/assert.rb +4 -0
  24. data/lib/aspera/cli/ascp_actions.rb +20 -41
  25. data/lib/aspera/cli/async_transfer_store.rb +12 -11
  26. data/lib/aspera/cli/bootstrapper.rb +14 -16
  27. data/lib/aspera/cli/command_line.rb +252 -0
  28. data/lib/aspera/cli/command_registry.rb +215 -37
  29. data/lib/aspera/cli/command_spec.rb +104 -15
  30. data/lib/aspera/cli/completion/ascli.bash +12 -0
  31. data/lib/aspera/cli/completion/ascli.fish +16 -0
  32. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  33. data/lib/aspera/cli/context.rb +3 -0
  34. data/lib/aspera/cli/deprecation.rb +37 -0
  35. data/lib/aspera/cli/extended_value.rb +6 -3
  36. data/lib/aspera/cli/formatter.rb +94 -80
  37. data/lib/aspera/cli/gem_checker.rb +1 -1
  38. data/lib/aspera/cli/hints.rb +7 -6
  39. data/lib/aspera/cli/http.rb +22 -34
  40. data/lib/aspera/cli/info.rb +3 -0
  41. data/lib/aspera/cli/mcp_tool.rb +47 -83
  42. data/lib/aspera/cli/option_declarator.rb +33 -42
  43. data/lib/aspera/cli/option_registry.rb +69 -0
  44. data/lib/aspera/cli/option_types.rb +105 -0
  45. data/lib/aspera/cli/option_value.rb +278 -0
  46. data/lib/aspera/cli/options.schema.yaml +124 -15
  47. data/lib/aspera/cli/parser.rb +333 -862
  48. data/lib/aspera/cli/plugins/alee.rb +7 -4
  49. data/lib/aspera/cli/plugins/aoc.rb +545 -518
  50. data/lib/aspera/cli/plugins/ats.rb +59 -80
  51. data/lib/aspera/cli/plugins/base.rb +221 -265
  52. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  53. data/lib/aspera/cli/plugins/config.rb +263 -184
  54. data/lib/aspera/cli/plugins/console.rb +103 -39
  55. data/lib/aspera/cli/plugins/cos.rb +6 -23
  56. data/lib/aspera/cli/plugins/factory.rb +3 -0
  57. data/lib/aspera/cli/plugins/faspex5.rb +204 -182
  58. data/lib/aspera/cli/plugins/faspio.rb +6 -11
  59. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  60. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  61. data/lib/aspera/cli/plugins/node.rb +300 -327
  62. data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
  63. data/lib/aspera/cli/plugins/preview.rb +96 -105
  64. data/lib/aspera/cli/plugins/server.rb +78 -53
  65. data/lib/aspera/cli/plugins/shares.rb +80 -131
  66. data/lib/aspera/cli/preset_actions.rb +44 -27
  67. data/lib/aspera/cli/preset_manager.rb +44 -19
  68. data/lib/aspera/cli/prompt.rb +36 -0
  69. data/lib/aspera/cli/result.rb +42 -36
  70. data/lib/aspera/cli/runner.rb +32 -59
  71. data/lib/aspera/cli/special_values.rb +5 -0
  72. data/lib/aspera/cli/sync_actions.rb +51 -46
  73. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  74. data/lib/aspera/cli/transfer_actions.rb +14 -9
  75. data/lib/aspera/cli/transfer_agent.rb +34 -38
  76. data/lib/aspera/cli/transfer_progress.rb +290 -55
  77. data/lib/aspera/cli/vault_manager.rb +0 -17
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +5 -3
  80. data/lib/aspera/coverage.rb +1 -1
  81. data/lib/aspera/environment.rb +35 -5
  82. data/lib/aspera/faspex_gw.rb +2 -1
  83. data/lib/aspera/faspex_postproc.rb +1 -0
  84. data/lib/aspera/graphql.rb +5 -5
  85. data/lib/aspera/json_rpc/client.rb +5 -5
  86. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  87. data/lib/aspera/keychain/factory.rb +2 -1
  88. data/lib/aspera/keychain/one_password_api.rb +1 -1
  89. data/lib/aspera/link_header.rb +2 -2
  90. data/lib/aspera/log.rb +47 -27
  91. data/lib/aspera/markdown.rb +2 -0
  92. data/lib/aspera/mime.rb +25 -0
  93. data/lib/aspera/node_emulator.rb +759 -0
  94. data/lib/aspera/oauth/base.rb +37 -26
  95. data/lib/aspera/oauth/factory.rb +7 -3
  96. data/lib/aspera/oauth/generic.rb +1 -1
  97. data/lib/aspera/oauth/json_credentials.rb +34 -0
  98. data/lib/aspera/oauth/jwt.rb +4 -5
  99. data/lib/aspera/oauth/web.rb +9 -8
  100. data/lib/aspera/oauth.rb +1 -0
  101. data/lib/aspera/persistency_folder.rb +1 -3
  102. data/lib/aspera/preview/file_types.rb +4 -4
  103. data/lib/aspera/preview/generator.rb +11 -1
  104. data/lib/aspera/preview/options.schema.yaml +119 -0
  105. data/lib/aspera/preview/terminal.rb +4 -3
  106. data/lib/aspera/preview/utils.rb +9 -6
  107. data/lib/aspera/products/connect.rb +1 -1
  108. data/lib/aspera/rainbow.rb +7 -0
  109. data/lib/aspera/rest/aspera_errors.rb +72 -0
  110. data/lib/aspera/rest/call_error.rb +27 -0
  111. data/lib/aspera/rest/client.rb +523 -0
  112. data/lib/aspera/rest/error_analyzer.rb +113 -0
  113. data/lib/aspera/rest/list.rb +149 -0
  114. data/lib/aspera/rest/parameters.rb +55 -0
  115. data/lib/aspera/rest/util.rb +176 -0
  116. data/lib/aspera/rest.rb +7 -621
  117. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  118. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  119. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  120. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  121. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  122. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  123. data/lib/aspera/schema/documentation.rb +13 -3
  124. data/lib/aspera/schema/reader.rb +12 -18
  125. data/lib/aspera/schema/registry.rb +23 -1
  126. data/lib/aspera/schema/validator.rb +92 -0
  127. data/lib/aspera/secret_hider.rb +36 -25
  128. data/lib/aspera/string_ext.rb +15 -0
  129. data/lib/aspera/temp_file_manager.rb +6 -5
  130. data/lib/aspera/transfer/parameters.rb +2 -0
  131. data/lib/aspera/transfer/spec.rb +1 -0
  132. data/lib/aspera/uri_reader.rb +11 -11
  133. data/lib/aspera/web_auth/index.html +147 -0
  134. data/lib/aspera/web_auth/server.rb +81 -0
  135. data.tar.gz.sig +0 -0
  136. metadata +43 -9
  137. metadata.gz.sig +0 -0
  138. data/lib/aspera/colors.rb +0 -79
  139. data/lib/aspera/node_simulator.rb +0 -344
  140. data/lib/aspera/preview/options.rb +0 -45
  141. data/lib/aspera/rest_call_error.rb +0 -25
  142. data/lib/aspera/rest_error_analyzer.rb +0 -111
  143. data/lib/aspera/rest_errors_aspera.rb +0 -58
  144. data/lib/aspera/rest_list.rb +0 -136
  145. data/lib/aspera/web_auth.rb +0 -211
@@ -25,7 +25,7 @@ module Aspera
25
25
  error = nil
26
26
  urls.each do |base_url|
27
27
  next unless base_url.match?(%r{^https?://})
28
- api = Rest.new(base_url: base_url)
28
+ api = Rest::Client.new(base_url: base_url)
29
29
  data, http = api.read(TEST_ENDPOINT, query: {format: :json}, ret: :both)
30
30
  next unless data['remote_orchestrator_info']
31
31
  url = http.uri.to_s
@@ -52,47 +52,42 @@ module Aspera
52
52
  username: options.get_option(:username, mandatory: true),
53
53
  password: options.get_option(:password, mandatory: true)
54
54
  },
55
- test_args: 'workflow list'
55
+ test_args: 'workflows list'
56
56
  }
57
57
  end
58
58
 
59
- option :result, description: "Specify result value as: 'work_step:parameter'"
60
- option :synchronous, description: 'Wait for completion', allowed: Type::BOOLEAN, default: false
61
- option :ret_style, description: 'How return type is requested in api', allowed: %i[header arg ext], default: :arg
62
- option :auth_style, description: 'Authentication type', allowed: %i[arg_pass head_basic apikey], default: :head_basic
59
+ option :apikey, description: 'API key (exclusive with username/password)'
60
+ option :auth_style, description: 'Authentication style', allowed: %i[basic query token], default: :token
61
+ option :ret_style, description: 'Method to specify the expected response format ("Accept")', allowed: %i[header query path], default: :query
62
+ option :result, description: "Specify result value as: 'work_step:parameter'", deprecation: {last: '4.27.2', message: 'use keys `step` and `variable` of argument `execution` of `workflows start`'}
63
+ option :synchronous, description: 'Wait for completion', allowed: Type::BOOLEAN, deprecation: {last: '4.27.2', message: 'use key `synchronous` of argument `execution` of `workflows start`'}
63
64
 
64
- def initialize(**_)
65
- super
66
- @api_orch = nil
67
- options.parse_options!
68
- end
69
-
70
- # Call orchestrator API, it's a bit special
65
+ # Call orchestrator API (handles ret_style negotiation and XML parsing)
71
66
  # @param endpoint [String] the endpoint to call
72
- # @param ret_style [Symbol] the return style, :header, :arg, :ext(extension)
67
+ # @param body [Hash] the body to pass (JSON); also implies POST
73
68
  # @param format [String] the format to request, 'json', 'xml', nil
74
- # @param args [Hash] the arguments to pass
69
+ # @param query [Hash] the arguments to pass as query parameters
75
70
  # @param xml_arrays [Boolean] if true, force arrays in xml parsing
76
71
  # @param http [Boolean] if true, returns the HttpResponse, else
77
- def call_ao(endpoint, ret_style: nil, format: 'json', args: nil, xml_arrays: true, http: false)
78
- # calls are all GET
79
- call_args = {operation: 'GET', subpath: "api/#{endpoint}", ret: :both, query: {}}
80
- ret_style = options.get_option(:ret_style, mandatory: true) if ret_style.nil?
81
- call_args[:query].merge!(args) unless args.nil?
72
+ def call_ao(endpoint, body: nil, format: 'json', query: nil, xml_arrays: true, http: false)
73
+ call_args = {operation: body.nil? ? 'GET' : 'POST', subpath: "api/#{endpoint}", ret: :both, query: {}}
74
+ call_args.merge!(body: body, content_type: Mime::JSON) unless body.nil? # rubocop:disable Performance/RedundantMerge
75
+ ret_style = options.get_option(:ret_style, mandatory: true)
76
+ call_args[:query].merge!(query) unless query.nil?
82
77
  unless format.nil?
83
78
  case ret_style
84
79
  when :header
85
80
  call_args[:headers] = {'Accept' => "application/#{format}"}
86
- when :arg
81
+ when :query
87
82
  call_args[:query][:format] = format
88
- when :ext
83
+ when :path
89
84
  call_args[:subpath] = "#{call_args[:subpath]}.#{format}"
90
- else Aspera.error_unexpected_value(ret_style)
85
+ else Aspera.error_unexpected_value(ret_style) { 'ret_style' }
91
86
  end
92
87
  end
93
- add_query = options.get_option(:query)
88
+ add_query = query_read_delete
94
89
  call_args[:query].merge!(add_query.symbolize_keys) unless add_query.nil?
95
- data, resp = @api_orch.call(**call_args)
90
+ data, resp = api_orch.call(**call_args)
96
91
  return resp if http
97
92
  result = format.eql?('xml') ? XmlSimple.xml_in(resp.body, {'ForceArray' => xml_arrays}) : data
98
93
  Log.dump(:data, result)
@@ -103,100 +98,128 @@ module Aspera
103
98
 
104
99
  # --- DSL ---
105
100
 
106
- command :health, description: 'Check Orchestrator API health', setup: :setup_api
107
- command :info, description: 'Ping the remote Orchestrator instance', setup: :setup_api, action: -> { Result::SingleObject.new(call_ao('remote_node_ping', format: 'xml', xml_arrays: false)) }
108
- command :processes, description: 'Show Orchestrator background process status', setup: :setup_api, action: -> { Result::ObjectList.new(call_ao('processes_status', format: 'xml')['process']) }
109
- command :monitors, description: 'Show Orchestrator monitor snapshot', setup: :setup_api, action: -> { Result::SingleObject.new(call_ao('monitor_snapshot')['monitor']) }
110
- command :plugins, description: 'Show Orchestrator plugin versions', setup: :setup_api, action: -> { Result::ObjectList.new(call_ao('plugin_version')['Plugin']) }
111
- command :workflows, description: 'Manage workflows', setup: :setup_api
112
- command :workorders, description: 'Manage work orders', setup: :setup_api
113
- command :workstep, description: 'Manage work steps', setup: :setup_api
101
+ command :health, description: 'Check Orchestrator API health'
102
+ command :info, description: 'Check that Orchestrator responds (ping)', action: ->(**) { Result::SingleObject.new(call_ao('remote_node_ping', format: 'xml', xml_arrays: false)) }
103
+ command :processes, description: 'Show Orchestrator background process status', action: ->(**) { Result::ObjectList.new(call_ao('processes_status', format: 'xml')['node'].flat_map { |n| n['process'] }) }
104
+ command :monitors, description: 'Show Orchestrator monitor snapshot', action: ->(**) { Result::SingleObject.new(call_ao('monitor_snapshot')['monitor']) }
105
+ command :workorders, description: 'Manage work orders'
106
+ command :workstep, description: 'Manage work steps'
107
+
108
+ commands_under :plugins do
109
+ command :list, description: 'Show Orchestrator plugin versions', action: ->(**) { Result::ObjectList.new(call_ao('plugin_version')['Plugin']) }
110
+ command :reload_set, description: 'Reload a set of plugins',
111
+ arguments: [{name: :plugin_set, type: Hash, schema: 'opts:components.schemas.OrchestratorReloadPluginSet'}],
112
+ action: ->(plugin_set:, **) { Result::SingleObject.new(call_ao('reload_plugin_set', body: plugin_set.transform_keys(&:to_sym))) }
113
+ end
114
114
 
115
115
  commands_under :workflows do
116
- command(:list, description: 'List all workflows', action: lambda do
117
- Result::ObjectList.new(
118
- call_ao('workflows_list')['workflows']['workflow'],
119
- fields: %w[id portable_id name published_status published_revision_id latest_revision_id last_modification]
120
- )
121
- end)
122
- command :status, description: 'Check running status of workflows',
123
- arguments: [{name: :wf_id, type: :identifier}],
124
- action: ->(wf_id:, **) { Result::ObjectList.new(call_ao(wf_id.eql?(SpecialValues::ALL) ? 'workflows_status' : "workflows_status/#{wf_id}")['workflows']['workflow']) }
125
- command :inputs, description: 'Fetch input specification for a workflow',
126
- arguments: [{name: :wf_id, type: :identifier}],
127
- action: ->(wf_id:, **) { Result::SingleObject.new(call_ao("workflow_inputs_spec/#{wf_id}")['workflow_inputs_spec']) }
128
- command :details, description: 'Check detailed running status of a workflow',
129
- arguments: [{name: :wf_id, type: :identifier}],
130
- action: ->(wf_id:, **) { Result::ObjectList.new(call_ao("workflow_details/#{wf_id}")['workflows']['workflow']['statuses']) }
131
- command :start, description: 'Initiate a work order (sync or async)',
132
- arguments: [{name: :wf_id, type: :identifier}, {name: :external_parameters, type: Hash, mandatory: false, default: {}}]
116
+ command :list, description: 'List all workflows'
117
+ command :status, description: 'Show running status of a workflow',
118
+ arguments: [{name: :workflow_id, type: :identifier}],
119
+ action: ->(workflow_id:, **) { Result::ObjectList.new(call_ao(workflow_id.eql?(SpecialValues::ALL) ? 'workflows_status' : "workflows_status/#{workflow_id}")['workflows']['workflow']) }
120
+ command :inputs, description: 'Show input specification of a workflow',
121
+ arguments: [{name: :workflow_id, type: :identifier}],
122
+ action: ->(workflow_id:, **) { Result::SingleObject.new(call_ao("workflow_inputs_spec/#{workflow_id}")['workflow_inputs_spec']) }
123
+ command :details, description: 'Show detailed running status of a workflow',
124
+ arguments: [{name: :workflow_id, type: :identifier}],
125
+ action: ->(workflow_id:, **) { Result::ObjectList.new(call_ao("workflow_details/#{workflow_id}")['workflows']['workflow']['statuses']) }
126
+ command :start, description: 'Start a workflow: create a work order (sync or async)',
127
+ arguments: [
128
+ {name: :workflow_id, type: :identifier},
129
+ {name: :parameters, type: Hash, mandatory: false, default: {}, schema: 'opts:components.schemas.OrchestratorInitiateParameters'},
130
+ {name: :execution, type: Hash, mandatory: false, default: {}, schema: 'opts:components.schemas.OrchestratorWorkflowStart'}
131
+ ]
132
+ command :import, description: 'Import a workflow',
133
+ arguments: [{name: :workflow, type: Hash, schema: 'opts:components.schemas.OrchestratorImportWorkflow'}],
134
+ action: ->(workflow:, **) { Result::SingleObject.new(call_ao('import_workflow', body: workflow.transform_keys(&:to_sym))) }
135
+ command :publish, description: 'Publish a workflow',
136
+ arguments: [{name: :workflow_id, type: :identifier}],
137
+ action: ->(workflow_id:, **) { Result::SingleObject.new(call_ao('publish_workflow', body: {workflow_id: workflow_id.to_i})) }
138
+ command :import_with_constraints, description: 'Import a workflow with constraint resolution',
139
+ arguments: [{name: :payload, type: Hash, schema: 'opts:components.schemas.OrchestratorImportWithConstraints'}],
140
+ action: ->(payload:, **) { Result::SingleObject.new(call_ao('import_with_constraints', body: payload.transform_keys(&:to_sym))) }
133
141
  command :export, description: 'Export a workflow',
134
- arguments: [{name: :wf_id, type: :identifier}],
135
- action: ->(wf_id:, **) { Result::Text.new(call_ao("export_workflow/#{wf_id}", format: nil, http: true).body) }
136
- command :workorders, description: 'Fetch all work orders from a workflow',
137
- arguments: [{name: :wf_id, type: :identifier}],
138
- action: ->(wf_id:, **) { Result::ObjectList.new(call_ao("work_orders_list/#{wf_id}")['work_orders']) }
139
- command :outputs, description: 'Fetch output specification for a workflow',
140
- arguments: [{name: :wf_id, type: :identifier}],
141
- action: ->(wf_id:, **) { Result::ObjectList.new(call_ao("workflow_outputs_spec/#{wf_id}")['workflow_outputs_spec']['output']) }
142
+ arguments: [{name: :workflow_id, type: :identifier}],
143
+ action: ->(workflow_id:, **) { Result::Text.new(call_ao("export_workflow/#{workflow_id}", format: nil, http: true).body) }
144
+ command :workorders, description: 'List work orders of a workflow',
145
+ arguments: [{name: :workflow_id, type: :identifier}],
146
+ action: ->(workflow_id:, **) { Result::ObjectList.new(call_ao("work_orders_list/#{workflow_id}")['work_orders']) }
147
+ command :outputs, description: 'Show output specification of a workflow',
148
+ arguments: [{name: :workflow_id, type: :identifier}],
149
+ action: ->(workflow_id:, **) { Result::ObjectList.new(call_ao("workflow_outputs_spec/#{workflow_id}")['workflow_outputs_spec']['output']) }
142
150
  end
143
151
 
144
152
  commands_under :workorders do
145
- command :status, description: 'Check the status of a work order',
146
- arguments: [{name: :wo_id, type: :identifier}],
147
- action: ->(wo_id:, **) { Result::SingleObject.new(call_ao("work_order_status/#{wo_id}")['work_order']) }
153
+ command :status, description: 'Show status of a work order',
154
+ arguments: [{name: :workorder_id, type: :identifier}],
155
+ action: ->(workorder_id:, **) { Result::SingleObject.new(call_ao("work_order_status/#{workorder_id}")['work_order']) }
148
156
  command :cancel, description: 'Cancel a work order',
149
- arguments: [{name: :wo_id, type: :identifier}],
150
- action: ->(wo_id:, **) { Result::SingleObject.new(call_ao("work_order_cancel/#{wo_id}")['work_order']) }
157
+ arguments: [{name: :workorder_id, type: :identifier}],
158
+ action: ->(workorder_id:, **) { Result::SingleObject.new(call_ao("work_order_cancel/#{workorder_id}")['work_order']) }
151
159
  command :reset, description: 'Reset a work order',
152
- arguments: [{name: :wo_id, type: :identifier}],
153
- action: ->(wo_id:, **) { Result::SingleObject.new(call_ao("work_order_reset/#{wo_id}")['work_order']) }
154
- command :output, description: 'Fetch output of a work order',
155
- arguments: [{name: :wo_id, type: :identifier}],
156
- action: ->(wo_id:, **) { Result::ObjectList.new(call_ao("work_order_output/#{wo_id}", format: 'xml')['variable']) }
160
+ arguments: [{name: :workorder_id, type: :identifier}],
161
+ action: ->(workorder_id:, **) { Result::SingleObject.new(call_ao("work_order_reset/#{workorder_id}")['work_order']) }
162
+ command :output, description: 'Show output of a work order',
163
+ arguments: [{name: :workorder_id, type: :identifier}],
164
+ action: ->(workorder_id:, **) { Result::ObjectList.new(call_ao("work_order_output/#{workorder_id}", format: 'xml')['variable']) }
157
165
  end
158
166
 
159
167
  commands_under :workstep do
160
- command :status, description: 'Check the status of a work step',
161
- arguments: [{name: :ws_id, type: :identifier}],
162
- action: ->(ws_id:, **) { Result::SingleObject.new(call_ao("work_step_status/#{ws_id}")) }
168
+ command :status, description: 'Show status of a work step',
169
+ arguments: [{name: :workstep_id, type: :identifier}],
170
+ action: ->(workstep_id:, **) { Result::SingleObject.new(call_ao("work_step_status/#{workstep_id}")) }
163
171
  command :cancel, description: 'Cancel a work step',
164
- arguments: [{name: :ws_id, type: :identifier}],
165
- action: ->(ws_id:, **) { Result::SingleObject.new(call_ao("work_step_cancel/#{ws_id}")) }
172
+ arguments: [{name: :workstep_id, type: :identifier}],
173
+ action: ->(workstep_id:, **) { Result::SingleObject.new(call_ao("work_step_cancel/#{workstep_id}")) }
166
174
  end
167
175
 
168
- # --- setup ---
176
+ # --- API ---
177
+
178
+ # Orchestrator REST API, built from CLI options on first use.
179
+ # @return [Rest::Client]
180
+ def api_orch
181
+ return @api_orch if @api_orch
182
+ base_url = options.get_option(:url, mandatory: true)
183
+ style = options.get_option(:auth_style, mandatory: true)
184
+ apikey = options.get_option(:apikey)
185
+ username = options.get_option(:username)
186
+ password = options.get_option(:password)
187
+ Aspera.assert(apikey.nil? || (username.nil? && password.nil?), type: Cli::BadArgument) { 'apikey and username/password are mutually exclusive' }
188
+ Aspera.assert(!apikey.nil? || (!username.nil? && !password.nil?), type: Cli::BadArgument) { 'provide either apikey or username and password' }
169
189
 
170
- # Build the Orchestrator REST API from CLI options.
171
- # @return [Hash] ctx with no extra keys (stores api in @api_orch instance var)
172
- def setup_api(**)
173
190
  auth_params =
174
- case options.get_option(:auth_style, mandatory: true)
175
- when :arg_pass
191
+ case style
192
+ when :basic
193
+ Aspera.assert(apikey.nil?, type: Cli::BadArgument) { 'basic auth style cannot be used with apikey, use token or query' }
176
194
  {
177
- type: :url,
178
- url_query: {
179
- 'login' => options.get_option(:username, mandatory: true),
180
- 'password' => options.get_option(:password, mandatory: true)
181
- }
195
+ type: :basic,
196
+ username: username,
197
+ password: password
182
198
  }
183
- when :head_basic
199
+ when :query
200
+ if apikey
201
+ {type: :url, url_query: {'apikey' => apikey}}
202
+ else
203
+ {type: :url, url_query: {'login' => username, 'password' => password}}
204
+ end
205
+ when :token
184
206
  {
185
- type: :basic,
186
- username: options.get_option(:username, mandatory: true),
187
- password: options.get_option(:password, mandatory: true)
207
+ type: :oauth2,
208
+ grant_method: :json_credentials,
209
+ base_url: base_url,
210
+ path_token: 'api/login',
211
+ token_field: 'token',
212
+ json: apikey ? {apikey: apikey} : {username: username, password: password}
188
213
  }
189
- when :apikey
190
- Aspera.error_not_implemented
214
+ else Aspera.error_unexpected_value(style)
191
215
  end
192
- @api_orch = Rest.new(
193
- base_url: options.get_option(:url, mandatory: true),
194
- auth: auth_params
216
+ @api_orch = Rest::Client.new(
217
+ base_url: base_url,
218
+ auth: auth_params
195
219
  )
196
- {}
197
220
  end
198
221
 
199
- def action_health
222
+ def action_health(**)
200
223
  nagios = Nagios.new
201
224
  begin
202
225
  info = call_ao('remote_node_ping', format: 'xml', xml_arrays: false)
@@ -209,26 +232,45 @@ module Aspera
209
232
  end
210
233
 
211
234
  # 2.1/2.2 Initiate a workorder (async / synchronous)
212
- def action_workflows_start(wf_id:, external_parameters:, **)
213
- call_params = {format: :json}
214
- # get external parameters if any
215
- external_parameters.each do |name, value|
216
- call_params["external_parameters[#{name}]"] = value
217
- end
218
- # synchronous call ?
219
- call_params['synchronous'] = true if options.get_option(:synchronous, mandatory: true)
220
- # expected result for synchro call ?
235
+ def action_workflows_list(**)
236
+ Result::ObjectList.new(
237
+ call_ao('workflows_list')['workflows']['workflow'],
238
+ fields: %w[id portable_id name published_status published_revision_id latest_revision_id last_modification]
239
+ )
240
+ end
241
+
242
+ # Keys of argument `execution` of `workflows start`
243
+ WORKFLOW_START_KEYS = %w[synchronous step variable].freeze
244
+ private_constant :WORKFLOW_START_KEYS
245
+
246
+ def action_workflows_start(workflow_id:, parameters:, execution:, **)
247
+ execution = execution.transform_keys(&:to_s)
248
+ unknown = execution.keys - WORKFLOW_START_KEYS
249
+ Aspera.assert(unknown.empty?, type: Cli::BadArgument) { "Unknown keys in execution: #{unknown.join(', ')}, expected: #{WORKFLOW_START_KEYS.join(', ')}" }
250
+ # Deprecated options, used when key not provided
251
+ execution['synchronous'] = options.get_option(:synchronous) unless execution.key?('synchronous')
221
252
  result_location = options.get_option(:result)
222
- unless result_location.nil?
253
+ unless result_location.nil? || execution.key?('step') || execution.key?('variable')
223
254
  fields = result_location.split(':')
224
255
  Aspera.assert(fields.length == 2, type: Cli::BadArgument) { "Expects: work_step:result_name : #{result_location}" }
225
- call_params['explicit_output_step'] = fields[0]
226
- call_params['explicit_output_variable'] = fields[1]
256
+ execution['step'], execution['variable'] = fields
257
+ end
258
+ # POST /api/initiate with JSON body {workflow_id:, external_parameters:}
259
+ json_body = {workflow_id: workflow_id.to_i, external_parameters: parameters}
260
+ # control params passed as query string (not part of the spec requestBody)
261
+ query_params = {}
262
+ Aspera.assert_type(execution['synchronous'], NilClass, *BoolValue::TYPES, type: Cli::BadArgument) { 'synchronous' }
263
+ query_params[:synchronous] = true if execution['synchronous'].eql?(true)
264
+ # expected result for synchro call ?
265
+ if execution.key?('step') || execution.key?('variable')
266
+ Aspera.assert(execution['step'].is_a?(String) && execution['variable'].is_a?(String), type: Cli::BadArgument) { 'Both step and variable are required' }
267
+ query_params[:explicit_output_step] = execution['step']
268
+ query_params[:explicit_output_variable] = execution['variable']
227
269
  # implicitly, call is synchronous
228
- call_params['synchronous'] = true
270
+ query_params[:synchronous] = true
229
271
  end
230
- result_data = call_ao("initiate/#{wf_id}", args: call_params)
231
- call_params['synchronous'] ? Result::Text.new(result_data) : Result::SingleObject.new(result_data)
272
+ result_data = call_ao('initiate', body: json_body, query: query_params.empty? ? nil : query_params)
273
+ query_params[:synchronous] ? Result::Text.new(result_data) : Result::SingleObject.new(result_data)
232
274
  end
233
275
  end
234
276
  end