aspera-cli 4.26.2 → 4.27.1
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +310 -21
- data/CONTRIBUTING.md +99 -14
- data/TODO.md +50 -0
- data/bin/ascli +3 -3
- data/docs/README.md +10977 -0
- data/docs/test-mcp-with-ai.md +202 -0
- data/lib/aspera/agent/base.rb +29 -7
- data/lib/aspera/agent/connect.rb +58 -21
- data/lib/aspera/agent/desktop.rb +53 -18
- data/lib/aspera/agent/direct.rb +71 -40
- data/lib/aspera/agent/factory.rb +3 -3
- data/lib/aspera/agent/httpgw.rb +55 -14
- data/lib/aspera/agent/node.rb +34 -2
- data/lib/aspera/agent/transferd.rb +48 -12
- data/lib/aspera/api/alee.rb +1 -1
- data/lib/aspera/api/aoc.rb +42 -44
- data/lib/aspera/api/cos_node.rb +10 -9
- data/lib/aspera/api/faspex.rb +16 -15
- data/lib/aspera/api/httpgw.rb +23 -23
- data/lib/aspera/api/node.rb +51 -70
- data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
- data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
- data/lib/aspera/ascmd.rb +32 -30
- data/lib/aspera/ascp/installation.rb +75 -69
- data/lib/aspera/ascp/management.rb +7 -8
- data/lib/aspera/assert.rb +12 -13
- data/lib/aspera/cli/ascp_actions.rb +155 -0
- data/lib/aspera/cli/async_transfer_store.rb +125 -0
- data/lib/aspera/cli/bootstrapper.rb +197 -0
- data/lib/aspera/cli/command_registry.rb +145 -0
- data/lib/aspera/cli/command_spec.rb +141 -0
- data/lib/aspera/cli/context.rb +19 -6
- data/lib/aspera/cli/error.rb +16 -0
- data/lib/aspera/cli/extended_value.rb +27 -26
- data/lib/aspera/cli/formatter.rb +95 -59
- data/lib/aspera/cli/gem_checker.rb +65 -0
- data/lib/aspera/cli/hints.rb +3 -3
- data/lib/aspera/cli/http.rb +74 -30
- data/lib/aspera/cli/info.rb +2 -0
- data/lib/aspera/cli/mailer.rb +97 -0
- data/lib/aspera/cli/mcp_tool.rb +225 -0
- data/lib/aspera/cli/option_declarator.rb +77 -0
- data/lib/aspera/cli/options.schema.yaml +634 -7
- data/lib/aspera/cli/parser.rb +1217 -0
- data/lib/aspera/cli/plugins/alee.rb +20 -22
- data/lib/aspera/cli/plugins/aoc.rb +1178 -881
- data/lib/aspera/cli/plugins/ats.rb +205 -162
- data/lib/aspera/cli/plugins/base.rb +703 -175
- data/lib/aspera/cli/plugins/basic_auth.rb +7 -9
- data/lib/aspera/cli/plugins/config.rb +434 -757
- data/lib/aspera/cli/plugins/console.rb +107 -65
- data/lib/aspera/cli/plugins/cos.rb +46 -33
- data/lib/aspera/cli/plugins/factory.rb +6 -6
- data/lib/aspera/cli/plugins/faspex5.rb +627 -388
- data/lib/aspera/cli/plugins/faspio.rb +54 -51
- data/lib/aspera/cli/plugins/httpgw.rb +18 -25
- data/lib/aspera/cli/plugins/mcp.rb +279 -0
- data/lib/aspera/cli/plugins/node.rb +988 -840
- data/lib/aspera/cli/plugins/oauth.rb +7 -10
- data/lib/aspera/cli/plugins/orchestrator.rb +112 -135
- data/lib/aspera/cli/plugins/preview.rb +195 -157
- data/lib/aspera/cli/plugins/server.rb +141 -91
- data/lib/aspera/cli/plugins/shares.rb +343 -110
- data/lib/aspera/cli/preset_actions.rb +159 -0
- data/lib/aspera/cli/preset_manager.rb +82 -39
- data/lib/aspera/cli/result.rb +78 -26
- data/lib/aspera/cli/runner.rb +323 -153
- data/lib/aspera/cli/secret_finder.rb +40 -0
- data/lib/aspera/cli/special_values.rb +1 -0
- data/lib/aspera/cli/sync_actions.rb +89 -64
- data/lib/aspera/cli/terminal_formatter.rb +3 -3
- data/lib/aspera/cli/transfer_actions.rb +90 -0
- data/lib/aspera/cli/transfer_agent.rb +129 -64
- data/lib/aspera/cli/transfer_progress.rb +9 -9
- data/lib/aspera/cli/vault_manager.rb +79 -0
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +28 -30
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +34 -34
- data/lib/aspera/command_line_converter.rb +1 -1
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +2 -1
- data/lib/aspera/dot_container.rb +12 -12
- data/lib/aspera/environment.rb +30 -23
- data/lib/aspera/exec_spec.rb +13 -0
- data/lib/aspera/faspex_gw.rb +5 -5
- data/lib/aspera/faspex_postproc.rb +16 -10
- data/lib/aspera/graphql.rb +37 -0
- data/lib/aspera/hash_ext.rb +8 -2
- data/lib/aspera/json_rpc/client.rb +62 -0
- data/lib/aspera/json_rpc/version.rb +7 -0
- data/lib/aspera/keychain/base.rb +10 -3
- data/lib/aspera/keychain/encrypted_hash.rb +92 -17
- data/lib/aspera/keychain/factory.rb +20 -8
- data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +23 -25
- data/lib/aspera/keychain/one_password_api.rb +86 -0
- data/lib/aspera/keychain/one_password_base.rb +34 -0
- data/lib/aspera/keychain/one_password_cli.rb +98 -0
- data/lib/aspera/link_header.rb +82 -0
- data/lib/aspera/log.rb +29 -15
- data/lib/aspera/markdown.rb +90 -7
- data/lib/aspera/nagios.rb +8 -8
- data/lib/aspera/node_simulator.rb +35 -27
- data/lib/aspera/oauth/base.rb +9 -10
- data/lib/aspera/oauth/boot.rb +5 -5
- data/lib/aspera/oauth/factory.rb +12 -10
- data/lib/aspera/oauth/jwt.rb +9 -11
- data/lib/aspera/oauth/web.rb +6 -6
- data/lib/aspera/persistency_action_once.rb +12 -12
- data/lib/aspera/persistency_folder.rb +18 -11
- data/lib/aspera/preview/file_types.rb +5 -5
- data/lib/aspera/preview/generator.rb +52 -49
- data/lib/aspera/preview/options.rb +3 -2
- data/lib/aspera/preview/terminal.rb +5 -5
- data/lib/aspera/preview/utils.rb +66 -19
- data/lib/aspera/products/connect.rb +2 -2
- data/lib/aspera/products/desktop.rb +1 -1
- data/lib/aspera/products/other.rb +3 -3
- data/lib/aspera/products/transferd.rb +4 -4
- data/lib/aspera/proxy_auto_config.rb +15 -14
- data/lib/aspera/rest.rb +159 -151
- data/lib/aspera/rest_error_analyzer.rb +6 -6
- data/lib/aspera/rest_errors_aspera.rb +0 -10
- data/lib/aspera/rest_list.rb +11 -10
- data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
- data/lib/aspera/schema/documentation.rb +65 -28
- data/lib/aspera/schema/reader.rb +136 -13
- data/lib/aspera/schema/registry.rb +60 -8
- data/lib/aspera/secret_hider.rb +12 -7
- data/lib/aspera/ssh.rb +64 -31
- data/lib/aspera/ssl.rb +5 -5
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/database.rb +2 -2
- data/lib/aspera/sync/operations.rb +22 -25
- data/lib/aspera/temp_file_manager.rb +25 -5
- data/lib/aspera/timer_limiter.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +24 -11
- data/lib/aspera/transfer/parameters.rb +36 -34
- data/lib/aspera/transfer/result.rb +74 -0
- data/lib/aspera/transfer/resumer.rb +10 -10
- data/lib/aspera/transfer/spec.rb +18 -1
- data/lib/aspera/transfer/spec.schema.yaml +12 -3
- data/lib/aspera/uri_reader.rb +52 -15
- data/lib/aspera/web_auth.rb +8 -7
- data/lib/aspera/web_server_simple.rb +15 -12
- data/lib/aspera/yaml.rb +5 -4
- data.tar.gz.sig +0 -0
- metadata +34 -6
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/manager.rb +0 -766
- data/lib/aspera/cli/plugins/faspex.rb +0 -539
- data/lib/aspera/json_rpc.rb +0 -52
- data/lib/aspera/transfer/uri.rb +0 -56
|
@@ -26,7 +26,7 @@ module Aspera
|
|
|
26
26
|
login_page = Rest
|
|
27
27
|
.new(base_url: url, redirect_max: 2)
|
|
28
28
|
.read('', headers: {'Accept'=>'text/html'})
|
|
29
|
-
|
|
29
|
+
Aspera.assert(login_page.include?('aspera-Shares')) { 'not Shares' }
|
|
30
30
|
if (m = login_page.match(/\(v([0-9a-f\.]+)\)/))
|
|
31
31
|
version = m[1]
|
|
32
32
|
if (m = login_page.match(/Patch level ([0-9]+)/))
|
|
@@ -91,129 +91,362 @@ module Aspera
|
|
|
91
91
|
|
|
92
92
|
def initialize(**_)
|
|
93
93
|
super
|
|
94
|
+
@api_shares_admin = nil
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
SAML_IMPORT_MANDATORY = %w[id name_id].freeze
|
|
97
|
-
SAML_IMPORT_ALLOWED = %w[email given_name surname]
|
|
98
|
+
SAML_IMPORT_ALLOWED = (%w[email given_name surname] + SAML_IMPORT_MANDATORY).freeze
|
|
98
99
|
|
|
99
|
-
ACTIONS = %i[health info files admin].freeze
|
|
100
100
|
# common to users and groups
|
|
101
101
|
USR_GRP_SETTINGS = %i[transfer_settings app_authorizations share_permissions].freeze
|
|
102
|
+
# share_permissions is read-only (Rails only exposes index+show)
|
|
103
|
+
SHARE_PERMISSIONS_OPS = %i[list show].freeze
|
|
104
|
+
# group users: Rails only exposes index+show+update+destroy (no create)
|
|
105
|
+
GROUP_USERS_OPS = (Operations::ALL - %i[create]).freeze
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
# --- DSL ---
|
|
108
|
+
|
|
109
|
+
command :health, description: 'Check Shares health'
|
|
110
|
+
command :info, description: 'Show server information', action: -> { Result::SingleObject.new(basic_auth_api(NODE_API_PATH).read('info', headers: {'Content-Type'=>'application/json'})) }
|
|
111
|
+
command :files, description: 'Browse and transfer files on Shares', aliases: [:repository], setup: :setup_shares_node
|
|
112
|
+
command :admin, description: 'Administer Shares', setup: :setup_admin
|
|
113
|
+
|
|
114
|
+
commands_under :admin do
|
|
115
|
+
commands_under :node do
|
|
116
|
+
crud_commands entity: 'data/nodes', api: :@api_shares_admin
|
|
117
|
+
end
|
|
118
|
+
commands_under :ldap_config, description: 'LDAP configuration' do
|
|
119
|
+
crud_commands entity: 'data/ldap_configs', api: :@api_shares_admin
|
|
120
|
+
end
|
|
121
|
+
command :share, description: 'Manage shares'
|
|
122
|
+
command :transfer_settings, description: 'Manage transfer settings'
|
|
123
|
+
command :user, description: 'Manage users'
|
|
124
|
+
command :group, description: 'Manage groups'
|
|
125
|
+
commands_under :smtp_server, description: 'SMTP server' do
|
|
126
|
+
crud_commands entity: 'data/smtp_server',
|
|
127
|
+
api: :@api_shares_admin,
|
|
128
|
+
is_singleton: true,
|
|
129
|
+
body_component: Schema::Registry::SHARES,
|
|
130
|
+
operations: %i[show create modify]
|
|
131
|
+
end
|
|
132
|
+
# user / group: sub-commands per location, generated for both entity types
|
|
133
|
+
%i[user group].each do |entity_type|
|
|
134
|
+
commands_under entity_type do
|
|
135
|
+
command :all, description: "#{entity_type.capitalize}s from all sources"
|
|
136
|
+
command :local, description: "Local #{entity_type}s"
|
|
137
|
+
command :ldap, description: "LDAP #{entity_type}s"
|
|
138
|
+
command :saml, description: "SAML #{entity_type}s"
|
|
139
|
+
|
|
140
|
+
# all: list/show/delete only (no create, no modify — users/groups/:all has no update route)
|
|
141
|
+
lookup_method_all = :"lookup_shares_#{entity_type}_all_id"
|
|
142
|
+
commands_under :all do
|
|
143
|
+
(Operations::ALL - %i[create modify]).each do |op|
|
|
144
|
+
id_args = Operations::GLOBAL.include?(op) ? {} : {arguments: [{name: :"#{entity_type}_id", type: :identifier, lookup: lookup_method_all}]}
|
|
145
|
+
command op, description: "#{op.capitalize} #{entity_type}s", **id_args
|
|
146
|
+
end
|
|
147
|
+
USR_GRP_SETTINGS.each do |setting|
|
|
148
|
+
# share_permissions: Rails only exposes index+show (read-only)
|
|
149
|
+
setting_ops = setting.eql?(:share_permissions) ? SHARE_PERMISSIONS_OPS : %i[show modify]
|
|
150
|
+
command setting,
|
|
151
|
+
description: "Manage #{setting} for a #{entity_type}",
|
|
152
|
+
arguments: [{name: :"#{entity_type}_id", type: :identifier, lookup: lookup_method_all}]
|
|
153
|
+
commands_under setting do
|
|
154
|
+
setting_ops.each do |op|
|
|
155
|
+
# share_permissions > show needs a permission identifier (list does not)
|
|
156
|
+
perm_id_arg = if setting.eql?(:share_permissions) && op.eql?(:show)
|
|
157
|
+
[{name: :permission_id, type: :identifier, lookup: :lookup_share_id}]
|
|
158
|
+
else
|
|
159
|
+
[]
|
|
160
|
+
end
|
|
161
|
+
command op,
|
|
162
|
+
description: "#{op.capitalize} #{setting} for a #{entity_type}",
|
|
163
|
+
arguments: perm_id_arg.empty? ? nil : perm_id_arg
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
if entity_type.eql?(:group)
|
|
168
|
+
command :users,
|
|
169
|
+
description: 'Manage users of a group',
|
|
170
|
+
arguments: [{name: :group_id, type: :identifier, lookup: :lookup_shares_group_all_id}]
|
|
171
|
+
commands_under :users do
|
|
172
|
+
GROUP_USERS_OPS.each do |op|
|
|
173
|
+
command op, description: "#{op.capitalize} users of a group"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# local: full CRUD only — no nested settings (Rails does not nest transfer_settings/app_authorizations/share_permissions under local_users/local_groups)
|
|
180
|
+
commands_under :local do
|
|
181
|
+
lookup_method_local = :"lookup_shares_#{entity_type}_local_id"
|
|
182
|
+
Operations::ALL.each do |op|
|
|
183
|
+
id_args = Operations::GLOBAL.include?(op) ? {} : {arguments: [{name: :"#{entity_type}_id", type: :identifier, lookup: lookup_method_local}]}
|
|
184
|
+
command op, description: "#{op.capitalize} #{entity_type}s", **id_args
|
|
185
|
+
end
|
|
186
|
+
if entity_type.eql?(:group)
|
|
187
|
+
command :users,
|
|
188
|
+
description: 'Manage users of a group',
|
|
189
|
+
arguments: [{name: :group_id, type: :identifier, lookup: :lookup_shares_group_local_id}]
|
|
190
|
+
commands_under :users do
|
|
191
|
+
GROUP_USERS_OPS.each do |op|
|
|
192
|
+
command op, description: "#{op.capitalize} users of a group"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# ldap: add only
|
|
199
|
+
commands_under :ldap do
|
|
200
|
+
command :add, description: "Add a LDAP #{entity_type}",
|
|
201
|
+
arguments: [{name: :entity_name, type: String, bulk: true}]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# saml: import only
|
|
205
|
+
commands_under :saml do
|
|
206
|
+
command :import, description: "Import a SAML #{entity_type}",
|
|
207
|
+
arguments: [{name: :entity_parameters, type: Hash, bulk: true}]
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
ENTITY_LOCATIONS = %i[all local].freeze
|
|
214
|
+
SHARE_DISPLAY_FIELDS = %w[id name node_id directory percent_free].freeze
|
|
215
|
+
private_constant :ENTITY_LOCATIONS, :SHARE_DISPLAY_FIELDS
|
|
216
|
+
|
|
217
|
+
# Lookup methods used by arguments:(:identifier, lookup:) on setting nodes.
|
|
218
|
+
# One method per entity_type/location combination; each returns the entity id.
|
|
219
|
+
%i[user group].each do |entity_type|
|
|
220
|
+
ENTITY_LOCATIONS.each do |location|
|
|
221
|
+
lookup_method = :"lookup_shares_#{entity_type}_#{location}_id"
|
|
222
|
+
define_method(lookup_method) do |field, value, **|
|
|
223
|
+
path = admin_entity_path(entity_type, location)
|
|
224
|
+
RestList.lookup_entity_generic(entity: entity_type, field: field, value: value) { @api_shares_admin.read(path) }['id']
|
|
115
225
|
end
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
226
|
+
private lookup_method
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
commands_under %i[admin share] do
|
|
231
|
+
crud_commands entity: 'data/shares',
|
|
232
|
+
api: :@api_shares_admin,
|
|
233
|
+
display_fields: SHARE_DISPLAY_FIELDS,
|
|
234
|
+
body_component: Schema::Registry::SHARES,
|
|
235
|
+
lookup: :lookup_share_id
|
|
236
|
+
command :user_permissions,
|
|
237
|
+
description: 'Manage user permissions on a share',
|
|
238
|
+
arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}]
|
|
239
|
+
command :group_permissions,
|
|
240
|
+
description: 'Manage group permissions on a share',
|
|
241
|
+
arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# share user_permissions and group_permissions: Rails only exposes index+show (read-only)
|
|
245
|
+
%i[user_permissions group_permissions].each do |perm_type|
|
|
246
|
+
commands_under [:admin, :share, perm_type] do
|
|
247
|
+
SHARE_PERMISSIONS_OPS.each do |op|
|
|
248
|
+
command op, description: "#{op.capitalize} #{perm_type}"
|
|
120
249
|
end
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
commands_under %i[admin transfer_settings] do
|
|
254
|
+
crud_commands entity: 'data/transfer_settings',
|
|
255
|
+
api: :@api_shares_admin,
|
|
256
|
+
is_singleton: true,
|
|
257
|
+
operations: %i[show modify]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# --- setup ---
|
|
261
|
+
|
|
262
|
+
# Build the Shares node API plugin and inject into ctx.
|
|
263
|
+
# @return [Hash] context hash containing :shares_node_plugin
|
|
264
|
+
def setup_shares_node(**)
|
|
265
|
+
api_shares_node = basic_auth_api(NODE_API_PATH)
|
|
266
|
+
{shares_node_plugin: Node.new(context: context, api: api_shares_node)}
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Build the admin REST API and store in ivar.
|
|
270
|
+
# @return [Hash] empty ctx (state stored in @api_shares_admin)
|
|
271
|
+
def setup_admin(**)
|
|
272
|
+
@api_shares_admin = basic_auth_api(ADMIN_API_PATH)
|
|
273
|
+
{}
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Lookup a share id by field/value using the admin API.
|
|
277
|
+
def lookup_share_id(field, value, **)
|
|
278
|
+
RestList.lookup_entity_generic(entity: 'share', field: field, value: value) { @api_shares_admin.read('data/shares') }['id']
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# --- health ---
|
|
282
|
+
|
|
283
|
+
def action_health
|
|
284
|
+
nagios = Nagios.new
|
|
285
|
+
shares_url = options.get_option(:url, mandatory: true)
|
|
286
|
+
health = self.class.health_check(shares_url)
|
|
287
|
+
nagios.add_ok('version', health[:version]) if health[:version].is_a?(String)
|
|
288
|
+
if health[:ping].is_a?(String)
|
|
289
|
+
nagios.add_ok('ping', health[:ping])
|
|
290
|
+
else
|
|
291
|
+
nagios.add_critical('ping', health[:ping].to_s)
|
|
292
|
+
end
|
|
293
|
+
if health[:api].is_a?(String)
|
|
294
|
+
nagios.add_ok('API', health[:api])
|
|
295
|
+
else
|
|
296
|
+
nagios.add_critical('API', health[:api].to_s)
|
|
297
|
+
end
|
|
298
|
+
Result::ObjectList.new(nagios.status_list)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# --- files sub-commands (restricted to COMMANDS_SHARES, delegated to Node) ---
|
|
302
|
+
|
|
303
|
+
commands_under :files do
|
|
304
|
+
Node::COMMANDS_SHARES.each do |cmd|
|
|
305
|
+
next if cmd.eql?(:sync) # intermediate node with sub-commands, declared separately below
|
|
306
|
+
spec = Node::COMMANDS_GEN3_SPEC[cmd] || {description: "Node #{cmd} command"}
|
|
307
|
+
command cmd, **spec
|
|
308
|
+
end
|
|
309
|
+
command :sync, description: 'Synchronize folders (Gen3)'
|
|
310
|
+
commands_under :sync do
|
|
311
|
+
Sync::Operations::DIRECTIONS.each do |dir|
|
|
312
|
+
command dir, description: "#{dir.capitalize}-sync (Gen3)", transfer_paths: :send, arguments: SyncActions::PATH_AND_INFO_ARGS
|
|
313
|
+
end
|
|
314
|
+
command :admin, description: 'Manage sync database (admin operations)'
|
|
315
|
+
SyncActions.register_sync_admin_commands(self, :admin)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# One handler per COMMANDS_SHARES command (except :sync which is an intermediate node).
|
|
320
|
+
Node::COMMANDS_SHARES.reject { |cmd| cmd.eql?(:sync) }.each do |cmd|
|
|
321
|
+
define_action_method([:files, cmd]) do |shares_node_plugin:, **ctx|
|
|
322
|
+
shares_node_plugin.dispatch_v3_command(cmd, **ctx)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Handlers for files > sync > <direction>: delegate to Node's [:sync, dir] leaf.
|
|
327
|
+
Sync::Operations::DIRECTIONS.each do |dir|
|
|
328
|
+
define_action_method([:files, :sync, dir]) do |shares_node_plugin:, **ctx|
|
|
329
|
+
shares_node_plugin.dispatch_from_registry([:sync, dir], ctx, skip_setup: true)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
# Handlers for files > sync > admin > <op>: delegate to Node's [:sync, :admin, op] leaf.
|
|
333
|
+
SyncActions::ADMIN_COMMANDS.each do |op|
|
|
334
|
+
define_action_method([:files, :sync, :admin, op]) do |shares_node_plugin:, **ctx|
|
|
335
|
+
shares_node_plugin.dispatch_from_registry([:sync, :admin, op], ctx, skip_setup: true)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
private
|
|
340
|
+
|
|
341
|
+
# @return [String] admin API path for the given entity_type and location
|
|
342
|
+
def admin_entity_path(entity_type, location)
|
|
343
|
+
prefix = location.eql?(:all) ? '' : "#{location}_"
|
|
344
|
+
"data/#{prefix}#{entity_type}s"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# --- admin > user|group handlers ---
|
|
348
|
+
|
|
349
|
+
# Shared handler for list/show/delete/create/modify on user or group.
|
|
350
|
+
# @param entity_type [Symbol] :user or :group
|
|
351
|
+
# @param location [Symbol] :all or :local
|
|
352
|
+
# @param op [Symbol] CRUD operation
|
|
353
|
+
def action_admin_entity_crud(entity_type, location, op)
|
|
354
|
+
path = admin_entity_path(entity_type, location)
|
|
355
|
+
lookup = ->(f, v) { RestList.lookup_entity_generic(entity: entity_type, field: f, value: v) { @api_shares_admin.read(path) }['id'] }
|
|
356
|
+
display_fields = entity_type.eql?(:user) ? %w[id user_id username first_name last_name email] : nil
|
|
357
|
+
display_fields&.push('directory_user') if entity_type.eql?(:user) && location.eql?(:all)
|
|
358
|
+
# :all excludes :create; :local has a documented POST+PUT requestBody for both users and groups
|
|
359
|
+
body_component = location.eql?(:local) ? Schema::Registry::SHARES : nil
|
|
360
|
+
id = Operations::INSTANCE.include?(op) ? options.instance_identifier(&lookup) : nil
|
|
361
|
+
send(:"entity_#{op}", api: @api_shares_admin, entity: path, id: id, display_fields: display_fields, body_component: body_component)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Shared handler for USR_GRP_SETTINGS (transfer_settings, app_authorizations, share_permissions)
|
|
365
|
+
# @param entity_type [Symbol] :user or :group
|
|
366
|
+
# @param location [Symbol] :all or :local
|
|
367
|
+
# @param setting [Symbol] one of USR_GRP_SETTINGS
|
|
368
|
+
# @param op [Symbol] CRUD operation
|
|
369
|
+
# entity_id: resolved via arguments:(:identifier) on each setting leaf command
|
|
370
|
+
def action_admin_entity_setting(entity_type, location, setting, op, entity_id:, permission_id: nil, **)
|
|
371
|
+
path = admin_entity_path(entity_type, location)
|
|
372
|
+
entity_path = "#{path}/#{entity_id}/#{setting}"
|
|
373
|
+
is_singleton = !setting.eql?(:share_permissions)
|
|
374
|
+
id = permission_id || (Operations::INSTANCE.include?(op) && !is_singleton ? options.instance_identifier { |f, v| lookup_share_id(f, v) } : nil)
|
|
375
|
+
send(:"entity_#{op}", api: @api_shares_admin, entity: entity_path, id: id, is_singleton: is_singleton)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Shared handler for :users (group only)
|
|
379
|
+
# group_id: resolved by Phase A via arguments:(:identifier) on the :users node
|
|
380
|
+
def action_admin_entity_users(entity_type, location, op, group_id:, **)
|
|
381
|
+
path = admin_entity_path(entity_type, location)
|
|
382
|
+
prefix = location.eql?(:all) ? '' : "#{location}_"
|
|
383
|
+
id = Operations::INSTANCE.include?(op) ? options.instance_identifier : nil
|
|
384
|
+
send(:"entity_#{op}", api: @api_shares_admin, entity: "#{path}/#{group_id}/#{prefix}users", id: id)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Generate action_admin_<user|group>_<location>_<verb> for all combinations
|
|
388
|
+
%i[user group].each do |entity_type|
|
|
389
|
+
# all: list/show/delete only (no create, no modify — no update route on /users and /groups)
|
|
390
|
+
# local: full CRUD only — no nested settings under local_users/local_groups
|
|
391
|
+
ENTITY_LOCATIONS.each do |location|
|
|
392
|
+
ops = location.eql?(:all) ? (Operations::ALL - %i[create modify]) : Operations::ALL
|
|
393
|
+
ops.each do |op|
|
|
394
|
+
define_action_method([:admin, entity_type, location, op]) do
|
|
395
|
+
action_admin_entity_crud(entity_type, location, op)
|
|
180
396
|
end
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
entity: entities_path,
|
|
190
|
-
command: entity_verb,
|
|
191
|
-
display_fields: display_fields,
|
|
192
|
-
&lookup_block
|
|
193
|
-
)
|
|
194
|
-
when *USR_GRP_SETTINGS # transfer_settings, app_authorizations, share_permissions
|
|
195
|
-
group_id = options.instance_identifier(&lookup_block)
|
|
196
|
-
entities_path = "#{entities_path}/#{group_id}/#{entity_verb}"
|
|
197
|
-
return entity_execute(api: api_shares_admin, entity: entities_path, is_singleton: !entity_verb.eql?(:share_permissions), &lookup_share)
|
|
198
|
-
when :import # saml
|
|
199
|
-
return do_bulk_operation(command: entity_verb, descr: 'user information') do |entity_parameters|
|
|
200
|
-
entity_parameters = entity_parameters.transform_keys{ |k| k.gsub(/\s+/, '_').downcase}
|
|
201
|
-
Aspera.assert_type(entity_parameters, Hash)
|
|
202
|
-
SAML_IMPORT_MANDATORY.each{ |p| raise "missing mandatory field: #{p}" if entity_parameters[p].nil?}
|
|
203
|
-
entity_parameters.each_key do |p|
|
|
204
|
-
raise "unsupported field: #{p}, use: #{SAML_IMPORT_ALLOWED.join(',')}" unless SAML_IMPORT_ALLOWED.include?(p)
|
|
397
|
+
end
|
|
398
|
+
unless location.eql?(:local)
|
|
399
|
+
USR_GRP_SETTINGS.each do |setting|
|
|
400
|
+
# share_permissions: Rails only exposes index+show (read-only)
|
|
401
|
+
setting_ops = setting.eql?(:share_permissions) ? SHARE_PERMISSIONS_OPS : %i[show modify]
|
|
402
|
+
setting_ops.each do |op|
|
|
403
|
+
define_action_method([:admin, entity_type, location, setting, op]) do |**kwargs|
|
|
404
|
+
action_admin_entity_setting(entity_type, location, setting, op, entity_id: kwargs[:"#{entity_type}_id"], permission_id: kwargs[:permission_id])
|
|
205
405
|
end
|
|
206
|
-
api_shares_admin.create("#{entities_path}/import", entity_parameters)
|
|
207
|
-
end
|
|
208
|
-
when :add # ldap
|
|
209
|
-
return do_bulk_operation(command: entity_verb, descr: "#{entity_type} name", values: String) do |entity_name|
|
|
210
|
-
api_shares_admin.create(entities_path, {entity_type=>entity_name})
|
|
211
406
|
end
|
|
212
|
-
when :users # group
|
|
213
|
-
return entity_execute(api: api_shares_admin, entity: "#{entities_path}/#{options.instance_identifier(&lookup_block)}/#{entities_prefix}users")
|
|
214
|
-
else Aspera.error_unexpected_value(entity_verb)
|
|
215
407
|
end
|
|
216
408
|
end
|
|
409
|
+
next unless entity_type.eql?(:group)
|
|
410
|
+
# group users: no create route (Rails only exposes index+show+update+destroy)
|
|
411
|
+
GROUP_USERS_OPS.each do |op|
|
|
412
|
+
define_action_method([:admin, entity_type, location, :users, op]) do |group_id:, **|
|
|
413
|
+
action_admin_entity_users(entity_type, location, op, group_id: group_id)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# ldap: add
|
|
419
|
+
define_action_method([:admin, entity_type, :ldap, :add]) do |entity_name:, **|
|
|
420
|
+
path = admin_entity_path(entity_type, :ldap)
|
|
421
|
+
bulk_result(entity_name, command: :add, id_result: entity_type.to_s) do |entity_name|
|
|
422
|
+
@api_shares_admin.create(path, {entity_type => entity_name})
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# saml: import
|
|
427
|
+
define_action_method([:admin, entity_type, :saml, :import]) do |entity_parameters:, **|
|
|
428
|
+
path = admin_entity_path(entity_type, :saml)
|
|
429
|
+
bulk_result(entity_parameters, command: :import) do |entity_parameters|
|
|
430
|
+
entity_parameters = entity_parameters.transform_keys { |k| k.gsub(/\s+/, '_').downcase }
|
|
431
|
+
Aspera.assert_type(entity_parameters, Hash)
|
|
432
|
+
SAML_IMPORT_MANDATORY.each { |p| raise "missing mandatory field: #{p}" if entity_parameters[p].nil? }
|
|
433
|
+
entity_parameters.each_key do |p|
|
|
434
|
+
Aspera.assert_values(p, SAML_IMPORT_ALLOWED) { 'SAML field' }
|
|
435
|
+
end
|
|
436
|
+
@api_shares_admin.create("#{path}/import", entity_parameters)
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Handlers for admin > share > user_permissions|group_permissions > op
|
|
442
|
+
# share_id: resolved by Phase A via arguments:(:identifier, lookup: :lookup_share_id) on the perm_type node
|
|
443
|
+
# Rails only exposes index+show for share permissions (read-only)
|
|
444
|
+
%i[user_permissions group_permissions].each do |perm_type|
|
|
445
|
+
SHARE_PERMISSIONS_OPS.each do |op|
|
|
446
|
+
define_action_method([:admin, :share, perm_type, op]) do |share_id:, **|
|
|
447
|
+
id = Operations::INSTANCE.include?(op) ? options.instance_identifier : nil
|
|
448
|
+
send(:"entity_#{op}", api: @api_shares_admin, entity: "data/shares/#{share_id}/#{perm_type}", id: id)
|
|
449
|
+
end
|
|
217
450
|
end
|
|
218
451
|
end
|
|
219
452
|
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/assert'
|
|
4
|
+
|
|
5
|
+
module Aspera
|
|
6
|
+
module Cli
|
|
7
|
+
# Mixin for Config plugin: preset CRUD actions
|
|
8
|
+
module PresetActions
|
|
9
|
+
# Used to identify the global default preset keyword
|
|
10
|
+
GLOBAL_DEFAULT_KEYWORD = 'GLOBAL'
|
|
11
|
+
# Display columns for preset overview
|
|
12
|
+
CONF_OVERVIEW_KEYS = %w[preset parameter value].freeze
|
|
13
|
+
|
|
14
|
+
def action_preset_list(**)
|
|
15
|
+
Result::ValueList.new(presets.config_presets.keys, name: 'name')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def action_preset_overview(**)
|
|
19
|
+
cp = presets.config_presets
|
|
20
|
+
# Display process modifies the value (hide secrets): we do not want to save removed secrets
|
|
21
|
+
data = PresetManager.deep_clone(cp)
|
|
22
|
+
formatter.hide_secrets(data)
|
|
23
|
+
result = []
|
|
24
|
+
data.each do |config, preset|
|
|
25
|
+
preset.each do |parameter, value|
|
|
26
|
+
result.push(CONF_OVERVIEW_KEYS.zip([config, parameter, value]).to_h)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
Result::ObjectList.new(result, fields: CONF_OVERVIEW_KEYS)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def action_preset_lookup(**)
|
|
33
|
+
Plugins::BasicAuth.declare_options(options, parse: true)
|
|
34
|
+
url = options.get_option(:url, mandatory: true)
|
|
35
|
+
user = options.get_option(:username, mandatory: true)
|
|
36
|
+
result = presets.lookup_preset(url: url, username: user)
|
|
37
|
+
Aspera.assert(!result.nil?, type: Error) { 'no such config found' }
|
|
38
|
+
Result::SingleObject.new(result)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def action_preset_secure(config_name: nil, **)
|
|
42
|
+
cp = presets.config_presets
|
|
43
|
+
preset_names = config_name.nil? ? cp.keys : [config_name]
|
|
44
|
+
preset_names.each do |preset_name|
|
|
45
|
+
preset = cp[preset_name]
|
|
46
|
+
next unless preset.is_a?(Hash)
|
|
47
|
+
preset.each_key do |option_name|
|
|
48
|
+
secure_preset_option(preset, preset_name, option_name)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
Result::Status.new('Secrets secured in vault: Make sure to save the vault password securely.')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
SECRET_KEYWORDS = %w[password secret].freeze
|
|
57
|
+
|
|
58
|
+
# If +option_name+ ends with a secret keyword and the vault is configured,
|
|
59
|
+
# move the clear-text value into the vault and replace it with a @vault: reference.
|
|
60
|
+
# @param preset [Hash] the preset hash (modified in place)
|
|
61
|
+
# @param preset_name [String] name used as base for the vault label
|
|
62
|
+
# @param option_name [String] option key to inspect
|
|
63
|
+
def secure_preset_option(preset, preset_name, option_name)
|
|
64
|
+
return unless SECRET_KEYWORDS.any? { |kw| option_name.end_with?(kw) }
|
|
65
|
+
# Never auto-secure the global preset: it holds vault credentials themselves
|
|
66
|
+
return if preset_name.eql?(presets.global_default_preset)
|
|
67
|
+
return if vault.nil?
|
|
68
|
+
value = preset[option_name]
|
|
69
|
+
# Already a vault reference or nil — nothing to do
|
|
70
|
+
return if value.nil? || value.to_s.start_with?('@vault:')
|
|
71
|
+
vault_label = preset_name
|
|
72
|
+
incr = 0
|
|
73
|
+
until vault.get(label: vault_label, exception: false).nil?
|
|
74
|
+
vault_label = "#{preset_name}#{incr}"
|
|
75
|
+
incr += 1
|
|
76
|
+
end
|
|
77
|
+
Log.log.info { "Securing #{preset_name}.#{option_name} -> vault label: #{vault_label}" }
|
|
78
|
+
vault.set({label: vault_label, password: value})
|
|
79
|
+
preset[option_name] = "@vault:#{vault_label}.password"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
public
|
|
83
|
+
|
|
84
|
+
def action_preset_show(name:, **)
|
|
85
|
+
name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
|
|
86
|
+
cp = presets.config_presets
|
|
87
|
+
raise "no such preset: #{name}" unless cp.key?(name)
|
|
88
|
+
Result::SingleObject.new(PresetManager.deep_clone(cp[name]))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def action_preset_delete(name:, **)
|
|
92
|
+
name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
|
|
93
|
+
cp = presets.config_presets
|
|
94
|
+
raise "no such preset: #{name}" unless cp.key?(name)
|
|
95
|
+
cp.delete(name)
|
|
96
|
+
Result::Status.new("Deleted: #{name}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def action_preset_get(name:, param_name:, **)
|
|
100
|
+
name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
|
|
101
|
+
cp = presets.config_presets
|
|
102
|
+
raise "no such preset: #{name}" unless cp.key?(name)
|
|
103
|
+
value = cp[name][param_name]
|
|
104
|
+
raise "no such option in preset #{name} : #{param_name}" if value.nil?
|
|
105
|
+
case value
|
|
106
|
+
when Numeric, String then return Result::Text.new(ExtendedValue.instance.evaluate(value.to_s, context: 'preset'))
|
|
107
|
+
end
|
|
108
|
+
Result::SingleObject.new(value)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def action_preset_unset(name:, param_name:, **)
|
|
112
|
+
name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
|
|
113
|
+
cp = presets.config_presets
|
|
114
|
+
raise "no such preset: #{name}" unless cp.key?(name)
|
|
115
|
+
cp[name].delete(param_name)
|
|
116
|
+
Result::Status.new("Removed: #{name}: #{param_name}")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def action_preset_set(name:, param_name:, param_value:, **)
|
|
120
|
+
name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
|
|
121
|
+
param_name = Parser.option_line_to_name(param_name)
|
|
122
|
+
presets.set_key(name, param_name, Parser.smart_convert(param_value))
|
|
123
|
+
secure_preset_option(presets.config_presets[name], name, param_name)
|
|
124
|
+
Result::Nothing.new
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def action_preset_initialize(name:, config_value:, **)
|
|
128
|
+
name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
|
|
129
|
+
cp = presets.config_presets
|
|
130
|
+
Log.log.warn { "configuration already exists: #{name}, overwriting" } if cp.key?(name)
|
|
131
|
+
cp[name] = config_value
|
|
132
|
+
Result::Status.new("Modified: #{@option_config_file}")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def action_preset_update(name:, **)
|
|
136
|
+
name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
|
|
137
|
+
unprocessed_options = options.unprocessed_options_with_value
|
|
138
|
+
Log.dump(:opts, unprocessed_options)
|
|
139
|
+
cp = presets.config_presets
|
|
140
|
+
cp[name] ||= {}
|
|
141
|
+
cp[name].merge!(unprocessed_options)
|
|
142
|
+
unprocessed_options.each_key { |k| secure_preset_option(cp[name], name, k) }
|
|
143
|
+
Result::Status.new("Updated: #{name}")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def action_preset_ask(name:, option_names:, **)
|
|
147
|
+
name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
|
|
148
|
+
cp = presets.config_presets
|
|
149
|
+
cp[name] ||= {}
|
|
150
|
+
option_names.each do |option_name|
|
|
151
|
+
option_value = options.get_interactive(option_name, check_option: true)
|
|
152
|
+
cp[name][option_name] = option_value
|
|
153
|
+
secure_preset_option(cp[name], name, option_name)
|
|
154
|
+
end
|
|
155
|
+
Result::Status.new("Updated: #{name}")
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|