aspera-cli 4.1.0 → 4.3.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +455 -229
  3. data/docs/Makefile +4 -4
  4. data/docs/README.erb.md +457 -126
  5. data/docs/test_env.conf +19 -2
  6. data/examples/aoc.rb +14 -3
  7. data/examples/faspex4.rb +89 -0
  8. data/lib/aspera/aoc.rb +38 -40
  9. data/lib/aspera/cli/main.rb +65 -33
  10. data/lib/aspera/cli/plugins/aoc.rb +54 -65
  11. data/lib/aspera/cli/plugins/ats.rb +2 -2
  12. data/lib/aspera/cli/plugins/config.rb +158 -137
  13. data/lib/aspera/cli/plugins/faspex.rb +111 -64
  14. data/lib/aspera/cli/plugins/faspex5.rb +35 -48
  15. data/lib/aspera/cli/plugins/node.rb +3 -2
  16. data/lib/aspera/cli/plugins/preview.rb +88 -55
  17. data/lib/aspera/cli/transfer_agent.rb +98 -62
  18. data/lib/aspera/cli/version.rb +1 -1
  19. data/lib/aspera/command_line_builder.rb +48 -31
  20. data/lib/aspera/cos_node.rb +34 -28
  21. data/lib/aspera/environment.rb +2 -2
  22. data/lib/aspera/fasp/aoc.rb +1 -1
  23. data/lib/aspera/fasp/installation.rb +68 -45
  24. data/lib/aspera/fasp/local.rb +89 -45
  25. data/lib/aspera/fasp/manager.rb +3 -0
  26. data/lib/aspera/fasp/node.rb +23 -1
  27. data/lib/aspera/fasp/parameters.rb +57 -86
  28. data/lib/aspera/fasp/parameters.yaml +531 -0
  29. data/lib/aspera/fasp/resume_policy.rb +13 -12
  30. data/lib/aspera/fasp/uri.rb +1 -1
  31. data/lib/aspera/id_generator.rb +22 -0
  32. data/lib/aspera/node.rb +14 -3
  33. data/lib/aspera/oauth.rb +135 -129
  34. data/lib/aspera/persistency_action_once.rb +11 -7
  35. data/lib/aspera/persistency_folder.rb +6 -26
  36. data/lib/aspera/rest.rb +3 -12
  37. data/lib/aspera/secrets.rb +20 -0
  38. data/lib/aspera/sync.rb +40 -35
  39. data/lib/aspera/timer_limiter.rb +22 -0
  40. data/lib/aspera/web_auth.rb +105 -0
  41. metadata +22 -3
  42. data/docs/transfer_spec.html +0 -99
@@ -5,6 +5,7 @@ require 'aspera/cli/transfer_agent'
5
5
  require 'aspera/aoc'
6
6
  require 'aspera/node'
7
7
  require 'aspera/persistency_action_once'
8
+ require 'aspera/id_generator'
8
9
  require 'securerandom'
9
10
  require 'resolv'
10
11
  require 'date'
@@ -15,7 +16,6 @@ module Aspera
15
16
  class Aoc < BasicAuthPlugin
16
17
  # special value for package id
17
18
  VAL_ALL='ALL'
18
- attr_reader :api_aoc
19
19
  def initialize(env)
20
20
  super(env)
21
21
  @default_workspace_id=nil
@@ -26,20 +26,20 @@ module Aspera
26
26
  @api_aoc=nil
27
27
  @url_token_data=nil
28
28
  @user_info=nil
29
- self.options.add_opt_list(:auth,Oauth.auth_types,'type of Oauth authentication')
29
+ @api_aoc=nil
30
+ self.options.add_opt_list(:auth,Oauth.auth_types,'OAuth type of authentication')
30
31
  self.options.add_opt_list(:operation,[:push,:pull],'client operation for transfers')
31
- self.options.add_opt_simple(:client_id,'API client identifier in application')
32
- self.options.add_opt_simple(:client_secret,'API client passcode')
33
- self.options.add_opt_simple(:redirect_uri,'API client redirect URI')
34
- self.options.add_opt_simple(:private_key,'RSA private key PEM value for JWT (prefix file path with @val:@file:)')
32
+ self.options.add_opt_simple(:client_id,'OAuth API client identifier in application')
33
+ self.options.add_opt_simple(:client_secret,'OAuth API client passcode')
34
+ self.options.add_opt_simple(:redirect_uri,'OAuth API client redirect URI')
35
+ self.options.add_opt_simple(:private_key,'OAuth JWT RSA private key PEM value (prefix file path with @val:@file:)')
35
36
  self.options.add_opt_simple(:workspace,'name of workspace')
36
37
  self.options.add_opt_simple(:name,'resource name')
37
38
  self.options.add_opt_simple(:path,'file or folder path')
38
39
  self.options.add_opt_simple(:link,'public link to shared resource')
39
40
  self.options.add_opt_simple(:new_user_option,'new user creation option')
40
41
  self.options.add_opt_simple(:from_folder,'share to share source folder')
41
- self.options.add_opt_simple(:scope,'scope for AoC API calls')
42
- self.options.add_opt_simple(:notify,'notify users that file was received')
42
+ self.options.add_opt_simple(:scope,'OAuth scope for AoC API calls')
43
43
  self.options.add_opt_boolean(:bulk,'bulk operation')
44
44
  self.options.add_opt_boolean(:default_ports,'use standard FASP ports or get from node api')
45
45
  self.options.set_option(:bulk,:no)
@@ -52,19 +52,15 @@ module Aspera
52
52
  self.options.parse_options!
53
53
  AoC.set_use_default_ports(self.options.get_option(:default_ports))
54
54
  return if env[:man_only]
55
- @api_aoc=AoC.new(aoc_params('api/v1'))
56
- # add access key secrets
57
- @api_aoc.add_secrets(self.config.get_secrets)
58
55
  end
59
56
 
60
- # call this to populate single AK secret in AoC API object, from options
61
- # make sure secret is available
62
- def find_ak_secret(ak,mandatory=true)
63
- # secret hash is already provisioned
64
- # optionally override with specific secret
65
- @api_aoc.add_secrets({ak=>self.config.get_secret(ak,mandatory)})
66
- # check that secret was provided as single value or dictionary
67
- raise CliBadArgument,"Please provide option secret or entry in option secrets for: #{ak}" unless @api_aoc.has_secret(ak) or !mandatory
57
+ def get_api
58
+ if @api_aoc.nil?
59
+ @api_aoc=AoC.new(aoc_params(AoC::API_V1))
60
+ # add keychain for access key secrets
61
+ @api_aoc.key_chain=@agents[:secret]
62
+ end
63
+ return @api_aoc
68
64
  end
69
65
 
70
66
  # cached user information
@@ -87,19 +83,29 @@ module Aspera
87
83
  return self.transfer.start(*@api_aoc.tr_spec(app,direction,node_file,ts_add))
88
84
  end
89
85
 
90
- NODE4_COMMANDS=[ :browse, :find, :mkdir, :rename, :delete, :upload, :download, :transfer, :http_node_download, :v3, :file, :bearer_token_node ]
86
+ NODE4_COMMANDS=[ :browse, :find, :mkdir, :rename, :delete, :upload, :download, :transfer, :http_node_download, :v3, :file, :bearer_token_node, :node_info ]
91
87
 
92
88
  def execute_node_gen4_command(command_repo,top_node_file)
93
89
  case command_repo
94
90
  when :bearer_token_node
95
91
  thepath=self.options.get_next_argument('path')
96
92
  node_file = @api_aoc.resolve_node_file(top_node_file,thepath)
97
- node_api=@api_aoc.get_node_api(node_file[:node_info],AoC::SCOPE_NODE_USER)
93
+ node_api=@api_aoc.get_node_api(node_file[:node_info],scope: AoC::SCOPE_NODE_USER, use_secret: false)
98
94
  return Main.result_status(node_api.oauth_token)
95
+ when :node_info
96
+ thepath=self.options.get_next_argument('path')
97
+ node_file = @api_aoc.resolve_node_file(top_node_file,thepath)
98
+ node_api=@api_aoc.get_node_api(node_file[:node_info],scope: AoC::SCOPE_NODE_USER, use_secret: false)
99
+ return {:type=>:single_object,:data=>{
100
+ url: node_file[:node_info]['url'],
101
+ username: node_file[:node_info]['access_key'],
102
+ password: node_api.oauth_token,
103
+ root_id: node_file[:file_id]
104
+ }}
99
105
  when :browse
100
106
  thepath=self.options.get_next_argument('path')
101
107
  node_file = @api_aoc.resolve_node_file(top_node_file,thepath)
102
- node_api=@api_aoc.get_node_api(node_file[:node_info],AoC::SCOPE_NODE_USER)
108
+ node_api=@api_aoc.get_node_api(node_file[:node_info],scope: AoC::SCOPE_NODE_USER)
103
109
  file_info = node_api.read("files/#{node_file[:file_id]}")[:data]
104
110
  if file_info['type'].eql?('folder')
105
111
  result=node_api.read("files/#{node_file[:file_id]}/files",self.options.get_option(:value,:optional))
@@ -111,28 +117,22 @@ module Aspera
111
117
  return {:type=>:object_list,:data=>items,:fields=>['name','type','recursive_size','size','modified_time','access_level']}
112
118
  when :find
113
119
  thepath=self.options.get_next_argument('path')
114
- exec_prefix='exec:'
115
- expression=self.options.get_option(:value,:optional)||"#{exec_prefix}true"
116
120
  node_file=@api_aoc.resolve_node_file(top_node_file,thepath)
117
- if expression.start_with?(exec_prefix)
118
- test_block=eval "lambda{|f|#{expression[exec_prefix.length..-1]}}"
119
- else
120
- test_block=lambda{|f|f['name'].match(/#{expression}/)}
121
- end
121
+ test_block=Aspera::Node.file_matcher(self.options.get_option(:value,:optional))
122
122
  return {:type=>:object_list,:data=>@api_aoc.find_files(node_file,test_block),:fields=>['path']}
123
123
  when :mkdir
124
124
  thepath=self.options.get_next_argument('path')
125
125
  containing_folder_path = thepath.split(AoC::PATH_SEPARATOR)
126
126
  new_folder=containing_folder_path.pop
127
127
  node_file = @api_aoc.resolve_node_file(top_node_file,containing_folder_path.join(AoC::PATH_SEPARATOR))
128
- node_api=@api_aoc.get_node_api(node_file[:node_info],AoC::SCOPE_NODE_USER)
128
+ node_api=@api_aoc.get_node_api(node_file[:node_info],scope: AoC::SCOPE_NODE_USER)
129
129
  result=node_api.create("files/#{node_file[:file_id]}/files",{:name=>new_folder,:type=>:folder})[:data]
130
130
  return Main.result_status("created: #{result['name']} (id=#{result['id']})")
131
131
  when :rename
132
132
  thepath=self.options.get_next_argument('source path')
133
133
  newname=self.options.get_next_argument('new name')
134
134
  node_file = @api_aoc.resolve_node_file(top_node_file,thepath)
135
- node_api=@api_aoc.get_node_api(node_file[:node_info],AoC::SCOPE_NODE_USER)
135
+ node_api=@api_aoc.get_node_api(node_file[:node_info],scope: AoC::SCOPE_NODE_USER)
136
136
  result=node_api.update("files/#{node_file[:file_id]}",{:name=>newname})[:data]
137
137
  return Main.result_status("renamed #{thepath} to #{newname}")
138
138
  when :delete
@@ -140,7 +140,7 @@ module Aspera
140
140
  return do_bulk_operation(thepath,'deleted','path') do |l_path|
141
141
  raise "expecting String (path), got #{l_path.class.name} (#{l_path})" unless l_path.is_a?(String)
142
142
  node_file = @api_aoc.resolve_node_file(top_node_file,l_path)
143
- node_api=@api_aoc.get_node_api(node_file[:node_info],AoC::SCOPE_NODE_USER)
143
+ node_api=@api_aoc.get_node_api(node_file[:node_info],scope: AoC::SCOPE_NODE_USER)
144
144
  result=node_api.delete("files/#{node_file[:file_id]}")[:data]
145
145
  {'path'=>l_path}
146
146
  end
@@ -163,7 +163,7 @@ module Aspera
163
163
  client_node_file = @api_aoc.resolve_node_file(client_home_node_file,client_folder)
164
164
  server_node_file = @api_aoc.resolve_node_file(server_home_node_file,server_folder)
165
165
  # force node as transfer agent
166
- @agents[:transfer].set_agent_instance(Fasp::Node.new(@api_aoc.get_node_api(client_node_file[:node_info],AoC::SCOPE_NODE_USER)))
166
+ @agents[:transfer].set_agent_instance(Fasp::Node.new(@api_aoc.get_node_api(client_node_file[:node_info],scope: AoC::SCOPE_NODE_USER)))
167
167
  # additional node to node TS info
168
168
  add_ts={
169
169
  'remote_access_key' => server_node_file[:node_info]['access_key'],
@@ -201,14 +201,14 @@ module Aspera
201
201
  raise CliBadArgument,'one file at a time only in HTTP mode' if source_paths.length > 1
202
202
  file_name = source_paths.first['source']
203
203
  node_file = @api_aoc.resolve_node_file(top_node_file,File.join(source_folder,file_name))
204
- node_api=@api_aoc.get_node_api(node_file[:node_info],AoC::SCOPE_NODE_USER)
204
+ node_api=@api_aoc.get_node_api(node_file[:node_info],scope: AoC::SCOPE_NODE_USER)
205
205
  node_api.call({:operation=>'GET',:subpath=>"files/#{node_file[:file_id]}/content",:save_to_file=>File.join(self.transfer.destination_folder('receive'),file_name)})
206
206
  return Main.result_status("downloaded: #{file_name}")
207
207
  when :v3
208
208
  # Note: other "common" actions are unauthorized with user scope
209
209
  command_legacy=self.options.get_next_command(Node::SIMPLE_ACTIONS)
210
210
  # TODO: shall we support all methods here ? what if there is a link ?
211
- node_api=@api_aoc.get_node_api(top_node_file[:node_info],AoC::SCOPE_NODE_USER)
211
+ node_api=@api_aoc.get_node_api(top_node_file[:node_info],scope: AoC::SCOPE_NODE_USER)
212
212
  return Node.new(@agents.merge(skip_basic_auth_options: true, node_api: node_api)).execute_action(command_legacy)
213
213
  when :file
214
214
  file_path=self.options.get_option(:path,:optional)
@@ -217,7 +217,7 @@ module Aspera
217
217
  else
218
218
  {node_info: top_node_file[:node_info],file_id: self.options.get_option(:id,:mandatory)}
219
219
  end
220
- node_api=@api_aoc.get_node_api(node_file[:node_info],AoC::SCOPE_NODE_USER)
220
+ node_api=@api_aoc.get_node_api(node_file[:node_info],scope: AoC::SCOPE_NODE_USER)
221
221
  command_node_file=self.options.get_next_command([:show,:permission,:modify])
222
222
  case command_node_file
223
223
  when :show
@@ -528,7 +528,7 @@ module Aspera
528
528
  startdate_persistency=PersistencyActionOnce.new(
529
529
  manager: @agents[:persistency],
530
530
  data: saved_date,
531
- ids: ['aoc_ana_date',self.options.get_option(:url,:mandatory),@workspace_name].push(filter_resource,filter_id))
531
+ ids: IdGenerator.from_list(['aoc_ana_date',self.options.get_option(:url,:mandatory),@workspace_name].push(filter_resource,filter_id)))
532
532
  start_datetime=saved_date.first
533
533
  stop_datetime=Time.now.utc.strftime('%FT%T.%LZ')
534
534
  #Log.log().error("start: #{start_datetime}")
@@ -537,18 +537,11 @@ module Aspera
537
537
  filter['start_time'] = start_datetime unless start_datetime.nil?
538
538
  filter['stop_time'] = stop_datetime
539
539
  end
540
- notification=self.options.get_option(:notify,:optional)
541
540
  events=analytics_api.read("#{filter_resource}/#{filter_id}/#{event_type}",option_url_query(filter))[:data][event_type]
542
541
  startdate_persistency.save unless startdate_persistency.nil?
543
- if !notification.nil?
544
- require 'erb'
545
- events.each do |transfer|
546
- email_to_send={}
547
- notification.each do |k,v|
548
- email_to_send[k.to_sym]=ERB.new(v).result(binding)
549
- end
550
- Log.log().error("send email: #{email_to_send}")
551
- self.config.send_email(email_to_send)
542
+ if !self.options.get_option(:notif_to,:optional).nil?
543
+ events.each do |tr_event|
544
+ self.config.send_email_template({ev: tr_event})
552
545
  end
553
546
  end
554
547
  return {:type=>:object_list,:data=>events}
@@ -646,7 +639,6 @@ module Aspera
646
639
  return Main.result_success
647
640
  when :v3,:v4
648
641
  res_data=@api_aoc.read(resource_instance_path)[:data]
649
- find_ak_secret(res_data['access_key'])
650
642
  api_node=@api_aoc.get_node_api(res_data)
651
643
  return Node.new(@agents.merge(skip_basic_auth_options: true, node_api: api_node)).execute_action if command.eql?(:v3)
652
644
  ak_data=api_node.call({:operation=>'GET',:subpath=>"access_keys/#{res_data['access_key']}",:headers=>{'Accept'=>'application/json'}})[:data]
@@ -672,7 +664,7 @@ module Aspera
672
664
  res_data=@api_aoc.read(resource_instance_path)[:data]
673
665
  node_file = @api_aoc.resolve_node_file({node_info: res_data, file_id: ak_data['root_file_id']},folder_path)
674
666
 
675
- #node_api=@api_aoc.get_node_api(node_file[:node_info],AoC::SCOPE_NODE_USER)
667
+ #node_api=@api_aoc.get_node_api(node_file[:node_info],scope: AoC::SCOPE_NODE_USER)
676
668
  #file_info = node_api.read("files/#{node_file[:file_id]}")[:data]
677
669
 
678
670
  access_id="ASPERA_ACCESS_KEY_ADMIN_WS_#{@workspace_id}"
@@ -704,16 +696,17 @@ module Aspera
704
696
  end
705
697
 
706
698
  # must be public
707
- ACTIONS=[ :apiinfo, :bearer_token, :organization, :tier_restrictions, :user, :workspace, :packages, :files, :gateway, :admin, :automation, :servers]
699
+ ACTIONS=[ :reminder, :bearer_token, :organization, :tier_restrictions, :user, :workspace, :packages, :files, :gateway, :admin, :automation, :servers]
708
700
 
709
701
  def execute_action
710
702
  command=self.options.get_next_command(ACTIONS)
703
+ get_api unless [:reminder,:servers].include?(command)
711
704
  case command
712
- when :apiinfo
713
- api_info={}
714
- num=1
715
- Resolv::DNS.open{|dns|dns.each_address('api.ibmaspera.com'){|a| api_info["api.#{num}"]=a;num+=1}}
716
- return {:type=>:single_object,:data=>api_info}
705
+ when :reminder
706
+ # send an email reminder with list of orgs
707
+ user_email=options.get_option(:username,:mandatory)
708
+ Rest.new(base_url: "#{AoC.api_base_url}/#{AoC::API_V1}").create('organization_reminders',{email: user_email})[:data]
709
+ return Main.result_status("List of organizations user is member of, has been sent by e-mail to #{user_email}")
717
710
  when :bearer_token
718
711
  return {:type=>:text,:data=>@api_aoc.oauth_token}
719
712
  when :organization
@@ -799,7 +792,7 @@ module Aspera
799
792
  skip_ids_persistency=PersistencyActionOnce.new(
800
793
  manager: @agents[:persistency],
801
794
  data: skip_ids_data,
802
- ids: ['aoc_recv',self.options.get_option(:url,:mandatory),@workspace_id].push(*@persist_ids))
795
+ id: IdGenerator.from_list(['aoc_recv',self.options.get_option(:url,:mandatory),@workspace_id].push(*@persist_ids)))
803
796
  end
804
797
  if ids_to_download.eql?(VAL_ALL)
805
798
  # get list of packages in inbox
@@ -820,14 +813,14 @@ module Aspera
820
813
  add_ts={'paths'=>[{'source'=>'.'}]}
821
814
  node_file = {node_info: node_info, file_id: package_info['contents_file_id']}
822
815
  statuses=transfer_start(AoC::PACKAGES_APP,'receive',node_file,AoC.package_tags(package_info,'download').merge(add_ts))
823
- result_transfer.push({'package'=>package_id,'status'=>statuses.map{|i|i.to_s}.join(',')})
816
+ result_transfer.push({'package'=>package_id,Main::STATUS_FIELD=>statuses})
824
817
  # update skip list only if all transfer sessions completed
825
818
  if TransferAgent.session_status(statuses).eql?(:success)
826
819
  skip_ids_data.push(package_id)
827
820
  skip_ids_persistency.save unless skip_ids_persistency.nil?
828
821
  end
829
822
  end
830
- return {:type=>:object_list,:data=>result_transfer}
823
+ return Main.result_transfer_multiple(result_transfer)
831
824
  when :show
832
825
  package_id=self.options.get_next_argument('package ID')
833
826
  package_info=@api_aoc.read("packages/#{package_id}")[:data]
@@ -847,7 +840,6 @@ module Aspera
847
840
  # get workspace related information
848
841
  set_workspace_info
849
842
  set_home_node_file
850
- find_ak_secret(@home_node_file[:node_info]['access_key'],false)
851
843
  command_repo=self.options.get_next_command(NODE4_COMMANDS.clone.concat([:short_link]))
852
844
  case command_repo
853
845
  when *NODE4_COMMANDS; return execute_node_gen4_command(command_repo,@home_node_file)
@@ -894,7 +886,7 @@ module Aspera
894
886
  end
895
887
  result=self.entity_action(@api_aoc,'short_links',nil,:id,'self')
896
888
  if result[:data].is_a?(Hash) and result[:data].has_key?('created_at') and result[:data]['resource_type'].eql?('UrlToken')
897
- node_api=@api_aoc.get_node_api(node_file[:node_info],AoC::SCOPE_NODE_USER)
889
+ node_api=@api_aoc.get_node_api(node_file[:node_info],scope: AoC::SCOPE_NODE_USER)
898
890
  # TODO: access level as arg
899
891
  access_levels=Aspera::Node::ACCESS_LEVELS #['delete','list','mkdir','preview','read','rename','write']
900
892
  perm_data={
@@ -957,17 +949,14 @@ module Aspera
957
949
  when :admin
958
950
  return execute_admin_action
959
951
  when :servers
960
- self.format.display_status("Beta feature: #{@api_aoc.params[:base_url]}")
961
- server_api=Rest.new(base_url: @api_aoc.params[:base_url])
962
- servers=server_api.read('servers')[:data]
963
- return {:type=>:object_list,:data=>servers}
952
+ return {:type=>:object_list,:data=>Rest.new(base_url: "#{AoC.api_base_url}/#{AoC::API_V1}").read('servers')[:data]}
964
953
  else
965
954
  raise "internal error: #{command}"
966
955
  end # action
967
956
  raise RuntimeError, "internal error: command shall return"
968
957
  end
969
958
 
970
- private :find_ak_secret,:c_user_info,:aoc_params,:set_workspace_info,:set_home_node_file,:do_bulk_operation,:resolve_package_recipients,:option_url_query,:assert_public_link_types,:execute_admin_action
959
+ private :c_user_info,:aoc_params,:set_workspace_info,:set_home_node_file,:do_bulk_operation,:resolve_package_recipients,:option_url_query,:assert_public_link_types,:execute_admin_action
971
960
  private_constant :VAL_ALL,:NODE4_COMMANDS
972
961
 
973
962
  end # AoC
@@ -107,7 +107,7 @@ module Aspera
107
107
  :auth => {
108
108
  :type => :basic,
109
109
  :username => access_key_id,
110
- :password => self.config.get_secret(access_key_id)}})
110
+ :password => @agents[:secret].get_secret(access_key_id)}})
111
111
  command=self.options.get_next_command(Node::COMMON_ACTIONS)
112
112
  return Node.new(@agents.merge(skip_basic_auth_options: true, node_api: api_node)).execute_action(command)
113
113
  when :cluster
@@ -116,7 +116,7 @@ module Aspera
116
116
  :auth => {
117
117
  :type => :basic,
118
118
  :username => access_key_id,
119
- :password => self.config.get_secret(access_key_id)
119
+ :password => @agents[:secret].get_secret(access_key_id)
120
120
  }}
121
121
  api_ak_auth=Rest.new(rest_params)
122
122
  return {:type=>:single_object, :data=>api_ak_auth.read("servers")[:data]}