aspera-cli 4.0.0.pre1

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +3592 -0
  3. data/bin/ascli +7 -0
  4. data/bin/asession +89 -0
  5. data/docs/Makefile +59 -0
  6. data/docs/README.erb.md +3012 -0
  7. data/docs/README.md +13 -0
  8. data/docs/diagrams.txt +49 -0
  9. data/docs/secrets.make +38 -0
  10. data/docs/test_env.conf +117 -0
  11. data/docs/transfer_spec.html +99 -0
  12. data/examples/aoc.rb +17 -0
  13. data/examples/proxy.pac +60 -0
  14. data/examples/transfer.rb +115 -0
  15. data/lib/aspera/api_detector.rb +60 -0
  16. data/lib/aspera/ascmd.rb +151 -0
  17. data/lib/aspera/ats_api.rb +43 -0
  18. data/lib/aspera/cli/basic_auth_plugin.rb +38 -0
  19. data/lib/aspera/cli/extended_value.rb +88 -0
  20. data/lib/aspera/cli/formater.rb +238 -0
  21. data/lib/aspera/cli/listener/line_dump.rb +17 -0
  22. data/lib/aspera/cli/listener/logger.rb +20 -0
  23. data/lib/aspera/cli/listener/progress.rb +52 -0
  24. data/lib/aspera/cli/listener/progress_multi.rb +91 -0
  25. data/lib/aspera/cli/main.rb +304 -0
  26. data/lib/aspera/cli/manager.rb +440 -0
  27. data/lib/aspera/cli/plugin.rb +90 -0
  28. data/lib/aspera/cli/plugins/alee.rb +24 -0
  29. data/lib/aspera/cli/plugins/ats.rb +231 -0
  30. data/lib/aspera/cli/plugins/bss.rb +71 -0
  31. data/lib/aspera/cli/plugins/config.rb +806 -0
  32. data/lib/aspera/cli/plugins/console.rb +62 -0
  33. data/lib/aspera/cli/plugins/cos.rb +106 -0
  34. data/lib/aspera/cli/plugins/faspex.rb +377 -0
  35. data/lib/aspera/cli/plugins/faspex5.rb +93 -0
  36. data/lib/aspera/cli/plugins/node.rb +438 -0
  37. data/lib/aspera/cli/plugins/oncloud.rb +937 -0
  38. data/lib/aspera/cli/plugins/orchestrator.rb +169 -0
  39. data/lib/aspera/cli/plugins/preview.rb +464 -0
  40. data/lib/aspera/cli/plugins/server.rb +216 -0
  41. data/lib/aspera/cli/plugins/shares.rb +63 -0
  42. data/lib/aspera/cli/plugins/shares2.rb +114 -0
  43. data/lib/aspera/cli/plugins/sync.rb +65 -0
  44. data/lib/aspera/cli/plugins/xnode.rb +115 -0
  45. data/lib/aspera/cli/transfer_agent.rb +251 -0
  46. data/lib/aspera/cli/version.rb +5 -0
  47. data/lib/aspera/colors.rb +39 -0
  48. data/lib/aspera/command_line_builder.rb +137 -0
  49. data/lib/aspera/fasp/aoc.rb +24 -0
  50. data/lib/aspera/fasp/connect.rb +99 -0
  51. data/lib/aspera/fasp/error.rb +21 -0
  52. data/lib/aspera/fasp/error_info.rb +60 -0
  53. data/lib/aspera/fasp/http_gw.rb +81 -0
  54. data/lib/aspera/fasp/installation.rb +240 -0
  55. data/lib/aspera/fasp/listener.rb +11 -0
  56. data/lib/aspera/fasp/local.rb +377 -0
  57. data/lib/aspera/fasp/manager.rb +69 -0
  58. data/lib/aspera/fasp/node.rb +88 -0
  59. data/lib/aspera/fasp/parameters.rb +235 -0
  60. data/lib/aspera/fasp/resume_policy.rb +76 -0
  61. data/lib/aspera/fasp/uri.rb +51 -0
  62. data/lib/aspera/faspex_gw.rb +196 -0
  63. data/lib/aspera/hash_ext.rb +28 -0
  64. data/lib/aspera/log.rb +80 -0
  65. data/lib/aspera/nagios.rb +71 -0
  66. data/lib/aspera/node.rb +14 -0
  67. data/lib/aspera/oauth.rb +319 -0
  68. data/lib/aspera/on_cloud.rb +421 -0
  69. data/lib/aspera/open_application.rb +72 -0
  70. data/lib/aspera/persistency_action_once.rb +42 -0
  71. data/lib/aspera/persistency_folder.rb +91 -0
  72. data/lib/aspera/preview/file_types.rb +300 -0
  73. data/lib/aspera/preview/generator.rb +258 -0
  74. data/lib/aspera/preview/image_error.png +0 -0
  75. data/lib/aspera/preview/options.rb +35 -0
  76. data/lib/aspera/preview/utils.rb +131 -0
  77. data/lib/aspera/preview/video_error.png +0 -0
  78. data/lib/aspera/proxy_auto_config.erb.js +287 -0
  79. data/lib/aspera/proxy_auto_config.rb +34 -0
  80. data/lib/aspera/rest.rb +296 -0
  81. data/lib/aspera/rest_call_error.rb +13 -0
  82. data/lib/aspera/rest_error_analyzer.rb +98 -0
  83. data/lib/aspera/rest_errors_aspera.rb +58 -0
  84. data/lib/aspera/ssh.rb +53 -0
  85. data/lib/aspera/sync.rb +82 -0
  86. data/lib/aspera/temp_file_manager.rb +37 -0
  87. data/lib/aspera/uri_reader.rb +25 -0
  88. metadata +288 -0
@@ -0,0 +1,60 @@
1
+ require 'aspera/log'
2
+ require 'aspera/rest'
3
+
4
+ module Aspera
5
+ # detect Aspera product by calling API
6
+ class ApiDetector
7
+ # @return a hash: {:product=>:node,:version=>'unknown'}
8
+ # if not found: {:product=>:unknown,:version=>'unknown'}
9
+ def self.discover_product(url)
10
+ #uri=URI.parse(url)
11
+ api=Rest.new({:base_url=>url})
12
+ # Node
13
+ begin
14
+ result=api.call({:operation=>'GET',:subpath=>'ping'})
15
+ if result[:http].body.eql?('')
16
+ return {:product=>:node,:version=>'unknown'}
17
+ end
18
+ rescue SocketError => e
19
+ raise e
20
+ rescue => e
21
+ Log.log.debug("not node (#{e.class}: #{e})")
22
+ end
23
+ # AoC
24
+ begin
25
+ result=api.call({:operation=>'GET',:subpath=>'',:headers=>{'Accept'=>'text/html'}})
26
+ if result[:http].body.include?('content="AoC"')
27
+ return {:product=>:aoc,:version=>'unknown'}
28
+ end
29
+ rescue SocketError => e
30
+ raise e
31
+ rescue => e
32
+ Log.log.debug("not aoc (#{e.class}: #{e})")
33
+ end
34
+ # Faspex
35
+ begin
36
+ result=api.call({:operation=>'POST',:subpath=>'aspera/faspex',:headers=>{'Accept'=>'application/xrds+xml'},:text_body_params=>''})
37
+ if result[:http].body.start_with?('<?xml')
38
+ res_s=XmlSimple.xml_in(result[:http].body, {"ForceArray" => false})
39
+ version=res_s['XRD']['application']['version']
40
+ #return JSON.pretty_generate(res_s)
41
+ end
42
+ return {:product=>:faspex,:version=>version}
43
+ rescue
44
+ Log.log.debug("not faspex")
45
+ end
46
+ # Shares
47
+ begin
48
+ result=api.read('node_api/app')
49
+ Log.log.warn("not supposed to work")
50
+ rescue RestCallError => e
51
+ if e.response.code.to_s.eql?('401') and e.response.body.eql?('{"error":{"user_message":"API user authentication failed"}}')
52
+ return {:product=>:shares,:version=>'unknown'}
53
+ end
54
+ Log.log.warn("not shares: #{e.response.code} #{e.response.body}")
55
+ rescue
56
+ end
57
+ return {:product=>:unknown,:version=>'unknown'}
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,151 @@
1
+ require 'aspera/log'
2
+
3
+ module Aspera
4
+ # Run +ascmd+ commands using specified executor (usually, remotely on transfer node)
5
+ # Equivalent of SDK "command client"
6
+ # execute: "ascmd -h" to get syntax
7
+ # Note: "ls" can take filters: as_ls -f *.txt -f *.bin /
8
+ class AsCmd
9
+ # list of supported actions
10
+ OPERATIONS=[:ls,:rm,:mv,:du,:info,:mkdir,:cp,:df,:md5sum]
11
+
12
+ # @param command_executor [Object] provides the "execute" method, taking a command to execute, and stdin to feed to it, typically: ssh or local
13
+ def initialize(command_executor)
14
+ @command_executor = command_executor
15
+ end
16
+
17
+ # execute an "as" command on a remote server
18
+ # @param [Symbol] one of OPERATIONS
19
+ # @param [Array] parameters for "as" command
20
+ # @return result of command, type depends on command (bool, array, hash)
21
+ def execute_single(action_sym,args=nil)
22
+ # concatenate arguments, enclose in double quotes, protect backslash and double quotes, add "as_" command and as_exit
23
+ stdin_input=(args||[]).map{|v| '"' + v.gsub(/["\\]/n) {|s| '\\' + s } + '"'}.unshift('as_'+action_sym.to_s).join(' ')+"\nas_exit\n"
24
+ # execute, get binary output
25
+ bytebuffer=@command_executor.execute('ascmd',stdin_input).unpack('C*')
26
+ # get hash or table result
27
+ result=self.class.parse(bytebuffer,:result)
28
+ raise "ERROR: unparsed bytes remaining" unless bytebuffer.empty?
29
+ # get and delete info,always present in results
30
+ system_info=result[:info]
31
+ result.delete(:info)
32
+ # make single file result like a folder
33
+ if result.has_key?(:file);result[:dir]=[result[:file]];result.delete(:file);end
34
+ # add type field for stats
35
+ if result.has_key?(:dir)
36
+ result[:dir].each do |file|
37
+ if file.has_key?(:smode)
38
+ # Converts the first character of the file mode (see 'man ls') into a type.
39
+ file[:type]=case file[:smode][0,1];when'd';:directory;when'-';:file;when'l';:link;else;:other;end
40
+ end
41
+ end
42
+ end
43
+ # for info, second overrides first, so restore it
44
+ case result.keys.length;when 0;result=system_info;when 1;result=result[result.keys.first];else raise "error";end
45
+ # raise error as exception
46
+ raise Error.new(result[:errno],result[:errstr],action_sym,args) if result.is_a?(Hash) and result.keys.sort == TYPES_DESCR[:error][:fields].map{|i|i[:name]}.sort
47
+ return result
48
+ end # execute_single
49
+
50
+ # This exception is raised when +ascmd+ returns an error.
51
+ class Error < StandardError
52
+ attr_reader :errno, :errstr, :command, :args
53
+ def initialize(errno,errstr,cmd,args);@errno=errno;@errstr=errstr;@command=cmd;@args=args;end
54
+
55
+ def message; "ascmd: (#{errno}) #{errstr}"; end
56
+
57
+ def extended_message; "ascmd: errno=#{errno} errstr=\"#{errstr}\" command=\"#{command}\" args=#{args}"; end
58
+ end # Error
59
+
60
+ private
61
+
62
+ # description of result structures (see ascmdtypes.h). Base types are big endian
63
+ # key = name of type
64
+ TYPES_DESCR={
65
+ :result =>{:decode=>:field_list,:fields=>[{:name=>:file,:is_a=>:stat},{:name=>:dir,:is_a=>:stat,:special=>:substruct},{:name=>:size,:is_a=>:size},{:name=>:error,:is_a=>:error},{:name=>:info,:is_a=>:info},{:name=>:success,:is_a=>nil,:special=>:return_true},{:name=>:exit,:is_a=>nil},{:name=>:df,:is_a=>:mnt,:special=>:restart_on_first},{:name=>:md5sum,:is_a=>:md5sum}]},
66
+ :stat =>{:decode=>:field_list,:fields=>[{:name=>:name,:is_a=>:zstr},{:name=>:size,:is_a=>:int64},{:name=>:mode,:is_a=>:int32,:check=>nil},{:name=>:zmode,:is_a=>:zstr},{:name=>:uid,:is_a=>:int32,:check=>nil},{:name=>:zuid,:is_a=>:zstr},{:name=>:gid,:is_a=>:int32,:check=>nil},{:name=>:zgid,:is_a=>:zstr},{:name=>:ctime,:is_a=>:epoch},{:name=>:zctime,:is_a=>:zstr},{:name=>:mtime,:is_a=>:epoch},{:name=>:zmtime,:is_a=>:zstr},{:name=>:atime,:is_a=>:epoch},{:name=>:zatime,:is_a=>:zstr},{:name=>:symlink,:is_a=>:zstr},{:name=>:errno,:is_a=>:int32},{:name=>:errstr,:is_a=>:zstr}]},
67
+ :info =>{:decode=>:field_list,:fields=>[{:name=>:platform,:is_a=>:zstr},{:name=>:version,:is_a=>:zstr},{:name=>:lang,:is_a=>:zstr},{:name=>:territory,:is_a=>:zstr},{:name=>:codeset,:is_a=>:zstr},{:name=>:lc_ctype,:is_a=>:zstr},{:name=>:lc_numeric,:is_a=>:zstr},{:name=>:lc_time,:is_a=>:zstr},{:name=>:lc_all,:is_a=>:zstr},{:name=>:dev,:is_a=>:zstr,:special=>:multiple},{:name=>:browse_caps,:is_a=>:zstr},{:name=>:protocol,:is_a=>:zstr}]},
68
+ :size =>{:decode=>:field_list,:fields=>[{:name=>:size,:is_a=>:int64},{:name=>:fcount,:is_a=>:int32},{:name=>:dcount,:is_a=>:int32},{:name=>:failed_fcount,:is_a=>:int32},{:name=>:failed_dcount,:is_a=>:int32}]},
69
+ :error =>{:decode=>:field_list,:fields=>[{:name=>:errno,:is_a=>:int32},{:name=>:errstr,:is_a=>:zstr}]},
70
+ :mnt =>{:decode=>:field_list,:fields=>[{:name=>:fs,:is_a=>:zstr},{:name=>:dir,:is_a=>:zstr},{:name=>:is_a,:is_a=>:zstr},{:name=>:total,:is_a=>:int64},{:name=>:used,:is_a=>:int64},{:name=>:free,:is_a=>:int64},{:name=>:fcount,:is_a=>:int64},{:name=>:errno,:is_a=>:int32},{:name=>:errstr,:is_a=>:zstr}]},
71
+ :md5sum =>{:decode=>:field_list,:fields=>[{:name=>:md5sum,:is_a=>:zstr}]},
72
+ :int8 =>{:decode=>:base,:unpack=>'C',:size=>1},
73
+ :int32 =>{:decode=>:base,:unpack=>'L>',:size=>4},
74
+ :int64 =>{:decode=>:base,:unpack=>'Q>',:size=>8},
75
+ :epoch =>{:decode=>:base,:unpack=>'Q>',:size=>8},
76
+ :zstr =>{:decode=>:base,:unpack=>'Z*'},
77
+ :blist =>{:decode=>:buffer_list}
78
+ }
79
+
80
+ # protocol enum start at one, but array index start at zero
81
+ ENUM_START=1
82
+
83
+ private_constant :TYPES_DESCR,:ENUM_START
84
+
85
+ # get description of structure's field, @param struct_name, @param typed_buffer provides field name
86
+ def self.field_description(struct_name,typed_buffer)
87
+ result=TYPES_DESCR[struct_name][:fields][typed_buffer[:btype]-ENUM_START]
88
+ raise "Unrecognized field for #{struct_name}: #{typed_buffer[:btype]}\n#{typed_buffer[:buffer]}" if result.nil?
89
+ return result
90
+ end
91
+
92
+ # decodes the provided buffer as provided type name
93
+ # @return a decoded type.
94
+ # :base : value, :buffer_list : an array of {btype,buffer}, :field_list : a hash, or array
95
+ def self.parse(buffer,type_name,indent_level=nil)
96
+ indent_level=(indent_level||-1)+1
97
+ type_descr=TYPES_DESCR[type_name]
98
+ raise "Unexpected type #{type_name}" if type_descr.nil?
99
+ Log.log.debug("#{" ."*indent_level}parse:#{type_name}:#{type_descr[:decode]}:#{buffer[0,16]}...".red)
100
+ result=nil
101
+ case type_descr[:decode]
102
+ when :base
103
+ num_bytes=type_name.eql?(:zstr) ? buffer.length : type_descr[:size]
104
+ raise "ERROR:not enough bytes" if buffer.length < num_bytes
105
+ byte_array=buffer.shift(num_bytes);byte_array=[byte_array] unless byte_array.is_a?(Array)
106
+ result=byte_array.pack('C*').unpack(type_descr[:unpack]).first
107
+ Log.log.debug("#{" ."*indent_level}-> base:#{byte_array} -> #{result}")
108
+ result=Time.at(result) if type_name.eql?(:epoch)
109
+ when :buffer_list
110
+ result = []
111
+ while !buffer.empty?
112
+ btype=parse(buffer,:int8,indent_level)
113
+ length=parse(buffer,:int32,indent_level)
114
+ raise "ERROR:not enough bytes" if buffer.length < length
115
+ value=buffer.shift(length)
116
+ result.push({:btype=>btype,:buffer=>value})
117
+ Log.log.debug("#{" ."*indent_level}:buffer_list[#{result.length-1}] #{result.last}")
118
+ end
119
+ when :field_list
120
+ # by default the result is one struct
121
+ result = {}
122
+ # get individual binary fields
123
+ parse(buffer,:blist,indent_level).each do |typed_buffer|
124
+ # what type of field is it ?
125
+ field_info=field_description(type_name,typed_buffer)
126
+ Log.log.debug("#{" ."*indent_level}+ field(special=#{field_info[:special]})=#{field_info[:name]}".green)
127
+ case field_info[:special]
128
+ when nil
129
+ result[field_info[:name]]=parse(typed_buffer[:buffer],field_info[:is_a],indent_level)
130
+ when :return_true
131
+ result[field_info[:name]]=true
132
+ when :substruct
133
+ result[field_info[:name]]=parse(typed_buffer[:buffer],:blist,indent_level).map{|r|parse(r[:buffer],field_info[:is_a],indent_level)}
134
+ when :multiple
135
+ result[field_info[:name]]||=[]
136
+ result[field_info[:name]].push(parse(typed_buffer[:buffer],field_info[:is_a],indent_level))
137
+ when :restart_on_first
138
+ fl=result[field_info[:name]]=[]
139
+ parse(typed_buffer[:buffer],:blist,indent_level).map do |tb|
140
+ fl.push({}) if tb[:btype].eql?(ENUM_START)
141
+ fi=field_description(field_info[:is_a],tb)
142
+ fl.last[fi[:name]]=parse(tb[:buffer],fi[:is_a],indent_level)
143
+ end
144
+ end
145
+ end
146
+ else raise "error: unknown decode:#{type_descr[:decode]}"
147
+ end # is_a
148
+ return result
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,43 @@
1
+ require 'aspera/log'
2
+ require 'aspera/rest'
3
+
4
+ module Aspera
5
+ class AtsApi < Rest
6
+ # currently supported clouds
7
+ # Note to Aspera: shall be an API call
8
+ CLOUD_NAME={
9
+ :aws =>'Amazon Web Services',
10
+ :azure =>'Microsoft Azure',
11
+ :google =>'Google Cloud',
12
+ :limelight =>'Limelight',
13
+ :rackspace =>'Rackspace',
14
+ :softlayer =>'IBM Cloud'
15
+ }
16
+
17
+ private_constant :CLOUD_NAME
18
+
19
+ def self.base_url;'https://ats.aspera.io';end
20
+
21
+ def initialize
22
+ super({:base_url=>AtsApi.base_url+'/pub/v1'})
23
+ # cache of server data
24
+ @all_servers_cache=nil
25
+ end
26
+
27
+ def cloud_names;CLOUD_NAME;end
28
+
29
+ # all available ATS servers
30
+ # NOTE to Aspera: an API shall be created to retrieve all servers at once
31
+ def all_servers
32
+ if @all_servers_cache.nil?
33
+ @all_servers_cache=[]
34
+ CLOUD_NAME.keys.each do |name|
35
+ read("servers/#{name.to_s.upcase}")[:data].each do |i|
36
+ @all_servers_cache.push(i)
37
+ end
38
+ end
39
+ end
40
+ return @all_servers_cache
41
+ end
42
+ end # AtsApi
43
+ end # Aspera
@@ -0,0 +1,38 @@
1
+ require 'aspera/rest'
2
+ require 'aspera/cli/plugin'
3
+
4
+ module Aspera
5
+ module Cli
6
+ # base class for applications supporting basic authentication
7
+ class BasicAuthPlugin < Plugin
8
+ def initialize(env)
9
+ super(env)
10
+ unless env[:skip_basic_auth_options]
11
+ self.options.add_opt_simple(:url,"URL of application, e.g. https://org.asperafiles.com")
12
+ self.options.add_opt_simple(:username,"username to log in")
13
+ self.options.add_opt_simple(:password,"user's password")
14
+ self.options.parse_options!
15
+ end
16
+ end
17
+ ACTIONS=[]
18
+
19
+ def execute_action
20
+ raise "do not execute action on this generic plugin"
21
+ end
22
+
23
+ # returns a Rest object with basic auth
24
+ def basic_auth_api(subpath=nil)
25
+ api_url=self.options.get_option(:url,:mandatory)
26
+ api_url=api_url+'/'+subpath unless subpath.nil?
27
+ return Rest.new({
28
+ :base_url => api_url,
29
+ :auth => {
30
+ :type => :basic,
31
+ :username => self.options.get_option(:username,:mandatory),
32
+ :password => self.options.get_option(:password,:mandatory)
33
+ }})
34
+ end
35
+
36
+ end # BasicAuthPlugin
37
+ end # Cli
38
+ end # Aspera
@@ -0,0 +1,88 @@
1
+ require 'aspera/cli/plugins/config'
2
+ require 'json'
3
+ require 'base64'
4
+ require 'zlib'
5
+ require 'csv'
6
+ require 'singleton'
7
+
8
+ module Aspera
9
+ module Cli
10
+ # command line extended values
11
+ class ExtendedValue
12
+ include Singleton
13
+ private
14
+ # decode comma separated table text
15
+ def self.decode_csvt(value)
16
+ col_titles=nil
17
+ hasharray=[]
18
+ CSV.parse(value).each do |values|
19
+ next if values.empty?
20
+ if col_titles.nil?
21
+ col_titles=values
22
+ else
23
+ entry={}
24
+ col_titles.each{|title|entry[title]=values.shift}
25
+ hasharray.push(entry)
26
+ end
27
+ end
28
+ value=hasharray
29
+ end
30
+
31
+ def initialize
32
+ @handlers={
33
+ :decoder=>{
34
+ 'base64' =>lambda{|v|Base64.decode64(v)},
35
+ 'json' =>lambda{|v|JSON.parse(v)},
36
+ 'zlib' =>lambda{|v|Zlib::Inflate.inflate(v)},
37
+ 'ruby' =>lambda{|v|eval(v)},
38
+ 'csvt' =>lambda{|v|ExtendedValue.decode_csvt(v)},
39
+ 'lines' =>lambda{|v|v.split("\n")},
40
+ 'list' =>lambda{|v|v[1..-1].split(v[0])}
41
+ },
42
+ :reader=>{
43
+ 'val' =>lambda{|v|v},
44
+ 'file' =>lambda{|v|File.read(File.expand_path(v))},
45
+ 'path' =>lambda{|v|File.expand_path(v)},
46
+ 'env' =>lambda{|v|ENV[v]},
47
+ 'stdin' =>lambda{|v|raise "no value allowed for stdin" unless v.empty?;STDIN.read}
48
+ }
49
+ # other handlers can be set using set_handler, e.g. preset is reader in config plugin
50
+ }
51
+ end
52
+ public
53
+
54
+ def modifiers;@handlers.keys.map{|i|@handlers[i].keys}.flatten;end
55
+
56
+ # add a new :reader or :decoder
57
+ # decoder can be chained, reader is last one on right
58
+ def set_handler(name,type,method)
59
+ raise "type must be one of #{@handlers.keys}" unless @handlers.keys.include?(type)
60
+ Log.log.debug("setting #{type} handler for #{name}")
61
+ @handlers[type][name]=method
62
+ end
63
+
64
+ # parse an option value if it is a String using supported extended value modifiers
65
+ # other value types are returned as is
66
+ def evaluate(value)
67
+ return value if !value.is_a?(String)
68
+ # first determine decoders, in reversed order
69
+ decoders_reversed=[]
70
+ while (m=value.match(/^@([^:]+):(.*)/)) and @handlers[:decoder].include?(m[1])
71
+ decoders_reversed.unshift(m[1])
72
+ value=m[2]
73
+ end
74
+ # then read value
75
+ @handlers[:reader].each do |reader,method|
76
+ if m=value.match(/^@#{reader}:(.*)/) then
77
+ value=method.call(m[1])
78
+ break
79
+ end
80
+ end
81
+ decoders_reversed.each do |decoder|
82
+ value=@handlers[:decoder][decoder].call(value)
83
+ end
84
+ return value
85
+ end # parse
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,238 @@
1
+ #require 'text-table'
2
+ require 'terminal-table'
3
+
4
+ #require 'fileutils'
5
+ require 'yaml'
6
+ require 'pp'
7
+
8
+ module Aspera
9
+ module Cli
10
+ # Take care of output
11
+ class Formater
12
+ FIELDS_ALL='ALL'
13
+ FIELDS_DEFAULT='DEF'
14
+ # supported output formats
15
+ DISPLAY_FORMATS=[:table,:ruby,:json,:jsonpp,:yaml,:csv,:nagios]
16
+ # user output levels
17
+ DISPLAY_LEVELS=[:info,:data,:error]
18
+ CSV_RECORD_SEPARATOR="\n"
19
+ CSV_FIELD_SEPARATOR=","
20
+
21
+ private_constant :FIELDS_ALL,:FIELDS_DEFAULT,:DISPLAY_FORMATS,:DISPLAY_LEVELS,:CSV_RECORD_SEPARATOR,:CSV_FIELD_SEPARATOR
22
+ attr_accessor :option_flat_hash
23
+
24
+ def initialize(opt_mgr)
25
+ @option_flat_hash=true
26
+ @opt_mgr=opt_mgr
27
+ @opt_mgr.set_obj_attr(:flat_hash,self,:option_flat_hash)
28
+ @opt_mgr.add_opt_list(:format,DISPLAY_FORMATS,"output format")
29
+ @opt_mgr.add_opt_list(:display,DISPLAY_LEVELS,"output only some information")
30
+ @opt_mgr.add_opt_simple(:fields,"comma separated list of fields, or #{FIELDS_ALL}, or #{FIELDS_DEFAULT}")
31
+ @opt_mgr.add_opt_simple(:select,"select only some items in lists, extended value: hash (column, value)")
32
+ @opt_mgr.add_opt_simple(:table_style,"table display style")
33
+ @opt_mgr.add_opt_boolean(:flat_hash,"display hash values as additional keys")
34
+ @opt_mgr.set_option(:format,:table)
35
+ @opt_mgr.set_option(:display,:info)
36
+ @opt_mgr.set_option(:fields,FIELDS_DEFAULT)
37
+ @opt_mgr.set_option(:table_style,':.:')
38
+ end
39
+
40
+ # main output method
41
+ def display_message(message_level,message)
42
+ display_level=@opt_mgr.get_option(:display,:mandatory)
43
+ case message_level
44
+ when :info; STDOUT.puts(message) if display_level.eql?(:info)
45
+ when :data; STDOUT.puts(message) unless display_level.eql?(:error)
46
+ when :error; STDERR.puts(message)
47
+ else raise "wrong message_level:#{message_level}"
48
+ end
49
+ end
50
+
51
+ def display_status(status)
52
+ display_message(:info,status)
53
+ end
54
+
55
+ # @param source [Hash] hash to modify
56
+ # @param keep_last [bool]
57
+ def self.flatten_object(source,keep_last)
58
+ newval={}
59
+ flatten_sub_hash_rec(source,keep_last,'',newval)
60
+ source.clear
61
+ source.merge!(newval)
62
+ end
63
+
64
+ # recursive function to modify a hash
65
+ # @param source [Hash] to be modified
66
+ # @param keep_last [bool] truer if last level is not
67
+ # @param prefix [String] true if last level is not
68
+ # @param dest [Hash] new hash flattened
69
+ def self.flatten_sub_hash_rec(source,keep_last,prefix,dest)
70
+ #is_simple_hash=source.is_a?(Hash) and source.values.inject(true){|m,v| xxx=!v.respond_to?(:each) and m;puts("->#{xxx}>#{v.respond_to?(:each)} #{v}-");xxx}
71
+ is_simple_hash=false
72
+ Log.log.debug("(#{keep_last})[#{is_simple_hash}] -#{source.values}- \n-#{source}-")
73
+ return source if keep_last and is_simple_hash
74
+ source.each do |k,v|
75
+ if v.is_a?(Hash) and ( !keep_last or !is_simple_hash )
76
+ flatten_sub_hash_rec(v,keep_last,prefix+k.to_s+'.',dest)
77
+ else
78
+ dest[prefix+k.to_s]=v
79
+ end
80
+ end
81
+ return nil
82
+ end
83
+
84
+ # special for Aspera on Cloud display node
85
+ # {"param" => [{"name"=>"foo","value"=>"bar"}]} will be expanded to {"param.foo" : "bar"}
86
+ def self.flatten_name_value_list(hash)
87
+ hash.keys.each do |k|
88
+ v=hash[k]
89
+ if v.is_a?(Array) and v.map{|i|i.class}.uniq.eql?([Hash]) and v.map{|i|i.keys}.flatten.sort.uniq.eql?(["name", "value"])
90
+ v.each do |pair|
91
+ hash["#{k}.#{pair["name"]}"]=pair["value"]
92
+ end
93
+ hash.delete(k)
94
+ end
95
+ end
96
+ end
97
+
98
+ def result_default_fields(results,table_rows_hash_val)
99
+ if results.has_key?(:fields) and !results[:fields].nil?
100
+ final_table_columns=results[:fields]
101
+ else
102
+ if !table_rows_hash_val.empty?
103
+ final_table_columns=table_rows_hash_val.first.keys
104
+ else
105
+ final_table_columns=['empty']
106
+ end
107
+ end
108
+ end
109
+
110
+ def result_all_fields(results,table_rows_hash_val)
111
+ raise "internal error: must be array" unless table_rows_hash_val.is_a?(Array)
112
+ # get the list of all column names used in all lines, not just frst one, as all lines may have different columns
113
+ return table_rows_hash_val.inject({}){|m,v|v.keys.each{|c|m[c]=true};m}.keys
114
+ end
115
+
116
+ # this method displays the results, especially the table format
117
+ def display_results(results)
118
+ raise "INTERNAL ERROR, result must be Hash (got: #{results.class}: #{results})" unless results.is_a?(Hash)
119
+ raise "INTERNAL ERROR, result must have type" unless results.has_key?(:type)
120
+ raise "INTERNAL ERROR, result must have data" unless results.has_key?(:data) or [:empty,:nothing].include?(results[:type])
121
+ res_data=results[:data]
122
+ # comma separated list in string format
123
+ user_asked_fields_list_str=@opt_mgr.get_option(:fields,:mandatory)
124
+ display_format=@opt_mgr.get_option(:format,:mandatory)
125
+ case display_format
126
+ when :nagios
127
+ Nagios.process(res_data)
128
+ when :ruby
129
+ display_message(:data,PP.pp(res_data,''))
130
+ when :json
131
+ display_message(:data,JSON.generate(res_data))
132
+ when :jsonpp
133
+ display_message(:data,JSON.pretty_generate(res_data))
134
+ when :yaml
135
+ display_message(:data,res_data.to_yaml)
136
+ when :table,:csv
137
+ case results[:type]
138
+ when :object_list # goes to table display
139
+ raise "internal error: unexpected type: #{res_data.class}, expecting Array" unless res_data.is_a?(Array)
140
+ # :object_list is an array of hash tables, where key=colum name
141
+ table_rows_hash_val = res_data
142
+ final_table_columns=nil
143
+ if @option_flat_hash
144
+ table_rows_hash_val.each do |obj|
145
+ self.class.flatten_object(obj,results[:option_expand_last])
146
+ end
147
+ end
148
+ final_table_columns=case user_asked_fields_list_str
149
+ when FIELDS_DEFAULT; result_default_fields(results,table_rows_hash_val)
150
+ when FIELDS_ALL; result_all_fields(results,table_rows_hash_val)
151
+ else
152
+ if user_asked_fields_list_str.start_with?('+')
153
+ result_default_fields(results,table_rows_hash_val).push(*user_asked_fields_list_str.gsub(/^\+/,'').split(','))
154
+ else
155
+ user_asked_fields_list_str.split(',')
156
+ end
157
+ end
158
+ when :single_object # goes to table display
159
+ # :single_object is a simple hash table (can be nested)
160
+ raise "internal error: expecting Hash: got #{res_data.class}: #{res_data}" unless res_data.is_a?(Hash)
161
+ final_table_columns = results[:columns] || ['key','value']
162
+ if @option_flat_hash
163
+ self.class.flatten_object(res_data,results[:option_expand_last])
164
+ self.class.flatten_name_value_list(res_data)
165
+ end
166
+ asked_fields=case user_asked_fields_list_str
167
+ when FIELDS_DEFAULT; results[:fields]||res_data.keys
168
+ when FIELDS_ALL; res_data.keys
169
+ else user_asked_fields_list_str.split(',')
170
+ end
171
+ table_rows_hash_val=asked_fields.map { |i| { final_table_columns.first => i, final_table_columns.last => res_data[i] } }
172
+ when :value_list # goes to table display
173
+ # :value_list is a simple array of values, name of column provided in the :name
174
+ final_table_columns = [results[:name]]
175
+ table_rows_hash_val=res_data.map { |i| { results[:name] => i } }
176
+ when :empty # no table
177
+ display_message(:info,'empty')
178
+ return
179
+ when :nothing # no result expected
180
+ Log.log.debug("no result expected")
181
+ return
182
+ when :status # no table
183
+ # :status displays a simple message
184
+ display_message(:info,res_data)
185
+ return
186
+ when :text # no table
187
+ # :status displays a simple message
188
+ display_message(:data,res_data)
189
+ return
190
+ when :other_struct # no table
191
+ # :other_struct is any other type of structure
192
+ display_message(:data,PP.pp(res_data,''))
193
+ return
194
+ else
195
+ raise "unknown data type: #{results[:type]}"
196
+ end
197
+ # here we expect: table_rows_hash_val and final_table_columns
198
+ raise "no field specified" if final_table_columns.nil?
199
+ if table_rows_hash_val.empty?
200
+ display_message(:info,'empty'.gray) unless display_format.eql?(:csv)
201
+ return
202
+ end
203
+ # convert to string with special function. here table_rows_hash_val is an array of hash
204
+ table_rows_hash_val=results[:textify].call(table_rows_hash_val) if results.has_key?(:textify)
205
+ filter=@opt_mgr.get_option(:select,:optional)
206
+ unless filter.nil?
207
+ raise CliBadArgument,"expecting hash for select" unless filter.is_a?(Hash)
208
+ filter.each{|k,v|table_rows_hash_val.select!{|i|i[k].eql?(v)}}
209
+ end
210
+
211
+ # convert data to string, and keep only display fields
212
+ final_table_rows=table_rows_hash_val.map { |r| final_table_columns.map { |c| r[c].to_s } }
213
+ # here : final_table_columns : list of column names
214
+ # here: final_table_rows : array of list of value
215
+ case display_format
216
+ when :table
217
+ style=@opt_mgr.get_option(:table_style,:mandatory).split('')
218
+ # display the table !
219
+ #display_message(:data,Text::Table.new(
220
+ #:head => final_table_columns,
221
+ #:rows => final_table_rows,
222
+ #:horizontal_boundary => style[0],
223
+ #:vertical_boundary => style[1],
224
+ #:boundary_intersection => style[2]))
225
+ display_message(:data,Terminal::Table.new(
226
+ :headings => final_table_columns,
227
+ :rows => final_table_rows,
228
+ :border_x => style[0],
229
+ :border_y => style[1],
230
+ :border_i => style[2]))
231
+ when :csv
232
+ display_message(:data,final_table_rows.map{|t| t.join(CSV_FIELD_SEPARATOR)}.join(CSV_RECORD_SEPARATOR))
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end