aspera-cli 4.0.0.pre1 → 4.0.0.pre2

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.
@@ -93,7 +93,7 @@ module Aspera
93
93
  return Main.result_status('modified')
94
94
  when :entitlement
95
95
  ak=ats_api_pub_v1.read("access_keys/#{access_key_id}")[:data]
96
- api_bss=OnCloud.metering_api(ak['license']['entitlement_id'],ak['license']['customer_id'])
96
+ api_bss=AoC.metering_api(ak['license']['entitlement_id'],ak['license']['customer_id'])
97
97
  return {:type=>:single_object, :data=>api_bss.read('entitlement')[:data]}
98
98
  when :delete
99
99
  res=ats_api_pub_v1.delete("access_keys/#{access_key_id}")
@@ -3,9 +3,10 @@ require 'aspera/cli/extended_value'
3
3
  require 'aspera/fasp/installation'
4
4
  require 'aspera/api_detector'
5
5
  require 'aspera/open_application'
6
- require 'aspera/on_cloud'
6
+ require 'aspera/aoc'
7
7
  require 'aspera/proxy_auto_config'
8
8
  require 'aspera/uri_reader'
9
+ require 'aspera/rest'
9
10
  require 'xmlsimple'
10
11
  require 'base64'
11
12
  require 'net/smtp'
@@ -38,7 +39,8 @@ module Aspera
38
39
  RUBY_FILE_EXT='.rb'
39
40
  AOC_COMMAND_V1='files'
40
41
  AOC_COMMAND_V2='aspera'
41
- AOC_COMMAND_V3='oncloud'
42
+ AOC_COMMAND_V3='aoc'
43
+ AOC_COMMAND_CURRENT=AOC_COMMAND_V3
42
44
  CONNECT_WEB_URL = 'https://d3gcli72yxqn2z.cloudfront.net/connect'
43
45
  CONNECT_VERSIONS = 'connectversions.js'
44
46
  DEMO='demo'
@@ -48,7 +50,7 @@ module Aspera
48
50
  self.options.add_option_preset(preset_by_name(value))
49
51
  end
50
52
 
51
- private_constant :ASPERA_HOME_FOLDER_NAME,:DEFAULT_CONFIG_FILENAME,:CONF_PRESET_CONFIG,:CONF_PRESET_VERSION,:CONF_PRESET_DEFAULT,:PROGRAM_NAME_V1,:PROGRAM_NAME_V2,:DEFAULT_REDIRECT,:ASPERA_PLUGINS_FOLDERNAME,:GEM_PLUGINS_FOLDER,:RUBY_FILE_EXT,:AOC_COMMAND_V1,:AOC_COMMAND_V2,:AOC_COMMAND_V3,:DEMO
53
+ private_constant :ASPERA_HOME_FOLDER_NAME,:DEFAULT_CONFIG_FILENAME,:CONF_PRESET_CONFIG,:CONF_PRESET_VERSION,:CONF_PRESET_DEFAULT,:PROGRAM_NAME_V1,:PROGRAM_NAME_V2,:DEFAULT_REDIRECT,:ASPERA_PLUGINS_FOLDERNAME,:GEM_PLUGINS_FOLDER,:RUBY_FILE_EXT,:AOC_COMMAND_V1,:AOC_COMMAND_V2,:AOC_COMMAND_V3,:AOC_COMMAND_CURRENT,:DEMO
52
54
  attr_accessor :option_ak_secret,:option_secrets
53
55
 
54
56
  def initialize(env,tool_name,help_url,version)
@@ -67,7 +69,8 @@ module Aspera
67
69
  @option_config_file=@conf_file_default
68
70
  @connect_versions=nil
69
71
  # set folder where generated FASP files are
70
- Fasp::Installation.instance.config_folder=@main_folder
72
+ Fasp::Installation.instance.folder=File.join(@main_folder,'sdk')
73
+ FileUtils.mkdir_p(Fasp::Installation.instance.folder)
71
74
  add_plugin_lookup_folder(File.join(@main_folder,ASPERA_PLUGINS_FOLDERNAME))
72
75
  add_plugin_lookup_folder(File.join(Main.gem_root,GEM_PLUGINS_FOLDER))
73
76
  # do file parameter first
@@ -405,7 +408,7 @@ module Aspera
405
408
  end
406
409
 
407
410
  def execute_action_ascp
408
- command=self.options.get_next_command([:connect,:use,:show,:products,:info])
411
+ command=self.options.get_next_command([:connect,:use,:show,:products,:info,:install])
409
412
  case command
410
413
  when :connect
411
414
  return execute_connect_action
@@ -429,12 +432,15 @@ module Aspera
429
432
  while line=stderr.gets do
430
433
  line.chomp!
431
434
  case line
432
- when /^DBG Path ([^ ]+) (dir|file) +: (.*)$/;data[$1]=$3
433
- when /^DBG Added module group:"([^"]+)" name:"([^"]+)", version:"([^"]+)" interface:"([^"]+)"$/;data[$2]=$4
435
+ when %r{^DBG Path ([^ ]+) (dir|file) +: (.*)$};data[$1]=$3
436
+ when %r{^DBG Added module group:"([^"]+)" name:"([^"]+)", version:"([^"]+)" interface:"([^"]+)"$};data[$2]=$4
437
+ when %r{^DBG License result \(/license/(\S+)\): (.+)$};data[$1]=$2
438
+ when %r{^LOG (.+) version ([0-9.]+)$};data['product_name']=$1;data['product_version']=$2
439
+ when %r{^LOG Initializing FASP version ([^,]+),};data['ascp_version']=$1
434
440
  end
435
441
  end
436
442
  end
437
- data['keypass']=Fasp::Installation.instance.bypass_keys.shift
443
+ data['keypass']=Fasp::Installation.instance.bypass_pass
438
444
  return {:type=>:single_object, :data=>data}
439
445
  when :products
440
446
  command=self.options.get_next_command([:list,:use])
@@ -448,7 +454,11 @@ module Aspera
448
454
  save_presets_to_config_file
449
455
  return {:type=>:status, :data=>"saved to default global preset #{preset_name}"}
450
456
  end
457
+ when :install
458
+ v=Fasp::Installation.instance.install_sdk
459
+ return {:type=>:status, :data=>"Installed version #{v}"}
451
460
  end
461
+ raise "unexpected case: #{command}"
452
462
  end
453
463
 
454
464
  ACTIONS=[:gem_path, :genkey,:plugins,:flush_tokens,:list,:overview,:open,:echo,:id,:documentation,:wizard,:export_to_cli,:detect,:coffee,:ascp,:email_test,:smtp_settings,:proxy_check,:folder,:file]
@@ -562,7 +572,7 @@ module Aspera
562
572
  case appli[:product]
563
573
  when :aoc
564
574
  self.format.display_status("Detected: Aspera on Cloud".bold)
565
- organization,instance_domain=OnCloud.parse_url(instance_url)
575
+ organization,instance_domain=AoC.parse_url(instance_url)
566
576
  aspera_preset_name='aoc_'+organization
567
577
  self.format.display_status("Preparing preset: #{aspera_preset_name}")
568
578
  # init defaults if necessary
@@ -580,7 +590,7 @@ module Aspera
580
590
  end
581
591
  # else generate path
582
592
  if private_key_path.empty?
583
- private_key_path=File.join(@main_folder,'aspera_on_cloud_key')
593
+ private_key_path=File.join(@main_folder,'aspera_aoc_key')
584
594
  end
585
595
  if File.exist?(private_key_path)
586
596
  self.format.display_status("Using existing key:")
@@ -592,7 +602,7 @@ module Aspera
592
602
  self.format.display_status("#{private_key_path}")
593
603
  pub_key_pem=OpenSSL::PKey::RSA.new(File.read(private_key_path)).public_key.to_s
594
604
  # define options
595
- require 'aspera/cli/plugins/oncloud'
605
+ require 'aspera/cli/plugins/aoc'
596
606
  # make username mandatory for jwt, this triggers interactive input
597
607
  self.options.get_option(:username,:mandatory)
598
608
  files_plugin=Plugins::Oncloud.new(@agents.merge({skip_basic_auth_options: true, private_key_path: private_key_path}))
@@ -634,7 +644,7 @@ module Aspera
634
644
  self.options.set_option(:redirect_uri,DEFAULT_REDIRECT)
635
645
  auto_set_pub_key=true
636
646
  auto_set_jwt=true
637
- self.options.set_option(:scope,OnCloud::SCOPE_FILES_ADMIN)
647
+ self.options.set_option(:scope,AoC::SCOPE_FILES_ADMIN)
638
648
  end
639
649
  files_plugin.update_aoc_api
640
650
  myself=files_plugin.api_aoc.read('self')[:data]
@@ -663,20 +673,20 @@ module Aspera
663
673
  @config_presets[CONF_PRESET_DEFAULT][AOC_COMMAND_V2]=aspera_preset_name
664
674
  self.format.display_status("saving config file")
665
675
  save_presets_to_config_file
666
- return Main.result_status("Done.\nYou can test with:\n#{@tool_name} aspera user info show")
676
+ return Main.result_status("Done.\nYou can test with:\n#{@tool_name} #{AOC_COMMAND_CURRENT} user info show")
667
677
  else
668
678
  raise CliBadArgument,"Supports only: aoc. Detected: #{appli}"
669
679
  end
670
680
  when :export_to_cli
671
681
  self.format.display_status("Exporting: Aspera on Cloud")
672
- require 'aspera/cli/plugins/oncloud'
682
+ require 'aspera/cli/plugins/aoc'
673
683
  # need url / username
674
684
  add_plugin_default_preset(AOC_COMMAND_V3.to_sym)
675
685
  files_plugin=Plugins::Oncloud.new(@agents) # TODO: is this line needed ?
676
686
  url=self.options.get_option(:url,:mandatory)
677
687
  cli_conf_file=Fasp::Installation.instance.cli_conf_file
678
688
  data=JSON.parse(File.read(cli_conf_file))
679
- organization,instance_domain=OnCloud.parse_url(url)
689
+ organization,instance_domain=AoC.parse_url(url)
680
690
  key_basename='org_'+organization+'.pem'
681
691
  key_file=File.join(File.dirname(File.dirname(cli_conf_file)),'etc',key_basename)
682
692
  File.write(key_file,self.options.get_option(:private_key,:mandatory))
@@ -686,7 +696,11 @@ module Aspera
686
696
  'privateKeyFilename' => key_basename,
687
697
  'username' => self.options.get_option(:username,:mandatory)
688
698
  }
689
- new_conf['clientId'],new_conf['clientSecret']=OnCloud.get_client_ids(self.options.get_option(:client_id,:optional),self.options.get_option(:client_secret,:optional))
699
+ new_conf['clientId']=self.options.get_option(:client_id,:optional)
700
+ new_conf['clientSecret']=self.options.get_option(:client_secret,:optional)
701
+ if new_conf['clientId'].nil?
702
+ new_conf['clientId'],new_conf['clientSecret']=AoC.get_client_info()
703
+ end
690
704
  entry=data['AoCAccounts'].select{|i|i['organization'].eql?(organization)}.first
691
705
  if entry.nil?
692
706
  data['AoCAccounts'].push(new_conf)
@@ -775,7 +789,7 @@ END_OF_MESSAGE
775
789
 
776
790
  def save_presets_to_config_file
777
791
  raise "no configuration loaded" if @config_presets.nil?
778
- FileUtils::mkdir_p(@main_folder) unless Dir.exist?(@main_folder)
792
+ FileUtils.mkdir_p(@main_folder) unless Dir.exist?(@main_folder)
779
793
  Log.log.debug "writing #{@option_config_file}"
780
794
  File.write(@option_config_file,@config_presets.to_yaml)
781
795
  end
@@ -1,14 +1,11 @@
1
- require 'aspera/cli/plugins/node'
2
1
  require 'aspera/cli/plugin'
3
- require 'xmlsimple'
2
+ require 'aspera/cli/plugins/node'
3
+ require 'aspera/cos_node'
4
4
 
5
5
  module Aspera
6
6
  module Cli
7
7
  module Plugins
8
8
  class Cos < Plugin
9
- # IBM Cloud authentication : + token
10
- IBM_CLOUD_OAUTH_URL='https://iam.cloud.ibm.com/identity'
11
- private_constant :IBM_CLOUD_OAUTH_URL
12
9
  def initialize(env)
13
10
  super(env)
14
11
  @service_creds=nil
@@ -34,7 +31,7 @@ module Aspera
34
31
  raise "one of: endpoint or service_credentials is required" if service_credentials.nil? and storage_endpoint.nil?
35
32
  raise "endpoint and service_credentials are mutually exclusive" unless service_credentials.nil? or storage_endpoint.nil?
36
33
  if service_credentials.nil?
37
- serv_cred_storage_api_key = self.options.get_option(:apikey,:mandatory)
34
+ service_api_key = self.options.get_option(:apikey,:mandatory)
38
35
  instance_id = self.options.get_option(:crn,:mandatory)
39
36
  else
40
37
  # check necessary contents
@@ -46,57 +43,20 @@ module Aspera
46
43
  # get options
47
44
  bucket_region=self.options.get_option(:region,:mandatory)
48
45
  # get API key from service credentials
49
- serv_cred_storage_api_key=service_credentials['apikey']
46
+ service_api_key=service_credentials['apikey']
50
47
  instance_id=service_credentials['resource_instance_id']
51
48
  # read endpoints from service provided in service credentials
52
49
  endpoints=Aspera::Rest.new({:base_url=>service_credentials['endpoints']}).read('')[:data]
53
50
  Aspera::Log.dump('endpoints',endpoints)
54
- storage_endpoint='https://'+endpoints['service-endpoints']['regional'][bucket_region]['public'][bucket_region]
51
+ storage_endpoint=endpoints.dig('service-endpoints','regional',bucket_region,'public',bucket_region)
52
+ raise "no such region: #{bucket_region}" if storage_endpoint.nil?
53
+ storage_endpoint='https://'+storage_endpoint
55
54
  end
56
-
57
- s3_api=Aspera::Rest.new({
58
- :base_url => storage_endpoint,
59
- :not_auth_codes => ['401','403'],
60
- :headers => {'ibm-service-instance-id' => instance_id},
61
- :auth => {
62
- :type => :oauth2,
63
- :base_url => IBM_CLOUD_OAUTH_URL,
64
- :grant => :ibm_apikey,
65
- :api_key => serv_cred_storage_api_key
66
- }})
67
- # read FASP connection information for bucket
68
- xml_result_text=s3_api.call({:operation=>'GET',:subpath=>bucket_name,:headers=>{'Accept'=>'application/xml'},:url_params=>{'faspConnectionInfo'=>nil}})[:http].body
69
- ats_info=XmlSimple.xml_in(xml_result_text, {'ForceArray' => false})
70
- Aspera::Log.dump('ats_info',ats_info)
71
- # get delegated token
72
- delegated_oauth=Oauth.new({
73
- :type => :oauth2,
74
- :base_url => IBM_CLOUD_OAUTH_URL,
75
- :grant => :delegated_refresh,
76
- :api_key => serv_cred_storage_api_key,
77
- :token_field=> 'delegated_refresh_token'
78
- })
79
- # to be placed in rest call header and in transfer tags
80
- aspera_storage_credentials={
81
- 'type' => 'token',
82
- 'token' => {'delegated_refresh_token'=>delegated_oauth.get_authorization().gsub(/^Bearer /,'')}
83
- }
84
- # transfer spec addition
85
- add_ts={'tags'=>{'aspera'=>{'node'=>{'storage_credentials'=>aspera_storage_credentials}}}}
86
- # set a general addon to transfer spec
87
- # here we choose to use the add_request_param
88
- #self.transfer.option_transfer_spec_deep_merge(add_ts)
89
- api_node=Rest.new({
90
- :base_url => ats_info['ATSEndpoint'],
91
- :headers => {'X-Aspera-Storage-Credentials'=>JSON.generate(aspera_storage_credentials)},
92
- :auth => {
93
- :type => :basic,
94
- :username => ats_info['AccessKey']['Id'],
95
- :password => ats_info['AccessKey']['Secret']}})
55
+ api_node=CosNode.new(bucket_name,storage_endpoint,instance_id,service_api_key)
96
56
  #command=self.options.get_next_command(Node::ACTIONS)
97
57
  #command=self.options.get_next_command(Node::COMMON_ACTIONS)
98
58
  command=self.options.get_next_command([:upload,:download,:info,:access_key,:api_details])
99
- node_plugin=Node.new(@agents.merge(skip_basic_auth_options: true, node_api: api_node, add_request_param: add_ts))
59
+ node_plugin=Node.new(@agents.merge(skip_basic_auth_options: true, node_api: api_node, add_request_param: api_node.add_ts))
100
60
  return node_plugin.execute_action(command)
101
61
  end
102
62
  end
@@ -1,5 +1,5 @@
1
1
  module Aspera
2
2
  module Cli
3
- VERSION = "4.0.0.pre1"
3
+ VERSION = "4.0.0.pre2"
4
4
  end
5
5
  end
@@ -0,0 +1,50 @@
1
+ require 'aspera/log'
2
+ require 'aspera/rest'
3
+ require 'xmlsimple'
4
+
5
+ module Aspera
6
+ class CosNode < Rest
7
+ attr_reader :add_ts
8
+ def initialize(bucket_name,storage_endpoint,instance_id,api_key,auth_url='https://iam.cloud.ibm.com/identity')
9
+ s3_api=Aspera::Rest.new({
10
+ :base_url => storage_endpoint,
11
+ :not_auth_codes => ['401','403'],
12
+ :headers => {'ibm-service-instance-id' => instance_id},
13
+ :auth => {
14
+ :type => :oauth2,
15
+ :base_url => auth_url,
16
+ :grant => :ibm_apikey,
17
+ :api_key => api_key
18
+ }})
19
+ # read FASP connection information for bucket
20
+ xml_result_text=s3_api.call({:operation=>'GET',:subpath=>bucket_name,:headers=>{'Accept'=>'application/xml'},:url_params=>{'faspConnectionInfo'=>nil}})[:http].body
21
+ ats_info=XmlSimple.xml_in(xml_result_text, {'ForceArray' => false})
22
+ Aspera::Log.dump('ats_info',ats_info)
23
+ # get delegated token
24
+ delegated_oauth=Oauth.new({
25
+ :type => :oauth2,
26
+ :base_url => auth_url,
27
+ :grant => :delegated_refresh,
28
+ :api_key => api_key,
29
+ :token_field=> 'delegated_refresh_token'
30
+ })
31
+ # to be placed in rest call header and in transfer tags
32
+ aspera_storage_credentials={
33
+ 'type' => 'token',
34
+ 'token' => {'delegated_refresh_token'=>delegated_oauth.get_authorization().gsub(/^Bearer /,'')}
35
+ }
36
+ # transfer spec addition
37
+ @add_ts={'tags'=>{'aspera'=>{'node'=>{'storage_credentials'=>aspera_storage_credentials}}}}
38
+ # set a general addon to transfer spec
39
+ # here we choose to use the add_request_param
40
+ #self.transfer.option_transfer_spec_deep_merge(@add_ts)
41
+ super({
42
+ :base_url => ats_info['ATSEndpoint'],
43
+ :headers => {'X-Aspera-Storage-Credentials'=>JSON.generate(aspera_storage_credentials)},
44
+ :auth => {
45
+ :type => :basic,
46
+ :username => ats_info['AccessKey']['Id'],
47
+ :password => ats_info['AccessKey']['Secret']}})
48
+ end
49
+ end
50
+ end
Binary file
Binary file
@@ -0,0 +1 @@
1
+ t1(�;�E��F�)P
@@ -0,0 +1,2 @@
1
+ ~�f��h���q
2
+ I��8VZs����y�0����S� r�t���j�*����{��y�%�<Z���
@@ -0,0 +1 @@
1
+ Q�3Cr�l�m���v���L�����]aj��8���3@���<RĖ�_���~mӽ�Nl=u~�7��}-��{u
@@ -0,0 +1,2 @@
1
+ x�}R�N�0 �#�����Ƥ����5J���:%?0ޞ,S;�n��� �z�>�y��)����)���M���R>����T@&�"���d�5Ö���"hV�#98��
2
+ u��դʬ�>��X�jZf�t�~Z��k^M���ÀN�Ԑ�2@M��W�h���}�E#I��:��h^�%�G*e#_/xu>g&~Dʒ��al���W�qhKi @�1�S�i� ll�6h`����<�:;O�!Y� a�4��j/<��3�MA�蜍�q��x��4���N:� ӃŴș�*���>�At��7x^�G����O�����
Binary file
@@ -0,0 +1,13 @@
1
+ require 'aspera/log'
2
+ require 'singleton'
3
+
4
+ module Aspera
5
+ # a simple binary data repository
6
+ class DataRepository
7
+ include Singleton
8
+ # get binary value from data repository
9
+ def get_bin(id)
10
+ File.read(File.join(File.expand_path(File.dirname(__FILE__)),'data',id.to_s),mode: 'rb')
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ require 'aspera/log'
2
+
3
+ module Aspera
4
+ # a simple binary data repository
5
+ class Environment
6
+ OS_WINDOWS = :windows
7
+ OS_X = :osx
8
+ OS_LINUX = :linux
9
+ OS_AIX = :aix
10
+ OS_LIST=[OS_WINDOWS,OS_X,OS_LINUX,OS_AIX]
11
+
12
+ def self.os
13
+ case RbConfig::CONFIG['host_os']
14
+ when /mswin/,/msys/,/mingw/,/cygwin/,/bccwin/,/wince/,/emc/
15
+ return OS_WINDOWS
16
+ when /darwin/,/mac os/
17
+ return OS_X
18
+ when /linux/
19
+ return OS_LINUX
20
+ when /aix/
21
+ return OS_AIX
22
+ else
23
+ raise "Unknown: #{RbConfig::CONFIG['host_os']}"
24
+ end
25
+ end
26
+ CPU_X86_64=:x86_64
27
+ CPU_PPC64=:ppc64
28
+ CPU_PPC64LE=:ppc64le
29
+ CPU_S390=:s390
30
+ CPU_LIST=[CPU_X86_64,CPU_PPC64,CPU_PPC64LE,CPU_S390]
31
+
32
+ def self.cpu
33
+ case RbConfig::CONFIG['host_cpu']
34
+ when /x86_64/
35
+ return :x86_64
36
+ when /powerpc/
37
+ return :ppc64le if os.eql?(OS_LINUX)
38
+ return :ppc64
39
+ when /s390/
40
+ return :s390
41
+ else # other
42
+ raise "Unknown: #{RbConfig::CONFIG['host_cpu']}"
43
+ end
44
+ end
45
+
46
+ def self.architecture
47
+ return "#{os}-#{cpu}"
48
+ end
49
+
50
+ def self.exe_extension
51
+ return '.exe' if os.eql?(OS_WINDOWS)
52
+ return ''
53
+ end
54
+ end
55
+ end
@@ -1,17 +1,17 @@
1
1
  require 'aspera/fasp/node'
2
2
  require 'aspera/log'
3
- require 'aspera/on_cloud.rb'
3
+ require 'aspera/aoc.rb'
4
4
 
5
5
  module Aspera
6
6
  module Fasp
7
7
  class Aoc < Node
8
- def initialize(on_cloud_options)
9
- @app=on_cloud_options[:app] || OnCloud::FILES_APP
10
- @api_oncloud=OnCloud.new(on_cloud_options)
8
+ def initialize(aoc_options)
9
+ @app=aoc_options[:app] || AoC::FILES_APP
10
+ @api_aoc=AoC.new(aoc_options)
11
11
  Log.log.warn("Under Development")
12
- server_node_file = @api_oncloud.resolve_node_file(server_home_node_file,server_folder)
12
+ server_node_file = @api_aoc.resolve_node_file(server_home_node_file,server_folder)
13
13
  # force node as transfer agent
14
- node_api=Fasp::Node.new(@api_oncloud.get_node_api(client_node_file[:node_info],OnCloud::SCOPE_NODE_USER))
14
+ node_api=Fasp::Node.new(@api_aoc.get_node_api(client_node_file[:node_info],AoC::SCOPE_NODE_USER))
15
15
  super(node_api)
16
16
  # additional node to node TS info
17
17
  @add_ts={
@@ -1,7 +1,7 @@
1
1
  require 'singleton'
2
2
  require 'aspera/log'
3
- require 'aspera/open_application' # current_os_type
4
-
3
+ require 'aspera/environment'
4
+ require 'aspera/data_repository'
5
5
  require 'xmlsimple'
6
6
  require 'zlib'
7
7
  require 'base64'
@@ -14,13 +14,17 @@ module Aspera
14
14
  # but the user can specify ascp location by calling:
15
15
  # Installation.instance.use_ascp_from_product(product_name)
16
16
  # or
17
- # Installation.instance.ascp_path=ascp_path
17
+ # Installation.instance.ascp_path=""
18
18
  class Installation
19
19
  include Singleton
20
20
  # currently used ascp executable
21
21
  attr_accessor :ascp_path
22
- # where key files are generated and used
23
- attr_accessor :config_folder
22
+ # location of SDK files
23
+ attr_accessor :folder
24
+ PRODUCT_CONNECT='Aspera Connect'
25
+ PRODUCT_CLI_V1='Aspera CLI'
26
+ PRODUCT_DRIVE='Aspera Drive'
27
+ PRODUCT_ENTSRV='Enterprise Server'
24
28
  # find ascp in named product (use value : FIRST_FOUND='FIRST' to just use first one)
25
29
  # or select one from installed_products()
26
30
  def use_ascp_from_product(product_name)
@@ -38,12 +42,18 @@ module Aspera
38
42
  # @return the list of installed products in format of product_locations
39
43
  def installed_products
40
44
  if @found_products.nil?
41
- @found_products=product_locations.select do |pl|
45
+ @found_products=product_locations
46
+ # add sdk as first search path
47
+ @found_products.unshift({# SDK
48
+ :expected =>'SDK',
49
+ :app_root =>@folder,
50
+ :sub_bin =>''
51
+ })
52
+ @found_products.select! do |pl|
42
53
  next false unless Dir.exist?(pl[:app_root])
43
54
  Log.log.debug("found #{pl[:app_root]}")
44
55
  sub_bin = pl[:sub_bin] || BIN_SUBFOLDER
45
- exec_ext = OpenApplication.current_os_type.eql?(:windows) ? '.exe' : ''
46
- pl[:ascp_path]=File.join(pl[:app_root],sub_bin,'ascp')+exec_ext
56
+ pl[:ascp_path]=File.join(pl[:app_root],sub_bin,'ascp'+Environment.exe_extension)
47
57
  next false unless File.exist?(pl[:ascp_path])
48
58
  product_info_file="#{pl[:app_root]}/#{PRODUCT_INFO}"
49
59
  if File.exist?(product_info_file)
@@ -59,7 +69,7 @@ module Aspera
59
69
  return @found_products
60
70
  end
61
71
 
62
- FILES=[:ascp,:ascp4,:ssh_bypass_key_dsa,:ssh_bypass_key_rsa,:fallback_cert,:fallback_key]
72
+ FILES=[:ascp,:ascp4,:ssh_bypass_key_dsa,:ssh_bypass_key_rsa,:aspera_license,:aspera_conf,:fallback_cert,:fallback_key]
63
73
 
64
74
  # get path of one resource file of currently activated product
65
75
  # keys and certs are generated locally... (they are well known values, arch. independant)
@@ -71,19 +81,42 @@ module Aspera
71
81
  # note that there might be a .exe at the end
72
82
  file=file.gsub('ascp','ascp4') if k.eql?(:ascp4)
73
83
  when :ssh_bypass_key_dsa
74
- file=File.join(@config_folder,'aspera_bypass_dsa.pem')
75
- File.write(file,Zlib::Inflate.inflate(Base64.decode64(SSH_BYPASS_DSA))) unless File.exist?(file)
84
+ file=File.join(@folder,'aspera_bypass_dsa.pem')
85
+ File.write(file,get_key('dsa',1)) unless File.exist?(file)
76
86
  File.chmod(0400,file)
77
87
  when :ssh_bypass_key_rsa
78
- file=File.join(@config_folder,'aspera_bypass_rsa.pem')
79
- File.write(file,Zlib::Inflate.inflate(Base64.decode64(SSH_BYPASS_RSA))) unless File.exist?(file)
88
+ file=File.join(@folder,'aspera_bypass_rsa.pem')
89
+ File.write(file,get_key('rsa',2)) unless File.exist?(file)
90
+ File.chmod(0400,file)
91
+ when :aspera_license
92
+ file=File.join(@folder,'aspera-license')
93
+ File.write(file,Base64.strict_encode64("#{Zlib::Inflate.inflate(DataRepository.instance.get_bin(6))}==SIGNATURE==\n#{Base64.strict_encode64(DataRepository.instance.get_bin(7))}")) unless File.exist?(file)
94
+ File.chmod(0400,file)
95
+ when :aspera_conf
96
+ file=File.join(@folder,'aspera.conf')
97
+ File.write(file,%Q{<?xml version='1.0' encoding='UTF-8'?>
98
+ <CONF version="2">
99
+ <default>
100
+ <file_system>
101
+ <storage_rc>
102
+ <adaptive>
103
+ true
104
+ </adaptive>
105
+ </storage_rc>
106
+ <resume_suffix>.aspera-ckpt</resume_suffix>
107
+ <partial_file_suffix>.partial</partial_file_suffix>
108
+ <replace_illegal_chars>_</replace_illegal_chars>
109
+ </file_system>
110
+ </default>
111
+ </CONF>
112
+ }) unless File.exist?(file)
80
113
  File.chmod(0400,file)
81
114
  when :fallback_cert,:fallback_key
82
- file_key=File.join(@config_folder,'aspera_fallback_key.pem')
83
- file_cert=File.join(@config_folder,'aspera_fallback_cert.pem')
115
+ file_key=File.join(@folder,'aspera_fallback_key.pem')
116
+ file_cert=File.join(@folder,'aspera_fallback_cert.pem')
84
117
  if !File.exist?(file_key) or !File.exist?(file_cert)
85
118
  require 'openssl'
86
- # create new self signed certificate forhttp fallback
119
+ # create new self signed certificate for http fallback
87
120
  private_key = OpenSSL::PKey::RSA.new(1024)
88
121
  cert = OpenSSL::X509::Certificate.new
89
122
  cert.subject = cert.issuer = OpenSSL::X509::Name.parse("/C=US/ST=California/L=Emeryville/O=Aspera Inc./OU=Corporate/CN=Aspera Inc./emailAddress=info@asperasoft.com")
@@ -108,7 +141,7 @@ module Aspera
108
141
 
109
142
  # @returns the file path of local connect where API's URI can be read
110
143
  def connect_uri
111
- connect=get_product_folders('Aspera Connect')
144
+ connect=get_product_folders(PRODUCT_CONNECT)
112
145
  folder=File.join(connect[:run_root],VARRUN_SUBFOLDER)
113
146
  ['','s'].each do |ext|
114
147
  uri_file=File.join(folder,"http#{ext}.uri")
@@ -122,30 +155,51 @@ module Aspera
122
155
 
123
156
  # @ return path to configuration file of aspera CLI
124
157
  def cli_conf_file
125
- connect=get_product_folders('Aspera CLI')
158
+ connect=get_product_folders(PRODUCT_CLI_V1)
126
159
  return File.join(connect[:app_root],BIN_SUBFOLDER,'.aspera_cli_conf')
127
160
  end
128
161
 
129
- # add Aspera private keys for web access, token based authorization
130
- def bypass_keys
131
- return [ "%08x-%04x-%04x-%04x-%04x%08x" % "t1(\xBF;\xF3E\xB5\xAB\x14F\x02\xC6\x7F)P".unpack("NnnnnN"),
132
- Installation.instance.path(:ssh_bypass_key_dsa),
133
- Installation.instance.path(:ssh_bypass_key_rsa) ]
162
+ # default bypass key phrase
163
+ def bypass_pass
164
+ return "%08x-%04x-%04x-%04x-%04x%08x" % DataRepository.instance.get_bin(3).unpack("NnnnnN")
134
165
  end
135
166
 
136
- # DEPRECATED ZONE
137
-
138
- def activated;Log.log.warn("deprecated, use ascp_path accessor");nil;end
139
-
140
- def activated=(product_name);Log.log.warn("deprecated, use method use_ascp_from_product");use_ascp_from_product(product_name);end
141
-
142
- def paths;Log.log.warn("deprecated, no replacement");raise "deprecated";end
167
+ def bypass_keys
168
+ return [:ssh_bypass_key_dsa,:ssh_bypass_key_rsa].map{|i|Installation.instance.path(i)}
169
+ end
143
170
 
144
- def paths=(res_paths)
145
- raise "must be a hash" unless res_paths.is_a?(Hash)
146
- raise "must have :ascp key" unless res_paths.has_key?(:ascp)
147
- Log.log.warn("deprecated, use method: ascp_path=")
148
- @ascp_path=res_paths[:ascp]
171
+ def install_sdk
172
+ require 'zip'
173
+ sdk_zip_path=File.join(Dir.tmpdir,'sdk.zip')
174
+ Aspera::Rest.new(base_url: SDK_URL).call(operation: 'GET',save_to_file: sdk_zip_path)
175
+ filter="/#{Environment.architecture}/"
176
+ ascp_path=nil
177
+ # first ensure license file is here so that ascp invokation for version works
178
+ self.path(:aspera_license)
179
+ self.path(:aspera_conf)
180
+ Zip::File.open(sdk_zip_path) do |zip_file|
181
+ zip_file.each do |entry|
182
+ if entry.name.include?(filter) and !entry.name.end_with?('/')
183
+ archive_file=File.join(@folder,File.basename(entry.name))
184
+ File.open(archive_file, 'wb') do |output_stream|
185
+ IO.copy_stream(entry.get_input_stream, output_stream)
186
+ end
187
+ if entry.name.include?('ascp')
188
+ FileUtils.chmod(0755,archive_file)
189
+ ascp_path=archive_file
190
+ end
191
+ end
192
+ end
193
+ end
194
+ File.unlink(sdk_zip_path) rescue nil # Windows may give error
195
+ ascp_version='n/a'
196
+ if !ascp_path.nil?
197
+ # get version from ascp, only after full extract, as windows requires SSL/TLS DLLs
198
+ m=%x{#{ascp_path} -A}.match(/ascp version (.*)/)
199
+ ascp_version=m[1] unless m.nil?
200
+ File.write(File.join(@folder,PRODUCT_INFO),"<product><name>IBM Aspera SDK</name><version>#{ascp_version}</version></product>")
201
+ end
202
+ return ascp_version
149
203
  end
150
204
 
151
205
  private
@@ -157,8 +211,9 @@ module Aspera
157
211
  PRODUCT_INFO='product-info.mf'
158
212
  # policy for product selection
159
213
  FIRST_FOUND='FIRST'
214
+ SDK_URL='https://eudemo.asperademo.com/aspera/faspex/sdk.zip'
160
215
 
161
- private_constant :BIN_SUBFOLDER,:ETC_SUBFOLDER,:VARRUN_SUBFOLDER,:PRODUCT_INFO
216
+ private_constant :BIN_SUBFOLDER,:ETC_SUBFOLDER,:VARRUN_SUBFOLDER,:PRODUCT_INFO,:SDK_URL
162
217
 
163
218
  # get some specific folder from specific applications: Connect or CLI
164
219
  def get_product_folders(name)
@@ -169,72 +224,73 @@ module Aspera
169
224
 
170
225
  def initialize
171
226
  @ascp_path=nil
172
- @config_folder='.'
227
+ @folder='.'
173
228
  @found_products=nil
174
229
  end
175
230
 
176
231
  # returns product folders depending on OS
177
232
  # fields
178
233
  # :expected M app name is taken from the manifest if present, else defaults to this value
179
- # :app_root M main forlder for the application
234
+ # :app_root M main folder for the application
180
235
  # :log_root O location of log files (Linux uses syslog)
181
236
  # :run_root O only for Connect Client, location of http port file
182
237
  # :sub_bin O subfolder with executables, default : bin
183
238
  def product_locations
184
- case OpenApplication.current_os_type
185
- when :windows; return [{
186
- :expected =>'Aspera Connect',
239
+ case Aspera::Environment.os
240
+ when Aspera::Environment::OS_WINDOWS; return [{
241
+ :expected =>PRODUCT_CONNECT,
187
242
  :app_root =>File.join(ENV['LOCALAPPDATA'],'Programs','Aspera','Aspera Connect'),
188
243
  :log_root =>File.join(ENV['LOCALAPPDATA'],'Aspera','Aspera Connect','var','log'),
189
244
  :run_root =>File.join(ENV['LOCALAPPDATA'],'Aspera','Aspera Connect')
190
245
  },{
191
- :expected =>'Aspera CLI',
246
+ :expected =>PRODUCT_CLI_V1,
192
247
  :app_root =>File.join('C:','Program Files','Aspera','cli'),
193
248
  :log_root =>File.join('C:','Program Files','Aspera','cli','var','log'),
194
249
  },{
195
- :expected =>'Enterprise Server',
250
+ :expected =>PRODUCT_ENTSRV,
196
251
  :app_root =>File.join('C:','Program Files','Aspera','Enterprise Server'),
197
252
  :log_root =>File.join('C:','Program Files','Aspera','Enterprise Server','var','log'),
198
253
  }]
199
- when :mac; return [{
200
- :expected =>'Aspera Connect',
254
+ when Aspera::Environment::OS_X; return [{
255
+ :expected =>PRODUCT_CONNECT,
201
256
  :app_root =>File.join(Dir.home,'Applications','Aspera Connect.app'),
202
257
  :log_root =>File.join(Dir.home,'Library','Logs','Aspera_Connect'),
203
258
  :run_root =>File.join(Dir.home,'Library','Application Support','Aspera','Aspera Connect'),
204
259
  :sub_bin =>File.join('Contents','Resources'),
205
260
  },{
206
- :expected =>'Aspera CLI',
261
+ :expected =>PRODUCT_CLI_V1,
207
262
  :app_root =>File.join(Dir.home,'Applications','Aspera CLI'),
208
263
  :log_root =>File.join(Dir.home,'Library','Logs','Aspera')
209
264
  },{
210
- :expected =>'Enterprise Server',
265
+ :expected =>PRODUCT_ENTSRV,
211
266
  :app_root =>File.join('','Library','Aspera'),
212
267
  :log_root =>File.join(Dir.home,'Library','Logs','Aspera'),
213
268
  },{
214
- :expected =>'Aspera Drive',
269
+ :expected =>PRODUCT_DRIVE,
215
270
  :app_root =>File.join('','Applications','Aspera Drive.app'),
216
271
  :log_root =>File.join(Dir.home,'Library','Logs','Aspera_Drive'),
217
272
  :sub_bin =>File.join('Contents','Resources'),
218
273
  }]
219
274
  else; return [{ # other: Linux and unix family
220
- :expected =>'Aspera Connect',
275
+ :expected =>PRODUCT_CONNECT,
221
276
  :app_root =>File.join(Dir.home,'.aspera','connect'),
222
277
  :run_root =>File.join(Dir.home,'.aspera','connect')
223
278
  },{
224
- :expected =>'Aspera CLI',
279
+ :expected =>PRODUCT_CLI_V1,
225
280
  :app_root =>File.join(Dir.home,'.aspera','cli'),
226
281
  },{
227
- :expected =>'Enterprise Server',
282
+ :expected =>PRODUCT_ENTSRV,
228
283
  :app_root =>File.join('','opt','aspera'),
229
284
  }]
230
285
  end
231
286
  end
232
287
 
233
- # not pass protected
234
- SSH_BYPASS_DSA='eJxtksuSojAAAO98xdypKYFggGOA8HQ0ICBywwiooARRIH79zu55+9qnrurv719M7PrbL3uPvkjsZyjBXyE+/hXfwo/vm+/ZNxEKzSay2zDybHhXub80t7HikDy4Ckta5DgeA4+vVbYyh9znxzbboRYzIWymBAxJsr3z9nCY1X7aEVm0rzJdKQ470q6jCbu0B7rXO6TdJFm5p7iieTnlN0JcSbiC13p6mfqy4QC0EaiMyVht5ou00Lh+l9J5ndbO3DPTR/kUoCcw4bNkoy5GvymbeRR4NYwLwH1nlVaWB7UIZVoFjKHeRaQnL0xU/vFcJX/Rxarz8qS+jQ+GM/moFQlekpAmD1Cn0yO6B4l2lcLMip+gUTxlV/wcHFnh0i0d9Mh8F8rYA8urKu0gZ3d0Pm01Z6GiQHlmPDD8pPGASsJfygmLz+ZHZgRuovS4NDZYwvMkF67Y2r6Na5iC/nGj4emOIG0XIYFuOfXIao4Y9aeS2dOaKXXvidRdh5I2+o5tPKXY1t5h8OiG2xHlH4fqqQbnPGzeUDjkb6aUVLJ6MX4UTPPG0nBFLF4DyHrfYLtY0vPkTDqucjvdbPeJSuaufp72TmI42UX4tIea7SaUUr1uI3QphmlFMMwuTqTPEih0lw35op3AdjJjEdf+AqAe9paDed1JkyckpdbAuzv7P/nznESRXhej8O8xvLX//94fHTzUbwo='
235
- # pass protected (but not fips compliant)
236
- SSH_BYPASS_RSA='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'
237
- private_constant :SSH_BYPASS_DSA,:SSH_BYPASS_RSA
288
+ def get_key(type,id)
289
+ hf=['begin','end'].map{|t|"-----#{t} #{type} private key-----".upcase}
290
+ bin=Base64.strict_encode64(DataRepository.instance.get_bin(id))
291
+ hf.insert(1,bin).join("\n")
292
+ end
293
+
238
294
  end # Installation
239
295
  end
240
296
  end