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.
- 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
|