aspera-cli 4.26.0 → 4.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +36 -4
- data/CONTRIBUTING.md +2 -5
- data/bin/ascli +2 -2
- data/lib/aspera/agent/direct.rb +1 -1
- data/lib/aspera/agent/factory.rb +4 -0
- data/lib/aspera/agent/httpgw.rb +1 -1
- data/lib/aspera/agent/node.rb +2 -2
- data/lib/aspera/api/aoc.rb +45 -37
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +7 -6
- data/lib/aspera/api/node.rb +9 -9
- data/lib/aspera/ascmd.rb +4 -4
- data/lib/aspera/ascp/installation.rb +13 -6
- data/lib/aspera/assert.rb +17 -13
- data/lib/aspera/cli/context.rb +58 -0
- data/lib/aspera/cli/extended_value.rb +9 -4
- data/lib/aspera/cli/formatter.rb +91 -187
- data/lib/aspera/cli/http.rb +167 -0
- data/lib/aspera/cli/manager.rb +132 -77
- data/lib/aspera/cli/options.schema.yaml +82 -0
- data/lib/aspera/cli/plugins/alee.rb +2 -2
- data/lib/aspera/cli/plugins/aoc.rb +113 -77
- data/lib/aspera/cli/plugins/ats.rb +16 -16
- data/lib/aspera/cli/plugins/base.rb +64 -49
- data/lib/aspera/cli/plugins/config.rb +172 -444
- data/lib/aspera/cli/plugins/console.rb +7 -7
- data/lib/aspera/cli/plugins/cos.rb +2 -2
- data/lib/aspera/cli/plugins/factory.rb +3 -0
- data/lib/aspera/cli/plugins/faspex.rb +22 -22
- data/lib/aspera/cli/plugins/faspex5.rb +68 -49
- data/lib/aspera/cli/plugins/faspio.rb +1 -1
- data/lib/aspera/cli/plugins/httpgw.rb +2 -2
- data/lib/aspera/cli/plugins/node.rb +80 -80
- data/lib/aspera/cli/plugins/orchestrator.rb +21 -26
- data/lib/aspera/cli/plugins/preview.rb +9 -9
- data/lib/aspera/cli/plugins/server.rb +7 -7
- data/lib/aspera/cli/plugins/shares.rb +2 -2
- data/lib/aspera/cli/preset_manager.rb +235 -0
- data/lib/aspera/cli/result.rb +310 -0
- data/lib/aspera/cli/{main.rb → runner.rb} +48 -130
- data/lib/aspera/cli/sync_actions.rb +15 -11
- data/lib/aspera/cli/terminal_formatter.rb +65 -0
- data/lib/aspera/cli/transfer_agent.rb +17 -15
- data/lib/aspera/cli/transfer_progress.rb +6 -6
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +5 -6
- data/lib/aspera/command_line_builder.rb +22 -18
- data/lib/aspera/data_repository.rb +4 -0
- data/lib/aspera/dot_container.rb +1 -1
- data/lib/aspera/environment.rb +25 -6
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/formatter_interface.rb +14 -0
- data/lib/aspera/hash_ext.rb +6 -0
- data/lib/aspera/json_rpc.rb +6 -5
- data/lib/aspera/keychain/base.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +1 -1
- data/lib/aspera/keychain/factory.rb +1 -1
- data/lib/aspera/keychain/macos_security.rb +1 -1
- data/lib/aspera/log.rb +10 -5
- data/lib/aspera/markdown.rb +4 -1
- data/lib/aspera/nagios.rb +2 -2
- data/lib/aspera/oauth/base.rb +5 -5
- data/lib/aspera/oauth/boot.rb +43 -0
- data/lib/aspera/oauth/factory.rb +38 -14
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/oauth.rb +1 -0
- data/lib/aspera/persistency_action_once.rb +2 -2
- data/lib/aspera/preview/file_types.rb +4 -0
- data/lib/aspera/products/connect.rb +3 -0
- data/lib/aspera/products/transferd.rb +2 -2
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +9 -5
- data/lib/aspera/rest_error_analyzer.rb +4 -0
- data/lib/aspera/rest_list.rb +10 -3
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +62811 -0
- data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +9637 -0
- data/lib/aspera/schema/async_tables.yaml +361 -0
- data/lib/aspera/schema/documentation.rb +107 -0
- data/lib/aspera/schema/reader.rb +75 -0
- data/lib/aspera/schema/registry.rb +70 -0
- data/lib/aspera/secret_hider.rb +4 -0
- data/lib/aspera/sync/conf.schema.yaml +0 -26
- data/lib/aspera/sync/database.rb +14 -10
- data/lib/aspera/sync/operations.rb +14 -10
- data/lib/aspera/temp_file_manager.rb +4 -0
- data/lib/aspera/transfer/faux_file.rb +1 -1
- data/lib/aspera/transfer/parameters.rb +4 -4
- data/lib/aspera/transfer/resumer.rb +2 -2
- data/lib/aspera/transfer/spec.rb +3 -3
- data/lib/aspera/transfer/spec.schema.yaml +18 -9
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/yaml.rb +4 -2
- data.tar.gz.sig +0 -0
- metadata +28 -15
- metadata.gz.sig +1 -1
- data/lib/aspera/transfer/spec_doc.rb +0 -76
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/assert'
|
|
4
|
+
|
|
5
|
+
module Aspera
|
|
6
|
+
module Cli
|
|
7
|
+
# Global objects shared with plugins
|
|
8
|
+
class Context
|
|
9
|
+
# @type [Array<Symbol>]
|
|
10
|
+
MEMBERS = %i[options transfer config formatter persistency man_header presets http_config].freeze
|
|
11
|
+
# @!attribute [rw] options
|
|
12
|
+
# @return [Manager] the command line options manager
|
|
13
|
+
# @!attribute [rw] transfer
|
|
14
|
+
# @return [TransferAgent] the transfer agent, used by transfer plugins
|
|
15
|
+
# @!attribute [rw] config
|
|
16
|
+
# @return [Plugins::Config] the configuration plugin, used by plugins to get configuration values and presets
|
|
17
|
+
# @!attribute [rw] formatter
|
|
18
|
+
# @return [Formatter] the formatter, used by plugins to display results and messages
|
|
19
|
+
# @!attribute [rw] persistency
|
|
20
|
+
# @return [Object] # whatever the type is
|
|
21
|
+
# @!attribute [rw] man_header
|
|
22
|
+
# @return [Boolean] whether to display the manual header in plugin help
|
|
23
|
+
# @!attribute [rw] presets
|
|
24
|
+
# @return [PresetManager] manages the YAML config file and preset resolution
|
|
25
|
+
# @!attribute [rw] http_config
|
|
26
|
+
# @return [Http] manages HTTP/S and TLS runtime options
|
|
27
|
+
attr_accessor(*MEMBERS)
|
|
28
|
+
|
|
29
|
+
# Initialize all members to nil, so that they are defined and can be validated later
|
|
30
|
+
# @return [nil]
|
|
31
|
+
def initialize
|
|
32
|
+
MEMBERS.each{ |i| instance_variable_set(:"@#{i}", nil)}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Validate that all members are set, raise exception if not
|
|
36
|
+
# @raise [Aspera::AssertionError] if any member is not set
|
|
37
|
+
# @return [nil]
|
|
38
|
+
def validate
|
|
39
|
+
MEMBERS.each do |i|
|
|
40
|
+
Aspera.assert(instance_variable_defined?(:"@#{i}")){"context member @#{i} is not defined"}
|
|
41
|
+
Aspera.assert(!instance_variable_get(:"@#{i}").nil?){"context member @#{i} is nil"}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if the context is in manual-only mode
|
|
46
|
+
# @return [Boolean] true if in manual-only mode
|
|
47
|
+
def only_manual?
|
|
48
|
+
transfer.eql?(:only_manual)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Set the context to manual-only mode
|
|
52
|
+
# @return [Symbol] :only_manual
|
|
53
|
+
def only_manual!
|
|
54
|
+
@transfer = :only_manual
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -15,6 +15,10 @@ require 'singleton'
|
|
|
15
15
|
module Aspera
|
|
16
16
|
module Cli
|
|
17
17
|
# Command line extended values
|
|
18
|
+
#
|
|
19
|
+
# @!method self.instance
|
|
20
|
+
# Returns the singleton instance of ExtendedValue
|
|
21
|
+
# @return [ExtendedValue] the singleton instance
|
|
18
22
|
class ExtendedValue
|
|
19
23
|
include Singleton
|
|
20
24
|
|
|
@@ -82,6 +86,7 @@ module Aspera
|
|
|
82
86
|
# Base handlers
|
|
83
87
|
# Other handlers can be set using `on`
|
|
84
88
|
# e.g. `preset` is reader in config plugin
|
|
89
|
+
# @type [Hash{Symbol => Proc}]
|
|
85
90
|
@handlers = {
|
|
86
91
|
val: lambda{ |i| i},
|
|
87
92
|
base64: lambda{ |i| Base64.decode64(i)},
|
|
@@ -91,12 +96,12 @@ module Aspera
|
|
|
91
96
|
uri: lambda{ |i| UriReader.read(i)},
|
|
92
97
|
json: lambda{ |i| ExtendedValue.JSON_parse(i)},
|
|
93
98
|
lines: lambda{ |i| i.split("\n")},
|
|
94
|
-
list: lambda{ |i| i[1
|
|
99
|
+
list: lambda{ |i| i[1..].split(i[0])},
|
|
95
100
|
none: lambda{ |i| ExtendedValue.assert_no_value(i, :none); nil}, # rubocop:disable Style/Semicolon
|
|
96
101
|
path: lambda{ |i| File.expand_path(i)},
|
|
97
102
|
re: lambda{ |i| Regexp.new(i, Regexp::MULTILINE)},
|
|
98
103
|
ruby: lambda{ |i| Environment.secure_eval(i, __FILE__, __LINE__)},
|
|
99
|
-
s: lambda
|
|
104
|
+
s: lambda(&:to_s),
|
|
100
105
|
secret: lambda{ |i| prompt = i.empty? ? 'secret' : i; $stdin.getpass("#{prompt}> ")}, # rubocop:disable Style/Semicolon
|
|
101
106
|
stdin: lambda{ |i| ExtendedValue.read_stdin(i)},
|
|
102
107
|
yaml: lambda{ |i| YAML.load(i)},
|
|
@@ -133,7 +138,7 @@ module Aspera
|
|
|
133
138
|
# Add a new handler
|
|
134
139
|
def on(name, &block)
|
|
135
140
|
Aspera.assert_type(name, Symbol){'name'}
|
|
136
|
-
Aspera.assert(block)
|
|
141
|
+
Aspera.assert(block){"block required when registering handler for #{name}"}
|
|
137
142
|
Log.log.debug{"Setting handler for #{name}"}
|
|
138
143
|
@handlers[name] = block
|
|
139
144
|
update_regex
|
|
@@ -145,7 +150,7 @@ module Aspera
|
|
|
145
150
|
# @param value [String] the value to parse
|
|
146
151
|
# @param context [String] Context in which evaluation is done
|
|
147
152
|
# @param allowed [Array<Class>,NilClass] Expected types
|
|
148
|
-
# @return [
|
|
153
|
+
# @return [String, Integer, Array, Hash, Boolean] Evaluated value
|
|
149
154
|
def evaluate(value, context:, allowed: nil)
|
|
150
155
|
return value unless value.is_a?(String)
|
|
151
156
|
Aspera.assert_array_all(allowed, Class) unless allowed.nil?
|
data/lib/aspera/cli/formatter.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# cspell:ignore jsonpp
|
|
4
4
|
require 'aspera/cli/special_values'
|
|
5
|
+
require 'aspera/cli/terminal_formatter'
|
|
5
6
|
require 'aspera/preview/terminal'
|
|
6
7
|
require 'aspera/secret_hider'
|
|
7
8
|
require 'aspera/environment'
|
|
@@ -20,70 +21,7 @@ module Aspera
|
|
|
20
21
|
module Cli
|
|
21
22
|
# Take care of CLI output on terminal
|
|
22
23
|
class Formatter
|
|
23
|
-
# remove a fields from the list
|
|
24
|
-
FIELDS_LESS = '-'
|
|
25
|
-
# supported output formats
|
|
26
|
-
DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
|
|
27
|
-
# user output levels
|
|
28
|
-
DISPLAY_LEVELS = %i[info data error].freeze
|
|
29
|
-
# column names for single object display in table
|
|
30
|
-
SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
|
|
31
|
-
# Terminal: vertical separator for list of strings.
|
|
32
|
-
STR_LST_SEP_VERT = "\n"
|
|
33
|
-
|
|
34
|
-
private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES, :STR_LST_SEP_VERT
|
|
35
|
-
|
|
36
24
|
class << self
|
|
37
|
-
# nicer display for boolean
|
|
38
|
-
# used by `spec_doc`
|
|
39
|
-
def tick(yes)
|
|
40
|
-
result =
|
|
41
|
-
if Environment.terminal_supports_unicode?
|
|
42
|
-
yes ? "\u2713" : "\u2717"
|
|
43
|
-
else
|
|
44
|
-
yes ? 'Y' : ' '
|
|
45
|
-
end
|
|
46
|
-
return result.green if yes
|
|
47
|
-
return result.red
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Highlight special values on terminal
|
|
51
|
-
# empty values are dim
|
|
52
|
-
# used by `spec_doc`
|
|
53
|
-
def special_format(what)
|
|
54
|
-
result = "<#{what}>"
|
|
55
|
-
return %w[null empty].any?{ |s| what.include?(s)} ? result.dim : result.reverse_color
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# For transfer spec table, build line for display in terminal
|
|
59
|
-
# used by `spec_doc`
|
|
60
|
-
def check_row(row)
|
|
61
|
-
row.each_key do |k|
|
|
62
|
-
row[k] = row[k].map{ |i| WordWrap.ww(i.to_s, 120).chomp}.join("\n") if row[k].is_a?(Array)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Give Markdown String, or matched data, return formatted string for terminal
|
|
67
|
-
# used by `spec_doc`
|
|
68
|
-
# @param match [MatchData,String]
|
|
69
|
-
def markdown_text(match)
|
|
70
|
-
if match.is_a?(String)
|
|
71
|
-
match = Markdown::FORMATS.match(match)
|
|
72
|
-
Aspera.assert(match)
|
|
73
|
-
end
|
|
74
|
-
Aspera.assert_type(match, MatchData)
|
|
75
|
-
if match[:entity]
|
|
76
|
-
Aspera.assert_values(match[:entity], 'bsol')
|
|
77
|
-
'\\'
|
|
78
|
-
elsif match[:bold]
|
|
79
|
-
match[:bold].to_s.blue
|
|
80
|
-
elsif match[:code]
|
|
81
|
-
match[:code].to_s.bold
|
|
82
|
-
else
|
|
83
|
-
Aspera.error_unexpected_value(match.to_s)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
25
|
# Replace special values with a readable version on terminal
|
|
88
26
|
def replace_specific_for_terminal(input_hash, string_list_separator)
|
|
89
27
|
hash_to_process = [input_hash]
|
|
@@ -92,14 +30,14 @@ module Aspera
|
|
|
92
30
|
current.each do |key, value|
|
|
93
31
|
case value
|
|
94
32
|
when NilClass
|
|
95
|
-
current[key] = special_format('null')
|
|
33
|
+
current[key] = TerminalFormatter.special_format('null')
|
|
96
34
|
when String
|
|
97
|
-
current[key] = special_format('empty string') if value.empty?
|
|
35
|
+
current[key] = TerminalFormatter.special_format('empty string') if value.empty?
|
|
98
36
|
when Proc
|
|
99
|
-
current[key] = special_format('lambda')
|
|
37
|
+
current[key] = TerminalFormatter.special_format('lambda')
|
|
100
38
|
when Array
|
|
101
39
|
if value.empty?
|
|
102
|
-
current[key] = special_format('empty list')
|
|
40
|
+
current[key] = TerminalFormatter.special_format('empty list')
|
|
103
41
|
elsif value.all?(String)
|
|
104
42
|
current[key] = value.join(string_list_separator)
|
|
105
43
|
else
|
|
@@ -109,7 +47,7 @@ module Aspera
|
|
|
109
47
|
end
|
|
110
48
|
when Hash
|
|
111
49
|
if value.empty?
|
|
112
|
-
current[key] = special_format('empty dict')
|
|
50
|
+
current[key] = TerminalFormatter.special_format('empty dict')
|
|
113
51
|
else
|
|
114
52
|
hash_to_process.push(value)
|
|
115
53
|
end
|
|
@@ -129,6 +67,17 @@ module Aspera
|
|
|
129
67
|
@spinner = nil
|
|
130
68
|
end
|
|
131
69
|
|
|
70
|
+
# Display item count if total is provided
|
|
71
|
+
def display_item_count(count, total)
|
|
72
|
+
return if total.nil?
|
|
73
|
+
count = count.to_i
|
|
74
|
+
total = total.to_i
|
|
75
|
+
return if total.eql?(0) && count.eql?(0)
|
|
76
|
+
count_msg = "Items: #{count}/#{total}"
|
|
77
|
+
count_msg = count_msg.bg_red unless count.eql?(total)
|
|
78
|
+
display_status(count_msg)
|
|
79
|
+
end
|
|
80
|
+
|
|
132
81
|
def long_operation(title = nil, action: :spin)
|
|
133
82
|
return unless Environment.terminal?
|
|
134
83
|
return if %i[error data].include?(@options[:display])
|
|
@@ -209,10 +158,16 @@ module Aspera
|
|
|
209
158
|
nil
|
|
210
159
|
end
|
|
211
160
|
|
|
212
|
-
# Main output method
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
161
|
+
# Main output method for displaying messages to the user
|
|
162
|
+
# This method is used by Result classes to output formatted data
|
|
163
|
+
# @param message_level [Symbol] The level of the message - must be one of: :data, :info, :error
|
|
164
|
+
# @param message [String] The message to display
|
|
165
|
+
# @param hide_secrets [Boolean] Whether to hide secrets in the message (default: true)
|
|
166
|
+
# @return [void]
|
|
167
|
+
# @note Message display behavior depends on the message_level:
|
|
168
|
+
# - +:data+ messages are displayed unless display level is +:error+
|
|
169
|
+
# - +:info+ messages are only displayed when display level is +:info+
|
|
170
|
+
# - +:error+ messages are always displayed on stderr
|
|
216
171
|
def display_message(message_level, message, hide_secrets: true)
|
|
217
172
|
message = SecretHider.instance.hide_secrets_in_string(message) if hide_secrets && message.is_a?(String) && hide_secrets?
|
|
218
173
|
case message_level
|
|
@@ -227,129 +182,65 @@ module Aspera
|
|
|
227
182
|
display_message(:info, status, **kwargs)
|
|
228
183
|
end
|
|
229
184
|
|
|
230
|
-
|
|
231
|
-
number = number.to_i
|
|
232
|
-
total = total.to_i
|
|
233
|
-
return if total.eql?(0) && number.eql?(0)
|
|
234
|
-
count_msg = "Items: #{number}/#{total}"
|
|
235
|
-
count_msg = count_msg.bg_red unless number.eql?(total)
|
|
236
|
-
display_status(count_msg)
|
|
237
|
-
end
|
|
238
|
-
|
|
185
|
+
# Check if secrets should be hidden
|
|
239
186
|
def hide_secrets?
|
|
240
187
|
!@options[:show_secrets] && !@options[:display].eql?(:data)
|
|
241
188
|
end
|
|
242
189
|
|
|
243
|
-
#
|
|
190
|
+
# Hides secrets in Hash or Array
|
|
244
191
|
def hide_secrets(data)
|
|
245
192
|
SecretHider.instance.deep_remove_secret(data) if hide_secrets?
|
|
246
193
|
end
|
|
247
194
|
|
|
248
|
-
# Display results
|
|
249
|
-
#
|
|
250
|
-
#
|
|
251
|
-
# @param
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
Log.
|
|
257
|
-
Log.dump(:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
hide_secrets(data)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
when :text
|
|
266
|
-
display_message(:data, data.to_s)
|
|
267
|
-
when :nagios
|
|
268
|
-
Nagios.process(data)
|
|
269
|
-
when :ruby
|
|
270
|
-
display_message(:data, PP.pp(filter_list_on_fields(data), +''))
|
|
271
|
-
when :json
|
|
272
|
-
display_message(:data, JSON.generate(filter_list_on_fields(data)))
|
|
273
|
-
when :jsonpp
|
|
274
|
-
display_message(:data, JSON.pretty_generate(filter_list_on_fields(data)))
|
|
275
|
-
when :yaml
|
|
276
|
-
display_message(:data, YAML.dump(filter_list_on_fields(data)))
|
|
277
|
-
when :image
|
|
278
|
-
# if object or list, then must be a single
|
|
279
|
-
case type
|
|
280
|
-
when :single_object, :object_list
|
|
281
|
-
data = [data] if type.eql?(:single_object)
|
|
282
|
-
raise BadArgument, 'image display requires a single result' unless data.length == 1
|
|
283
|
-
fields = compute_fields(data, fields)
|
|
284
|
-
raise BadArgument, 'select a single field to display' unless fields.length == 1
|
|
285
|
-
data = data.first
|
|
286
|
-
raise BadArgument, 'no such field' unless data.key?(fields.first)
|
|
287
|
-
data = data[fields.first]
|
|
288
|
-
end
|
|
289
|
-
Aspera.assert_type(data, String){'URL or blob for image'}
|
|
290
|
-
# Check if URL
|
|
291
|
-
data =
|
|
292
|
-
begin
|
|
293
|
-
# just validate
|
|
294
|
-
URI.parse(data)
|
|
295
|
-
if Environment.instance.url_method.eql?(:text)
|
|
296
|
-
UriReader.read(data)
|
|
297
|
-
else
|
|
298
|
-
Environment.instance.open_uri(data)
|
|
299
|
-
display_message(:info, "Opened URL in browser: #{data}")
|
|
300
|
-
:done
|
|
301
|
-
end
|
|
302
|
-
rescue URI::InvalidURIError
|
|
303
|
-
data
|
|
304
|
-
end
|
|
305
|
-
# try base64
|
|
306
|
-
data = begin
|
|
307
|
-
Base64.strict_decode64(data)
|
|
308
|
-
rescue
|
|
309
|
-
data
|
|
310
|
-
end
|
|
311
|
-
# here, data is the image blob
|
|
312
|
-
display_message(:data, Preview::Terminal.build(data, **@options[:image].symbolize_keys)) unless data.eql?(:done)
|
|
313
|
-
when :table, :csv
|
|
314
|
-
case type
|
|
315
|
-
when :single_object
|
|
316
|
-
# :single_object is a Hash, where key=column name
|
|
317
|
-
Aspera.assert_type(data, Hash){'result'}
|
|
318
|
-
if data.empty?
|
|
319
|
-
display_message(:data, self.class.special_format('empty dict'))
|
|
320
|
-
else
|
|
321
|
-
data = DotContainer.new(data).to_dotted if @options[:flat_hash]
|
|
322
|
-
display_table([data], compute_fields([data], fields), single: true)
|
|
323
|
-
end
|
|
324
|
-
when :object_list
|
|
325
|
-
# :object_list is an Array of Hash, where key=column name
|
|
326
|
-
Aspera.assert_array_all(data, Hash){'result'}
|
|
327
|
-
data = data.map{ |obj| DotContainer.new(obj).to_dotted} if @options[:flat_hash]
|
|
328
|
-
display_table(data, compute_fields(data, fields), single: type.eql?(:single_object))
|
|
329
|
-
when :value_list
|
|
330
|
-
# :value_list is a simple array of values, name of column provided in `name`
|
|
331
|
-
display_table(data.map{ |i| {name => i}}, [name])
|
|
332
|
-
when :special # no table
|
|
333
|
-
if data.eql?(:nothing)
|
|
334
|
-
Log.log.debug('no result expected')
|
|
335
|
-
return
|
|
336
|
-
end
|
|
337
|
-
display_message(:info, self.class.special_format(data.to_s))
|
|
338
|
-
return
|
|
339
|
-
when :status # no table
|
|
340
|
-
# :status displays a simple message
|
|
341
|
-
display_message(:info, data)
|
|
342
|
-
when :text # no table
|
|
343
|
-
# :status displays a simple message
|
|
344
|
-
display_message(:data, data)
|
|
345
|
-
else Aspera.error_unexpected_value(type){'data type'}
|
|
346
|
-
end
|
|
347
|
-
else Aspera.error_unexpected_value(@options[:format]){'format'}
|
|
348
|
-
end
|
|
195
|
+
# Display results using the Visitor pattern
|
|
196
|
+
# Each Result subclass knows how to format itself by calling the appropriate
|
|
197
|
+
# formatter method, eliminating the need for type checking in the formatter.
|
|
198
|
+
# @param result [Result] Result object to display
|
|
199
|
+
def display_results(result)
|
|
200
|
+
require 'aspera/cli/result'
|
|
201
|
+
Aspera.assert_type(result, Cli::Result){'result must be a Result object'}
|
|
202
|
+
|
|
203
|
+
Log.log.debug{"display_results: result class=#{result.class.name}"}
|
|
204
|
+
Log.dump(:data, result.data, level: :trace1)
|
|
205
|
+
Log.dump(:fields, result.fields, level: :trace1)
|
|
206
|
+
|
|
207
|
+
# Hide secrets in data
|
|
208
|
+
hide_secrets(result.data)
|
|
209
|
+
|
|
210
|
+
# Use the Visitor pattern: delegate formatting to the result object
|
|
211
|
+
result.format(self)
|
|
349
212
|
end
|
|
350
|
-
|
|
213
|
+
# Public methods exposed to Result classes for formatting
|
|
351
214
|
|
|
352
|
-
|
|
215
|
+
# Get the current format type
|
|
216
|
+
def format_type
|
|
217
|
+
@options[:format]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Set the format type (used by Image result)
|
|
221
|
+
def set_format_type(format)
|
|
222
|
+
@options[:format] = format
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Hide secrets in a string
|
|
226
|
+
def hide_secrets_in_string(string)
|
|
227
|
+
SecretHider.instance.hide_secrets_in_string(string)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Get special format string
|
|
231
|
+
def special_format(text)
|
|
232
|
+
TerminalFormatter.special_format(text)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Check if flat_hash option is enabled
|
|
236
|
+
def flat_hash?
|
|
237
|
+
@options[:flat_hash]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Get image options
|
|
241
|
+
def image_options
|
|
242
|
+
@options[:image].symbolize_keys
|
|
243
|
+
end
|
|
353
244
|
|
|
354
245
|
# @return all fields of all objects in list of objects
|
|
355
246
|
def all_fields(data)
|
|
@@ -379,7 +270,7 @@ module Aspera
|
|
|
379
270
|
removal = false
|
|
380
271
|
if item[0].eql?(FIELDS_LESS)
|
|
381
272
|
removal = true
|
|
382
|
-
item = item
|
|
273
|
+
item = item.delete_prefix(FIELDS_LESS)
|
|
383
274
|
end
|
|
384
275
|
case item
|
|
385
276
|
when SpecialValues::ALL
|
|
@@ -439,7 +330,7 @@ module Aspera
|
|
|
439
330
|
Aspera.assert_array_all(fields, String)
|
|
440
331
|
if object_array.empty?
|
|
441
332
|
# no display for csv
|
|
442
|
-
display_message(:info,
|
|
333
|
+
display_message(:info, TerminalFormatter.special_format('empty')) if @options[:format].eql?(:table)
|
|
443
334
|
return
|
|
444
335
|
end
|
|
445
336
|
filter_columns_on_select(object_array)
|
|
@@ -501,6 +392,19 @@ module Aspera
|
|
|
501
392
|
end
|
|
502
393
|
nil
|
|
503
394
|
end
|
|
395
|
+
|
|
396
|
+
# remove a fields from the list
|
|
397
|
+
FIELDS_LESS = '-'
|
|
398
|
+
# supported output formats
|
|
399
|
+
DISPLAY_FORMATS = %i[text nagios ruby json jsonpp yaml table csv image].freeze
|
|
400
|
+
# user output levels
|
|
401
|
+
DISPLAY_LEVELS = %i[info data error].freeze
|
|
402
|
+
# column names for single object display in table
|
|
403
|
+
SINGLE_OBJECT_COLUMN_NAMES = %i[field value].freeze
|
|
404
|
+
# Terminal: vertical separator for list of strings.
|
|
405
|
+
STR_LST_SEP_VERT = "\n"
|
|
406
|
+
|
|
407
|
+
private_constant :FIELDS_LESS, :DISPLAY_FORMATS, :DISPLAY_LEVELS, :SINGLE_OBJECT_COLUMN_NAMES, :STR_LST_SEP_VERT
|
|
504
408
|
end
|
|
505
409
|
end
|
|
506
410
|
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/cli/special_values'
|
|
4
|
+
require 'aspera/log'
|
|
5
|
+
require 'aspera/assert'
|
|
6
|
+
require 'aspera/line_logger'
|
|
7
|
+
require 'openssl'
|
|
8
|
+
|
|
9
|
+
module Aspera
|
|
10
|
+
module Cli
|
|
11
|
+
# Encapsulates all HTTP/S and TLS runtime configuration options.
|
|
12
|
+
# Extracted from Plugins::Config so it can be referenced independently
|
|
13
|
+
# via Context#http_config without coupling to the plugin machinery.
|
|
14
|
+
class Http
|
|
15
|
+
# Certificate file extensions recognized when scanning a folder
|
|
16
|
+
CERT_EXT = %w[crt cer pem der].freeze
|
|
17
|
+
# OpenSSL constant that disables peer verification (VERIFY_NONE)
|
|
18
|
+
SELF_SIGNED_CERT = OpenSSL::SSL.const_get(:enon_yfirev.to_s.upcase.reverse) # cspell: disable-line
|
|
19
|
+
|
|
20
|
+
private_constant :CERT_EXT, :SELF_SIGNED_CERT
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@insecure = false
|
|
24
|
+
@warn_insecure = true
|
|
25
|
+
@ignore_cert_host_port = []
|
|
26
|
+
@http_options = {}
|
|
27
|
+
@ssl_warned_urls = []
|
|
28
|
+
@certificate_store = nil
|
|
29
|
+
@certificate_paths = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_accessor :insecure, :warn_insecure, :http_options
|
|
33
|
+
attr_reader :ignore_cert_host_port
|
|
34
|
+
|
|
35
|
+
# Declare all HTTP/S CLI options, with handlers pointing to self.
|
|
36
|
+
# Called once from Config#initialize after this object is instantiated.
|
|
37
|
+
# @param options [Aspera::Cli::Manager]
|
|
38
|
+
def declare_options(options)
|
|
39
|
+
options.declare(:insecure, 'HTTP/S: Do not validate any certificate', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :insecure}, default: false)
|
|
40
|
+
options.declare(:ignore_certificate, 'HTTP/S: Do not validate certificate for these URLs', allowed: [Array, NilClass], handler: {o: self, m: :ignore_cert_host_port})
|
|
41
|
+
options.declare(:warn_insecure, 'HTTP/S: Issue a warning if certificate is ignored', allowed: Allowed::TYPES_BOOLEAN, handler: {o: self, m: :warn_insecure}, default: true)
|
|
42
|
+
options.declare(:cert_stores, 'HTTP/S: List of folder with trusted certificates', allowed: Allowed::TYPES_STRING_ARRAY, handler: {o: self, m: :trusted_cert_locations})
|
|
43
|
+
options.declare(:http_options, 'HTTP/S: Options for HTTP/S socket', allowed: Hash, handler: {o: self, m: :http_options}, default: {})
|
|
44
|
+
options.declare(:http_proxy, 'HTTP/S: URL for proxy with optional credentials', handler: {o: self, m: :http_proxy})
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# Proxy
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def http_proxy
|
|
52
|
+
ENV['http_proxy']
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def http_proxy=(value)
|
|
56
|
+
URI.parse(value)
|
|
57
|
+
ENV['http_proxy'] = value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
# Per-URL certificate ignore list
|
|
62
|
+
# ------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def ignore_cert_host_port=(url_list)
|
|
65
|
+
url_list.each do |url|
|
|
66
|
+
uri = URI.parse(url)
|
|
67
|
+
raise "Expecting https scheme: #{url}" unless uri.scheme.eql?('https')
|
|
68
|
+
@ignore_cert_host_port.push([uri.host, uri.port].freeze)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Should the certificate be ignored for this host/port?
|
|
73
|
+
# Also logs a warning the first time (if warn_insecure is set).
|
|
74
|
+
def ignore_cert?(address, port)
|
|
75
|
+
endpoint = [address, port].freeze
|
|
76
|
+
ignore_cert = @insecure || @ignore_cert_host_port.any?(endpoint)
|
|
77
|
+
if ignore_cert && @warn_insecure
|
|
78
|
+
base_url = "https://#{address}:#{port}"
|
|
79
|
+
unless @ssl_warned_urls.include?(base_url)
|
|
80
|
+
Log.log.warn{"Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production."}
|
|
81
|
+
@ssl_warned_urls.push(base_url)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
Log.log.debug{"ignore cert? #{endpoint} -> #{ignore_cert}"}
|
|
85
|
+
ignore_cert
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
# Trusted certificate store / paths
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
# Add files, folders or the default OS locations to the cert store.
|
|
93
|
+
# @param path_list [Array<String>]
|
|
94
|
+
def trusted_cert_locations=(path_list)
|
|
95
|
+
Aspera.assert_type(path_list, Array){'cert locations'}
|
|
96
|
+
if @certificate_store.nil?
|
|
97
|
+
Log.log.debug('Creating SSL Cert store')
|
|
98
|
+
@certificate_store = OpenSSL::X509::Store.new
|
|
99
|
+
@certificate_paths = []
|
|
100
|
+
end
|
|
101
|
+
path_list.each do |path|
|
|
102
|
+
Aspera.assert_type(path, String){'Expecting a String for certificate location'}
|
|
103
|
+
paths_to_add = [path]
|
|
104
|
+
Log.log.debug{"Adding cert location: #{path}"}
|
|
105
|
+
if path.eql?(SpecialValues::DEF)
|
|
106
|
+
@certificate_store.set_default_paths
|
|
107
|
+
paths_to_add = [OpenSSL::X509::DEFAULT_CERT_DIR]
|
|
108
|
+
paths_to_add.push(OpenSSL::X509::DEFAULT_CERT_FILE) unless defined?(JRUBY_VERSION)
|
|
109
|
+
paths_to_add.select!{ |f| File.exist?(f)}
|
|
110
|
+
elsif File.file?(path)
|
|
111
|
+
@certificate_store.add_file(path)
|
|
112
|
+
elsif File.directory?(path)
|
|
113
|
+
@certificate_store.add_path(path)
|
|
114
|
+
else
|
|
115
|
+
raise "No such file or folder: #{path}"
|
|
116
|
+
end
|
|
117
|
+
paths_to_add.each do |p|
|
|
118
|
+
pp = [File.realpath(p)]
|
|
119
|
+
if File.directory?(p)
|
|
120
|
+
pp = Dir.entries(p)
|
|
121
|
+
.map{ |e| File.realpath(File.join(p, e))}
|
|
122
|
+
.select{ |entry| File.file?(entry)}
|
|
123
|
+
.select{ |entry| CERT_EXT.any?{ |ext| entry.end_with?(ext)}}
|
|
124
|
+
end
|
|
125
|
+
@certificate_paths.concat(pp)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
@certificate_paths.uniq!
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Return cert file paths (computes OS defaults lazily if never set).
|
|
132
|
+
def trusted_cert_locations
|
|
133
|
+
if @certificate_paths.nil?
|
|
134
|
+
self.trusted_cert_locations = [SpecialValues::DEF]
|
|
135
|
+
locations = @certificate_paths
|
|
136
|
+
# Restore to "lazy" state so next call recomputes if store was reset
|
|
137
|
+
@certificate_paths = @certificate_store = nil
|
|
138
|
+
return locations
|
|
139
|
+
end
|
|
140
|
+
@certificate_paths
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
# HTTP session callback
|
|
145
|
+
# Called every time a new Net::HTTP session is opened.
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
# @param http_session [Net::HTTP]
|
|
149
|
+
def update_session(http_session)
|
|
150
|
+
http_session.set_debug_output(LineLogger.new(:trace2)) if Log.instance.logger.trace2?
|
|
151
|
+
if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
|
|
152
|
+
http_session.verify_mode = SELF_SIGNED_CERT
|
|
153
|
+
end
|
|
154
|
+
http_session.cert_store = @certificate_store if @certificate_store
|
|
155
|
+
Log.log.debug{"Using cert store #{http_session.cert_store} (#{@certificate_store})"} unless http_session.cert_store.nil?
|
|
156
|
+
@http_options.each do |k, v|
|
|
157
|
+
method = "#{k}=".to_sym
|
|
158
|
+
if http_session.respond_to?(method)
|
|
159
|
+
http_session.send(method, v)
|
|
160
|
+
else
|
|
161
|
+
Log.log.error{"Unknown HTTP session attribute: #{k}"}
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|