aspera-cli 4.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +3592 -0
- data/bin/ascli +7 -0
- data/bin/asession +89 -0
- data/docs/Makefile +59 -0
- data/docs/README.erb.md +3012 -0
- data/docs/README.md +13 -0
- data/docs/diagrams.txt +49 -0
- data/docs/secrets.make +38 -0
- data/docs/test_env.conf +117 -0
- data/docs/transfer_spec.html +99 -0
- data/examples/aoc.rb +17 -0
- data/examples/proxy.pac +60 -0
- data/examples/transfer.rb +115 -0
- data/lib/aspera/api_detector.rb +60 -0
- data/lib/aspera/ascmd.rb +151 -0
- data/lib/aspera/ats_api.rb +43 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +38 -0
- data/lib/aspera/cli/extended_value.rb +88 -0
- data/lib/aspera/cli/formater.rb +238 -0
- data/lib/aspera/cli/listener/line_dump.rb +17 -0
- data/lib/aspera/cli/listener/logger.rb +20 -0
- data/lib/aspera/cli/listener/progress.rb +52 -0
- data/lib/aspera/cli/listener/progress_multi.rb +91 -0
- data/lib/aspera/cli/main.rb +304 -0
- data/lib/aspera/cli/manager.rb +440 -0
- data/lib/aspera/cli/plugin.rb +90 -0
- data/lib/aspera/cli/plugins/alee.rb +24 -0
- data/lib/aspera/cli/plugins/ats.rb +231 -0
- data/lib/aspera/cli/plugins/bss.rb +71 -0
- data/lib/aspera/cli/plugins/config.rb +806 -0
- data/lib/aspera/cli/plugins/console.rb +62 -0
- data/lib/aspera/cli/plugins/cos.rb +106 -0
- data/lib/aspera/cli/plugins/faspex.rb +377 -0
- data/lib/aspera/cli/plugins/faspex5.rb +93 -0
- data/lib/aspera/cli/plugins/node.rb +438 -0
- data/lib/aspera/cli/plugins/oncloud.rb +937 -0
- data/lib/aspera/cli/plugins/orchestrator.rb +169 -0
- data/lib/aspera/cli/plugins/preview.rb +464 -0
- data/lib/aspera/cli/plugins/server.rb +216 -0
- data/lib/aspera/cli/plugins/shares.rb +63 -0
- data/lib/aspera/cli/plugins/shares2.rb +114 -0
- data/lib/aspera/cli/plugins/sync.rb +65 -0
- data/lib/aspera/cli/plugins/xnode.rb +115 -0
- data/lib/aspera/cli/transfer_agent.rb +251 -0
- data/lib/aspera/cli/version.rb +5 -0
- data/lib/aspera/colors.rb +39 -0
- data/lib/aspera/command_line_builder.rb +137 -0
- data/lib/aspera/fasp/aoc.rb +24 -0
- data/lib/aspera/fasp/connect.rb +99 -0
- data/lib/aspera/fasp/error.rb +21 -0
- data/lib/aspera/fasp/error_info.rb +60 -0
- data/lib/aspera/fasp/http_gw.rb +81 -0
- data/lib/aspera/fasp/installation.rb +240 -0
- data/lib/aspera/fasp/listener.rb +11 -0
- data/lib/aspera/fasp/local.rb +377 -0
- data/lib/aspera/fasp/manager.rb +69 -0
- data/lib/aspera/fasp/node.rb +88 -0
- data/lib/aspera/fasp/parameters.rb +235 -0
- data/lib/aspera/fasp/resume_policy.rb +76 -0
- data/lib/aspera/fasp/uri.rb +51 -0
- data/lib/aspera/faspex_gw.rb +196 -0
- data/lib/aspera/hash_ext.rb +28 -0
- data/lib/aspera/log.rb +80 -0
- data/lib/aspera/nagios.rb +71 -0
- data/lib/aspera/node.rb +14 -0
- data/lib/aspera/oauth.rb +319 -0
- data/lib/aspera/on_cloud.rb +421 -0
- data/lib/aspera/open_application.rb +72 -0
- data/lib/aspera/persistency_action_once.rb +42 -0
- data/lib/aspera/persistency_folder.rb +91 -0
- data/lib/aspera/preview/file_types.rb +300 -0
- data/lib/aspera/preview/generator.rb +258 -0
- data/lib/aspera/preview/image_error.png +0 -0
- data/lib/aspera/preview/options.rb +35 -0
- data/lib/aspera/preview/utils.rb +131 -0
- data/lib/aspera/preview/video_error.png +0 -0
- data/lib/aspera/proxy_auto_config.erb.js +287 -0
- data/lib/aspera/proxy_auto_config.rb +34 -0
- data/lib/aspera/rest.rb +296 -0
- data/lib/aspera/rest_call_error.rb +13 -0
- data/lib/aspera/rest_error_analyzer.rb +98 -0
- data/lib/aspera/rest_errors_aspera.rb +58 -0
- data/lib/aspera/ssh.rb +53 -0
- data/lib/aspera/sync.rb +82 -0
- data/lib/aspera/temp_file_manager.rb +37 -0
- data/lib/aspera/uri_reader.rb +25 -0
- 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
|
data/lib/aspera/ascmd.rb
ADDED
@@ -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
|