aspera-cli 4.13.0 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +81 -7
  4. data/CONTRIBUTING.md +22 -6
  5. data/README.md +2038 -1080
  6. data/bin/ascli +18 -9
  7. data/bin/asession +12 -14
  8. data/examples/dascli +1 -1
  9. data/examples/proxy.pac +1 -1
  10. data/examples/rubyc +24 -0
  11. data/lib/aspera/aoc.rb +219 -159
  12. data/lib/aspera/ascmd.rb +25 -14
  13. data/lib/aspera/cli/basic_auth_plugin.rb +12 -9
  14. data/lib/aspera/cli/error.rb +17 -0
  15. data/lib/aspera/cli/extended_value.rb +47 -12
  16. data/lib/aspera/cli/formatter.rb +260 -179
  17. data/lib/aspera/cli/hints.rb +80 -0
  18. data/lib/aspera/cli/main.rb +104 -156
  19. data/lib/aspera/cli/manager.rb +259 -209
  20. data/lib/aspera/cli/plugin.rb +123 -63
  21. data/lib/aspera/cli/plugins/alee.rb +2 -3
  22. data/lib/aspera/cli/plugins/aoc.rb +341 -261
  23. data/lib/aspera/cli/plugins/ats.rb +22 -21
  24. data/lib/aspera/cli/plugins/bss.rb +5 -5
  25. data/lib/aspera/cli/plugins/config.rb +578 -627
  26. data/lib/aspera/cli/plugins/console.rb +44 -6
  27. data/lib/aspera/cli/plugins/cos.rb +15 -17
  28. data/lib/aspera/cli/plugins/faspex.rb +114 -100
  29. data/lib/aspera/cli/plugins/faspex5.rb +411 -264
  30. data/lib/aspera/cli/plugins/node.rb +354 -259
  31. data/lib/aspera/cli/plugins/orchestrator.rb +61 -29
  32. data/lib/aspera/cli/plugins/preview.rb +82 -90
  33. data/lib/aspera/cli/plugins/server.rb +79 -32
  34. data/lib/aspera/cli/plugins/shares.rb +55 -42
  35. data/lib/aspera/cli/sync_actions.rb +68 -0
  36. data/lib/aspera/cli/transfer_agent.rb +66 -73
  37. data/lib/aspera/cli/transfer_progress.rb +74 -0
  38. data/lib/aspera/cli/version.rb +1 -1
  39. data/lib/aspera/colors.rb +12 -8
  40. data/lib/aspera/command_line_builder.rb +14 -11
  41. data/lib/aspera/cos_node.rb +3 -2
  42. data/lib/aspera/data/6 +0 -0
  43. data/lib/aspera/environment.rb +24 -9
  44. data/lib/aspera/fasp/agent_aspera.rb +126 -0
  45. data/lib/aspera/fasp/agent_base.rb +31 -77
  46. data/lib/aspera/fasp/agent_connect.rb +25 -21
  47. data/lib/aspera/fasp/agent_direct.rb +89 -103
  48. data/lib/aspera/fasp/agent_httpgw.rb +231 -149
  49. data/lib/aspera/fasp/agent_node.rb +41 -34
  50. data/lib/aspera/fasp/agent_trsdk.rb +75 -32
  51. data/lib/aspera/fasp/error_info.rb +4 -2
  52. data/lib/aspera/fasp/faux_file.rb +52 -0
  53. data/lib/aspera/fasp/installation.rb +53 -195
  54. data/lib/aspera/fasp/management.rb +244 -0
  55. data/lib/aspera/fasp/parameters.rb +71 -37
  56. data/lib/aspera/fasp/parameters.yaml +76 -8
  57. data/lib/aspera/fasp/products.rb +162 -0
  58. data/lib/aspera/fasp/resume_policy.rb +3 -3
  59. data/lib/aspera/fasp/transfer_spec.rb +7 -6
  60. data/lib/aspera/fasp/uri.rb +26 -24
  61. data/lib/aspera/faspex_gw.rb +2 -2
  62. data/lib/aspera/faspex_postproc.rb +2 -2
  63. data/lib/aspera/hash_ext.rb +14 -4
  64. data/lib/aspera/json_rpc.rb +49 -0
  65. data/lib/aspera/keychain/macos_security.rb +13 -13
  66. data/lib/aspera/line_logger.rb +23 -0
  67. data/lib/aspera/log.rb +58 -16
  68. data/lib/aspera/node.rb +157 -92
  69. data/lib/aspera/oauth.rb +37 -19
  70. data/lib/aspera/open_application.rb +4 -4
  71. data/lib/aspera/persistency_action_once.rb +1 -1
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/file_types.rb +4 -2
  74. data/lib/aspera/preview/generator.rb +22 -35
  75. data/lib/aspera/preview/options.rb +2 -0
  76. data/lib/aspera/preview/terminal.rb +73 -16
  77. data/lib/aspera/preview/utils.rb +21 -28
  78. data/lib/aspera/proxy_auto_config.js +2 -2
  79. data/lib/aspera/rest.rb +136 -68
  80. data/lib/aspera/rest_call_error.rb +1 -1
  81. data/lib/aspera/rest_error_analyzer.rb +15 -14
  82. data/lib/aspera/rest_errors_aspera.rb +37 -34
  83. data/lib/aspera/secret_hider.rb +18 -15
  84. data/lib/aspera/ssh.rb +5 -2
  85. data/lib/aspera/sync.rb +127 -119
  86. data/lib/aspera/temp_file_manager.rb +10 -3
  87. data/lib/aspera/web_auth.rb +10 -7
  88. data/lib/aspera/web_server_simple.rb +9 -4
  89. data.tar.gz.sig +0 -0
  90. metadata +34 -17
  91. metadata.gz.sig +0 -0
  92. data/docs/test_env.conf +0 -186
  93. data/lib/aspera/cli/listener/line_dump.rb +0 -19
  94. data/lib/aspera/cli/listener/logger.rb +0 -22
  95. data/lib/aspera/cli/listener/progress.rb +0 -50
  96. data/lib/aspera/cli/listener/progress_multi.rb +0 -84
  97. data/lib/aspera/cli/plugins/sync.rb +0 -44
  98. data/lib/aspera/data/7 +0 -0
  99. data/lib/aspera/fasp/listener.rb +0 -13
@@ -1,25 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # cspell:ignore initdemo genkey asperasoft
3
4
  require 'aspera/cli/basic_auth_plugin'
4
5
  require 'aspera/cli/extended_value'
5
6
  require 'aspera/cli/version'
6
7
  require 'aspera/cli/formatter'
7
8
  require 'aspera/cli/info'
9
+ require 'aspera/cli/transfer_progress'
8
10
  require 'aspera/fasp/installation'
11
+ require 'aspera/fasp/products'
9
12
  require 'aspera/fasp/parameters'
10
13
  require 'aspera/fasp/transfer_spec'
11
14
  require 'aspera/fasp/error_info'
15
+ require 'aspera/keychain/encrypted_hash'
16
+ require 'aspera/keychain/macos_security'
12
17
  require 'aspera/proxy_auto_config'
13
18
  require 'aspera/open_application'
14
19
  require 'aspera/persistency_action_once'
15
20
  require 'aspera/id_generator'
16
- require 'aspera/keychain/encrypted_hash'
17
- require 'aspera/keychain/macos_security'
18
- require 'aspera/aoc'
21
+ require 'aspera/persistency_folder'
22
+ require 'aspera/line_logger'
19
23
  require 'aspera/rest'
20
- require 'xmlsimple'
21
- require 'base64'
22
- require 'net/smtp'
24
+ require 'aspera/log'
23
25
  require 'open3'
24
26
  require 'date'
25
27
  require 'erb'
@@ -38,27 +40,21 @@ module Aspera
38
40
  CONF_PRESET_VERSION = 'version'
39
41
  CONF_PRESET_DEFAULT = 'default'
40
42
  CONF_PRESET_GLOBAL = 'global_common_defaults'
43
+ GLOBAL_DEFAULT_KEYWORD = 'GLOBAL'
41
44
  CONF_PLUGIN_SYM = :config # Plugins::Config.name.split('::').last.downcase.to_sym
42
45
  CONF_GLOBAL_SYM = :config
43
- # old tool name
44
- PROGRAM_NAME_V1 = 'aslmcli'
45
- PROGRAM_NAME_V2 = 'mlia'
46
- # default redirect for AoC web auth
47
- DEFAULT_REDIRECT = 'http://localhost:12345'
48
46
  # folder containing custom plugins in user's config folder
49
47
  ASPERA_PLUGINS_FOLDERNAME = 'plugins'
48
+ PERSISTENCY_FOLDER = 'persist_store'
50
49
  RUBY_FILE_EXT = '.rb'
51
- AOC_COMMAND_V1 = 'files'
52
- AOC_COMMAND_V2 = 'aspera'
53
- AOC_COMMAND_V3 = 'aoc'
54
- AOC_COMMAND_CURRENT = AOC_COMMAND_V3
50
+ ASPERA = 'aspera'
55
51
  SERVER_COMMAND = 'server'
52
+ APP_NAME_SDK = 'sdk'
56
53
  CONNECT_WEB_URL = 'https://d3gcli72yxqn2z.cloudfront.net/connect'
57
- CONNECT_VERSIONS = 'connectversions.js'
54
+ CONNECT_VERSIONS = 'connectversions.js' # cspell: disable-line
58
55
  TRANSFER_SDK_ARCHIVE_URL = 'https://ibm.biz/aspera_transfer_sdk'
59
56
  DEMO = 'demo'
60
- DEMO_SERVER_PRESET = 'demoserver'
61
- AOC_PATH_API_CLIENTS = 'admin/api-clients'
57
+ DEMO_SERVER_PRESET = 'demoserver' # cspell: disable-line
62
58
  EMAIL_TEST_TEMPLATE = <<~END_OF_TEMPLATE
63
59
  From: <%=from_name%> <<%=from_email%>>
64
60
  To: <<%=to%>>
@@ -67,135 +63,308 @@ module Aspera
67
63
  This email was sent to test #{PROGRAM_NAME}.
68
64
  END_OF_TEMPLATE
69
65
  # special extended values
70
- EXTV_INCLUDE_PRESETS = :incps
71
- EXTV_PRESET = :preset
72
- EXTV_VAULT = :vault
66
+ EXTEND_PRESET = :preset
67
+ EXTEND_VAULT = :vault
73
68
  PRESET_DIG_SEPARATOR = '.'
74
69
  DEFAULT_CHECK_NEW_VERSION_DAYS = 7
75
- DEFAULT_PRIV_KEY_FILENAME = 'aspera_aoc_key' # pragma: allowlist secret
76
- DEFAULT_PRIVKEY_LENGTH = 4096
70
+ DEFAULT_PRIV_KEY_FILENAME = 'my_private_key.pem' # pragma: allowlist secret
71
+ DEFAULT_PRIV_KEY_LENGTH = 4096
77
72
  COFFEE_IMAGE = 'https://enjoyjava.com/wp-content/uploads/2018/01/How-to-make-strong-coffee.jpg'
73
+ WIZARD_RESULT_KEYS = %i[preset_value test_args].freeze
74
+ GEM_CHECK_DATE_FMT = '%Y/%m/%d'
75
+ # for testing only
76
+ SELF_SIGNED_CERT = OpenSSL::SSL.const_get(:enon_yfirev.to_s.upcase.reverse) # cspell: disable-line
78
77
  private_constant :DEFAULT_CONFIG_FILENAME,
79
78
  :CONF_PRESET_CONFIG,
80
79
  :CONF_PRESET_VERSION,
81
80
  :CONF_PRESET_DEFAULT,
82
81
  :CONF_PRESET_GLOBAL,
83
- :PROGRAM_NAME_V1,
84
- :PROGRAM_NAME_V2,
85
- :DEFAULT_REDIRECT,
86
82
  :ASPERA_PLUGINS_FOLDERNAME,
87
83
  :RUBY_FILE_EXT,
88
- :AOC_COMMAND_V1,
89
- :AOC_COMMAND_V2,
90
- :AOC_COMMAND_V3,
91
- :AOC_COMMAND_CURRENT,
84
+ :ASPERA,
92
85
  :DEMO,
93
86
  :TRANSFER_SDK_ARCHIVE_URL,
94
- :AOC_PATH_API_CLIENTS,
95
87
  :DEMO_SERVER_PRESET,
96
88
  :EMAIL_TEST_TEMPLATE,
97
- :EXTV_INCLUDE_PRESETS,
98
- :EXTV_PRESET,
99
- :EXTV_VAULT,
89
+ :EXTEND_PRESET,
90
+ :EXTEND_VAULT,
100
91
  :DEFAULT_CHECK_NEW_VERSION_DAYS,
101
92
  :DEFAULT_PRIV_KEY_FILENAME,
102
93
  :SERVER_COMMAND,
103
94
  :PRESET_DIG_SEPARATOR,
104
- :COFFEE_IMAGE
95
+ :COFFEE_IMAGE,
96
+ :WIZARD_RESULT_KEYS,
97
+ :SELF_SIGNED_CERT,
98
+ :PERSISTENCY_FOLDER,
99
+ :DEFAULT_PRIV_KEY_LENGTH
100
+
101
+ class << self
102
+ def generate_rsa_private_key(path:, length: DEFAULT_PRIV_KEY_LENGTH)
103
+ require 'openssl'
104
+ priv_key = OpenSSL::PKey::RSA.new(length)
105
+ File.write(path, priv_key.to_s)
106
+ File.write("#{path}.pub", priv_key.public_key.to_s)
107
+ Environment.restrict_file_access(path)
108
+ Environment.restrict_file_access("#{path}.pub")
109
+ nil
110
+ end
111
+
112
+ # folder containing plugins in the gem's main folder
113
+ def gem_plugins_folder
114
+ File.dirname(File.expand_path(__FILE__))
115
+ end
116
+
117
+ # name of englobing module
118
+ # @return "Aspera::Cli::Plugins"
119
+ def module_full_name
120
+ return Module.nesting[2].to_s
121
+ end
122
+
123
+ # @return main folder where code is, i.e. .../lib
124
+ # go up as many times as englobing modules (not counting class, as it is a file)
125
+ def gem_src_root
126
+ File.expand_path(module_full_name.gsub('::', '/').gsub(%r{[^/]+}, '..'), gem_plugins_folder)
127
+ end
128
+
129
+ # instantiate a plugin
130
+ # plugins must be Capitalized
131
+ def plugin_class(plugin_name_sym)
132
+ # Module.nesting[2] is Aspera::Cli::Plugins
133
+ return Object.const_get("#{module_full_name}::#{plugin_name_sym.to_s.capitalize}")
134
+ end
135
+
136
+ # deep clone hash so that it does not get modified in case of display and secret hide
137
+ def protect_presets(val)
138
+ return JSON.parse(JSON.generate(val))
139
+ end
140
+
141
+ # return product family folder (~/.aspera)
142
+ def module_family_folder
143
+ user_home_folder = Dir.home
144
+ raise Cli::Error, "Home folder does not exist: #{user_home_folder}. Check your user environment." unless Dir.exist?(user_home_folder)
145
+ return File.join(user_home_folder, ASPERA_HOME_FOLDER_NAME)
146
+ end
147
+
148
+ # return product config folder (~/.aspera/<name>)
149
+ def default_app_main_folder(app_name:)
150
+ raise 'app_name must be a non-empty String' unless app_name.is_a?(String) && !app_name.empty?
151
+ return File.join(module_family_folder, app_name)
152
+ end
153
+ end # self
154
+
105
155
  def initialize(env, params)
106
- raise 'env and params must be Hash' unless env.is_a?(Hash) && params.is_a?(Hash)
107
- raise 'missing param' unless %i[name help version gem].sort.eql?(params.keys.sort)
156
+ raise 'Internal Error: env and params must be Hash' unless env.is_a?(Hash) && params.is_a?(Hash)
157
+ raise 'Internal Error: missing param' unless %i[gem help name version].eql?(params.keys.sort)
158
+ # we need to defer parsing of options until we have the config file, so we can use @extend with @preset
108
159
  super(env)
109
160
  @info = params
110
- @main_folder = default_app_main_folder
111
161
  @plugins = {}
112
162
  @plugin_lookup_folders = []
113
163
  @use_plugin_defaults = true
114
164
  @config_presets = nil
165
+ @config_checksum_on_disk = nil
115
166
  @connect_versions = nil
116
167
  @vault = nil
117
- @conf_file_default = File.join(@main_folder, DEFAULT_CONFIG_FILENAME)
118
- @option_config_file = @conf_file_default
119
168
  @pac_exec = nil
169
+ @sdk_default_location = false
170
+ @option_insecure = false
171
+ @option_ignore_cert_host_port = []
172
+ @option_http_options = {}
173
+ @ssl_warned_urls = []
174
+ @option_cache_tokens = true
175
+ @proxy_credentials = nil
176
+ @main_folder = nil
177
+ @option_config_file = nil
178
+ @certificate_store = nil
179
+ @certificate_paths = nil
180
+ @progress_bar = nil
181
+ # option to set main folder
182
+ options.declare(
183
+ :home, 'Home folder for tool',
184
+ handler: {o: self, m: :main_folder},
185
+ types: String,
186
+ default: self.class.default_app_main_folder(app_name: @info[:name]))
187
+ options.parse_options!
120
188
  Log.log.debug{"#{@info[:name]} folder: #{@main_folder}"}
121
- # set folder for FASP SDK
189
+ # data persistency manager
190
+ env[:persistency] = PersistencyFolder.new(File.join(@main_folder, PERSISTENCY_FOLDER))
191
+ # set folders for plugin lookup
122
192
  add_plugin_lookup_folder(self.class.gem_plugins_folder)
123
193
  add_plugin_lookup_folder(File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME))
124
- # do file parameter first
125
- options.set_obj_attr(:config_file, self, :option_config_file)
126
- options.add_opt_simple(:config_file, "read parameters from file in YAML format, current=#{@option_config_file}")
194
+ # option to set config file
195
+ options.declare(
196
+ :config_file, 'Path to YAML file with preset configuration',
197
+ handler: {o: self, m: :option_config_file},
198
+ default: File.join(@main_folder, DEFAULT_CONFIG_FILENAME))
127
199
  options.parse_options!
128
- # read correct file (set @config_presets)
200
+ # read config file (set @config_presets)
129
201
  read_config_file
130
202
  # add preset handler (needed for smtp)
131
- ExtendedValue.instance.set_handler(EXTV_PRESET, lambda{|v|preset_by_name(v)})
132
- ExtendedValue.instance.set_handler(EXTV_INCLUDE_PRESETS, lambda{|v|expanded_with_preset_includes(v)})
133
- ExtendedValue.instance.set_handler(EXTV_VAULT, lambda{|v|vault_value(v)})
203
+ ExtendedValue.instance.set_handler(EXTEND_PRESET, lambda{|v|preset_by_name(v)})
204
+ ExtendedValue.instance.set_handler(EXTEND_VAULT, lambda{|v|vault_value(v)})
134
205
  # load defaults before it can be overridden
135
206
  add_plugin_default_preset(CONF_GLOBAL_SYM)
207
+ # vault options
208
+ options.declare(:secret, 'Secret for access keys')
209
+ options.declare(:vault, 'Vault for secrets', types: Hash)
210
+ options.declare(:vault_password, 'Vault password')
136
211
  options.parse_options!
137
- options.set_obj_attr(:ascp_path, Fasp::Installation.instance, :ascp_path)
138
- options.set_obj_attr(:sdk_folder, Fasp::Installation.instance, :sdk_folder)
139
- options.set_obj_attr(:use_product, self, :option_use_product)
140
- options.set_obj_attr(:preset, self, :option_preset)
141
- options.set_obj_attr(:plugin_folder, self, :option_plugin_folder)
142
- options.add_opt_switch(:no_default, '-N', 'do not load default configuration for plugin') { @use_plugin_defaults = false }
143
- options.add_opt_boolean(:override, 'Wizard: override existing value')
144
- options.add_opt_boolean(:use_generic_client, 'Wizard: AoC: use global or org specific jwt client id')
145
- options.add_opt_boolean(:default, 'Wizard: set as default configuration for specified plugin (also: update)')
146
- options.add_opt_boolean(:test_mode, 'Wizard: skip private key check step')
147
- options.add_opt_simple(:preset, '-PVALUE', 'load the named option preset from current config file')
148
- options.add_opt_simple(:pkeypath, 'Wizard: path to private key for JWT')
149
- options.add_opt_simple(:ascp_path, 'Path to ascp')
150
- options.add_opt_simple(:use_product, 'Use ascp from specified product')
151
- options.add_opt_simple(:smtp, 'SMTP configuration (extended value: hash)')
152
- options.add_opt_simple(:fpac, 'Proxy auto configuration script')
153
- options.add_opt_simple(:proxy_credentials, 'HTTP proxy credentials (Array with user and password)')
154
- options.add_opt_simple(:secret, 'Secret for access keys')
155
- options.add_opt_simple(:vault, 'Vault for secrets')
156
- options.add_opt_simple(:vault_password, 'Vault password')
157
- options.add_opt_simple(:sdk_url, 'URL to get SDK')
158
- options.add_opt_simple(:sdk_folder, 'SDK folder path')
159
- options.add_opt_simple(:notif_to, 'Email recipient for notification of transfers')
160
- options.add_opt_simple(:notif_template, 'Email ERB template for notification of transfers')
161
- options.add_opt_simple(:version_check_days, Integer, 'Period in days to check new version (zero to disable)')
162
- options.add_opt_simple(:plugin_folder, 'Folder where to find additional plugins')
163
- options.set_option(:use_generic_client, true)
164
- options.set_option(:test_mode, false)
165
- options.set_option(:default, true)
166
- options.set_option(:version_check_days, DEFAULT_CHECK_NEW_VERSION_DAYS)
167
- options.set_option(:sdk_url, TRANSFER_SDK_ARCHIVE_URL)
168
- options.set_option(:sdk_folder, File.join(@main_folder, 'sdk'))
169
- options.set_option(:override, :no)
212
+ # declare generic plugin options only after handlers are declared
213
+ Plugin.declare_generic_options(options)
214
+ # configuration options
215
+ options.declare(:no_default, 'Do not load default configuration for plugin', values: :none, short: 'N') { @use_plugin_defaults = false }
216
+ options.declare(:preset, 'Load the named option preset from current config file', short: 'P', handler: {o: self, m: :option_preset})
217
+ options.declare(:version_check_days, 'Period in days to check new version (zero to disable)', coerce: Integer, default: DEFAULT_CHECK_NEW_VERSION_DAYS)
218
+ options.declare(:plugin_folder, 'Folder where to find additional plugins', handler: {o: self, m: :option_plugin_folder})
219
+ # wizard options
220
+ options.declare(:override, 'Wizard: override existing value', values: :bool, default: :no)
221
+ options.declare(:default, 'Wizard: set as default configuration for specified plugin (also: update)', values: :bool, default: true)
222
+ options.declare(:test_mode, 'Wizard: skip private key check step', values: :bool, default: false)
223
+ options.declare(:key_path, 'Wizard: path to private key for JWT')
224
+ # Transfer SDK options
225
+ options.declare(:ascp_path, 'Path to ascp', handler: {o: Fasp::Installation.instance, m: :ascp_path})
226
+ options.declare(:use_product, 'Use ascp from specified product', handler: {o: self, m: :option_use_product})
227
+ options.declare(:sdk_url, 'URL to get SDK', default: TRANSFER_SDK_ARCHIVE_URL)
228
+ options.declare(:sdk_folder, 'SDK folder path', handler: {o: Fasp::Installation.instance, m: :sdk_folder})
229
+ options.declare(:progress_bar, 'Display progress bar', values: :bool, default: Environment.terminal?)
230
+ # email options
231
+ options.declare(:smtp, 'SMTP configuration', types: Hash)
232
+ options.declare(:notify_to, 'Email recipient for notification of transfers')
233
+ options.declare(:notify_template, 'Email ERB template for notification of transfers')
234
+ # HTTP options
235
+ options.declare(:insecure, 'Do not validate any HTTPS certificate', values: :bool, handler: {o: self, m: :option_insecure}, default: :no)
236
+ options.declare(:ignore_certificate, 'List of HTTPS url where to no validate certificate', types: Array, handler: {o: self, m: :option_ignore_cert_host_port})
237
+ options.declare(:cert_stores, 'List of folder with trusted certificates', types: [Array, String], handler: {o: self, m: :trusted_cert_locations})
238
+ options.declare(:http_options, 'Options for HTTP/S socket', types: Hash, handler: {o: self, m: :option_http_options}, default: {})
239
+ options.declare(:cache_tokens, 'Save and reuse Oauth tokens', values: :bool, handler: {o: self, m: :option_cache_tokens})
240
+ options.declare(:fpac, 'Proxy auto configuration script')
241
+ options.declare(:proxy_credentials, 'HTTP proxy credentials (user and password)', types: Array)
170
242
  options.parse_options!
243
+ @progress_bar = TransferProgress.new if options.get_option(:progress_bar)
244
+ # Check SDK folder is set or not, for compatibility, we check in two places
245
+ sdk_folder = Fasp::Installation.instance.sdk_folder rescue nil
246
+ if sdk_folder.nil?
247
+ @sdk_default_location = true
248
+ Log.log.debug('SDK folder is not set, checking default')
249
+ # new location
250
+ sdk_folder = self.class.default_app_main_folder(app_name: APP_NAME_SDK)
251
+ Log.log.debug{"checking: #{sdk_folder}"}
252
+ if !Dir.exist?(sdk_folder)
253
+ Log.log.debug{"not exists: #{sdk_folder}"}
254
+ # former location
255
+ former_sdk_folder = File.join(self.class.default_app_main_folder(app_name: @info[:name]), APP_NAME_SDK)
256
+ Log.log.debug{"checking: #{former_sdk_folder}"}
257
+ sdk_folder = former_sdk_folder if Dir.exist?(former_sdk_folder)
258
+ end
259
+ Log.log.debug{"using: #{sdk_folder}"}
260
+ Fasp::Installation.instance.sdk_folder = sdk_folder
261
+ end
171
262
  pac_script = options.get_option(:fpac)
172
263
  # create PAC executor
173
264
  @pac_exec = Aspera::ProxyAutoConfig.new(pac_script).register_uri_generic unless pac_script.nil?
174
- proxy_creds = options.get_option(:proxy_credentials)
175
- if !proxy_creds.nil?
176
- raise CliBadArgument, 'proxy credentials shall be an array (#{proxy_creds.class})' unless proxy_creds.is_a?(Array)
177
- raise CliBadArgument, 'proxy credentials shall have two elements (#{proxy_creds.length})' unless proxy_creds.length.eql?(2)
178
- @pac_exec.proxy_user = Rest.proxy_user = proxy_creds[0]
179
- @pac_exec.proxy_pass = Rest.proxy_pass = proxy_creds[1]
265
+ proxy_user_pass = options.get_option(:proxy_credentials)
266
+ if !proxy_user_pass.nil?
267
+ raise Cli::BadArgument, "proxy_credentials shall have two elements (#{proxy_user_pass.length})" unless proxy_user_pass.length.eql?(2)
268
+ @proxy_credentials = {user: proxy_user_pass[0], pass: proxy_user_pass[1]}
269
+ @pac_exec.proxy_user = @proxy_credentials[:user]
270
+ @pac_exec.proxy_pass = @proxy_credentials[:pass]
271
+ end
272
+ Rest.set_parameters(
273
+ user_agent: PROGRAM_NAME,
274
+ session_cb: lambda{|http_session|update_http_session(http_session)},
275
+ progress_bar: @progress_bar)
276
+ Oauth.persist_mgr = persistency if @option_cache_tokens
277
+ Fasp::Parameters.file_list_folder = File.join(@main_folder, 'filelists') # cspell: disable-line
278
+ Aspera::RestErrorAnalyzer.instance.log_file = File.join(@main_folder, 'rest_exceptions.log')
279
+ # register aspera REST call error handlers
280
+ Aspera::RestErrorsAspera.register_handlers
281
+ end
282
+
283
+ attr_accessor :main_folder, :option_cache_tokens, :option_insecure, :option_http_options
284
+ attr_reader :option_ignore_cert_host_port, :progress_bar
285
+
286
+ def trusted_cert_locations=(path_list)
287
+ path_list = [path_list] unless path_list.is_a?(Array)
288
+ if @certificate_store.nil?
289
+ Log.log.debug('Creating SSL Cert store')
290
+ @certificate_store = OpenSSL::X509::Store.new
291
+ @certificate_store.set_default_paths
292
+ @certificate_paths = []
180
293
  end
294
+
295
+ path_list.each do |path|
296
+ raise 'Expecting a String for cert location' unless path.is_a?(String)
297
+ Log.log.debug("Adding cert location: #{path}")
298
+ if path.eql?(ExtendedValue::DEF)
299
+ path = OpenSSL::X509::DEFAULT_CERT_DIR
300
+ @certificate_store.add_path(path)
301
+ @certificate_paths.push(path)
302
+ path = OpenSSL::X509::DEFAULT_CERT_FILE
303
+ @certificate_store.add_file(path)
304
+ elsif File.file?(path)
305
+ @certificate_store.add_file(path)
306
+ elsif File.directory?(path)
307
+ @certificate_store.add_path(path)
308
+ else
309
+ raise "No such file or folder: #{path}"
310
+ end
311
+ @certificate_paths.push(path)
312
+ end
313
+ end
314
+
315
+ def trusted_cert_locations(files_only: false)
316
+ locations = if @certificate_paths.nil?
317
+ [OpenSSL::X509::DEFAULT_CERT_DIR, OpenSSL::X509::DEFAULT_CERT_FILE]
318
+ else
319
+ @certificate_paths
320
+ end
321
+ locations = locations.select{|f|File.file?(f)} if files_only
322
+ return locations
181
323
  end
182
324
 
183
- # env var name to override the app's main folder
184
- # default main folder is $HOME/<vendor main app folder>/<program name>
185
- def conf_dir_env_var
186
- return "#{@info[:name]}_home".upcase
325
+ def option_ignore_cert_host_port=(url_list)
326
+ url_list.each do |url|
327
+ uri = URI.parse(url)
328
+ @option_ignore_cert_host_port.push([uri.host, uri.port].freeze)
329
+ end
187
330
  end
188
331
 
189
- def default_app_main_folder
190
- # find out application main folder
191
- app_folder = ENV[conf_dir_env_var]
192
- # if env var undefined or empty
193
- if app_folder.nil? || app_folder.empty?
194
- user_home_folder = Dir.home
195
- raise CliError, "Home folder does not exist: #{user_home_folder}. Check your user environment or use #{conf_dir_env_var}." unless Dir.exist?(user_home_folder)
196
- app_folder = File.join(user_home_folder, ASPERA_HOME_FOLDER_NAME, @info[:name])
332
+ def ignore_cert?(address, port)
333
+ endpoint = [address, port].freeze
334
+ Log.log.debug{"ignore cert? #{endpoint}"}
335
+ return false unless @option_insecure || @option_ignore_cert_host_port.any?(endpoint)
336
+ base_url = "https://#{address}:#{port}"
337
+ if !@ssl_warned_urls.include?(base_url)
338
+ formatter.display_message(
339
+ :error,
340
+ "#{Formatter::WARNING_FLASH} Ignoring certificate for: #{base_url}. Do not deactivate certificate verification in production.")
341
+ @ssl_warned_urls.push(base_url)
342
+ end
343
+ return true
344
+ end
345
+
346
+ # called every time a new REST HTTP session is opened to set user-provided options
347
+ # @param http_session [Net::HTTP] the newly created HTTP/S session object
348
+ def update_http_session(http_session)
349
+ http_session.set_debug_output(LineLogger.new(:trace2)) if Log.instance.logger.trace2?
350
+ # Rest.io_http_session(http_session).debug_output = Log.log
351
+ http_session.verify_mode = SELF_SIGNED_CERT if http_session.use_ssl? && ignore_cert?(http_session.address, http_session.port)
352
+ http_session.cert_store = @certificate_store if @certificate_store
353
+ Log.log.debug{"using cert store #{http_session.cert_store} (#{@certificate_store})"} unless http_session.cert_store.nil?
354
+ if @proxy_credentials
355
+ http_session.proxy_user = @proxy_credentials[:user]
356
+ http_session.proxy_pass = @proxy_credentials[:pass]
357
+ end
358
+ @option_http_options.each do |k, v|
359
+ method = "#{k}=".to_sym
360
+ # check if accessor is a method of Net::HTTP
361
+ # continue_timeout= read_timeout= write_timeout=
362
+ if http_session.respond_to?(method)
363
+ http_session.send(method, v)
364
+ else
365
+ Log.log.error{"no such HTTP session attribute: #{k}"}
366
+ end
197
367
  end
198
- return app_folder
199
368
  end
200
369
 
201
370
  def check_gem_version
@@ -208,7 +377,7 @@ module Aspera
208
377
  end
209
378
  if Gem::Version.new(Environment.ruby_version) < Gem::Version.new(RUBY_FUTURE_MINIMUM_VERSION)
210
379
  Log.log.warn do
211
- "Note that a future version will require Ruby version #{RUBY_FUTURE_MINIMUM_VERSION} at minimum, "\
380
+ "Note that a future version will require Ruby version #{RUBY_FUTURE_MINIMUM_VERSION} at minimum, " \
212
381
  "you are using #{Environment.ruby_version}"
213
382
  end
214
383
  end
@@ -222,8 +391,7 @@ module Aspera
222
391
 
223
392
  def periodic_check_newer_gem_version
224
393
  # get verification period
225
- delay_days = options.get_option(:version_check_days, is_type: :mandatory)
226
- Log.log.info{"check days: #{delay_days}"}
394
+ delay_days = options.get_option(:version_check_days, mandatory: true)
227
395
  # check only if not zero day
228
396
  return if delay_days.eql?(0)
229
397
  # get last date from persistency
@@ -234,17 +402,11 @@ module Aspera
234
402
  id: 'version_last_check')
235
403
  # get persisted date or nil
236
404
  current_date = Date.today
237
- last_check_days =
238
- begin
239
- current_date - Date.strptime(last_check_array.first, '%Y/%m/%d')
240
- rescue StandardError
241
- # negative value will force check
242
- -1
243
- end
244
- Log.log.debug{"days elapsed: #{last_check_days}"}
245
- return if last_check_days < delay_days
405
+ last_check_days = (current_date - Date.strptime(last_check_array.first, GEM_CHECK_DATE_FMT)) rescue nil
406
+ Log.log.debug{"gem check new version: #{delay_days}, #{last_check_days}, #{current_date}, #{last_check_array}"}
407
+ return if !last_check_days.nil? && last_check_days < delay_days
246
408
  # generate timestamp
247
- last_check_array[0] = current_date.strftime('%Y/%m/%d')
409
+ last_check_array[0] = current_date.strftime(GEM_CHECK_DATE_FMT)
248
410
  check_date_persist.save
249
411
  # compare this version and the one on internet
250
412
  check_data = check_gem_version
@@ -263,7 +425,7 @@ module Aspera
263
425
  Log.log.debug{"javascript=[\n#{connect_versions_javascript}\n]"}
264
426
  # get javascript object only
265
427
  found = connect_versions_javascript.match(/^.*? = (.*);/)
266
- raise CliError, 'Problem when getting connect versions from internet' if found.nil?
428
+ raise Cli::Error, 'Problem when getting connect versions from internet' if found.nil?
267
429
  all_data = JSON.parse(found[1])
268
430
  @connect_versions = all_data['entries']
269
431
  end
@@ -281,66 +443,45 @@ module Aspera
281
443
  return nil
282
444
  end
283
445
 
284
- private
285
-
286
- def generate_rsa_private_key(private_key_path, length)
287
- require 'openssl'
288
- priv_key = OpenSSL::PKey::RSA.new(length)
289
- File.write(private_key_path, priv_key.to_s)
290
- File.write(private_key_path + '.pub', priv_key.public_key.to_s)
291
- Environment.restrict_file_access(private_key_path)
292
- Environment.restrict_file_access(private_key_path + '.pub')
293
- nil
294
- end
295
-
296
- class << self
297
- # folder containing plugins in the gem's main folder
298
- def gem_plugins_folder
299
- File.dirname(File.expand_path(__FILE__))
300
- end
301
-
302
- # name of englobing module
303
- # @return "Aspera::Cli::Plugins"
304
- def module_full_name
305
- return Module.nesting[2].to_s
446
+ # get the default global preset, or init a new one
447
+ def global_default_preset
448
+ global_default_preset_name = get_plugin_default_config_name(CONF_GLOBAL_SYM)
449
+ if global_default_preset_name.nil?
450
+ global_default_preset_name = CONF_PRESET_GLOBAL.to_s
451
+ set_preset_key(CONF_PRESET_DEFAULT, CONF_GLOBAL_SYM, global_default_preset_name)
306
452
  end
453
+ return global_default_preset_name
454
+ end
307
455
 
308
- # @return main folder where code is, i.e. .../lib
309
- # go up as many times as englobing modules (not counting class, as it is a file)
310
- def gem_src_root
311
- File.expand_path(module_full_name.gsub('::', '/').gsub(%r{[^/]+}, '..'), gem_plugins_folder)
456
+ def set_preset_key(preset, param_name, param_value)
457
+ raise "Parameter name must be a String or Symbol, not #{param_name.class}" unless [String, Symbol].include?(param_name.class)
458
+ param_name = param_name.to_s
459
+ selected_preset = @config_presets[preset]
460
+ if selected_preset.nil?
461
+ Log.log.debug{"No such preset name: #{preset}, initializing"}
462
+ selected_preset = @config_presets[preset] = {}
312
463
  end
313
-
314
- # instantiate a plugin
315
- # plugins must be Capitalized
316
- def plugin_class(plugin_name_sym)
317
- # Module.nesting[2] is Aspera::Cli::Plugins
318
- return Object.const_get("#{module_full_name}::#{plugin_name_sym.to_s.capitalize}")
464
+ raise "expecting Hash for #{preset}.#{param_name}" unless selected_preset.is_a?(Hash)
465
+ if selected_preset.key?(param_name)
466
+ if selected_preset[param_name].eql?(param_value)
467
+ Log.log.warn{"keeping same value for #{preset}: #{param_name}: #{param_value}"}
468
+ return
469
+ end
470
+ Log.log.warn{"overwriting value: #{selected_preset[param_name]}"}
319
471
  end
472
+ selected_preset[param_name] = param_value
473
+ formatter.display_status("Updated: #{preset}: #{param_name} <- #{param_value}")
474
+ nil
320
475
  end
321
476
 
322
477
  # set parameter and value in global config
323
478
  # creates one if none already created
324
479
  # @return preset name that contains global default
325
480
  def set_global_default(key, value)
326
- # get default preset if it exists
327
- global_default_preset_name = get_plugin_default_config_name(CONF_GLOBAL_SYM)
328
- if global_default_preset_name.nil?
329
- global_default_preset_name = CONF_PRESET_GLOBAL
330
- @config_presets[CONF_PRESET_DEFAULT] ||= {}
331
- @config_presets[CONF_PRESET_DEFAULT][CONF_GLOBAL_SYM.to_s] = global_default_preset_name
332
- end
333
- @config_presets[global_default_preset_name] ||= {}
334
- @config_presets[global_default_preset_name][key.to_s] = value
335
- formatter.display_status("Updated: #{global_default_preset_name}: #{key} <- #{value}")
336
- save_presets_to_config_file
337
- return global_default_preset_name
481
+ set_preset_key(global_default_preset, key, value)
338
482
  end
339
483
 
340
- public
341
-
342
484
  # $HOME/.aspera/`program_name`
343
- attr_reader :main_folder
344
485
  attr_reader :gem_url, :plugins
345
486
  attr_accessor :option_config_file
346
487
 
@@ -348,40 +489,17 @@ module Aspera
348
489
  # @param config_name name of the preset in config file
349
490
  # @param include_path used to detect and avoid include loops
350
491
  def preset_by_name(config_name, include_path=[])
351
- raise CliError, 'loop in include' if include_path.include?(config_name)
492
+ raise Cli::Error, 'loop in include' if include_path.include?(config_name)
352
493
  include_path = include_path.clone # avoid messing up if there are multiple branches
353
494
  current = @config_presets
354
495
  config_name.split(PRESET_DIG_SEPARATOR).each do |name|
355
- raise CliError, "Expecting Hash for sub key: #{include_path} (#{current.class})" unless current.is_a?(Hash)
496
+ raise Cli::Error, "Expecting Hash for sub key: #{include_path} (#{current.class})" unless current.is_a?(Hash)
356
497
  include_path.push(name)
357
498
  current = current[name]
358
- raise CliError, "No such config preset: #{include_path}" if current.nil?
359
- end
360
- case current
361
- when Hash then return expanded_with_preset_includes(current, include_path)
362
- when String then return ExtendedValue.instance.evaluate(current)
363
- else return current
499
+ raise Cli::Error, "No such config preset: #{include_path}" if current.nil?
364
500
  end
365
- end
366
-
367
- # @return the hash value with 'incps' keys expanded to include other presets
368
- # @param hash_val
369
- # @param include_path to avoid inclusion loop
370
- def expanded_with_preset_includes(hash_val, include_path=[])
371
- raise CliError, "#{EXTV_INCLUDE_PRESETS} requires a Hash, have #{hash_val.class}" unless hash_val.is_a?(Hash)
372
- if hash_val.key?(EXTV_INCLUDE_PRESETS)
373
- memory = hash_val.clone
374
- includes = memory[EXTV_INCLUDE_PRESETS]
375
- memory.delete(EXTV_INCLUDE_PRESETS)
376
- hash_val = {}
377
- raise "#{EXTV_INCLUDE_PRESETS} must be an Array" unless includes.is_a?(Array)
378
- raise "#{EXTV_INCLUDE_PRESETS} must contain names" unless includes.map(&:class).uniq.eql?([String])
379
- includes.each do |preset_name|
380
- hash_val.merge!(preset_by_name(preset_name, include_path))
381
- end
382
- hash_val.merge!(memory)
383
- end
384
- return hash_val
501
+ current = self.class.protect_presets(current) unless current.is_a?(String)
502
+ return ExtendedValue.instance.evaluate(current)
385
503
  end
386
504
 
387
505
  def option_use_product=(value)
@@ -417,97 +535,46 @@ module Aspera
417
535
  end
418
536
  end
419
537
 
420
- def convert_preset_path(old_name, new_name, files_to_copy)
421
- old_subpath = File.join('', ASPERA_HOME_FOLDER_NAME, old_name, '')
422
- new_subpath = File.join('', ASPERA_HOME_FOLDER_NAME, new_name, '')
423
- # convert possible keys located in config folder
424
- @config_presets.values.select{|p|p.is_a?(Hash)}.each do |preset|
425
- preset.values.select{|v|v.is_a?(String) && v.include?(old_subpath)}.each do |value|
426
- old_val = value.clone
427
- included_path = File.expand_path(old_val.gsub(/^@file:/, ''))
428
- files_to_copy.push(included_path) unless files_to_copy.include?(included_path) || !File.exist?(included_path)
429
- value.gsub!(old_subpath, new_subpath)
430
- Log.log.warn{"Converted config value: #{old_val} -> #{value}"}
431
- end
432
- end
433
- end
434
-
435
- def convert_preset_plugin_name(old_name, new_name)
436
- default_preset = @config_presets[CONF_PRESET_DEFAULT]
437
- return unless default_preset.is_a?(Hash) && default_preset.key?(old_name)
438
- default_preset[new_name] = default_preset[old_name]
439
- default_preset.delete(old_name)
440
- Log.log.warn{"Converted plugin default: #{old_name} -> #{new_name}"}
538
+ def config_checksum
539
+ JSON.generate(@config_presets).hash
441
540
  end
442
541
 
443
542
  # read config file and validate format
444
- # tries to convert from older version if possible and required
445
543
  def read_config_file
446
544
  Log.log.debug{"config file is: #{@option_config_file}".red}
447
- conf_file_v1 = File.join(Dir.home, ASPERA_HOME_FOLDER_NAME, PROGRAM_NAME_V1, DEFAULT_CONFIG_FILENAME)
448
- conf_file_v2 = File.join(Dir.home, ASPERA_HOME_FOLDER_NAME, PROGRAM_NAME_V2, DEFAULT_CONFIG_FILENAME)
449
545
  # files search for configuration, by default the one given by user
450
546
  search_files = [@option_config_file]
451
- # if default file, then also look for older versions
452
- search_files.push(conf_file_v2, conf_file_v1) if @option_config_file.eql?(@conf_file_default)
453
547
  # find first existing file (or nil)
454
548
  conf_file_to_load = search_files.find{|f| File.exist?(f)}
455
- # require save if old version of file
456
- save_required = !@option_config_file.eql?(conf_file_to_load)
457
549
  # if no file found, create default config
458
550
  if conf_file_to_load.nil?
459
- Log.log.warn{"No config file found. Creating empty configuration file: #{@option_config_file}"}
460
- @config_presets = {CONF_PRESET_CONFIG => {CONF_PRESET_VERSION => @info[:version]}}
551
+ Log.log.warn{"No config file found. New configuration file: #{@option_config_file}"}
552
+ @config_presets = {CONF_PRESET_CONFIG => {CONF_PRESET_VERSION => 'new file'}}
553
+ # @config_checksum_on_disk is nil
461
554
  else
462
555
  Log.log.debug{"loading #{@option_config_file}"}
463
556
  @config_presets = YAML.load_file(conf_file_to_load)
557
+ @config_checksum_on_disk = config_checksum
464
558
  end
465
559
  files_to_copy = []
466
- Log.log.debug{"Available_presets: #{@config_presets}"}
560
+ Log.log.debug{Log.dump('Available_presets', @config_presets)}
467
561
  raise 'Expecting YAML Hash' unless @config_presets.is_a?(Hash)
468
562
  # check there is at least the config section
469
- if !@config_presets.key?(CONF_PRESET_CONFIG)
470
- raise "Cannot find key: #{CONF_PRESET_CONFIG}"
471
- end
563
+ raise "Cannot find key: #{CONF_PRESET_CONFIG}" unless @config_presets.key?(CONF_PRESET_CONFIG)
472
564
  version = @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION]
473
- if version.nil?
474
- raise 'No version found in config section.'
475
- end
476
- # oldest compatible conf file format, update to latest version when an incompatible change is made
477
- # check compatibility of version of conf file
478
- config_tested_version = '0.4.5'
479
- if Gem::Version.new(version) < Gem::Version.new(config_tested_version)
480
- raise "Unsupported config file version #{version}. Expecting min version #{config_tested_version}"
481
- end
482
- config_tested_version = '0.6.15'
483
- if Gem::Version.new(version) < Gem::Version.new(config_tested_version)
484
- convert_preset_plugin_name(AOC_COMMAND_V1, AOC_COMMAND_V2)
485
- version = @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION] = config_tested_version
486
- save_required = true
487
- end
488
- config_tested_version = '0.8.10'
489
- if Gem::Version.new(version) <= Gem::Version.new(config_tested_version)
490
- convert_preset_path(PROGRAM_NAME_V1, PROGRAM_NAME_V2, files_to_copy)
491
- version = @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION] = config_tested_version
492
- save_required = true
493
- end
494
- config_tested_version = '1.0'
495
- if Gem::Version.new(version) <= Gem::Version.new(config_tested_version)
496
- convert_preset_plugin_name(AOC_COMMAND_V2, AOC_COMMAND_V3)
497
- convert_preset_path(PROGRAM_NAME_V2, @info[:name], files_to_copy)
498
- version = @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION] = config_tested_version
499
- save_required = true
500
- end
565
+ raise 'No version found in config section.' if version.nil?
501
566
  Log.log.debug{"conf version: #{version}"}
502
- # Place new compatibility code here
503
- if save_required
504
- Log.log.warn('Saving automatic conversion.')
505
- @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION] = @info[:version]
506
- save_presets_to_config_file
567
+ # VVV if there are any conversion needed, those happen here.
568
+ # fix bug in 4.4 (creating key "true" in "default" preset)
569
+ @config_presets[CONF_PRESET_DEFAULT].delete(true) if @config_presets[CONF_PRESET_DEFAULT].is_a?(Hash)
570
+ # ^^^ Place new compatibility code before this line
571
+ # set version to current
572
+ @config_presets[CONF_PRESET_CONFIG][CONF_PRESET_VERSION] = @info[:version]
573
+ unless files_to_copy.empty?
507
574
  Log.log.warn('Copying referenced files')
508
575
  files_to_copy.each do |file|
509
576
  FileUtils.cp(file, @main_folder)
510
- Log.log.warn{"..#{file} -> #{@main_folder}"}
577
+ Log.log.warn{"#{file} -> #{@main_folder}"}
511
578
  end
512
579
  end
513
580
  rescue Psych::SyntaxError => e
@@ -522,7 +589,7 @@ module Aspera
522
589
  Log.log.warn{"Renamed config file to #{new_name}."}
523
590
  Log.log.warn('Manual Conversion is required. Next time, a new empty file will be created.')
524
591
  end
525
- raise CliError, e.to_s
592
+ raise Cli::Error, e.to_s
526
593
  end
527
594
 
528
595
  # find plugins in defined paths
@@ -552,42 +619,41 @@ module Aspera
552
619
  @plugins[plugin_symbol] = {source: path, require_stanza: req}
553
620
  end
554
621
 
555
- def identify_plugin_for_url(url)
622
+ # Find a plugin, and issue the "require"
623
+ # @return [Hash] plugin info: { product: , url:, version: }
624
+ def identify_plugins_for_url
625
+ app_url = options.get_next_argument('url', mandatory: true)
626
+ check_only = options.get_next_argument('plugin name', mandatory: false)
627
+ check_only = check_only.to_sym unless check_only.nil?
628
+ found_apps = []
556
629
  plugins.each do |plugin_name_sym, plugin_info|
557
630
  # no detection for internal plugin
558
631
  next if plugin_name_sym.eql?(CONF_PLUGIN_SYM)
632
+ next if check_only && !check_only.eql?(plugin_name_sym)
559
633
  # load plugin class
560
634
  require plugin_info[:require_stanza]
561
- c = self.class.plugin_class(plugin_name_sym)
562
- next unless c.respond_to?(:detect)
563
- current_url = url
635
+ detect_plugin_class = self.class.plugin_class(plugin_name_sym)
636
+ # requires detection method
637
+ next unless detect_plugin_class.respond_to?(:detect)
564
638
  detection_info = nil
565
- # first try : direct
566
639
  begin
567
- detection_info = c.detect(current_url)
640
+ detection_info = detect_plugin_class.detect(app_url)
568
641
  rescue OpenSSL::SSL::SSLError => e
569
642
  Log.log.warn(e.message)
570
- Log.log.warn('Use option --insecure=yes to ignore certificate') if e.message.include?('cert')
643
+ Log.log.warn('Use option --insecure=yes to allow unchecked certificate') if e.message.include?('cert')
571
644
  rescue StandardError => e
572
- Log.log.debug{"Cannot detect #{plugin_name_sym} : #{e.class}/#{e.message}"}
573
- end
574
- # second try : is there a redirect ?
575
- if detection_info.nil?
576
- begin
577
- # TODO: check if redirect ?
578
- new_url = Rest.new(base_url: url).call(operation: 'GET', subpath: '', redirect_max: 1)[:http].uri.to_s
579
- unless url.eql?(new_url)
580
- detection_info = c.detect(new_url)
581
- current_url = new_url
582
- end
583
- rescue StandardError => e
584
- Log.log.debug{"Cannot detect #{plugin_name_sym} : #{e.message}"}
585
- end
645
+ Log.log.debug{"detect error: #{e}"}
646
+ next
586
647
  end
648
+ next if detection_info.nil?
649
+ raise 'internal error' if detection_info.key?(:url) && !detection_info[:url].is_a?(String)
650
+ app_name = detect_plugin_class.respond_to?(:application_name) ? detect_plugin_class.application_name : detect_plugin_class.name.split('::').last
587
651
  # if there is a redirect, then the detector can override the url.
588
- return {product: plugin_name_sym, url: current_url}.merge(detection_info) unless detection_info.nil?
652
+ found_apps.push({product: plugin_name_sym, name: app_name, url: app_url, version: 'unknown'}.merge(detection_info))
589
653
  end # loop
590
- raise "No known application found at #{url}"
654
+ raise "No known application found at #{app_url}" if found_apps.empty?
655
+ raise 'Internal error' unless found_apps.all?{|a|a.keys.all?(Symbol)}
656
+ return found_apps
591
657
  end
592
658
 
593
659
  def execute_connect_action
@@ -595,7 +661,7 @@ module Aspera
595
661
  if %i[info version].include?(command)
596
662
  connect_id = options.get_next_argument('id or title')
597
663
  one_res = connect_versions.find{|i|i['id'].eql?(connect_id) || i['title'].eql?(connect_id)}
598
- raise CliNoSuchId.new(:connect, connect_id) if one_res.nil?
664
+ raise Cli::NoSuchIdentifier.new(:connect, connect_id) if one_res.nil?
599
665
  end
600
666
  case command
601
667
  when :list
@@ -638,21 +704,14 @@ module Aspera
638
704
  ascp_path = options.get_next_argument('path to ascp')
639
705
  ascp_version = Fasp::Installation.instance.get_ascp_version(ascp_path)
640
706
  formatter.display_status("ascp version: #{ascp_version}")
641
- preset_name = set_global_default(:ascp_path, ascp_path)
642
- return Main.result_status("Saved to default global preset #{preset_name}")
707
+ set_global_default(:ascp_path, ascp_path)
708
+ return Main.result_nothing
643
709
  when :show # shows files used
644
710
  return {type: :status, data: Fasp::Installation.instance.path(:ascp)}
645
711
  when :info # shows files used
646
- data = Fasp::Installation::FILES.each_with_object({}) do |v, m|
647
- m[v.to_s] =
648
- begin
649
- Fasp::Installation.instance.path(v)
650
- rescue => e
651
- e.message
652
- end
653
- end
712
+ data = Fasp::Installation.instance.file_paths
654
713
  # read PATHs from ascp directly, and pvcl modules as well
655
- Open3.popen3(Fasp::Installation.instance.path(:ascp), '-DDL-') do |_stdin, _stdout, stderr, thread|
714
+ Open3.popen3(data['ascp'], '-DDL-') do |_stdin, _stdout, stderr, thread|
656
715
  last_line = ''
657
716
  while (line = stderr.gets)
658
717
  line.chomp!
@@ -660,8 +719,11 @@ module Aspera
660
719
  case line
661
720
  when /^DBG Path ([^ ]+) (dir|file) +: (.*)$/
662
721
  data[Regexp.last_match(1)] = Regexp.last_match(3)
663
- when /^DBG Added module group:"([^"]+)" name:"([^"]+)", version:"([^"]+)" interface:"([^"]+)"$/
664
- data[Regexp.last_match(2)] = "#{Regexp.last_match(4)} #{Regexp.last_match(1)} v#{Regexp.last_match(3)}"
722
+ when /^DBG Added module group:"(?<module>[^"]+)" name:"(?<scheme>[^"]+)", version:"(?<version>[^"]+)" interface:"(?<interface>[^"]+)"$/
723
+ c = Regexp.last_match.named_captures.symbolize_keys
724
+ data[c[:interface]] ||= {}
725
+ data[c[:interface]][c[:module]] ||= []
726
+ data[c[:interface]][c[:module]].push("#{c[:scheme]} v#{c[:version]}")
665
727
  when %r{^DBG License result \(/license/(\S+)\): (.+)$}
666
728
  data[Regexp.last_match(1)] = Regexp.last_match(2)
667
729
  when /^LOG (.+) version ([0-9.]+)$/
@@ -675,21 +737,34 @@ module Aspera
675
737
  raise last_line
676
738
  end
677
739
  end
678
- data['keypass'] = Fasp::Installation.instance.bypass_pass
740
+ # ascp's openssl directory
741
+ ascp_file = data['ascp']
742
+ File.binread(ascp_file).scan(/[\x20-\x7E]{4,}/) do |match|
743
+ if (m = match.match(/OPENSSLDIR.*"(.*)"/))
744
+ data['openssldir'] = m[1]
745
+ end
746
+ end if File.file?(ascp_file)
747
+ data['uuid'] = Fasp::Installation.instance.ssh_cert_uuid
748
+ # log is "-" no need to display
749
+ data.delete('log')
750
+ # show command line transfer spec
751
+ data['ts'] = transfer.updated_ts
679
752
  return {type: :single_object, data: data}
680
753
  when :products
681
754
  command = options.get_next_command(%i[list use])
682
755
  case command
683
756
  when :list
684
- return {type: :object_list, data: Fasp::Installation.instance.installed_products, fields: %w[name app_root]}
757
+ return {type: :object_list, data: Fasp::Products.installed_products, fields: %w[name app_root]}
685
758
  when :use
686
759
  default_product = options.get_next_argument('product name')
687
760
  Fasp::Installation.instance.use_ascp_from_product(default_product)
688
- preset_name = set_global_default(:ascp_path, Fasp::Installation.instance.path(:ascp))
689
- return Main.result_status("Saved to default global preset #{preset_name}")
761
+ set_global_default(:ascp_path, Fasp::Installation.instance.path(:ascp))
762
+ return Main.result_nothing
690
763
  end
691
764
  when :install
692
- v = Fasp::Installation.instance.install_sdk(options.get_option(:sdk_url, is_type: :mandatory))
765
+ # reset to default location, if older default was used
766
+ Fasp::Installation.instance.sdk_folder = self.class.default_app_main_folder(app_name: APP_NAME_SDK) if @sdk_default_location
767
+ v = Fasp::Installation.instance.install_sdk(options.get_option(:sdk_url, mandatory: true))
693
768
  return Main.result_status("Installed version #{v}")
694
769
  when :spec
695
770
  return {
@@ -718,24 +793,23 @@ module Aspera
718
793
  def execute_preset(action: nil, name: nil)
719
794
  action = options.get_next_command(PRESET_ALL_ACTIONS) if action.nil?
720
795
  name = instance_identifier if name.nil? && PRESET_INSTANCE_ACTIONS.include?(action)
796
+ name = global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
721
797
  # those operations require existing option
722
798
  raise "no such preset: #{name}" if PRESET_EXIST_ACTIONS.include?(action) && !@config_presets.key?(name)
723
- selected_preset = @config_presets[name]
724
799
  case action
725
800
  when :list
726
801
  return {type: :value_list, data: @config_presets.keys, name: 'name'}
727
802
  when :overview
728
- return {type: :object_list, data: Formatter.flatten_config_overview(@config_presets)}
803
+ # display process modifies the value (hide secrets): we do not want to save removed secrets
804
+ return {type: :config_over, data: self.class.protect_presets(@config_presets)}
729
805
  when :show
730
- raise "no such config: #{name}" if selected_preset.nil?
731
- return {type: :single_object, data: selected_preset}
806
+ return {type: :single_object, data: self.class.protect_presets(@config_presets[name])}
732
807
  when :delete
733
808
  @config_presets.delete(name)
734
- save_presets_to_config_file
735
809
  return Main.result_status("Deleted: #{name}")
736
810
  when :get
737
811
  param_name = options.get_next_argument('parameter name')
738
- value = selected_preset[param_name]
812
+ value = @config_presets[name][param_name]
739
813
  raise "no such option in preset #{name} : #{param_name}" if value.nil?
740
814
  case value
741
815
  when Numeric, String then return {type: :text, data: ExtendedValue.instance.evaluate(value.to_s)}
@@ -743,30 +817,20 @@ module Aspera
743
817
  return {type: :single_object, data: value}
744
818
  when :unset
745
819
  param_name = options.get_next_argument('parameter name')
746
- selected_preset.delete(param_name)
747
- save_presets_to_config_file
820
+ @config_presets[name].delete(param_name)
748
821
  return Main.result_status("Removed: #{name}: #{param_name}")
749
822
  when :set
750
823
  param_name = options.get_next_argument('parameter name')
751
- param_value = options.get_next_argument('parameter value')
752
824
  param_name = Manager.option_line_to_name(param_name)
753
- if !@config_presets.key?(name)
754
- Log.log.debug{"no such config name: #{name}, initializing"}
755
- selected_preset = @config_presets[name] = {}
756
- end
757
- if selected_preset.key?(param_name)
758
- Log.log.warn{"overwriting value: #{selected_preset[param_name]}"}
759
- end
760
- selected_preset[param_name] = param_value
761
- save_presets_to_config_file
762
- return Main.result_status("Updated: #{name}: #{param_name} <- #{param_value}")
825
+ param_value = options.get_next_argument('parameter value')
826
+ set_preset_key(name, param_name, param_value)
827
+ return Main.result_nothing
763
828
  when :initialize
764
829
  config_value = options.get_next_argument('extended value', type: Hash)
765
830
  if @config_presets.key?(name)
766
831
  Log.log.warn{"configuration already exists: #{name}, overwriting"}
767
832
  end
768
833
  @config_presets[name] = config_value
769
- save_presets_to_config_file
770
834
  return Main.result_status("Modified: #{@option_config_file}")
771
835
  when :update
772
836
  # get unprocessed options
@@ -774,9 +838,6 @@ module Aspera
774
838
  Log.log.debug{"opts=#{unprocessed_options}"}
775
839
  @config_presets[name] ||= {}
776
840
  @config_presets[name].merge!(unprocessed_options)
777
- # fix bug in 4.4 (creating key "true" in "default" preset)
778
- @config_presets[CONF_PRESET_DEFAULT].delete(true) if @config_presets[CONF_PRESET_DEFAULT].is_a?(Hash)
779
- save_presets_to_config_file
780
841
  return Main.result_status("Updated: #{name}")
781
842
  when :ask
782
843
  options.ask_missing_mandatory = :yes
@@ -785,12 +846,11 @@ module Aspera
785
846
  option_value = options.get_interactive(:option, option_name)
786
847
  @config_presets[name][option_name] = option_value
787
848
  end
788
- save_presets_to_config_file
789
849
  return Main.result_status("Updated: #{name}")
790
850
  when :lookup
791
- BasicAuthPlugin.register_options(@agents)
792
- url = options.get_option(:url, is_type: :mandatory)
793
- user = options.get_option(:username, is_type: :mandatory)
851
+ BasicAuthPlugin.declare_options(options)
852
+ url = options.get_option(:url, mandatory: true)
853
+ user = options.get_option(:username, mandatory: true)
794
854
  result = lookup_preset(url: url, username: user)
795
855
  raise 'no such config found' if result.nil?
796
856
  return {type: :single_object, data: result}
@@ -823,17 +883,16 @@ module Aspera
823
883
  end
824
884
 
825
885
  ACTIONS = %i[
826
- id
827
886
  preset
828
887
  open
829
888
  documentation
830
889
  genkey
890
+ remote_certificate
831
891
  gem
832
- plugin
892
+ plugins
833
893
  flush_tokens
834
894
  echo
835
895
  wizard
836
- export_to_cli
837
896
  detect
838
897
  coffee
839
898
  ascp
@@ -844,19 +903,13 @@ module Aspera
844
903
  file
845
904
  check_update
846
905
  initdemo
847
- vault].concat(PRESET_GBL_ACTIONS).freeze
906
+ vault
907
+ throw].freeze
848
908
 
849
- # "config" plugin
909
+ # Main action procedure for plugin
850
910
  def execute_action
851
911
  action = options.get_next_command(ACTIONS)
852
912
  case action
853
- when *PRESET_GBL_ACTIONS # older syntax
854
- Log.log.warn{"This syntax is deprecated, use command: preset #{action}"}
855
- return execute_preset(action: action)
856
- when :id # older syntax
857
- identifier = options.get_next_argument('config name')
858
- Log.log.warn{"This syntax is deprecated, use command: preset <verb> #{identifier}"}
859
- return execute_preset(name: identifier)
860
913
  when :preset # newer syntax
861
914
  return execute_preset
862
915
  when :open
@@ -864,31 +917,45 @@ module Aspera
864
917
  return Main.result_nothing
865
918
  when :documentation
866
919
  section = options.get_next_argument('private key file path', mandatory: false)
867
- section = '#' + section unless section.nil?
920
+ section = "##{section}" unless section.nil?
868
921
  OpenApplication.instance.uri("#{@info[:help]}#{section}")
869
922
  return Main.result_nothing
870
923
  when :genkey # generate new rsa key
871
924
  private_key_path = options.get_next_argument('private key file path')
872
- private_key_length = options.get_next_argument('size in bits', mandatory: false) || DEFAULT_PRIVKEY_LENGTH
873
- generate_rsa_private_key(private_key_path, private_key_length)
874
- return Main.result_status('Generated key: ' + private_key_path)
925
+ private_key_length = options.get_next_argument('size in bits', mandatory: false, type: Integer, default: DEFAULT_PRIV_KEY_LENGTH)
926
+ self.class.generate_rsa_private_key(path: private_key_path, length: private_key_length)
927
+ return Main.result_status("Generated #{private_key_length} bit RSA key: #{private_key_path}")
928
+ when :remote_certificate
929
+ remote_url = options.get_next_argument('remote URL')
930
+ @option_insecure = true
931
+ remote_certificate = Rest.start_http_session(remote_url).peer_cert
932
+ remote_certificate.subject.to_a.find { |name, _, _| name == 'CN' }[1]
933
+ formatter.display_status("CN=#{remote_certificate.subject.to_a.find { |name, _, _| name == 'CN' }[1] rescue ''}")
934
+ return Main.result_status(remote_certificate.to_pem)
875
935
  when :echo # display the content of a value given on command line
876
- result = {type: :other_struct, data: options.get_next_argument('value')}
877
- # special for csv
878
- result[:type] = :object_list if result[:data].is_a?(Array) && result[:data].first.is_a?(Hash)
879
- result[:type] = :single_object if result[:data].is_a?(Hash)
880
- return result
936
+ return Formatter.auto_type(options.get_next_argument('value'))
881
937
  when :flush_tokens
882
938
  deleted_files = Oauth.flush_tokens
883
939
  return {type: :value_list, data: deleted_files, name: 'file'}
884
- when :plugin
940
+ when :plugins
885
941
  case options.get_next_command(%i[list create])
886
942
  when :list
887
- return {type: :object_list, data: @plugins.keys.map { |i| { 'plugin' => i.to_s, 'path' => @plugins[i][:source] } }, fields: %w[plugin path]}
943
+ result = []
944
+ @plugins.each do |name, info|
945
+ require info[:require_stanza]
946
+ plugin_class = self.class.plugin_class(name)
947
+ result.push({
948
+ plugin: name,
949
+ detect: Formatter.tick(plugin_class.respond_to?(:detect)),
950
+ wizard: Formatter.tick(plugin_class.respond_to?(:wizard)),
951
+ path: info[:source]
952
+ })
953
+ end
954
+ return {type: :object_list, data: result, fields: %w[plugin detect wizard path]}
888
955
  when :create
889
956
  plugin_name = options.get_next_argument('name', expected: :single).downcase
890
- plugin_folder = options.get_next_argument('folder', expected: :single, mandatory: false) || File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME)
891
- plugin_file = File.join(plugin_folder, "#{plugin_name}.rb")
957
+ destination_folder = options.get_next_argument('folder', expected: :single, mandatory: false) || File.join(@main_folder, ASPERA_PLUGINS_FOLDERNAME)
958
+ plugin_file = File.join(destination_folder, "#{plugin_name}.rb")
892
959
  content = <<~END_OF_PLUGIN_CODE
893
960
  require 'aspera/cli/plugin'
894
961
  module Aspera
@@ -905,81 +972,20 @@ module Aspera
905
972
  File.write(plugin_file, content)
906
973
  return Main.result_status("Created #{plugin_file}")
907
974
  end
908
- when :wizard
975
+ when :detect, :wizard
909
976
  # interactive mode
910
977
  options.ask_missing_mandatory = true
911
- # register url option
912
- BasicAuthPlugin.register_options(@agents)
913
- params = {}
914
- # get from option, or ask
915
- params[:instance_url] = options.get_option(:url, is_type: :mandatory)
916
- # allow user to tell the preset name
917
- params[:preset_name] = options.get_option(:id)
918
- # allow user to specify type of application
919
- params[:application] = options.get_option(:value)
920
- params[:application] = params[:application].nil? ? identify_plugin_for_url(params[:instance_url])[:product] : params[:application].to_sym
921
- params[:plugin_name] = params[:application]
922
- params[:test_args] = '<replace per app>'
923
- case params[:application]
924
- when :faspex5
925
- wizard_faspex5(params)
926
- when :aoc
927
- wizard_aoc(params)
928
- else
929
- raise CliBadArgument, "Supports only: aoc. Detected: #{params[:application]}"
930
- end # product
931
- if params[:option_default]
932
- formatter.display_status("Setting config preset as default for #{params[:plugin_name]}")
933
- @config_presets[CONF_PRESET_DEFAULT][params[:plugin_name]] = params[:preset_name]
934
- else
935
- params[:test_args] = "-P#{params[:preset_name]} #{params[:test_args]}"
936
- end
937
- formatter.display_status('Saving config file.')
938
- save_presets_to_config_file
939
- return Main.result_status("Done.\nYou can test with:\n#{@info[:name]} #{params[:test_args]}")
940
- when :export_to_cli # this method shall be deprecated in the future: it was used to export configuration to "aspera.exe" CLI
941
- formatter.display_status('Exporting: Aspera on Cloud')
942
- require 'aspera/cli/plugins/aoc'
943
- # need url / username
944
- add_plugin_default_preset(AOC_COMMAND_V3.to_sym)
945
- # instantiate AoC plugin
946
- self.class.plugin_class(AOC_COMMAND_CURRENT).new(@agents) # TODO: is this line needed ? get options ?
947
- url = options.get_option(:url, is_type: :mandatory)
948
- cli_conf_file = Fasp::Installation.instance.cli_conf_file
949
- data = JSON.parse(File.read(cli_conf_file))
950
- organization, instance_domain = AoC.parse_url(url)
951
- key_basename = 'org_' + organization + '.pem'
952
- key_file = File.join(File.dirname(File.dirname(cli_conf_file)), 'etc', key_basename)
953
- File.write(key_file, options.get_option(:private_key, is_type: :mandatory))
954
- new_conf = {
955
- 'organization' => organization,
956
- 'hostname' => [organization, instance_domain].join('.'),
957
- 'privateKeyFilename' => key_basename,
958
- 'username' => options.get_option(:username, is_type: :mandatory)
959
- }
960
- new_conf['clientId'] = options.get_option(:client_id)
961
- new_conf['clientSecret'] = options.get_option(:client_secret)
962
- if new_conf['clientId'].nil?
963
- new_conf['clientId'], new_conf['clientSecret'] = AoC.get_client_info
964
- end
965
- entry = data['AoCAccounts'].find{|i|i['organization'].eql?(organization)}
966
- if entry.nil?
967
- data['AoCAccounts'].push(new_conf)
968
- formatter.display_status("Creating new aoc entry: #{organization}")
969
- else
970
- formatter.display_status("Updating existing aoc entry: #{organization}")
971
- entry.merge!(new_conf)
972
- end
973
- File.write(cli_conf_file, JSON.pretty_generate(data))
974
- return Main.result_status("Updated: #{cli_conf_file}")
975
- when :detect
976
- # need url / username
977
- BasicAuthPlugin.register_options(@agents)
978
- return {type: :single_object, data: identify_plugin_for_url(options.get_option(:url, is_type: :mandatory))}
978
+ # detect plugins by url and optional query
979
+ apps = identify_plugins_for_url.freeze
980
+ return {
981
+ type: :object_list,
982
+ data: apps
983
+ } if action.eql?(:detect)
984
+ return wizard_find(apps)
979
985
  when :coffee
980
986
  if OpenApplication.instance.url_method.eql?(:text)
981
987
  require 'aspera/preview/terminal'
982
- return Main.result_status(Preview::Terminal.build(Rest.new(base_url: COFFEE_IMAGE).read('')[:http].body, reserved_lines: 3))
988
+ return Main.result_status(Preview::Terminal.build(Rest.new(base_url: COFFEE_IMAGE).read('')[:http].body))
983
989
  end
984
990
  OpenApplication.instance.uri(COFFEE_IMAGE)
985
991
  return Main.result_nothing
@@ -1002,7 +1008,7 @@ module Aspera
1002
1008
  return {type: :single_object, data: email_settings}
1003
1009
  when :proxy_check
1004
1010
  # ensure fpac was provided
1005
- options.get_option(:fpac, is_type: :mandatory)
1011
+ options.get_option(:fpac, mandatory: true)
1006
1012
  server_url = options.get_next_argument('server url')
1007
1013
  return Main.result_status(@pac_exec.find_proxy_for_url(server_url))
1008
1014
  when :check_update
@@ -1013,9 +1019,9 @@ module Aspera
1013
1019
  else
1014
1020
  Log.log.info{"Creating Demo server preset: #{DEMO_SERVER_PRESET}"}
1015
1021
  @config_presets[DEMO_SERVER_PRESET] = {
1016
- 'url' => 'ssh://' + DEMO + '.asperasoft.com:33001',
1017
- 'username' => AOC_COMMAND_V2,
1018
- 'ssAP'.downcase.reverse + 'drow'.reverse => DEMO + AOC_COMMAND_V2
1022
+ 'url' => "ssh://#{DEMO}.asperasoft.com:33001",
1023
+ 'username' => ASPERA,
1024
+ 'ssAP'.downcase.reverse + 'drow'.reverse => DEMO + ASPERA # cspell:disable-line
1019
1025
  }
1020
1026
  end
1021
1027
  @config_presets[CONF_PRESET_DEFAULT] ||= {}
@@ -1027,40 +1033,138 @@ module Aspera
1027
1033
  @config_presets[CONF_PRESET_DEFAULT][SERVER_COMMAND] = DEMO_SERVER_PRESET
1028
1034
  Log.log.info{"Setting server default preset to : #{DEMO_SERVER_PRESET}"}
1029
1035
  end
1030
- save_presets_to_config_file
1031
1036
  return Main.result_status('Done')
1032
1037
  when :vault then execute_vault
1038
+ when :throw
1039
+ # :type [String]
1040
+ options
1041
+ exception_class_name = options.get_next_argument('exception class name', mandatory: true)
1042
+ exception_text = options.get_next_argument('exception text', mandatory: true)
1043
+ exception_class = Object.const_get(exception_class_name)
1044
+ raise "#{exception_class} is not an exception: #{exception_class.class}" unless exception_class <= Exception
1045
+ raise exception_class, exception_text
1033
1046
  else raise 'INTERNAL ERROR: wrong case'
1034
1047
  end
1035
1048
  end
1036
1049
 
1037
- # @return email server setting with defaults if not defined
1050
+ def wizard_find(apps)
1051
+ identification = if apps.length.eql?(1)
1052
+ Log.log.debug{"Detected: #{identification}"}
1053
+ apps.first
1054
+ else
1055
+ formatter.display_status('Multiple applications detected, please select from:')
1056
+ formatter.display_results({type: :object_list, data: apps, fields: %w[product url version]})
1057
+ answer = options.prompt_user_input_in_list('product', apps.map{|a|a[:product]})
1058
+ apps.find{|a|a[:product].eql?(answer)}
1059
+ end
1060
+ wiz_url = identification[:url]
1061
+ Log.log.debug{Log.dump(:identification, identification, :ruby)}
1062
+ formatter.display_status("Using: #{identification[:name]} at #{wiz_url}".bold)
1063
+ # set url for instantiation of plugin
1064
+ options.add_option_preset({url: wiz_url})
1065
+ # instantiate plugin: command line options will be known and wizard can be called
1066
+ wiz_plugin_class = self.class.plugin_class(identification[:product])
1067
+ raise Cli::BadArgument, "Detected: #{identification[:product]}, but this application has no wizard" unless wiz_plugin_class.respond_to?(:wizard)
1068
+ # instantiate plugin: command line options will be known, e.g. private_key
1069
+ plugin_instance = wiz_plugin_class.new(@agents)
1070
+ wiz_params = {
1071
+ object: plugin_instance
1072
+ }
1073
+ # is private key needed ?
1074
+ if options.known_options.key?(:private_key) &&
1075
+ (!wiz_plugin_class.respond_to?(:private_key_required?) || wiz_plugin_class.private_key_required?(wiz_url))
1076
+ # lets see if path to priv key is provided
1077
+ private_key_path = options.get_option(:key_path)
1078
+ # give a chance to provide
1079
+ if private_key_path.nil?
1080
+ formatter.display_status('Please provide the path to your private RSA key, or nothing to generate one:')
1081
+ private_key_path = options.get_option(:key_path, mandatory: true).to_s
1082
+ # private_key_path = File.expand_path(private_key_path)
1083
+ end
1084
+ # else generate path
1085
+ if private_key_path.empty?
1086
+ private_key_path = File.join(@main_folder, DEFAULT_PRIV_KEY_FILENAME)
1087
+ end
1088
+ if File.exist?(private_key_path)
1089
+ formatter.display_status('Using existing key:')
1090
+ else
1091
+ formatter.display_status("Generating #{DEFAULT_PRIV_KEY_LENGTH} bit RSA key...")
1092
+ Config.generate_rsa_private_key(path: private_key_path)
1093
+ formatter.display_status('Created key:')
1094
+ end
1095
+ formatter.display_status(private_key_path)
1096
+ private_key_pem = File.read(private_key_path)
1097
+ options.set_option(:private_key, private_key_pem)
1098
+ wiz_params[:private_key_path] = private_key_path
1099
+ wiz_params[:pub_key_pem] = OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s
1100
+ end
1101
+ Log.log.debug{Log.dump(:wiz_params, wiz_params)}
1102
+ # finally, call the wizard
1103
+ wizard_result = wiz_plugin_class.wizard(**wiz_params)
1104
+ Log.log.debug{"wizard result: #{wizard_result}"}
1105
+ raise "Internal error: missing or extra keys in wizard result: #{wizard_result.keys}" unless WIZARD_RESULT_KEYS.eql?(wizard_result.keys.sort)
1106
+ # get preset name from user or default
1107
+ wiz_preset_name = options.get_option(:id)
1108
+ if wiz_preset_name.nil?
1109
+ elements = [
1110
+ identification[:product],
1111
+ URI.parse(wiz_url).host
1112
+ ]
1113
+ elements.push(options.get_option(:username, mandatory: true)) unless wizard_result[:preset_value].key?(:link)
1114
+ wiz_preset_name = elements.join('_').strip.downcase.gsub(/[^a-z0-9]/, '_').squeeze('_')
1115
+ end
1116
+ # test mode does not change conf file
1117
+ return {type: :single_object, data: wizard_result} if options.get_option(:test_mode)
1118
+ # Write configuration file
1119
+ formatter.display_status("Preparing preset: #{wiz_preset_name}")
1120
+ # init defaults if necessary
1121
+ @config_presets[CONF_PRESET_DEFAULT] ||= {}
1122
+ option_override = options.get_option(:override, mandatory: true)
1123
+ raise Cli::Error, "A default configuration already exists for plugin '#{identification[:product]}' (use --override=yes or --default=no)" \
1124
+ if !option_override && options.get_option(:default, mandatory: true) && @config_presets[CONF_PRESET_DEFAULT].key?(identification[:product])
1125
+ raise Cli::Error, "Preset already exists: #{wiz_preset_name} (use --override=yes or --id=<name>)" \
1126
+ if !option_override && @config_presets.key?(wiz_preset_name)
1127
+ @config_presets[wiz_preset_name] = wizard_result[:preset_value].stringify_keys
1128
+ test_args = wizard_result[:test_args]
1129
+ if options.get_option(:default, mandatory: true)
1130
+ formatter.display_status("Setting config preset as default for #{identification[:product]}")
1131
+ @config_presets[CONF_PRESET_DEFAULT][identification[:product].to_s] = wiz_preset_name
1132
+ else
1133
+ test_args = "-P#{wiz_preset_name} #{test_args}"
1134
+ end
1135
+ # TODO: actually test the command
1136
+ return Main.result_status("You can test with:\n#{@info[:name]} #{identification[:product]} #{test_args}")
1137
+ end
1138
+
1139
+ # @return [Hash] email server setting with defaults if not defined
1038
1140
  def email_settings
1039
- smtp = options.get_option(:smtp, is_type: :mandatory)
1141
+ smtp = options.get_option(:smtp, mandatory: true)
1040
1142
  # change string keys into symbol keys
1041
- smtp = smtp.keys.each_with_object({}){|v, m|m[v.to_sym] = smtp[v]; }
1143
+ smtp = smtp.symbolize_keys
1042
1144
  # defaults
1043
- smtp[:tls] ||= true
1044
- smtp[:port] ||= smtp[:tls] ? 587 : 25
1145
+ smtp[:tls] = !smtp[:ssl] unless smtp.key?(:tls)
1146
+ smtp[:port] ||= if smtp[:tls]
1147
+ 587
1148
+ elsif smtp[:ssl]
1149
+ 465
1150
+ else
1151
+ 25
1152
+ end
1045
1153
  smtp[:from_email] ||= smtp[:username] if smtp.key?(:username)
1046
1154
  smtp[:from_name] ||= smtp[:from_email].gsub(/@.*$/, '').gsub(/[^a-zA-Z]/, ' ').capitalize if smtp.key?(:username)
1047
1155
  smtp[:domain] ||= smtp[:from_email].gsub(/^.*@/, '') if smtp.key?(:from_email)
1048
1156
  # check minimum required
1049
1157
  %i[server port domain].each do |n|
1050
- raise "Missing smtp parameter: #{n}" unless smtp.key?(n)
1158
+ raise "Missing mandatory smtp parameter: #{n}" unless smtp.key?(n)
1051
1159
  end
1052
1160
  Log.log.debug{"smtp=#{smtp}"}
1053
1161
  return smtp
1054
1162
  end
1055
1163
 
1056
- # create a clean binding (ruby variable environment)
1057
- def empty_binding
1058
- Kernel.binding
1059
- end
1060
-
1164
+ # send email using ERB template
1061
1165
  def send_email_template(email_template_default: nil, values: {})
1062
- values[:to] ||= options.get_option(:notif_to, is_type: :mandatory)
1063
- notif_template = options.get_option(:notif_template, is_type: email_template_default.nil? ? :mandatory : :optional) || email_template_default
1166
+ values[:to] ||= options.get_option(:notify_to, mandatory: true)
1167
+ notify_template = options.get_option(:notify_template, mandatory: email_template_default.nil?) || email_template_default
1064
1168
  mail_conf = email_settings
1065
1169
  values[:from_name] ||= mail_conf[:from_name]
1066
1170
  values[:from_email] ||= mail_conf[:from_email]
@@ -1070,56 +1174,66 @@ module Aspera
1070
1174
  start_options = [mail_conf[:domain]]
1071
1175
  start_options.push(mail_conf[:username], mail_conf[:password], :login) if mail_conf.key?(:username) && mail_conf.key?(:password)
1072
1176
  # create a binding with only variables defined in values
1073
- template_binding = empty_binding
1177
+ template_binding = Environment.empty_binding
1074
1178
  # add variables to binding
1075
1179
  values.each do |k, v|
1076
1180
  raise "key (#{k.class}) must be Symbol" unless k.is_a?(Symbol)
1077
1181
  template_binding.local_variable_set(k, v)
1078
1182
  end
1079
1183
  # execute template
1080
- msg_with_headers = ERB.new(notif_template).result(template_binding)
1081
- Log.dump(:msg_with_headers, msg_with_headers)
1184
+ msg_with_headers = ERB.new(notify_template).result(template_binding)
1185
+ Log.log.debug{Log.dump(:msg_with_headers, msg_with_headers)}
1186
+ require 'net/smtp'
1082
1187
  smtp = Net::SMTP.new(mail_conf[:server], mail_conf[:port])
1083
1188
  smtp.enable_starttls if mail_conf[:tls]
1189
+ smtp.enable_tls if mail_conf[:ssl]
1084
1190
  smtp.start(*start_options) do |smtp_session|
1085
1191
  smtp_session.send_message(msg_with_headers, values[:from_email], values[:to])
1086
1192
  end
1193
+ nil
1087
1194
  end
1088
1195
 
1089
- def save_presets_to_config_file
1196
+ # Save current configuration to config file
1197
+ # return true if file was saved
1198
+ def save_config_file_if_needed
1090
1199
  raise 'no configuration loaded' if @config_presets.nil?
1091
- FileUtils.mkdir_p(@main_folder) unless Dir.exist?(@main_folder)
1092
- Log.log.debug{"Writing #{@option_config_file}"}
1093
- File.write(@option_config_file, @config_presets.to_yaml)
1200
+ current_checksum = config_checksum
1201
+ return false if @config_checksum_on_disk.eql?(current_checksum)
1202
+ FileUtils.mkdir_p(@main_folder)
1094
1203
  Environment.restrict_file_access(@main_folder)
1095
- Environment.restrict_file_access(@option_config_file)
1204
+ Log.log.info{"Writing #{@option_config_file}"}
1205
+ formatter.display_status('Saving config file.')
1206
+ Environment.write_file_restricted(@option_config_file, force: true) {@config_presets.to_yaml}
1207
+ @config_checksum_on_disk = current_checksum
1208
+ return true
1096
1209
  end
1097
1210
 
1098
1211
  # returns [String] name if config_presets has default
1099
1212
  # returns nil if there is no config or bypass default params
1100
- def get_plugin_default_config_name(plugin_sym)
1213
+ def get_plugin_default_config_name(plugin_name_sym)
1101
1214
  raise 'internal error: config_presets shall be defined' if @config_presets.nil?
1102
1215
  if !@use_plugin_defaults
1103
1216
  Log.log.debug('skip default config')
1104
1217
  return nil
1105
1218
  end
1106
1219
  if @config_presets.key?(CONF_PRESET_DEFAULT) &&
1107
- @config_presets[CONF_PRESET_DEFAULT].key?(plugin_sym.to_s)
1108
- default_config_name = @config_presets[CONF_PRESET_DEFAULT][plugin_sym.to_s]
1220
+ @config_presets[CONF_PRESET_DEFAULT].key?(plugin_name_sym.to_s)
1221
+ default_config_name = @config_presets[CONF_PRESET_DEFAULT][plugin_name_sym.to_s]
1109
1222
  if !@config_presets.key?(default_config_name)
1110
1223
  Log.log.error do
1111
- "Default config name [#{default_config_name}] specified for plugin [#{plugin_sym}], but it does not exist in config file.\n"\
1112
- 'Please fix the issue: either create preset with one parameter: '\
1113
- "(#{@info[:name]} config id #{default_config_name} init @json:'{}') or remove default (#{@info[:name]} config id default remove #{plugin_sym})."
1224
+ "Default config name [#{default_config_name}] specified for plugin [#{plugin_name_sym}], but it does not exist in config file.\n" \
1225
+ 'Please fix the issue: either create preset with one parameter: ' \
1226
+ "(#{@info[:name]} config id #{default_config_name} init @json:'{}') or remove default (#{@info[:name]} config id default remove #{plugin_name_sym})."
1114
1227
  end
1115
1228
  end
1116
- raise CliError, "Config name [#{default_config_name}] must be a hash, check config file." if !@config_presets[default_config_name].is_a?(Hash)
1229
+ raise Cli::Error, "Config name [#{default_config_name}] must be a hash, check config file." if !@config_presets[default_config_name].is_a?(Hash)
1117
1230
  return default_config_name
1118
1231
  end
1119
1232
  return nil
1120
1233
  end # get_plugin_default_config_name
1121
1234
 
1122
- ALLOWED_KEYS = %i[password username description].freeze
1235
+ # TODO: delete: ALLOWED_KEYS = %i[password username description].freeze
1236
+ # @return [Hash] result of execution of vault command
1123
1237
  def execute_vault
1124
1238
  command = options.get_next_command(%i[list show create delete password])
1125
1239
  case command
@@ -1147,21 +1261,22 @@ module Aspera
1147
1261
  end
1148
1262
  end
1149
1263
 
1264
+ # @return [String] value from vault matching <name>.<param>
1150
1265
  def vault_value(name)
1151
1266
  m = name.match(/^(.+)\.(.+)$/)
1152
1267
  raise 'vault name shall match <name>.<param>' if m.nil?
1268
+ # this raise exception if label not found:
1153
1269
  info = vault.get(label: m[1])
1154
- # raise "no such vault entry: #{m[1]}" if info.nil?
1155
1270
  value = info[m[2].to_sym]
1156
1271
  raise "no such entry value: #{m[2]}" if value.nil?
1157
1272
  return value
1158
1273
  end
1159
1274
 
1275
+ # @return [Object] vault, from options or cache
1160
1276
  def vault
1161
1277
  if @vault.nil?
1162
1278
  vault_info = options.get_option(:vault) || {'type' => 'file', 'name' => 'vault.bin'}
1163
- vault_password = options.get_option(:vault_password, is_type: :mandatory)
1164
- raise 'vault must be Hash' unless vault_info.is_a?(Hash)
1279
+ vault_password = options.get_option(:vault_password, mandatory: true)
1165
1280
  vault_type = vault_info['type'] || 'file'
1166
1281
  vault_name = vault_info['name'] || (vault_type.eql?('file') ? 'vault.bin' : PROGRAM_NAME)
1167
1282
  case vault_type
@@ -1173,12 +1288,11 @@ module Aspera
1173
1288
  case Environment.os
1174
1289
  when Environment::OS_X
1175
1290
  @vault = Keychain::MacosSystem.new(vault_name, vault_password)
1176
- when Environment::OS_WINDOWS, Environment::OS_LINUX, Environment::OS_AIX
1177
- raise 'not implemented'
1178
- else raise 'Error, OS not supported'
1291
+ else
1292
+ raise 'not implemented for this OS'
1179
1293
  end
1180
1294
  else
1181
- raise CliBadArgument, "Unknown vault type: #{vault_type}"
1295
+ raise Cli::BadArgument, "Unknown vault type: #{vault_type}"
1182
1296
  end
1183
1297
  end
1184
1298
  raise 'No vault defined' if @vault.nil?
@@ -1197,7 +1311,7 @@ module Aspera
1197
1311
  @config_presets.each do |_k, v|
1198
1312
  next unless v.is_a?(Hash)
1199
1313
  conf_url = v['url'].is_a?(String) ? canonical_url(v['url']) : nil
1200
- return v if conf_url.eql?(url) && v['username'].eql?(username)
1314
+ return self.class.protect_presets(v) if conf_url.eql?(url) && v['username'].eql?(username)
1201
1315
  end
1202
1316
  nil
1203
1317
  end
@@ -1214,169 +1328,6 @@ module Aspera
1214
1328
  end
1215
1329
  return secret
1216
1330
  end
1217
-
1218
- def wizard_aoc(params)
1219
- formatter.display_status('Detected: Aspera on Cloud'.bold)
1220
- params[:plugin_name] = AOC_COMMAND_CURRENT
1221
- organization = AoC.parse_url(params[:instance_url]).first
1222
- # if not defined by user, generate name
1223
- params[:preset_name] = [params[:application], organization].join('_') if params[:preset_name].nil?
1224
- formatter.display_status("Preparing preset: #{params[:preset_name]}")
1225
- # init defaults if necessary
1226
- @config_presets[CONF_PRESET_DEFAULT] ||= {}
1227
- option_override = options.get_option(:override, is_type: :mandatory)
1228
- params[:option_default] = options.get_option(:default, is_type: :mandatory)
1229
- Log.log.error{"override=#{option_override} -> #{option_override.class}"}
1230
- raise CliError, "A default configuration already exists for plugin '#{params[:plugin_name]}' (use --override=yes or --default=no)" \
1231
- if !option_override && params[:option_default] && @config_presets[CONF_PRESET_DEFAULT].key?(params[:plugin_name])
1232
- raise CliError, "Preset already exists: #{params[:preset_name]} (use --override=yes or --id=<name>)" \
1233
- if !option_override && @config_presets.key?(params[:preset_name])
1234
- # lets see if path to priv key is provided
1235
- private_key_path = options.get_option(:pkeypath)
1236
- # give a chance to provide
1237
- if private_key_path.nil?
1238
- formatter.display_status('Please provide path to your private RSA key, or empty to generate one:')
1239
- private_key_path = options.get_option(:pkeypath, is_type: :mandatory).to_s
1240
- end
1241
- # else generate path
1242
- if private_key_path.empty?
1243
- private_key_path = File.join(@main_folder, DEFAULT_PRIV_KEY_FILENAME)
1244
- end
1245
- if File.exist?(private_key_path)
1246
- formatter.display_status('Using existing key:')
1247
- else
1248
- formatter.display_status("Generating #{DEFAULT_PRIVKEY_LENGTH} bit RSA key...")
1249
- generate_rsa_private_key(private_key_path, DEFAULT_PRIVKEY_LENGTH)
1250
- formatter.display_status('Created:')
1251
- end
1252
- formatter.display_status(private_key_path)
1253
- pub_key_pem = OpenSSL::PKey::RSA.new(File.read(private_key_path)).public_key.to_s
1254
- # declare command line options for AoC
1255
- require 'aspera/cli/plugins/aoc'
1256
- # make username mandatory for jwt, this triggers interactive input
1257
- options.get_option(:username, is_type: :mandatory)
1258
- # instantiate AoC plugin, so that command line options are known
1259
- aoc_api = self.class.plugin_class(params[:plugin_name]).new(@agents.merge({skip_basic_auth_options: true, private_key_path: private_key_path})).aoc_api
1260
- auto_set_pub_key = false
1261
- auto_set_jwt = false
1262
- use_browser_authentication = false
1263
- if options.get_option(:use_generic_client)
1264
- formatter.display_status('Using global client_id.')
1265
- formatter.display_status('Please Login to your Aspera on Cloud instance.'.red)
1266
- formatter.display_status('Navigate to your "Account Settings"'.red)
1267
- formatter.display_status('Check or update the value of "Public Key" to be:'.red.blink)
1268
- formatter.display_status(pub_key_pem.to_s)
1269
- if !options.get_option(:test_mode)
1270
- formatter.display_status('Once updated or validated, press enter.')
1271
- OpenApplication.instance.uri(params[:instance_url])
1272
- $stdin.gets
1273
- end
1274
- else
1275
- formatter.display_status('Using organization specific client_id.')
1276
- if options.get_option(:client_id).nil? || options.get_option(:client_secret, is_type: :optional).nil?
1277
- formatter.display_status('Please login to your Aspera on Cloud instance.'.red)
1278
- formatter.display_status('Go to: Apps->Admin->Organization->Integrations')
1279
- formatter.display_status('Create or check if there is an existing integration named:')
1280
- formatter.display_status("- name: #{@info[:name]}")
1281
- formatter.display_status("- redirect uri: #{DEFAULT_REDIRECT}")
1282
- formatter.display_status('- origin: localhost')
1283
- formatter.display_status('Once created or identified,')
1284
- formatter.display_status('Please enter:'.red)
1285
- end
1286
- OpenApplication.instance.uri("#{params[:instance_url]}/#{AOC_PATH_API_CLIENTS}")
1287
- options.get_option(:client_id, is_type: :mandatory)
1288
- options.get_option(:client_secret, is_type: :mandatory)
1289
- use_browser_authentication = true
1290
- end
1291
- if use_browser_authentication
1292
- formatter.display_status('We will use web authentication to bootstrap.')
1293
- auto_set_pub_key = true
1294
- auto_set_jwt = true
1295
- aoc_api.oauth.generic_parameters[:grant_method] = :web
1296
- aoc_api.oauth.generic_parameters[:scope] = AoC::SCOPE_FILES_ADMIN
1297
- aoc_api.oauth.specific_parameters[:redirect_uri] = DEFAULT_REDIRECT
1298
- end
1299
- myself = aoc_api.read('self')[:data]
1300
- if auto_set_pub_key
1301
- raise CliError, 'Public key is already set in profile (use --override=yes)' unless myself['public_key'].empty? || option_override
1302
- formatter.display_status('Updating profile with new key')
1303
- aoc_api.update("users/#{myself['id']}", {'public_key' => pub_key_pem})
1304
- end
1305
- if auto_set_jwt
1306
- formatter.display_status('Enabling JWT for client')
1307
- aoc_api.update("clients/#{options.get_option(:client_id)}", {'jwt_grant_enabled' => true, 'explicit_authorization_required' => false})
1308
- end
1309
- formatter.display_status("Creating new config preset: #{params[:preset_name]}")
1310
- @config_presets[params[:preset_name]] = {
1311
- :url.to_s => options.get_option(:url),
1312
- :username.to_s => myself['email'],
1313
- :auth.to_s => :jwt.to_s,
1314
- :private_key.to_s => '@file:' + private_key_path
1315
- }
1316
- # set only if non nil
1317
- %i[client_id client_secret].each do |s|
1318
- o = options.get_option(s)
1319
- @config_presets[params[:preset_name]][s.to_s] = o unless o.nil?
1320
- end
1321
- params[:test_args] = "#{params[:plugin_name]} user profile show"
1322
- end
1323
-
1324
- def wizard_faspex5(params)
1325
- formatter.display_status('Detected: Faspex v5'.bold)
1326
- # if not defined by user, generate unique name
1327
- params[:preset_name] = [params[:application]].concat(URI.parse(params[:instance_url]).host.gsub(/[^a-z0-9.]/, '').split('.')).join('_') \
1328
- if params[:preset_name].nil?
1329
- formatter.display_status("Preparing preset: #{params[:preset_name]}")
1330
- # init defaults if necessary
1331
- @config_presets[CONF_PRESET_DEFAULT] ||= {}
1332
- option_override = options.get_option(:override, is_type: :mandatory)
1333
- params[:option_default] = options.get_option(:default, is_type: :mandatory)
1334
- Log.log.error{"override=#{option_override} -> #{option_override.class}"}
1335
- raise CliError, "A default configuration already exists for plugin '#{params[:plugin_name]}' (use --override=yes or --default=no)" \
1336
- if !option_override && params[:option_default] && @config_presets[CONF_PRESET_DEFAULT].key?(params[:plugin_name])
1337
- raise CliError, "Preset already exists: #{params[:preset_name]} (use --override=yes or --id=<name>)" \
1338
- if !option_override && @config_presets.key?(params[:preset_name])
1339
- # lets see if path to priv key is provided
1340
- private_key_path = options.get_option(:pkeypath)
1341
- # give a chance to provide
1342
- if private_key_path.nil?
1343
- formatter.display_status('Please provide path to your private RSA key, or empty to generate one:')
1344
- private_key_path = options.get_option(:pkeypath, is_type: :mandatory).to_s
1345
- end
1346
- # else generate path
1347
- if private_key_path.empty?
1348
- private_key_path = File.join(@main_folder, DEFAULT_PRIV_KEY_FILENAME)
1349
- end
1350
- if File.exist?(private_key_path)
1351
- formatter.display_status('Using existing key:')
1352
- else
1353
- formatter.display_status("Generating #{DEFAULT_PRIVKEY_LENGTH} bit RSA key...")
1354
- generate_rsa_private_key(private_key_path, DEFAULT_PRIVKEY_LENGTH)
1355
- formatter.display_status('Created:')
1356
- end
1357
- formatter.display_status(private_key_path)
1358
- pub_key_pem = OpenSSL::PKey::RSA.new(File.read(private_key_path)).public_key.to_s
1359
- # declare command line options for AoC
1360
- require 'aspera/cli/plugins/faspex5'
1361
- self.class.plugin_class(params[:plugin_name]).new(@agents.merge({skip_basic_auth_options: true}))
1362
- formatter.display_status('Please login to Faspex 5.'.red)
1363
- OpenApplication.instance.uri(params[:instance_url])
1364
- formatter.display_status('Navigate to: 𓃑 → Admin → Configurations → API clients')
1365
- formatter.display_status('Create a client with:')
1366
- formatter.display_status('- JWT enabled')
1367
- formatter.display_status('- The following public key:')
1368
- formatter.display_status(pub_key_pem.to_s)
1369
- formatter.display_status('Once created, copy the following parameters:')
1370
- @config_presets[params[:preset_name]] = {
1371
- :url.to_s => options.get_option(:url),
1372
- :username.to_s => options.get_option(:username),
1373
- :auth.to_s => :jwt.to_s,
1374
- :private_key.to_s => '@file:' + private_key_path,
1375
- :client_id.to_s => options.get_option(:client_id, is_type: :mandatory),
1376
- :client_secret.to_s => options.get_option(:client_secret, is_type: :mandatory)
1377
- }
1378
- params[:test_args] = "#{params[:plugin_name]} user profile show"
1379
- end
1380
1331
  end
1381
1332
  end
1382
1333
  end