aspera-cli 4.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
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