aspera-cli 4.26.2 → 4.27.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 (136) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +281 -21
  4. data/CONTRIBUTING.md +98 -7
  5. data/bin/ascli +3 -3
  6. data/docs/README.md +10757 -0
  7. data/docs/test-mcp-with-ai.md +202 -0
  8. data/lib/aspera/agent/base.rb +20 -6
  9. data/lib/aspera/agent/connect.rb +53 -16
  10. data/lib/aspera/agent/desktop.rb +52 -17
  11. data/lib/aspera/agent/direct.rb +26 -24
  12. data/lib/aspera/agent/httpgw.rb +1 -1
  13. data/lib/aspera/agent/node.rb +32 -0
  14. data/lib/aspera/agent/transferd.rb +39 -3
  15. data/lib/aspera/api/alee.rb +1 -1
  16. data/lib/aspera/api/aoc.rb +14 -16
  17. data/lib/aspera/api/cos_node.rb +1 -1
  18. data/lib/aspera/api/faspex.rb +15 -14
  19. data/lib/aspera/api/httpgw.rb +4 -4
  20. data/lib/aspera/api/node.rb +26 -23
  21. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  22. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  23. data/lib/aspera/ascmd.rb +21 -19
  24. data/lib/aspera/ascp/installation.rb +37 -38
  25. data/lib/aspera/ascp/management.rb +2 -3
  26. data/lib/aspera/cli/ascp_actions.rb +155 -0
  27. data/lib/aspera/cli/async_transfer_store.rb +81 -0
  28. data/lib/aspera/cli/bootstrapper.rb +197 -0
  29. data/lib/aspera/cli/command_registry.rb +145 -0
  30. data/lib/aspera/cli/command_spec.rb +138 -0
  31. data/lib/aspera/cli/context.rb +16 -3
  32. data/lib/aspera/cli/error.rb +16 -0
  33. data/lib/aspera/cli/extended_value.rb +2 -1
  34. data/lib/aspera/cli/formatter.rb +41 -27
  35. data/lib/aspera/cli/gem_checker.rb +65 -0
  36. data/lib/aspera/cli/hints.rb +2 -2
  37. data/lib/aspera/cli/http.rb +70 -19
  38. data/lib/aspera/cli/info.rb +2 -0
  39. data/lib/aspera/cli/mailer.rb +97 -0
  40. data/lib/aspera/cli/mcp_tool.rb +198 -0
  41. data/lib/aspera/cli/options.schema.yaml +364 -7
  42. data/lib/aspera/cli/{manager.rb → parser.rb} +334 -142
  43. data/lib/aspera/cli/plugins/alee.rb +20 -22
  44. data/lib/aspera/cli/plugins/aoc.rb +1166 -853
  45. data/lib/aspera/cli/plugins/ats.rb +200 -161
  46. data/lib/aspera/cli/plugins/base.rb +457 -100
  47. data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
  48. data/lib/aspera/cli/plugins/config.rb +428 -754
  49. data/lib/aspera/cli/plugins/console.rb +106 -64
  50. data/lib/aspera/cli/plugins/cos.rb +44 -32
  51. data/lib/aspera/cli/plugins/factory.rb +4 -4
  52. data/lib/aspera/cli/plugins/faspex.rb +294 -257
  53. data/lib/aspera/cli/plugins/faspex5.rb +585 -357
  54. data/lib/aspera/cli/plugins/faspio.rb +49 -51
  55. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  56. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  57. data/lib/aspera/cli/plugins/node.rb +998 -794
  58. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  59. data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
  60. data/lib/aspera/cli/plugins/preview.rb +180 -139
  61. data/lib/aspera/cli/plugins/server.rb +124 -79
  62. data/lib/aspera/cli/plugins/shares.rb +301 -107
  63. data/lib/aspera/cli/preset_actions.rb +139 -0
  64. data/lib/aspera/cli/preset_manager.rb +13 -12
  65. data/lib/aspera/cli/result.rb +61 -11
  66. data/lib/aspera/cli/runner.rb +275 -141
  67. data/lib/aspera/cli/secret_finder.rb +40 -0
  68. data/lib/aspera/cli/special_values.rb +1 -0
  69. data/lib/aspera/cli/sync_actions.rb +83 -59
  70. data/lib/aspera/cli/terminal_formatter.rb +1 -1
  71. data/lib/aspera/cli/transfer_actions.rb +83 -0
  72. data/lib/aspera/cli/transfer_agent.rb +116 -51
  73. data/lib/aspera/cli/transfer_progress.rb +3 -3
  74. data/lib/aspera/cli/vault_manager.rb +57 -0
  75. data/lib/aspera/cli/version.rb +1 -1
  76. data/lib/aspera/cli/wizard.rb +21 -20
  77. data/lib/aspera/command_line_builder.rb +22 -22
  78. data/lib/aspera/data_repository.rb +2 -1
  79. data/lib/aspera/dot_container.rb +4 -4
  80. data/lib/aspera/environment.rb +18 -11
  81. data/lib/aspera/exec_spec.rb +13 -0
  82. data/lib/aspera/faspex_gw.rb +1 -1
  83. data/lib/aspera/faspex_postproc.rb +4 -3
  84. data/lib/aspera/graphql.rb +35 -0
  85. data/lib/aspera/hash_ext.rb +6 -0
  86. data/lib/aspera/json_rpc/client.rb +62 -0
  87. data/lib/aspera/json_rpc/version.rb +7 -0
  88. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  89. data/lib/aspera/keychain/factory.rb +3 -3
  90. data/lib/aspera/keychain/macos_security.rb +3 -3
  91. data/lib/aspera/link_header.rb +82 -0
  92. data/lib/aspera/log.rb +17 -3
  93. data/lib/aspera/markdown.rb +85 -2
  94. data/lib/aspera/node_simulator.rb +3 -1
  95. data/lib/aspera/oauth/base.rb +5 -5
  96. data/lib/aspera/oauth/boot.rb +2 -2
  97. data/lib/aspera/oauth/factory.rb +7 -5
  98. data/lib/aspera/oauth/jwt.rb +4 -4
  99. data/lib/aspera/oauth/web.rb +2 -2
  100. data/lib/aspera/persistency_action_once.rb +8 -8
  101. data/lib/aspera/persistency_folder.rb +10 -3
  102. data/lib/aspera/preview/file_types.rb +3 -3
  103. data/lib/aspera/preview/generator.rb +3 -3
  104. data/lib/aspera/preview/utils.rb +1 -1
  105. data/lib/aspera/products/connect.rb +1 -1
  106. data/lib/aspera/products/other.rb +1 -1
  107. data/lib/aspera/products/transferd.rb +1 -1
  108. data/lib/aspera/proxy_auto_config.rb +7 -6
  109. data/lib/aspera/rest.rb +10 -36
  110. data/lib/aspera/rest_error_analyzer.rb +3 -3
  111. data/lib/aspera/rest_errors_aspera.rb +0 -10
  112. data/lib/aspera/rest_list.rb +3 -3
  113. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
  114. data/lib/aspera/schema/documentation.rb +57 -26
  115. data/lib/aspera/schema/reader.rb +59 -10
  116. data/lib/aspera/schema/registry.rb +39 -6
  117. data/lib/aspera/secret_hider.rb +7 -2
  118. data/lib/aspera/ssl.rb +4 -4
  119. data/lib/aspera/sync/conf.schema.yaml +2 -2
  120. data/lib/aspera/sync/operations.rb +10 -13
  121. data/lib/aspera/temp_file_manager.rb +1 -1
  122. data/lib/aspera/timer_limiter.rb +1 -1
  123. data/lib/aspera/transfer/faux_file.rb +24 -11
  124. data/lib/aspera/transfer/parameters.rb +23 -21
  125. data/lib/aspera/transfer/result.rb +74 -0
  126. data/lib/aspera/transfer/resumer.rb +6 -6
  127. data/lib/aspera/transfer/spec.rb +18 -1
  128. data/lib/aspera/transfer/spec.schema.yaml +12 -3
  129. data/lib/aspera/uri_reader.rb +2 -2
  130. data/lib/aspera/web_auth.rb +2 -2
  131. data/lib/aspera/web_server_simple.rb +8 -5
  132. data/lib/aspera/yaml.rb +2 -1
  133. data.tar.gz.sig +0 -0
  134. metadata +25 -3
  135. metadata.gz.sig +0 -0
  136. data/lib/aspera/json_rpc.rb +0 -52
@@ -15,23 +15,94 @@ module Aspera
15
15
  # Access Aspera Transfer Service
16
16
  # https://developer.ibm.com/aspera/docs/ats-api-reference/creating-ats-api-keys/
17
17
  class Ats < Base
18
+ application_name 'Aspera Transfer Service'
18
19
  # columns for list of cloud providers
19
20
  CLOUD_TABLE = %w[id name].freeze
20
21
  private_constant :CLOUD_TABLE
22
+
23
+ option :ibm_api_key, description: 'IBM API key, see https://cloud.ibm.com/iam/apikeys'
24
+ option :instance, description: 'ATS instance in ibm cloud'
25
+ option :ats_key, description: 'ATS key identifier (ats_xxx)'
26
+ option :ats_secret, description: 'ATS key secret'
27
+ option :cloud, description: 'Cloud provider'
28
+ option :region, description: 'Cloud region'
29
+
21
30
  def initialize(api: nil, **base_args)
22
31
  super(**base_args)
23
32
  @ats_api_open = Api::Ats.new
24
33
  @ats_api_auth = api
25
- options.declare(:ibm_api_key, 'IBM API key, see https://cloud.ibm.com/iam/apikeys')
26
- options.declare(:instance, 'ATS instance in ibm cloud')
27
- options.declare(:ats_key, 'ATS key identifier (ats_xxx)')
28
- options.declare(:ats_secret, 'ATS key secret')
29
- options.declare(:cloud, 'Cloud provider')
30
- options.declare(:region, 'Cloud region')
31
34
  options.parse_options!
32
35
  Node.declare_options(options)
33
36
  end
34
37
 
38
+ # --- DSL ---
39
+
40
+ command :cluster, description: 'Display general ATS cluster information (public API, no auth)'
41
+ command :access_key, description: 'Manage ATS access keys'
42
+ command :api_key, description: 'Manage credential to access ATS API', condition: :api_key_available?
43
+ command :aws_trust_policy, description: 'Show AWS trust policy', action: lambda{Result::SingleObject.new(ats_api.read('aws/trustpolicy', {region: options.get_option(:region, mandatory: true)}))}
44
+
45
+ commands_under(:cluster) do
46
+ command :clouds, description: 'List cloud providers', action: lambda{Result::ObjectList.new(@ats_api_open.cloud_names.map{ |k, v| CLOUD_TABLE.zip([k, v]).to_h})}
47
+ command :list, description: 'List ATS servers', action: lambda{Result::ObjectList.new(@ats_api_open.all_servers, fields: %w[id cloud region])}
48
+ command :show, description: 'Show a specific server'
49
+ end
50
+
51
+ commands_under(:access_key) do
52
+ command :create, description: 'Create an access key',
53
+ arguments: [{name: :params, type: Hash, mandatory: false, default: {}}]
54
+ command(:list, description: 'List access keys', action: lambda do
55
+ res = ats_api.read('access_keys', query_read_delete(default: {'offset' => 0, 'max_results' => 1000}))
56
+ Result::ObjectList.new(res['data'], fields: ['name', 'id', 'created.at', 'modified.at'])
57
+ end)
58
+ command :show, description: 'Show an access key',
59
+ arguments: [{name: :access_key_id, type: :identifier}],
60
+ action: ->(access_key_id:, **){Result::SingleObject.new(ats_api.read("access_keys/#{access_key_id}"))}
61
+ command :modify, description: 'Modify an access key',
62
+ arguments: [{name: :access_key_id, type: :identifier}, {name: :params, type: Hash}]
63
+ command :delete, description: 'Delete an access key',
64
+ arguments: [{name: :access_key_id, type: :identifier}]
65
+ command :node, description: 'Execute node commands via ATS access key',
66
+ arguments: [{name: :access_key_id, type: :identifier}],
67
+ setup: :setup_ak_node
68
+ command :cluster, description: 'Show cluster info for an access key',
69
+ arguments: [{name: :access_key_id, type: :identifier}]
70
+ command :entitlement, description: 'Show ATS entitlement for an access key',
71
+ arguments: [{name: :access_key_id, type: :identifier}]
72
+ end
73
+
74
+ commands_under(%i[access_key node]) do
75
+ Node::COMMANDS_GEN4.each do |cmd|
76
+ command(cmd, description: "Node Gen4 #{cmd} command")
77
+ end
78
+ end
79
+
80
+ commands_under(:api_key) do
81
+ command(:instances, description: 'List ATS instances in IBM Cloud', action: lambda do
82
+ instances = ats_api_v2_auth_ibm.read('instances')
83
+ Log.log.warn{"more instances remaining: #{instances['remaining']}"} unless instances['remaining'].to_i.eql?(0)
84
+ Result::ValueList.new(instances['data'], name: 'instance')
85
+ end)
86
+ command :create, description: 'Create an ATS API key',
87
+ arguments: [{name: :params, type: Hash, mandatory: false, default: {}}],
88
+ action: ->(params:, **){Result::SingleObject.new(build_ats_ibm_api_with_instance.create('api_keys', params))}
89
+ command :list, description: 'List ATS API keys', action: lambda{Result::ValueList.new(build_ats_ibm_api_with_instance.read('api_keys', {'offset' => 0, 'max_results' => 1000})['data'], name: 'ats_id')}
90
+ command :show, description: 'Show an ATS API key',
91
+ arguments: [{name: :api_key_id, type: :identifier}],
92
+ action: ->(api_key_id:, **){Result::SingleObject.new(build_ats_ibm_api_with_instance.read("api_keys/#{api_key_id}"))}
93
+ command :delete, description: 'Delete an ATS API key',
94
+ arguments: [{name: :api_key_id, type: :identifier}]
95
+ end
96
+
97
+ # --- conditions ---
98
+
99
+ # api_key sub-tree is only available when authenticated via ATS key (not injected API)
100
+ def api_key_available?
101
+ @ats_api_auth.nil?
102
+ end
103
+
104
+ # --- helpers ---
105
+
35
106
  def server_by_cloud_region
36
107
  # TODO: provide list ?
37
108
  cloud = options.get_option(:cloud, mandatory: true).upcase
@@ -52,113 +123,6 @@ module Aspera
52
123
  )
53
124
  end
54
125
 
55
- def execute_action_access_key
56
- commands = %i[create list show modify delete node cluster entitlement]
57
- command = options.get_next_command(commands)
58
- # those do not require access key id
59
- access_key_id = options.instance_identifier unless %i[create list].include?(command)
60
- case command
61
- when :create
62
- params = value_create_modify(command: command, default: {})
63
- server_data = nil
64
- # if transfer_server_id not provided, get it from command line options
65
- if !params.key?('transfer_server_id')
66
- server_data = server_by_cloud_region
67
- params['transfer_server_id'] = server_data['id']
68
- end
69
- Log.log.debug{"using params: #{params}".bg_red.gray}
70
- if params.key?('storage')
71
- case params['storage']['type']
72
- # here we need somehow to map storage type to field to get for auth end point
73
- when 'ibm-s3'
74
- server_data2 = nil
75
- if server_data.nil?
76
- server_data2 = @ats_api_open.all_servers.find{ |s| s['id'].eql?(params['transfer_server_id'])}
77
- raise "no such transfer server id: #{params['transfer_server_id']}" if server_data2.nil?
78
- else
79
- server_data2 = @ats_api_open.all_servers.find do |s|
80
- s['cloud'].eql?(server_data['cloud']) &&
81
- s['region'].eql?(server_data['region']) &&
82
- s.key?('s3_authentication_endpoint')
83
- end
84
- raise "no such transfer server id: #{params['transfer_server_id']}" if server_data2.nil?
85
- # specific one do not have s3 end point in id
86
- params['transfer_server_id'] = server_data2['id']
87
- end
88
- params['storage']['endpoint'] = server_data2['s3_authentication_endpoint'] if !params['storage'].key?('authentication_endpoint')
89
- end
90
- end
91
- res = ats_api.create('access_keys', params)
92
- return Result::SingleObject.new(res)
93
- # TODO : action : modify, with "PUT"
94
- when :list
95
- params = query_read_delete(default: {'offset' => 0, 'max_results' => 1000})
96
- res = ats_api.read('access_keys', params)
97
- return Result::ObjectList.new(res['data'], fields: ['name', 'id', 'created.at', 'modified.at'])
98
- when :show
99
- res = ats_api.read("access_keys/#{access_key_id}")
100
- return Result::SingleObject.new(res)
101
- when :modify
102
- params = value_create_modify(command: command)
103
- params['id'] = access_key_id
104
- ats_api.update("access_keys/#{access_key_id}", params)
105
- return Result::Status.new('modified')
106
- when :entitlement
107
- ak = ats_api.read("access_keys/#{access_key_id}")
108
- api_bss = Api::Alee.new(ak['license']['entitlement_id'], ak['license']['customer_id'])
109
- return Result::SingleObject.new(api_bss.read('entitlement'))
110
- when :delete
111
- ats_api.delete("access_keys/#{access_key_id}")
112
- return Result::Status.new("deleted #{access_key_id}")
113
- when :node
114
- ak_data = ats_api.read("access_keys/#{access_key_id}")
115
- server_data = @ats_api_open.all_servers.find{ |i| i['id'].start_with?(ak_data['transfer_server_id'])}
116
- raise Cli::Error, 'no such server found' if server_data.nil?
117
- node_url = server_data['transfer_setup_url']
118
- api_node = Api::Node.new(
119
- base_url: node_url,
120
- auth: {
121
- type: :basic,
122
- username: access_key_id,
123
- password: config.lookup_secret(url: node_url, username: access_key_id)
124
- }
125
- )
126
- command = options.get_next_command(Node::COMMANDS_GEN4)
127
- return Node.new(context: context, api: api_node).execute_command_gen4(command, ak_data['root_file_id'])
128
- when :cluster
129
- ats_url = ats_api.base_url
130
- api_ak_auth = Rest.new(
131
- base_url: ats_url,
132
- auth: {
133
- type: :basic,
134
- username: access_key_id,
135
- password: config.lookup_secret(url: ats_url, username: access_key_id)
136
- }
137
- )
138
- return Result::SingleObject.new(api_ak_auth.read('servers'))
139
- else Aspera.error_unexpected_value(command)
140
- end
141
- end
142
-
143
- def execute_action_cluster_open
144
- command = options.get_next_command(%i[clouds list show])
145
- case command
146
- when :clouds
147
- return Result::ObjectList.new(@ats_api_open.cloud_names.map{ |k, v| CLOUD_TABLE.zip([k, v]).to_h})
148
- when :list
149
- return Result::ObjectList.new(@ats_api_open.all_servers, fields: %w[id cloud region])
150
- when :show
151
- if options.get_option(:cloud) || options.get_option(:region)
152
- server_data = server_by_cloud_region
153
- else
154
- server_id = options.instance_identifier
155
- server_data = @ats_api_open.all_servers.find{ |i| i['id'].eql?(server_id)}
156
- raise BadIdentifier.new('server', server_id) if server_data.nil?
157
- end
158
- return Result::SingleObject.new(server_data)
159
- end
160
- end
161
-
162
126
  def ats_api_v2_auth_ibm(rest_add_headers = {})
163
127
  return Rest.new(
164
128
  base_url: "#{Api::Ats::SERVICE_BASE_URL}/v2",
@@ -177,60 +141,135 @@ module Aspera
177
141
  )
178
142
  end
179
143
 
180
- def execute_action_api_key
181
- command = options.get_next_command(%i[instances create list show delete])
182
- concerned_id = options.instance_identifier if %i[show delete].include?(command)
183
- rest_add_header = {}
184
- if !command.eql?(:instances)
185
- instance = options.get_option(:instance)
186
- if instance.nil?
187
- # Take the first Aspera on Cloud transfer service instance ID if not provided by user
188
- instance = ats_api_v2_auth_ibm.read('instances')['data'].first
189
- formatter.display_status("using first instance: #{instance}")
144
+ def action_cluster_show
145
+ if options.get_option(:cloud) || options.get_option(:region)
146
+ server_data = server_by_cloud_region
147
+ else
148
+ server_id = options.instance_identifier
149
+ server_data = @ats_api_open.all_servers.find{ |i| i['id'].eql?(server_id)}
150
+ raise BadIdentifier.new('server', server_id) if server_data.nil?
151
+ end
152
+ Result::SingleObject.new(server_data)
153
+ end
154
+
155
+ def action_access_key_create(params: {}, **)
156
+ server_data = nil
157
+ # if transfer_server_id not provided, get it from command line options
158
+ if !params.key?('transfer_server_id')
159
+ server_data = server_by_cloud_region
160
+ params['transfer_server_id'] = server_data['id']
161
+ end
162
+ Log.log.debug{"using params: #{params}".bg_red.gray}
163
+ if params.key?('storage')
164
+ case params['storage']['type']
165
+ # here we need somehow to map storage type to field to get for auth end point
166
+ when 'ibm-s3'
167
+ server_data2 = nil
168
+ if server_data.nil?
169
+ server_data2 = @ats_api_open.all_servers.find{ |s| s['id'].eql?(params['transfer_server_id'])}
170
+ raise "no such transfer server id: #{params['transfer_server_id']}" if server_data2.nil?
171
+ else
172
+ server_data2 = @ats_api_open.all_servers.find do |s|
173
+ s['cloud'].eql?(server_data['cloud']) &&
174
+ s['region'].eql?(server_data['region']) &&
175
+ s.key?('s3_authentication_endpoint')
176
+ end
177
+ raise "no such transfer server id: #{params['transfer_server_id']}" if server_data2.nil?
178
+ # specific one do not have s3 end point in id
179
+ params['transfer_server_id'] = server_data2['id']
180
+ end
181
+ params['storage']['endpoint'] = server_data2['s3_authentication_endpoint'] if !params['storage'].key?('authentication_endpoint')
190
182
  end
191
- rest_add_header = {'X-ATS-Service-Instance-Id' => instance}
192
183
  end
193
- ats_ibm_api = ats_api_v2_auth_ibm(rest_add_header)
194
- case command
195
- when :instances
196
- instances = ats_ibm_api.read('instances')
197
- Log.log.warn{"more instances remaining: #{instances['remaining']}"} unless instances['remaining'].to_i.eql?(0)
198
- return Result::ValueList.new(instances['data'], name: 'instance')
199
- when :create
200
- created_key = ats_ibm_api.create('api_keys', value_create_modify(command: command, default: {}))
201
- return Result::SingleObject.new(created_key)
202
- when :list # list known api keys in ATS (this require an api_key ...)
203
- res = ats_ibm_api.read('api_keys', {'offset' => 0, 'max_results' => 1000})
204
- return Result::ValueList.new(res['data'], name: 'ats_id')
205
- when :show # show one of api_key in ATS
206
- res = ats_ibm_api.read("api_keys/#{concerned_id}")
207
- return Result::SingleObject.new(res)
208
- when :delete
209
- ats_ibm_api.delete("api_keys/#{concerned_id}")
210
- return Result::Status.new("deleted #{concerned_id}")
211
- else Aspera.error_unexpected_value(command)
184
+ res = ats_api.create('access_keys', params)
185
+ return Result::SingleObject.new(res)
186
+ # TODO : action : modify, with "PUT"
187
+ end
188
+
189
+ def action_access_key_modify(params:, access_key_id:, **)
190
+ params['id'] = access_key_id
191
+ ats_api.update("access_keys/#{access_key_id}", params)
192
+ return Result::Status.new('modified')
193
+ end
194
+
195
+ def action_access_key_delete(access_key_id:, **)
196
+ ats_api.delete("access_keys/#{access_key_id}")
197
+ Result::Status.new("deleted #{access_key_id}")
198
+ end
199
+
200
+ def action_access_key_entitlement(access_key_id:, **)
201
+ ak = ats_api.read("access_keys/#{access_key_id}")
202
+ api_bss = Api::Alee.new(ak['license']['entitlement_id'], ak['license']['customer_id'])
203
+ return Result::SingleObject.new(api_bss.read('entitlement'))
204
+ end
205
+
206
+ # Build the Node plugin for an ATS access key.
207
+ # access_key_id: is already in ctx via arguments: on the :node command.
208
+ # @return [Hash] context hash containing :ak_node_plugin and :ak_root_file_id
209
+ def setup_ak_node(access_key_id:, **)
210
+ ak_data = ats_api.read("access_keys/#{access_key_id}")
211
+ server_data = @ats_api_open.all_servers.find{ |i| i['id'].start_with?(ak_data['transfer_server_id'])}
212
+ Aspera.assert(!server_data.nil?, type: Cli::Error){'no such server found'}
213
+ node_url = server_data['transfer_setup_url']
214
+ api_node = Api::Node.new(
215
+ base_url: node_url,
216
+ auth: {
217
+ type: :basic,
218
+ username: access_key_id,
219
+ password: context.secret_finder.lookup(url: node_url, username: access_key_id)
220
+ }
221
+ )
222
+ {
223
+ ak_node_plugin: Node.new(context: context, api: api_node),
224
+ ak_root_file_id: ak_data['root_file_id']
225
+ }
226
+ end
227
+
228
+ # One handler per COMMANDS_GEN4 - delegates to the Node plugin's DSL registry.
229
+ Node::COMMANDS_GEN4.each do |cmd|
230
+ define_action_method([:access_key, :node, cmd]) do |ak_node_plugin:, ak_root_file_id:, **|
231
+ # For permission: the handler consumes the path first then re-dispatches to sub-commands.
232
+ # Calling dispatch_from_registry with skip_setup would bypass path consumption and fail.
233
+ next ak_node_plugin.send(:"action_access_keys_do_#{cmd}", do_root_file_id: ak_root_file_id) if cmd.eql?(:permission)
234
+ ak_node_plugin.dispatch_from_registry([:access_keys, :do, cmd], {do_root_file_id: ak_root_file_id}, skip_setup: true)
212
235
  end
213
236
  end
214
237
 
215
- ACTIONS = %i[cluster access_key api_key aws_trust_policy].freeze
216
-
217
- # called for legacy and AoC
218
- def execute_action
219
- actions = ACTIONS.dup
220
- actions.delete(:api_key) unless @ats_api_auth.nil?
221
- command = options.get_next_command(actions)
222
- case command
223
- when :cluster # display general ATS cluster information, this uses public API, no auth
224
- return execute_action_cluster_open
225
- when :access_key
226
- return execute_action_access_key
227
- when :api_key # manage credential to access ATS API
228
- return execute_action_api_key
229
- when :aws_trust_policy
230
- res = ats_api.read('aws/trustpolicy', {region: options.get_option(:region, mandatory: true)})
231
- return Result::SingleObject.new(res)
232
- else Aspera.error_unexpected_value(command)
238
+ def action_api_key_instances
239
+ instances = ats_api_v2_auth_ibm.read('instances')
240
+ Log.log.warn{"more instances remaining: #{instances['remaining']}"} unless instances['remaining'].to_i.eql?(0)
241
+ Result::ValueList.new(instances['data'], name: 'instance')
242
+ end
243
+
244
+ def action_api_key_delete(api_key_id:, **)
245
+ build_ats_ibm_api_with_instance.delete("api_keys/#{api_key_id}")
246
+ Result::Status.new("deleted #{api_key_id}")
247
+ end
248
+
249
+ def action_access_key_cluster(access_key_id:, **)
250
+ ats_url = ats_api.base_url
251
+ api_ak_auth = Rest.new(
252
+ base_url: ats_url,
253
+ auth: {
254
+ type: :basic,
255
+ username: access_key_id,
256
+ password: context.secret_finder.lookup(url: ats_url, username: access_key_id)
257
+ }
258
+ )
259
+ return Result::SingleObject.new(api_ak_auth.read('servers'))
260
+ end
261
+
262
+ private
263
+
264
+ # Build the IBM Cloud ATS v2 API with an instance header.
265
+ # Reads instance from options; falls back to first available instance.
266
+ def build_ats_ibm_api_with_instance
267
+ instance = options.get_option(:instance)
268
+ if instance.nil?
269
+ instance = ats_api_v2_auth_ibm.read('instances')['data'].first
270
+ formatter.display_status("using first instance: #{instance}")
233
271
  end
272
+ ats_api_v2_auth_ibm({'X-ATS-Service-Instance-Id' => instance})
234
273
  end
235
274
  end
236
275
  end