aspera-cli 4.16.0 → 4.17.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +50 -19
- data/CONTRIBUTING.md +3 -1
- data/README.md +965 -793
- data/bin/asession +29 -21
- data/lib/aspera/{fasp/agent_alpha.rb → agent/alpha.rb} +26 -25
- data/lib/aspera/{fasp/agent_base.rb → agent/base.rb} +15 -12
- data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
- data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +49 -53
- data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +20 -19
- data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +20 -33
- data/lib/aspera/{fasp/agent_trsdk.rb → agent/trsdk.rb} +11 -11
- data/lib/aspera/api/aoc.rb +586 -0
- data/lib/aspera/api/ats.rb +46 -0
- data/lib/aspera/api/cos_node.rb +95 -0
- data/lib/aspera/api/node.rb +344 -0
- data/lib/aspera/ascmd.rb +46 -10
- data/lib/aspera/{fasp → ascp}/installation.rb +5 -5
- data/lib/aspera/{fasp → ascp}/management.rb +3 -8
- data/lib/aspera/{fasp → ascp}/products.rb +1 -1
- data/lib/aspera/assert.rb +30 -30
- data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
- data/lib/aspera/cli/extended_value.rb +1 -1
- data/lib/aspera/cli/formatter.rb +13 -13
- data/lib/aspera/cli/hints.rb +5 -5
- data/lib/aspera/cli/main.rb +35 -28
- data/lib/aspera/cli/manager.rb +25 -24
- data/lib/aspera/cli/plugin.rb +22 -15
- data/lib/aspera/cli/plugin_factory.rb +61 -0
- data/lib/aspera/cli/plugins/alee.rb +7 -7
- data/lib/aspera/cli/plugins/aoc.rb +83 -77
- data/lib/aspera/cli/plugins/ats.rb +32 -33
- data/lib/aspera/cli/plugins/bss.rb +3 -4
- data/lib/aspera/cli/plugins/config.rb +169 -186
- data/lib/aspera/cli/plugins/console.rb +8 -6
- data/lib/aspera/cli/plugins/cos.rb +19 -18
- data/lib/aspera/cli/plugins/faspex.rb +61 -54
- data/lib/aspera/cli/plugins/faspex5.rb +150 -103
- data/lib/aspera/cli/plugins/node.rb +68 -73
- data/lib/aspera/cli/plugins/orchestrator.rb +34 -44
- data/lib/aspera/cli/plugins/preview.rb +31 -31
- data/lib/aspera/cli/plugins/server.rb +31 -33
- data/lib/aspera/cli/plugins/shares.rb +13 -11
- data/lib/aspera/cli/sync_actions.rb +8 -8
- data/lib/aspera/cli/transfer_agent.rb +32 -19
- data/lib/aspera/cli/transfer_progress.rb +1 -1
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +5 -0
- data/lib/aspera/command_line_builder.rb +14 -14
- data/lib/aspera/coverage.rb +1 -2
- data/lib/aspera/data_repository.rb +1 -1
- data/lib/aspera/environment.rb +2 -3
- data/lib/aspera/faspex_gw.rb +5 -6
- data/lib/aspera/faspex_postproc.rb +1 -1
- data/lib/aspera/id_generator.rb +2 -2
- data/lib/aspera/json_rpc.rb +5 -5
- data/lib/aspera/keychain/encrypted_hash.rb +6 -6
- data/lib/aspera/keychain/macos_security.rb +27 -22
- data/lib/aspera/log.rb +2 -2
- data/lib/aspera/nagios.rb +3 -3
- data/lib/aspera/node_simulator.rb +5 -6
- data/lib/aspera/oauth/base.rb +143 -0
- data/lib/aspera/oauth/factory.rb +124 -0
- data/lib/aspera/oauth/generic.rb +34 -0
- data/lib/aspera/oauth/jwt.rb +51 -0
- data/lib/aspera/oauth/url_json.rb +31 -0
- data/lib/aspera/oauth/web.rb +50 -0
- data/lib/aspera/oauth.rb +5 -331
- data/lib/aspera/open_application.rb +7 -7
- data/lib/aspera/persistency_action_once.rb +4 -4
- data/lib/aspera/persistency_folder.rb +2 -2
- data/lib/aspera/preview/generator.rb +5 -5
- data/lib/aspera/preview/terminal.rb +3 -2
- data/lib/aspera/preview/utils.rb +3 -3
- data/lib/aspera/proxy_auto_config.rb +4 -4
- data/lib/aspera/rest.rb +175 -144
- data/lib/aspera/rest_errors_aspera.rb +3 -3
- data/lib/aspera/resumer.rb +77 -0
- data/lib/aspera/ssh.rb +6 -1
- data/lib/aspera/{fasp → transfer}/error.rb +3 -3
- data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
- data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
- data/lib/aspera/{fasp → transfer}/parameters.rb +58 -89
- data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +18 -16
- data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
- data/lib/aspera/{fasp → transfer}/sync.rb +32 -32
- data/lib/aspera/{fasp → transfer}/uri.rb +9 -8
- data/lib/aspera/web_server_simple.rb +11 -3
- data.tar.gz.sig +0 -0
- metadata +36 -63
- metadata.gz.sig +0 -0
- data/lib/aspera/aoc.rb +0 -601
- data/lib/aspera/ats_api.rb +0 -47
- data/lib/aspera/cos_node.rb +0 -94
- data/lib/aspera/fasp/resume_policy.rb +0 -79
- data/lib/aspera/node.rb +0 -339
@@ -29,20 +29,20 @@ module Aspera
|
|
29
29
|
# Called by provider of definition before constructor of this class so that params_definition has all mandatory fields
|
30
30
|
def normalize_description(full_description)
|
31
31
|
full_description.each do |name, options|
|
32
|
-
assert_type(options, Hash){name}
|
32
|
+
Aspera.assert_type(options, Hash){name}
|
33
33
|
unsupported_keys = options.keys - OPTIONS_KEYS
|
34
|
-
assert(unsupported_keys.empty?){"Unsupported definition keys: #{unsupported_keys}"}
|
35
|
-
assert(options.key?(:cli)){"Missing key: cli for #{name}"}
|
36
|
-
assert_type(options[:cli], Hash){'Key: cli'}
|
37
|
-
assert(options[:cli].key?(:type)){'Missing key: cli.type'}
|
38
|
-
assert_values(options[:cli][:type], CLI_OPTION_TYPES){"Unsupported processing type for #{name}"}
|
34
|
+
Aspera.assert(unsupported_keys.empty?){"Unsupported definition keys: #{unsupported_keys}"}
|
35
|
+
Aspera.assert(options.key?(:cli)){"Missing key: cli for #{name}"}
|
36
|
+
Aspera.assert_type(options[:cli], Hash){'Key: cli'}
|
37
|
+
Aspera.assert(options[:cli].key?(:type)){'Missing key: cli.type'}
|
38
|
+
Aspera.assert_values(options[:cli][:type], CLI_OPTION_TYPES){"Unsupported processing type for #{name}"}
|
39
39
|
# by default : optional
|
40
40
|
options[:mandatory] ||= false
|
41
41
|
options[:desc] ||= ''
|
42
42
|
options[:desc] = "DEPRECATED: #{options[:deprecation]}\n#{options[:desc]}" if options.key?(:deprecation)
|
43
43
|
cli = options[:cli]
|
44
44
|
unsupported_cli_keys = cli.keys - CLI_KEYS
|
45
|
-
assert(unsupported_cli_keys.empty?){"Unsupported cli keys: #{unsupported_cli_keys}"}
|
45
|
+
Aspera.assert(unsupported_cli_keys.empty?){"Unsupported cli keys: #{unsupported_cli_keys}"}
|
46
46
|
# by default : string, unless it's without arg
|
47
47
|
options[:accepted_types] ||= options[:cli][:type].eql?(:opt_without_arg) ? :bool : :string
|
48
48
|
# single type is placed in array
|
@@ -111,7 +111,7 @@ module Aspera
|
|
111
111
|
end
|
112
112
|
processing_type = read ? :get_value : options[:cli][:type]
|
113
113
|
# check mandatory parameter (nil is valid value)
|
114
|
-
raise
|
114
|
+
raise Transfer::Error, "Missing mandatory parameter: #{name}" if options[:mandatory] && !@param_hash.key?(name)
|
115
115
|
parameter_value = @param_hash[name]
|
116
116
|
# no default setting
|
117
117
|
# parameter_value=options[:default] if parameter_value.nil? and options.has_key?(:default)
|
@@ -123,11 +123,11 @@ module Aspera
|
|
123
123
|
when :hash then Hash
|
124
124
|
when :int then Integer
|
125
125
|
when :bool then [TrueClass, FalseClass]
|
126
|
-
else error_unexpected_value(type_symbol)
|
126
|
+
else Aspera.error_unexpected_value(type_symbol)
|
127
127
|
end
|
128
128
|
end.flatten
|
129
129
|
# check that value is of expected type
|
130
|
-
raise
|
130
|
+
raise Transfer::Error, "#{name} is : #{parameter_value.class} (#{parameter_value}), shall be #{options[:accepted_types]}, " \
|
131
131
|
unless parameter_value.nil? || expected_classes.include?(parameter_value.class)
|
132
132
|
# special processing will be requested with type get_value
|
133
133
|
@used_param_names.push(name) unless processing_type.eql?(:special)
|
@@ -149,10 +149,10 @@ module Aspera
|
|
149
149
|
# :convert has name of class and encoding method
|
150
150
|
conversion_class, conversion_method = options[:cli][:convert].split('.')
|
151
151
|
converted_value = Kernel.const_get(conversion_class).send(conversion_method, parameter_value)
|
152
|
-
raise
|
152
|
+
raise Transfer::Error, "unsupported #{name}: #{parameter_value}" if converted_value.nil?
|
153
153
|
parameter_value = converted_value
|
154
154
|
when NilClass
|
155
|
-
else error_unexpected_value(options[:cli][:convert].class)
|
155
|
+
else Aspera.error_unexpected_value(options[:cli][:convert].class)
|
156
156
|
end
|
157
157
|
|
158
158
|
case processing_type
|
@@ -161,14 +161,14 @@ module Aspera
|
|
161
161
|
when :ignore, :special # ignore this parameter or process later
|
162
162
|
return
|
163
163
|
when :envvar # set in env var
|
164
|
-
assert(options[:cli].key?(:variable)){'missing key: cli.variable'}
|
164
|
+
Aspera.assert(options[:cli].key?(:variable)){'missing key: cli.variable'}
|
165
165
|
@result[:env][options[:cli][:variable]] = parameter_value
|
166
166
|
when :opt_without_arg # if present and true : just add option without value
|
167
167
|
add_param = false
|
168
168
|
case parameter_value
|
169
169
|
when false then nil # nothing to put on command line, no creation by default
|
170
170
|
when true then add_param = true
|
171
|
-
else error_unexpected_value(parameter_value){name}
|
171
|
+
else Aspera.error_unexpected_value(parameter_value){name}
|
172
172
|
end
|
173
173
|
add_param = !add_param if options[:add_on_false]
|
174
174
|
add_command_line_options([options[:cli][:switch]]) if add_param
|
data/lib/aspera/coverage.rb
CHANGED
@@ -4,8 +4,7 @@
|
|
4
4
|
if ENV.key?('ENABLE_COVERAGE')
|
5
5
|
require 'simplecov'
|
6
6
|
require 'securerandom'
|
7
|
-
# compute
|
8
|
-
# use dirname instead of gsub, in case folder separator is not /
|
7
|
+
# compute development top folder based on this source location
|
9
8
|
development_root = 3.times.inject(File.realpath(__FILE__)) { |p, _| File.dirname(p) }
|
10
9
|
SimpleCov.root(development_root)
|
11
10
|
SimpleCov.enable_for_subprocesses if SimpleCov.respond_to?(:enable_for_subprocesses)
|
@@ -30,7 +30,7 @@ module Aspera
|
|
30
30
|
return format('%08x-%04x-%04x-%04x-%04x%08x', *raw_data.unpack('NnnnnN'))
|
31
31
|
when :'aspera.global-cli-client', :'aspera.drive'
|
32
32
|
return Base64.urlsafe_encode64(raw_data)
|
33
|
-
else error_unexpected_value(name)
|
33
|
+
else Aspera.error_unexpected_value(name)
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
data/lib/aspera/environment.rb
CHANGED
@@ -56,8 +56,7 @@ module Aspera
|
|
56
56
|
when /s390/
|
57
57
|
return CPU_S390
|
58
58
|
when /arm/, /aarch64/
|
59
|
-
|
60
|
-
return CPU_X86_64 if os.eql?(OS_X)
|
59
|
+
return CPU_ARM64
|
61
60
|
end
|
62
61
|
raise "Unknown CPU: #{RbConfig::CONFIG['host_cpu']}"
|
63
62
|
end
|
@@ -90,7 +89,7 @@ module Aspera
|
|
90
89
|
|
91
90
|
# value is provided in block
|
92
91
|
def write_file_restricted(path, force: false, mode: nil)
|
93
|
-
assert(block_given?, exception_class: Aspera::InternalError)
|
92
|
+
Aspera.assert(block_given?, exception_class: Aspera::InternalError)
|
94
93
|
if force || !File.exist?(path)
|
95
94
|
# Windows may give error
|
96
95
|
File.unlink(path) rescue nil
|
data/lib/aspera/faspex_gw.rb
CHANGED
@@ -8,12 +8,11 @@ require 'json'
|
|
8
8
|
module Aspera
|
9
9
|
# Simulate the Faspex 4 /send API and creates a package on Aspera on Cloud or Faspex 5
|
10
10
|
class Faspex4GWServlet < WEBrick::HTTPServlet::AbstractServlet
|
11
|
-
# @param app_api
|
11
|
+
# @param app_api
|
12
12
|
# @param app_context [String]
|
13
13
|
def initialize(server, app_api, app_context)
|
14
|
-
assert_values(app_api.class.name, ['Aspera::AoC', 'Aspera::Rest'])
|
14
|
+
Aspera.assert_values(app_api.class.name, ['Aspera::Api::AoC', 'Aspera::Rest'])
|
15
15
|
super(server)
|
16
|
-
# typed: Aspera::AoC
|
17
16
|
@app_api = app_api
|
18
17
|
@app_context = app_context
|
19
18
|
end
|
@@ -51,7 +50,7 @@ module Aspera
|
|
51
50
|
operation: 'POST',
|
52
51
|
subpath: "packages/#{package['id']}/transfer_spec/upload",
|
53
52
|
headers: {'Accept' => 'application/json'},
|
54
|
-
url_params: {transfer_type:
|
53
|
+
url_params: {transfer_type: Cli::Plugins::Faspex5::TRANSFER_CONNECT},
|
55
54
|
json_params: {paths: [{'destination'=>'/'}]}
|
56
55
|
)[:data]
|
57
56
|
transfer_spec.delete('authentication')
|
@@ -72,11 +71,11 @@ module Aspera
|
|
72
71
|
# compare string, as class is not yet known here
|
73
72
|
faspex_package_create_result =
|
74
73
|
case @app_api.class.name
|
75
|
-
when 'Aspera::AoC'
|
74
|
+
when 'Aspera::Api::AoC'
|
76
75
|
faspex4_send_to_aoc(faspex_pkg_parameters)
|
77
76
|
when 'Aspera::Rest'
|
78
77
|
faspex4_send_to_faspex5(faspex_pkg_parameters)
|
79
|
-
else error_unexpected_value(@app_api.class.name)
|
78
|
+
else Aspera.error_unexpected_value(@app_api.class.name)
|
80
79
|
end
|
81
80
|
Log.log.info{"faspex_package_create_result=#{faspex_package_create_result}"}
|
82
81
|
response.status = 200
|
@@ -12,7 +12,7 @@ module Aspera
|
|
12
12
|
class Faspex4PostProcServlet < WEBrick::HTTPServlet::AbstractServlet
|
13
13
|
ALLOWED_PARAMETERS = %i[root script_folder fail_on_error timeout_seconds].freeze
|
14
14
|
def initialize(server, parameters)
|
15
|
-
assert_type(parameters, Hash)
|
15
|
+
Aspera.assert_type(parameters, Hash)
|
16
16
|
@parameters = parameters.symbolize_keys
|
17
17
|
Log.log.debug{Log.dump(:post_proc_parameters, @parameters)}
|
18
18
|
raise "unexpected key in parameters config: only: #{ALLOWED_PARAMETERS.join(', ')}" if @parameters.keys.any?{|k|!ALLOWED_PARAMETERS.include?(k)}
|
data/lib/aspera/id_generator.rb
CHANGED
@@ -17,10 +17,10 @@ module Aspera
|
|
17
17
|
i.is_a?(String) && i.start_with?('https://') ? URI.parse(i).host : i.to_s
|
18
18
|
end.join(ID_SEPARATOR)
|
19
19
|
end
|
20
|
-
assert_type(object_id, String)
|
20
|
+
Aspera.assert_type(object_id, String)
|
21
21
|
return object_id
|
22
22
|
.gsub(WINDOWS_PROTECTED_CHAR, PROTECTED_CHAR_REPLACE) # remove windows forbidden chars
|
23
|
-
.gsub('.', PROTECTED_CHAR_REPLACE)
|
23
|
+
.gsub('.', PROTECTED_CHAR_REPLACE) # keep dot for extension only (nicer)
|
24
24
|
.downcase
|
25
25
|
end
|
26
26
|
end
|
data/lib/aspera/json_rpc.rb
CHANGED
@@ -35,11 +35,11 @@ module Aspera
|
|
35
35
|
params: args,
|
36
36
|
id: @request_id += 1
|
37
37
|
})[:data]
|
38
|
-
assert_type(data, Hash){'response'}
|
39
|
-
assert(data['jsonrpc'] == JSON_RPC_VERSION){'bad version in response'}
|
40
|
-
assert(data.key?('id')){'missing id in response'}
|
41
|
-
assert(!(data.key?('error') && data.key?('result'))){'both error and response'}
|
42
|
-
assert(
|
38
|
+
Aspera.assert_type(data, Hash){'response'}
|
39
|
+
Aspera.assert(data['jsonrpc'] == JSON_RPC_VERSION){'bad version in response'}
|
40
|
+
Aspera.assert(data.key?('id')){'missing id in response'}
|
41
|
+
Aspera.assert(!(data.key?('error') && data.key?('result'))){'both error and response'}
|
42
|
+
Aspera.assert(
|
43
43
|
!data.key?('error') ||
|
44
44
|
data['error'].is_a?(Hash) &&
|
45
45
|
data['error']['code'].is_a?(Integer) &&
|
@@ -17,7 +17,7 @@ module Aspera
|
|
17
17
|
CONTENT_KEYS = %i[label username password url description].freeze
|
18
18
|
FILE_KEYS = %w[version type cipher data].sort.freeze
|
19
19
|
def initialize(path, current_password)
|
20
|
-
assert_type(path, String){'path to vault file'}
|
20
|
+
Aspera.assert_type(path, String){'path to vault file'}
|
21
21
|
@path = path
|
22
22
|
@all_secrets = {}
|
23
23
|
vault_encrypted_data = nil
|
@@ -25,7 +25,7 @@ module Aspera
|
|
25
25
|
vault_file = File.read(@path)
|
26
26
|
if vault_file.start_with?('---')
|
27
27
|
vault_info = YAML.parse(vault_file).to_ruby
|
28
|
-
assert(vault_info.keys.sort == FILE_KEYS){'Invalid vault file'}
|
28
|
+
Aspera.assert(vault_info.keys.sort == FILE_KEYS){'Invalid vault file'}
|
29
29
|
@cipher_name = vault_info['cipher']
|
30
30
|
vault_encrypted_data = vault_info['data']
|
31
31
|
else
|
@@ -65,11 +65,11 @@ module Aspera
|
|
65
65
|
# set a secret
|
66
66
|
# @param options [Hash] with keys :label, :username, :password, :url, :description
|
67
67
|
def set(options)
|
68
|
-
assert_type(options, Hash){'options'}
|
68
|
+
Aspera.assert_type(options, Hash){'options'}
|
69
69
|
unsupported = options.keys - CONTENT_KEYS
|
70
|
-
assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
70
|
+
Aspera.assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
71
71
|
options.each_pair do |k, v|
|
72
|
-
assert_type(v, String){k.to_s}
|
72
|
+
Aspera.assert_type(v, String){k.to_s}
|
73
73
|
end
|
74
74
|
label = options.delete(:label)
|
75
75
|
raise "secret #{label} already exist, delete first" if @all_secrets.key?(label)
|
@@ -94,7 +94,7 @@ module Aspera
|
|
94
94
|
end
|
95
95
|
|
96
96
|
def get(label:, exception: true)
|
97
|
-
assert(@all_secrets.key?(label)){"Label not found: #{label}"} if exception
|
97
|
+
Aspera.assert(@all_secrets.key?(label)){"Label not found: #{label}"} if exception
|
98
98
|
result = @all_secrets[label].clone
|
99
99
|
result[:label] = label if result.is_a?(Hash)
|
100
100
|
return result
|
@@ -4,6 +4,7 @@
|
|
4
4
|
require 'aspera/cli/info'
|
5
5
|
require 'aspera/log'
|
6
6
|
require 'aspera/assert'
|
7
|
+
require 'open3'
|
7
8
|
|
8
9
|
# enhance the gem to support other key chains
|
9
10
|
module Aspera
|
@@ -11,6 +12,8 @@ module Aspera
|
|
11
12
|
module MacosSecurity
|
12
13
|
# keychain based on macOS keychain, using `security` command line
|
13
14
|
class Keychain
|
15
|
+
# https://www.unix.com/man-page/osx/1/security/
|
16
|
+
SECURITY_UTILITY = 'security'
|
14
17
|
DOMAINS = %i[user system common dynamic].freeze
|
15
18
|
LIST_OPTIONS = {
|
16
19
|
domain: :c
|
@@ -38,25 +41,27 @@ module Aspera
|
|
38
41
|
url = options&.delete(:url)
|
39
42
|
if !url.nil?
|
40
43
|
uri = URI.parse(url)
|
41
|
-
assert(uri.scheme.eql?('https')){'only https'}
|
44
|
+
Aspera.assert(uri.scheme.eql?('https')){'only https'}
|
42
45
|
options[:protocol] = 'htps' # cspell: disable-line
|
43
46
|
raise 'host required in URL' if uri.host.nil?
|
44
47
|
options[:server] = uri.host
|
45
48
|
options[:path] = uri.path unless ['', '/'].include?(uri.path)
|
46
49
|
options[:port] = uri.port unless uri.port.eql?(443) && !url.include?(':443/')
|
47
50
|
end
|
48
|
-
|
51
|
+
command_line = [SECURITY_UTILITY, command]
|
49
52
|
options&.each do |k, v|
|
50
|
-
assert(supported.key?(k)){"unknown option: #{k}"}
|
53
|
+
Aspera.assert(supported.key?(k)){"unknown option: #{k}"}
|
51
54
|
next if v.nil?
|
52
|
-
|
53
|
-
|
55
|
+
command_line.push("-#{supported[k]}")
|
56
|
+
command_line.push(v.shellescape) unless v.empty?
|
54
57
|
end
|
55
|
-
|
56
|
-
Log.log.debug{"executing>>#{
|
57
|
-
|
58
|
-
Log.log.debug{"
|
59
|
-
|
58
|
+
command_line.push(last_opt) unless last_opt.nil?
|
59
|
+
Log.log.debug{"executing>>#{command_line.join(' ')}"}
|
60
|
+
stdout, stderr, status = Open3.capture3(*command_line)
|
61
|
+
Log.log.debug{"status=#{status}, stderr=#{stderr}"}
|
62
|
+
Log.log.trace1{"stdout=#{stdout}"}
|
63
|
+
raise "#{SECURITY_UTILITY} failed: #{status.exitstatus} : #{stderr}" unless status.success?
|
64
|
+
return stdout
|
60
65
|
end
|
61
66
|
|
62
67
|
def key_chains(output)
|
@@ -72,7 +77,7 @@ module Aspera
|
|
72
77
|
end
|
73
78
|
|
74
79
|
def list(options={})
|
75
|
-
assert_values(options[:domain], DOMAINS, exception_class: ArgumentError){'domain'} unless options[:domain].nil?
|
80
|
+
Aspera.assert_values(options[:domain], DOMAINS, exception_class: ArgumentError){'domain'} unless options[:domain].nil?
|
76
81
|
key_chains(execute('list-key_chains', options, LIST_OPTIONS))
|
77
82
|
end
|
78
83
|
|
@@ -91,15 +96,15 @@ module Aspera
|
|
91
96
|
end
|
92
97
|
|
93
98
|
def password(operation, pass_type, options)
|
94
|
-
assert_values(operation, %i[add find delete]){'operation'}
|
95
|
-
assert_values(pass_type, %i[generic internet]){'pass_type'}
|
96
|
-
assert_type(options, Hash)
|
99
|
+
Aspera.assert_values(operation, %i[add find delete]){'operation'}
|
100
|
+
Aspera.assert_values(pass_type, %i[generic internet]){'pass_type'}
|
101
|
+
Aspera.assert_type(options, Hash)
|
97
102
|
missing = (operation.eql?(:add) ? %i[account service password] : %i[label]) - options.keys
|
98
|
-
assert(missing.empty?){"missing options: #{missing}"}
|
103
|
+
Aspera.assert(missing.empty?){"missing options: #{missing}"}
|
99
104
|
options[:getpass] = '' if operation.eql?(:find)
|
100
105
|
output = self.class.execute("#{operation}-#{pass_type}-password", options, ADD_PASS_OPTIONS, @path)
|
101
106
|
raise output.gsub(/^.*: /, '') if output.start_with?('security: ')
|
102
|
-
return
|
107
|
+
return unless operation.eql?(:find)
|
103
108
|
attributes = {}
|
104
109
|
output.split("\n").each do |line|
|
105
110
|
case line
|
@@ -129,18 +134,18 @@ module Aspera
|
|
129
134
|
end
|
130
135
|
|
131
136
|
def set(options)
|
132
|
-
assert_type(options, Hash){'options'}
|
137
|
+
Aspera.assert_type(options, Hash){'options'}
|
133
138
|
unsupported = options.keys - %i[label username password url description]
|
134
|
-
assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
139
|
+
Aspera.assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
135
140
|
@keychain.password(
|
136
141
|
:add, :generic, service: options[:label],
|
137
142
|
account: options[:username] || 'none', password: options[:password], comment: options[:description])
|
138
143
|
end
|
139
144
|
|
140
145
|
def get(options)
|
141
|
-
assert_type(options, Hash){'options'}
|
146
|
+
Aspera.assert_type(options, Hash){'options'}
|
142
147
|
unsupported = options.keys - %i[label]
|
143
|
-
assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
148
|
+
Aspera.assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
144
149
|
info = @keychain.password(:find, :generic, label: options[:label])
|
145
150
|
raise 'not found' if info.nil?
|
146
151
|
result = options.clone
|
@@ -155,9 +160,9 @@ module Aspera
|
|
155
160
|
end
|
156
161
|
|
157
162
|
def delete(options)
|
158
|
-
assert_type(options, Hash){'options'}
|
163
|
+
Aspera.assert_type(options, Hash){'options'}
|
159
164
|
unsupported = options.keys - %i[label]
|
160
|
-
assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
165
|
+
Aspera.assert(unsupported.empty?){"unsupported options: #{unsupported}"}
|
161
166
|
raise 'delete not implemented, use macos keychain app'
|
162
167
|
end
|
163
168
|
end
|
data/lib/aspera/log.rb
CHANGED
@@ -14,7 +14,7 @@ class Logger
|
|
14
14
|
TRACE_MAX = 2
|
15
15
|
# add custom level to logger severity
|
16
16
|
module Severity
|
17
|
-
1.upto(TRACE_MAX).each { |level| const_set("TRACE#{level}", - level)}
|
17
|
+
1.upto(TRACE_MAX).each { |level| const_set(:"TRACE#{level}", - level)}
|
18
18
|
end
|
19
19
|
# quick access to label
|
20
20
|
SEVERITY_LABEL = Severity.constants.each_with_object({}) { |name, hash| hash[Severity.const_get(name)] = name}
|
@@ -99,7 +99,7 @@ module Aspera
|
|
99
99
|
Logger::Severity.constants.each do |name|
|
100
100
|
return name.downcase.to_sym if @logger.level.eql?(Logger::Severity.const_get(name))
|
101
101
|
end
|
102
|
-
error_unexpected_value(@logger.level){'log level'}
|
102
|
+
Aspera.error_unexpected_value(@logger.level){'log level'}
|
103
103
|
end
|
104
104
|
|
105
105
|
# change underlying logger, but keep log level
|
data/lib/aspera/nagios.rb
CHANGED
@@ -23,10 +23,10 @@ module Aspera
|
|
23
23
|
class << self
|
24
24
|
# process results of a analysis and display status and exit with code
|
25
25
|
def process(data)
|
26
|
-
assert_type(data, Array)
|
27
|
-
assert(!data.empty?){'data is empty'}
|
26
|
+
Aspera.assert_type(data, Array)
|
27
|
+
Aspera.assert(!data.empty?){'data is empty'}
|
28
28
|
%w[status component message].each do |c|
|
29
|
-
assert(data.first.key?(c)){"result must have #{c}"}
|
29
|
+
Aspera.assert(data.first.key?(c)){"result must have #{c}"}
|
30
30
|
end
|
31
31
|
res_errors = data.reject{|s|s['status'].eql?('ok')}
|
32
32
|
# keep only errors in case of problem, other ok are assumed so
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'aspera/ascp/installation'
|
4
|
+
require 'aspera/agent/direct'
|
3
5
|
require 'aspera/log'
|
4
|
-
require 'aspera/fasp/installation'
|
5
|
-
require 'aspera/fasp/agent_direct'
|
6
6
|
require 'webrick'
|
7
7
|
require 'json'
|
8
8
|
|
@@ -11,12 +11,12 @@ module Aspera
|
|
11
11
|
class NodeSimulatorServlet < WEBrick::HTTPServlet::AbstractServlet
|
12
12
|
PATH_TRANSFERS = '/ops/transfers'
|
13
13
|
PATH_ONE_TRANSFER = %r{/ops/transfers/(.+)$}
|
14
|
-
# @param app_api [
|
14
|
+
# @param app_api [Api::AoC]
|
15
15
|
# @param app_context [String]
|
16
16
|
def initialize(server, credentials, transfer)
|
17
17
|
super(server)
|
18
18
|
@credentials = credentials
|
19
|
-
@xfer_manager =
|
19
|
+
@xfer_manager = Agent::Direct.new
|
20
20
|
end
|
21
21
|
|
22
22
|
def do_POST(request, response)
|
@@ -27,7 +27,6 @@ module Aspera
|
|
27
27
|
result = session[:ts].clone
|
28
28
|
result['id'] = job_id
|
29
29
|
set_json_response(response, result)
|
30
|
-
Log.log.debug{">>> transfer started: #{job_id}"}
|
31
30
|
else
|
32
31
|
set_json_response(response, [{error: 'Bad request'}], code: 400)
|
33
32
|
end
|
@@ -36,7 +35,7 @@ module Aspera
|
|
36
35
|
def do_GET(request, response)
|
37
36
|
case request.path
|
38
37
|
when '/info'
|
39
|
-
info =
|
38
|
+
info = Ascp::Installation.instance.ascp_info
|
40
39
|
set_json_response(response, {
|
41
40
|
application: 'node',
|
42
41
|
current_time: Time.now.utc.iso8601(0),
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aspera/oauth/factory'
|
4
|
+
require 'aspera/log'
|
5
|
+
require 'aspera/assert'
|
6
|
+
require 'aspera/id_generator'
|
7
|
+
require 'date'
|
8
|
+
|
9
|
+
module Aspera
|
10
|
+
module OAuth
|
11
|
+
# Implement OAuth 2 for the REST client and generate a bearer token
|
12
|
+
# call get_authorization() to get a token.
|
13
|
+
# bearer tokens are kept in memory and also in a file cache for later re-use
|
14
|
+
# if a token is expired (api returns 4xx), call again get_authorization(refresh: true)
|
15
|
+
# https://tools.ietf.org/html/rfc6749
|
16
|
+
class Base
|
17
|
+
# scope can be modified after creation
|
18
|
+
attr_writer :scope
|
19
|
+
|
20
|
+
# [M]=mandatory [D]=has default value [O]=Optional/nil
|
21
|
+
# @param base_url [M] URL of authentication API
|
22
|
+
# @param auth [O] basic auth parameters
|
23
|
+
# @param client_id [O]
|
24
|
+
# @param client_secret [O]
|
25
|
+
# @param scope [O]
|
26
|
+
# @param path_token [D] API end point to create a token
|
27
|
+
# @param token_field [D] field in result that contains the token
|
28
|
+
def initialize(
|
29
|
+
base_url:,
|
30
|
+
auth: nil,
|
31
|
+
client_id: nil,
|
32
|
+
client_secret: nil,
|
33
|
+
scope: nil,
|
34
|
+
path_token: 'token', # default endpoint for /token to generate token
|
35
|
+
token_field: 'access_token' # field with token in result of call to path_token
|
36
|
+
)
|
37
|
+
Aspera.assert_type(base_url, String)
|
38
|
+
Aspera.assert(respond_to?(:create_token), 'create_token method must be defined', exception_class: InternalError)
|
39
|
+
@base_url = base_url
|
40
|
+
@path_token = path_token
|
41
|
+
@token_field = token_field
|
42
|
+
@client_id = client_id
|
43
|
+
@client_secret = client_secret
|
44
|
+
@scope = scope
|
45
|
+
@identifiers = []
|
46
|
+
@identifiers.push(auth[:username]) if auth.is_a?(Hash) && auth.key?(:username)
|
47
|
+
# this is the OAuth API
|
48
|
+
@api = Rest.new(
|
49
|
+
base_url: @base_url,
|
50
|
+
redirect_max: 2,
|
51
|
+
auth: auth)
|
52
|
+
end
|
53
|
+
|
54
|
+
# helper method to create token as per RFC
|
55
|
+
def create_token_call(www_params)
|
56
|
+
Log.log.debug{'Generating a new token'.bg_green}
|
57
|
+
return @api.call(
|
58
|
+
operation: 'POST',
|
59
|
+
subpath: @path_token,
|
60
|
+
headers: {'Accept' => 'application/json'},
|
61
|
+
www_body_params: www_params)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return Hash with optional general parameters
|
65
|
+
def optional_scope_client_id(add_secret: false)
|
66
|
+
call_params = {}
|
67
|
+
call_params[:scope] = @scope unless @scope.nil?
|
68
|
+
call_params[:client_id] = @client_id unless @client_id.nil?
|
69
|
+
call_params[:client_secret] = @client_secret if add_secret && !@client_id.nil?
|
70
|
+
return call_params
|
71
|
+
end
|
72
|
+
|
73
|
+
# OAuth v2 token generation
|
74
|
+
# @param use_refresh_token set to true to force refresh or re-generation (if previous failed)
|
75
|
+
def get_authorization(use_refresh_token: false, use_cache: true)
|
76
|
+
# generate token unique identifier for persistency (memory/disk cache)
|
77
|
+
token_id = IdGenerator.from_list(Factory.id(
|
78
|
+
@base_url,
|
79
|
+
@grant_method,
|
80
|
+
@identifiers,
|
81
|
+
@scope
|
82
|
+
))
|
83
|
+
|
84
|
+
# get token_data from cache (or nil), token_data is what is returned by /token
|
85
|
+
token_data = Factory.instance.persist_mgr.get(token_id) if use_cache
|
86
|
+
token_data = JSON.parse(token_data) unless token_data.nil?
|
87
|
+
# Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
|
88
|
+
# might help in case the transfer agent cannot refresh himself
|
89
|
+
# `direct` agent is equipped with refresh code
|
90
|
+
if !use_refresh_token && !token_data.nil?
|
91
|
+
decoded_token = OAuth::Factory.instance.decode_token(token_data[@token_field])
|
92
|
+
Log.log.debug{Log.dump('decoded_token', decoded_token)} unless decoded_token.nil?
|
93
|
+
if decoded_token.is_a?(Hash)
|
94
|
+
expires_at_sec =
|
95
|
+
if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
|
96
|
+
elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
|
97
|
+
end
|
98
|
+
# force refresh if we see a token too close from expiration
|
99
|
+
use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < OAuth::Factory.instance.globals[:token_expiration_guard_sec]
|
100
|
+
Log.log.debug{"Expiration: #{expires_at_sec} / #{use_refresh_token}"}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# an API was already called, but failed, we need to regenerate or refresh
|
105
|
+
if use_refresh_token
|
106
|
+
if token_data.is_a?(Hash) && token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
|
107
|
+
# save possible refresh token, before deleting the cache
|
108
|
+
refresh_token = token_data['refresh_token']
|
109
|
+
end
|
110
|
+
# delete cache
|
111
|
+
Factory.instance.persist_mgr.delete(token_id)
|
112
|
+
token_data = nil
|
113
|
+
# lets try the existing refresh token
|
114
|
+
if !refresh_token.nil?
|
115
|
+
Log.log.info{"refresh=[#{refresh_token}]".bg_green}
|
116
|
+
# try to refresh
|
117
|
+
# note: AoC admin token has no refresh, and lives by default 1800secs
|
118
|
+
resp = create_token_call(optional_scope_client_id.merge(grant_type: 'refresh_token', refresh_token: refresh_token))
|
119
|
+
if resp[:http].code.start_with?('2')
|
120
|
+
# save only if success
|
121
|
+
json_data = resp[:http].body
|
122
|
+
token_data = JSON.parse(json_data)
|
123
|
+
Factory.instance.persist_mgr.put(token_id, json_data)
|
124
|
+
else
|
125
|
+
Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# no cache, nor refresh: generate a token
|
131
|
+
if token_data.nil?
|
132
|
+
resp = create_token
|
133
|
+
json_data = resp[:http].body
|
134
|
+
token_data = JSON.parse(json_data)
|
135
|
+
Factory.instance.persist_mgr.put(token_id, json_data)
|
136
|
+
end # if ! in_cache
|
137
|
+
Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
|
138
|
+
# ok we shall have a token here
|
139
|
+
return OAuth::Factory.bearer_build(token_data[@token_field])
|
140
|
+
end
|
141
|
+
end # OAuth
|
142
|
+
end
|
143
|
+
end
|