aspera-cli 4.7.0 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.md +1267 -999
- data/bin/ascli +20 -1
- data/bin/asession +37 -34
- data/docs/test_env.conf +7 -3
- data/examples/aoc.rb +13 -12
- data/examples/dascli +23 -0
- data/examples/faspex4.rb +34 -29
- data/examples/{transfer.rb → node.rb} +31 -59
- data/examples/server.rb +93 -0
- data/lib/aspera/aoc.rb +153 -143
- data/lib/aspera/ascmd.rb +56 -45
- data/lib/aspera/ats_api.rb +9 -6
- data/lib/aspera/cli/basic_auth_plugin.rb +18 -16
- data/lib/aspera/cli/extended_value.rb +33 -30
- data/lib/aspera/cli/formater.rb +105 -111
- data/lib/aspera/cli/info.rb +3 -2
- data/lib/aspera/cli/listener/line_dump.rb +1 -0
- data/lib/aspera/cli/listener/logger.rb +1 -0
- data/lib/aspera/cli/listener/progress.rb +13 -12
- data/lib/aspera/cli/listener/progress_multi.rb +21 -20
- data/lib/aspera/cli/main.rb +110 -90
- data/lib/aspera/cli/manager.rb +99 -88
- data/lib/aspera/cli/plugin.rb +98 -39
- data/lib/aspera/cli/plugins/alee.rb +6 -5
- data/lib/aspera/cli/plugins/aoc.rb +581 -450
- data/lib/aspera/cli/plugins/ats.rb +84 -83
- data/lib/aspera/cli/plugins/bss.rb +30 -27
- data/lib/aspera/cli/plugins/config.rb +488 -397
- data/lib/aspera/cli/plugins/console.rb +17 -15
- data/lib/aspera/cli/plugins/cos.rb +26 -35
- data/lib/aspera/cli/plugins/faspex.rb +206 -172
- data/lib/aspera/cli/plugins/faspex5.rb +109 -74
- data/lib/aspera/cli/plugins/node.rb +379 -189
- data/lib/aspera/cli/plugins/orchestrator.rb +71 -65
- data/lib/aspera/cli/plugins/preview.rb +131 -122
- data/lib/aspera/cli/plugins/server.rb +50 -150
- data/lib/aspera/cli/plugins/shares.rb +61 -27
- data/lib/aspera/cli/plugins/sync.rb +15 -14
- data/lib/aspera/cli/transfer_agent.rb +75 -64
- data/lib/aspera/cli/version.rb +2 -1
- data/lib/aspera/colors.rb +29 -28
- data/lib/aspera/command_line_builder.rb +50 -43
- data/lib/aspera/cos_node.rb +64 -38
- data/lib/aspera/data_repository.rb +1 -0
- data/lib/aspera/environment.rb +33 -10
- data/lib/aspera/fasp/agent_base.rb +35 -30
- data/lib/aspera/fasp/agent_connect.rb +35 -30
- data/lib/aspera/fasp/agent_direct.rb +68 -60
- data/lib/aspera/fasp/agent_httpgw.rb +71 -64
- data/lib/aspera/fasp/agent_node.rb +24 -23
- data/lib/aspera/fasp/agent_trsdk.rb +19 -20
- data/lib/aspera/fasp/error.rb +2 -1
- data/lib/aspera/fasp/error_info.rb +79 -68
- data/lib/aspera/fasp/installation.rb +130 -126
- data/lib/aspera/fasp/listener.rb +1 -0
- data/lib/aspera/fasp/parameters.rb +71 -60
- data/lib/aspera/fasp/parameters.yaml +69 -17
- data/lib/aspera/fasp/resume_policy.rb +14 -11
- data/lib/aspera/fasp/transfer_spec.rb +6 -5
- data/lib/aspera/fasp/uri.rb +25 -24
- data/lib/aspera/faspex_gw.rb +83 -72
- data/lib/aspera/hash_ext.rb +23 -13
- data/lib/aspera/id_generator.rb +16 -13
- data/lib/aspera/keychain/encrypted_hash.rb +61 -46
- data/lib/aspera/keychain/macos_security.rb +26 -24
- data/lib/aspera/log.rb +35 -39
- data/lib/aspera/nagios.rb +36 -28
- data/lib/aspera/node.rb +19 -19
- data/lib/aspera/oauth.rb +120 -100
- data/lib/aspera/open_application.rb +25 -22
- data/lib/aspera/persistency_action_once.rb +9 -8
- data/lib/aspera/persistency_folder.rb +13 -9
- data/lib/aspera/preview/file_types.rb +261 -266
- data/lib/aspera/preview/generator.rb +74 -73
- data/lib/aspera/preview/image_error.png +0 -0
- data/lib/aspera/preview/options.rb +7 -6
- data/lib/aspera/preview/utils.rb +30 -33
- data/lib/aspera/preview/video_error.png +0 -0
- data/lib/aspera/proxy_auto_config.rb +27 -23
- data/lib/aspera/rest.rb +73 -74
- data/lib/aspera/rest_call_error.rb +1 -0
- data/lib/aspera/rest_error_analyzer.rb +23 -19
- data/lib/aspera/rest_errors_aspera.rb +43 -40
- data/lib/aspera/secret_hider.rb +74 -0
- data/lib/aspera/ssh.rb +13 -10
- data/lib/aspera/sync.rb +49 -47
- data/lib/aspera/temp_file_manager.rb +7 -5
- data/lib/aspera/timer_limiter.rb +9 -8
- data/lib/aspera/uri_reader.rb +17 -18
- data/lib/aspera/web_auth.rb +17 -15
- data.tar.gz.sig +5 -0
- metadata +119 -35
- metadata.gz.sig +0 -0
- data/bin/dascli +0 -13
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'aspera/cli/basic_auth_plugin'
|
|
3
4
|
require 'aspera/cli/plugins/node'
|
|
4
5
|
require 'aspera/cli/plugins/config'
|
|
@@ -19,30 +20,34 @@ module Aspera
|
|
|
19
20
|
module Plugins
|
|
20
21
|
class Faspex < BasicAuthPlugin
|
|
21
22
|
# required hash key for source in config
|
|
22
|
-
KEY_NODE='node' # value must be hash with url, username, password
|
|
23
|
-
KEY_PATH='path' # value must be same sub-path as in Faspex's node
|
|
24
|
-
VAL_ALL='ALL'
|
|
23
|
+
KEY_NODE = 'node' # value must be hash with url, username, password
|
|
24
|
+
KEY_PATH = 'path' # value must be same sub-path as in Faspex's node
|
|
25
|
+
VAL_ALL = 'ALL'
|
|
25
26
|
# added field in result that identifies the package
|
|
26
|
-
PACKAGE_MATCH_FIELD='package_id'
|
|
27
|
+
PACKAGE_MATCH_FIELD = 'package_id'
|
|
27
28
|
# list of supported atoms
|
|
28
|
-
ATOM_MAILBOXES=[
|
|
29
|
+
ATOM_MAILBOXES = %i[inbox archive sent].freeze
|
|
29
30
|
# allowed parameters for inbox.atom
|
|
30
|
-
ATOM_PARAMS=[
|
|
31
|
+
ATOM_PARAMS = %w[page count startIndex].freeze
|
|
31
32
|
# with special parameters (from Plugin class) : max and pmax
|
|
32
|
-
ATOM_EXT_PARAMS=ATOM_PARAMS+[MAX_ITEMS, MAX_PAGES]
|
|
33
|
+
ATOM_EXT_PARAMS = ATOM_PARAMS + [MAX_ITEMS, MAX_PAGES]
|
|
33
34
|
# sub path in url for public link delivery
|
|
34
|
-
PUB_LINK_EXTERNAL_MATCH='external_deliveries/'
|
|
35
|
-
private_constant :KEY_NODE,:KEY_PATH,:VAL_ALL,:PACKAGE_MATCH_FIELD,:ATOM_MAILBOXES,
|
|
36
|
-
|
|
35
|
+
PUB_LINK_EXTERNAL_MATCH = 'external_deliveries/'
|
|
36
|
+
private_constant :KEY_NODE,:KEY_PATH,:VAL_ALL,:PACKAGE_MATCH_FIELD,:ATOM_MAILBOXES,:ATOM_PARAMS,
|
|
37
|
+
:ATOM_EXT_PARAMS,:PUB_LINK_EXTERNAL_MATCH
|
|
37
38
|
|
|
38
39
|
class << self
|
|
39
40
|
def detect(base_url)
|
|
40
|
-
api=Rest.new(
|
|
41
|
-
result=api.call(
|
|
41
|
+
api = Rest.new(base_url: base_url)
|
|
42
|
+
result = api.call(
|
|
43
|
+
operation: 'POST',
|
|
44
|
+
subpath: 'aspera/faspex',
|
|
45
|
+
headers: {'Accept' => 'application/xrds+xml', 'Content-type' => 'text/plain'},
|
|
46
|
+
text_body_params: '')
|
|
42
47
|
# 4.x
|
|
43
48
|
if result[:http].body.start_with?('<?xml')
|
|
44
|
-
res_s=XmlSimple.xml_in(result[:http].body, {'ForceArray' => false})
|
|
45
|
-
version=res_s['XRD']['application']['version']
|
|
49
|
+
res_s = XmlSimple.xml_in(result[:http].body, {'ForceArray' => false})
|
|
50
|
+
version = res_s['XRD']['application']['version']
|
|
46
51
|
return {version: version}
|
|
47
52
|
end
|
|
48
53
|
return nil
|
|
@@ -50,15 +55,15 @@ module Aspera
|
|
|
50
55
|
|
|
51
56
|
# extract elements from anonymous faspex link
|
|
52
57
|
def get_link_data(publink)
|
|
53
|
-
publink_uri=URI.parse(publink)
|
|
54
|
-
raise CliBadArgument, 'public link does not match Faspex format' unless (m=publink_uri.path.match(/^(.*)\/(external.*)$/))
|
|
55
|
-
base=m[1]
|
|
56
|
-
subpath=m[2]
|
|
57
|
-
port_add=publink_uri.port.eql?(publink_uri.default_port) ? '' : ":#{publink_uri.port}"
|
|
58
|
-
result={
|
|
59
|
-
base_url:
|
|
60
|
-
subpath:
|
|
61
|
-
query:
|
|
58
|
+
publink_uri = URI.parse(publink)
|
|
59
|
+
raise CliBadArgument, 'public link does not match Faspex format' unless (m = publink_uri.path.match(/^(.*)\/(external.*)$/))
|
|
60
|
+
base = m[1]
|
|
61
|
+
subpath = m[2]
|
|
62
|
+
port_add = publink_uri.port.eql?(publink_uri.default_port) ? '' : ":#{publink_uri.port}"
|
|
63
|
+
result = {
|
|
64
|
+
base_url: "#{publink_uri.scheme}://#{publink_uri.host}#{port_add}#{base}",
|
|
65
|
+
subpath: subpath,
|
|
66
|
+
query: URI.decode_www_form(publink_uri.query).each_with_object({}){|v,h|h[v.first] = v.last;}
|
|
62
67
|
}
|
|
63
68
|
Log.dump('publink',result)
|
|
64
69
|
return result
|
|
@@ -70,7 +75,7 @@ module Aspera
|
|
|
70
75
|
raise CliBadArgument, 'package has no link (deleted?)' if raise_no_link
|
|
71
76
|
return nil
|
|
72
77
|
end
|
|
73
|
-
result=entry['link'].
|
|
78
|
+
result = entry['link'].find{|e| e['rel'].eql?('package')}['href']
|
|
74
79
|
# tags in the end of URL is not well % encoded... there are "=" that should be %3D
|
|
75
80
|
# TODO: enter ticket to Faspex ?
|
|
76
81
|
###XXif m=result.match(/(=+)$/);result.gsub!(/=+$/,"#{"%3D"*m[1].length}");end
|
|
@@ -79,7 +84,7 @@ module Aspera
|
|
|
79
84
|
|
|
80
85
|
def textify_package_list(table_data)
|
|
81
86
|
return table_data.map do |e|
|
|
82
|
-
e.keys.each {|k| e[k]=e[k].first if e[k].is_a?(Array) && (e[k].length == 1)}
|
|
87
|
+
e.keys.each {|k| e[k] = e[k].first if e[k].is_a?(Array) && (e[k].length == 1)}
|
|
83
88
|
e['items'] = e.has_key?('link') ? e['link'].length : 0
|
|
84
89
|
e
|
|
85
90
|
end
|
|
@@ -87,7 +92,7 @@ module Aspera
|
|
|
87
92
|
|
|
88
93
|
# field_sym : :id or :name
|
|
89
94
|
def get_source_id(source_list,source_name)
|
|
90
|
-
source_ids=source_list.select { |i| i['name'].eql?(source_name) }
|
|
95
|
+
source_ids = source_list.select { |i| i['name'].eql?(source_name) }
|
|
91
96
|
if source_ids.empty?
|
|
92
97
|
raise CliError,%Q(No such Faspex source "#{source_name}" in [#{source_list.map{|i| %Q("#{i['name']}")}.join(', ')}])
|
|
93
98
|
end
|
|
@@ -96,8 +101,8 @@ module Aspera
|
|
|
96
101
|
end
|
|
97
102
|
|
|
98
103
|
def initialize(env)
|
|
99
|
-
@api_v3=nil
|
|
100
|
-
@api_v4=nil
|
|
104
|
+
@api_v3 = nil
|
|
105
|
+
@api_v4 = nil
|
|
101
106
|
super(env)
|
|
102
107
|
options.add_opt_simple(:link,'public link for specific operation')
|
|
103
108
|
options.add_opt_simple(:delivery_info,'package delivery information (extended value)')
|
|
@@ -111,23 +116,23 @@ module Aspera
|
|
|
111
116
|
|
|
112
117
|
def api_v3
|
|
113
118
|
if @api_v3.nil?
|
|
114
|
-
@api_v3=basic_auth_api
|
|
119
|
+
@api_v3 = basic_auth_api
|
|
115
120
|
end
|
|
116
121
|
return @api_v3
|
|
117
122
|
end
|
|
118
123
|
|
|
119
124
|
def api_v4
|
|
120
125
|
if @api_v4.nil?
|
|
121
|
-
faspex_api_base=options.get_option(:url
|
|
122
|
-
@api_v4=Rest.new({
|
|
123
|
-
base_url:
|
|
124
|
-
auth:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
faspex_api_base = options.get_option(:url,is_type: :mandatory)
|
|
127
|
+
@api_v4 = Rest.new({
|
|
128
|
+
base_url: faspex_api_base + '/api',
|
|
129
|
+
auth: {
|
|
130
|
+
type: :oauth2,
|
|
131
|
+
base_url: faspex_api_base + '/auth/oauth2',
|
|
132
|
+
auth: {type: :basic, username: options.get_option(:username,is_type: :mandatory), password: options.get_option(:password,is_type: :mandatory)},
|
|
133
|
+
crtype: :generic,
|
|
134
|
+
generic: {grant_type: 'password'},
|
|
135
|
+
scope: 'admin'
|
|
131
136
|
}})
|
|
132
137
|
end
|
|
133
138
|
return @api_v4
|
|
@@ -135,48 +140,48 @@ module Aspera
|
|
|
135
140
|
|
|
136
141
|
# query supports : {"startIndex":10,"count":1,"page":109,"max":2,"pmax":1}
|
|
137
142
|
def mailbox_filtered_entries(stop_at_id: nil)
|
|
138
|
-
recipient_names=[options.get_option(:recipient
|
|
143
|
+
recipient_names = [options.get_option(:recipient) || options.get_option(:username,is_type: :mandatory)]
|
|
139
144
|
# some workgroup messages have no star in recipient name
|
|
140
145
|
recipient_names.push(recipient_names.first[1..-1]) if recipient_names.first.start_with?('*')
|
|
141
146
|
# mailbox is in ATOM_MAILBOXES
|
|
142
|
-
mailbox=options.get_option(:box
|
|
147
|
+
mailbox = options.get_option(:box,is_type: :mandatory)
|
|
143
148
|
# parameters
|
|
144
|
-
mailbox_query=options.get_option(:query
|
|
145
|
-
max_items=nil
|
|
146
|
-
max_pages=nil
|
|
147
|
-
result=[]
|
|
149
|
+
mailbox_query = options.get_option(:query)
|
|
150
|
+
max_items = nil
|
|
151
|
+
max_pages = nil
|
|
152
|
+
result = []
|
|
148
153
|
if !mailbox_query.nil?
|
|
149
154
|
raise 'query: must be Hash or nil' unless mailbox_query.is_a?(Hash)
|
|
150
|
-
raise "query: supported params: #{ATOM_EXT_PARAMS}" unless (mailbox_query.keys-ATOM_EXT_PARAMS).empty?
|
|
155
|
+
raise "query: supported params: #{ATOM_EXT_PARAMS}" unless (mailbox_query.keys - ATOM_EXT_PARAMS).empty?
|
|
151
156
|
raise 'query: startIndex and page are exclusive' if mailbox_query.has_key?('startIndex') && mailbox_query.has_key?('page')
|
|
152
|
-
max_items=mailbox_query[MAX_ITEMS]
|
|
157
|
+
max_items = mailbox_query[MAX_ITEMS]
|
|
153
158
|
mailbox_query.delete(MAX_ITEMS)
|
|
154
|
-
max_pages=mailbox_query[MAX_PAGES]
|
|
159
|
+
max_pages = mailbox_query[MAX_PAGES]
|
|
155
160
|
mailbox_query.delete(MAX_PAGES)
|
|
156
161
|
end
|
|
157
162
|
loop do
|
|
158
163
|
# get a batch of package information
|
|
159
164
|
# order: first batch is latest packages, and then in a batch ids are increasing
|
|
160
|
-
atom_xml=api_v3.call({operation: 'GET',subpath: "#{mailbox}.atom",headers: {'Accept'=>'application/xml'},url_params: mailbox_query})[:http].body
|
|
161
|
-
box_data=XmlSimple.xml_in(atom_xml, {'ForceArray' => true})
|
|
165
|
+
atom_xml = api_v3.call({operation: 'GET',subpath: "#{mailbox}.atom",headers: {'Accept' => 'application/xml'},url_params: mailbox_query})[:http].body
|
|
166
|
+
box_data = XmlSimple.xml_in(atom_xml, {'ForceArray' => true})
|
|
162
167
|
Log.dump(:box_data,box_data)
|
|
163
|
-
items=box_data.has_key?('entry') ? box_data['entry'] : []
|
|
168
|
+
items = box_data.has_key?('entry') ? box_data['entry'] : []
|
|
164
169
|
Log.log.debug("new items: #{items.count}")
|
|
165
170
|
# it is the end if page is empty
|
|
166
171
|
break if items.empty?
|
|
167
|
-
stop_condition=false
|
|
172
|
+
stop_condition = false
|
|
168
173
|
# results will be sorted in reverse id
|
|
169
|
-
items.
|
|
170
|
-
package[PACKAGE_MATCH_FIELD]=
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
items.reverse_each do |package|
|
|
175
|
+
package[PACKAGE_MATCH_FIELD] =
|
|
176
|
+
case mailbox
|
|
177
|
+
when :inbox,:archive
|
|
178
|
+
recipient = package['to'].find{|i|recipient_names.include?(i['name'].first)}
|
|
179
|
+
recipient.nil? ? nil : recipient['recipient_delivery_id'].first
|
|
180
|
+
else # :sent
|
|
181
|
+
package['delivery_id'].first
|
|
182
|
+
end
|
|
178
183
|
# if we look for a specific package
|
|
179
|
-
stop_condition=true if !stop_at_id.nil? && stop_at_id.eql?(package[PACKAGE_MATCH_FIELD])
|
|
184
|
+
stop_condition = true if !stop_at_id.nil? && stop_at_id.eql?(package[PACKAGE_MATCH_FIELD])
|
|
180
185
|
# keep only those for the specified recipient,
|
|
181
186
|
result.push(package) unless package[PACKAGE_MATCH_FIELD].nil?
|
|
182
187
|
end
|
|
@@ -185,16 +190,16 @@ module Aspera
|
|
|
185
190
|
Log.log.debug("total items: #{result.count}")
|
|
186
191
|
# reach the limit ?
|
|
187
192
|
if !max_items.nil? && (result.count >= max_items)
|
|
188
|
-
result=result.slice(0,max_items) if result.count > max_items
|
|
193
|
+
result = result.slice(0,max_items) if result.count > max_items
|
|
189
194
|
break
|
|
190
195
|
end
|
|
191
|
-
link=box_data['link'].
|
|
196
|
+
link = box_data['link'].find{|i|i['rel'].eql?('next')}
|
|
192
197
|
Log.log.debug("link: #{link}")
|
|
193
198
|
# no next link
|
|
194
199
|
break if link.nil?
|
|
195
200
|
# replace parameters with the ones from next link
|
|
196
|
-
params=CGI.parse(URI.parse(link['href']).query)
|
|
197
|
-
mailbox_query=params.keys.each_with_object({}){|i,m|;m[i]=params[i].first;}
|
|
201
|
+
params = CGI.parse(URI.parse(link['href']).query)
|
|
202
|
+
mailbox_query = params.keys.each_with_object({}){|i,m|;m[i] = params[i].first;}
|
|
198
203
|
Log.log.debug("query: #{mailbox_query}")
|
|
199
204
|
break if !max_pages.nil? && (mailbox_query['page'].to_i > max_pages)
|
|
200
205
|
end
|
|
@@ -203,43 +208,46 @@ module Aspera
|
|
|
203
208
|
|
|
204
209
|
# retrieve transfer spec from pub link for send package
|
|
205
210
|
def send_publink_to_ts(public_link_url,package_create_params)
|
|
206
|
-
delivery_info=package_create_params['delivery']
|
|
211
|
+
delivery_info = package_create_params['delivery']
|
|
207
212
|
# pub link user
|
|
208
|
-
link_data=self.class.get_link_data(public_link_url)
|
|
213
|
+
link_data = self.class.get_link_data(public_link_url)
|
|
209
214
|
if !['external/submissions/new','external/dropbox_submissions/new'].include?(link_data[:subpath])
|
|
210
215
|
raise CliBadArgument,"pub link is #{link_data[:subpath]}, expecting external/submissions/new"
|
|
211
216
|
end
|
|
212
|
-
create_path=link_data[:subpath].split('/')[0..-2].join('/')
|
|
213
|
-
package_create_params
|
|
214
|
-
delivery_info
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
api_public_link=Rest.new({base_url: link_data[:base_url]})
|
|
217
|
+
create_path = link_data[:subpath].split('/')[0..-2].join('/')
|
|
218
|
+
package_create_params[:passcode] = link_data[:query]['passcode']
|
|
219
|
+
delivery_info[:transfer_type] = 'connect'
|
|
220
|
+
delivery_info[:source_paths_list] = transfer.ts_source_paths.map{|i|i['source']}.join("\r\n")
|
|
221
|
+
api_public_link = Rest.new({base_url: link_data[:base_url]})
|
|
218
222
|
# Hum, as this does not always work (only user, but not dropbox), we get the javascript and need hack
|
|
219
223
|
#pkg_created=api_public_link.create(create_path,package_create_params)[:data]
|
|
220
224
|
# so extract data from javascript
|
|
221
|
-
pkgdatares=api_public_link.call({
|
|
225
|
+
pkgdatares = api_public_link.call({
|
|
226
|
+
operation: 'POST',
|
|
227
|
+
subpath: create_path,
|
|
228
|
+
json_params: package_create_params,
|
|
229
|
+
headers: {'Accept' => 'text/javascript'}})[:http].body
|
|
222
230
|
# get args of function call
|
|
223
|
-
pkgdatares.
|
|
231
|
+
pkgdatares.delete!("\n") # one line
|
|
224
232
|
pkgdatares.gsub!(/^[^"]+\("\{/,'{') # delete header
|
|
225
233
|
pkgdatares.gsub!(/"\);[^"]+$/,'"') # delete trailer
|
|
226
234
|
pkgdatares.gsub!(/\}", *"/,'},"') # between two args
|
|
227
235
|
pkgdatares.gsub!('\\"','"') # remove protecting quote
|
|
228
236
|
begin
|
|
229
|
-
pkgdatares=JSON.parse("[#{pkgdatares}]")
|
|
237
|
+
pkgdatares = JSON.parse("[#{pkgdatares}]")
|
|
230
238
|
rescue JSON::ParserError # => e
|
|
231
239
|
raise 'Link not valid'
|
|
232
240
|
end
|
|
233
241
|
return pkgdatares.first
|
|
234
242
|
end
|
|
235
243
|
|
|
236
|
-
ACTIONS=[
|
|
244
|
+
ACTIONS = %i[health package source me dropbox v4 address_book login_methods].freeze
|
|
237
245
|
|
|
238
246
|
def execute_action
|
|
239
|
-
command=options.get_next_command(ACTIONS)
|
|
247
|
+
command = options.get_next_command(ACTIONS)
|
|
240
248
|
case command
|
|
241
249
|
when :health
|
|
242
|
-
nagios=Nagios.new
|
|
250
|
+
nagios = Nagios.new
|
|
243
251
|
begin
|
|
244
252
|
api_v3.read('me')
|
|
245
253
|
nagios.add_ok('faspex api','accessible')
|
|
@@ -248,143 +256,164 @@ module Aspera
|
|
|
248
256
|
end
|
|
249
257
|
return nagios.result
|
|
250
258
|
when :package
|
|
251
|
-
command_pkg=options.get_next_command([
|
|
259
|
+
command_pkg = options.get_next_command(%i[send recv list])
|
|
252
260
|
case command_pkg
|
|
253
261
|
when :list
|
|
254
|
-
return {
|
|
262
|
+
return {
|
|
263
|
+
type: :object_list,
|
|
264
|
+
data: mailbox_filtered_entries,
|
|
265
|
+
fields: [PACKAGE_MATCH_FIELD,'title','items'],
|
|
266
|
+
textify: lambda {|table_data|Faspex.textify_package_list(table_data)}
|
|
267
|
+
}
|
|
255
268
|
when :send
|
|
256
|
-
delivery_info=options.get_option(:delivery_info
|
|
269
|
+
delivery_info = options.get_option(:delivery_info,is_type: :mandatory)
|
|
257
270
|
raise CliBadArgument,'delivery_info must be hash, refer to doc' unless delivery_info.is_a?(Hash)
|
|
258
271
|
# actual parameter to faspex API
|
|
259
|
-
package_create_params={'delivery'=>delivery_info}
|
|
260
|
-
public_link_url=options.get_option(:link
|
|
272
|
+
package_create_params = {'delivery' => delivery_info}
|
|
273
|
+
public_link_url = options.get_option(:link)
|
|
261
274
|
if public_link_url.nil?
|
|
262
275
|
# authenticated user
|
|
263
|
-
delivery_info['sources']||=[{'paths'=>[]}]
|
|
264
|
-
first_source=delivery_info['sources'].first
|
|
276
|
+
delivery_info['sources'] ||= [{'paths' => []}]
|
|
277
|
+
first_source = delivery_info['sources'].first
|
|
265
278
|
first_source['paths'].push(*transfer.ts_source_paths.map{|i|i['source']})
|
|
266
|
-
source_name=options.get_option(:source_name
|
|
279
|
+
source_name = options.get_option(:source_name)
|
|
267
280
|
if !source_name.nil?
|
|
268
|
-
source_list=api_v3.call({operation: 'GET',subpath: 'source_shares',headers: {'Accept'=>'application/json'}})[:data]['items']
|
|
269
|
-
source_id=self.class.get_source_id(source_list,source_name)
|
|
270
|
-
first_source['id']=source_id
|
|
281
|
+
source_list = api_v3.call({operation: 'GET',subpath: 'source_shares',headers: {'Accept' => 'application/json'}})[:data]['items']
|
|
282
|
+
source_id = self.class.get_source_id(source_list,source_name)
|
|
283
|
+
first_source['id'] = source_id
|
|
271
284
|
end
|
|
272
|
-
pkg_created=api_v3.call({
|
|
285
|
+
pkg_created = api_v3.call({
|
|
286
|
+
operation: 'POST',
|
|
287
|
+
subpath: 'send',
|
|
288
|
+
json_params: package_create_params,
|
|
289
|
+
headers: {'Accept' => 'application/json'}
|
|
290
|
+
})[:data]
|
|
273
291
|
if !source_name.nil?
|
|
274
292
|
# no transfer spec if remote source
|
|
275
293
|
return {data: [pkg_created['links']['status']],type: :value_list,name: 'link'}
|
|
276
294
|
end
|
|
277
295
|
raise CliBadArgument,'expecting one session exactly' if pkg_created['xfer_sessions'].length != 1
|
|
278
|
-
transfer_spec=pkg_created['xfer_sessions'].first
|
|
296
|
+
transfer_spec = pkg_created['xfer_sessions'].first
|
|
279
297
|
# use source from cmd line, this one only contains destination (already in dest root)
|
|
280
298
|
transfer_spec.delete('paths')
|
|
281
299
|
else # publink
|
|
282
|
-
transfer_spec=send_publink_to_ts(public_link_url,package_create_params)
|
|
300
|
+
transfer_spec = send_publink_to_ts(public_link_url,package_create_params)
|
|
283
301
|
end
|
|
284
302
|
#Log.dump('transfer_spec',transfer_spec)
|
|
285
303
|
return Main.result_transfer(transfer.start(transfer_spec,{src: :node_gen3}))
|
|
286
304
|
when :recv
|
|
287
|
-
link_url=options.get_option(:link
|
|
305
|
+
link_url = options.get_option(:link)
|
|
288
306
|
# list of faspex ID/URI to download
|
|
289
|
-
pkg_id_uri=nil
|
|
290
|
-
skip_ids_data=[]
|
|
291
|
-
skip_ids_persistency=nil
|
|
307
|
+
pkg_id_uri = nil
|
|
308
|
+
skip_ids_data = []
|
|
309
|
+
skip_ids_persistency = nil
|
|
292
310
|
case link_url
|
|
293
311
|
when nil # usual case: no link
|
|
294
|
-
if options.get_option(:once_only
|
|
295
|
-
skip_ids_persistency=PersistencyActionOnce.new(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
312
|
+
if options.get_option(:once_only,is_type: :mandatory)
|
|
313
|
+
skip_ids_persistency = PersistencyActionOnce.new(
|
|
314
|
+
manager: @agents[:persistency],
|
|
315
|
+
data: skip_ids_data,
|
|
316
|
+
id: IdGenerator.from_list([
|
|
317
|
+
'faspex_recv',
|
|
318
|
+
options.get_option(:url,is_type: :mandatory),
|
|
319
|
+
options.get_option(:username,is_type: :mandatory),
|
|
320
|
+
options.get_option(:box,is_type: :mandatory).to_s
|
|
321
|
+
]))
|
|
299
322
|
end
|
|
300
323
|
# get command line parameters
|
|
301
|
-
delivid=instance_identifier
|
|
324
|
+
delivid = instance_identifier
|
|
302
325
|
raise 'empty id' if delivid.empty?
|
|
303
|
-
recipient=options.get_option(:recipient
|
|
326
|
+
recipient = options.get_option(:recipient)
|
|
304
327
|
if delivid.eql?(VAL_ALL)
|
|
305
|
-
pkg_id_uri=mailbox_filtered_entries.map{|i|{id: i[PACKAGE_MATCH_FIELD],uri: self.class.get_fasp_uri_from_entry(i, raise_no_link: false)}}
|
|
306
|
-
# TODO : remove ids from skip not present in inbox to avoid growing too big
|
|
307
|
-
# skip_ids_data.select!{|id|pkg_id_uri.select{|p|p[:id].eql?(id)}}
|
|
308
|
-
pkg_id_uri.reject!{|i|skip_ids_data.include?(i[:id])}
|
|
328
|
+
pkg_id_uri = mailbox_filtered_entries.map{|i|{id: i[PACKAGE_MATCH_FIELD],uri: self.class.get_fasp_uri_from_entry(i, raise_no_link: false)}}
|
|
309
329
|
elsif !recipient.nil? && recipient.start_with?('*')
|
|
310
|
-
found_package_link=mailbox_filtered_entries(stop_at_id: delivid).
|
|
330
|
+
found_package_link = mailbox_filtered_entries(stop_at_id: delivid).find{|p|p[PACKAGE_MATCH_FIELD].eql?(delivid)}['link'].first['href']
|
|
311
331
|
raise 'Not Found. Dropbox and Workgroup packages can use the link option with faspe:' if found_package_link.nil?
|
|
312
|
-
pkg_id_uri=[{id: delivid,uri: found_package_link}]
|
|
332
|
+
pkg_id_uri = [{id: delivid,uri: found_package_link}]
|
|
313
333
|
else
|
|
314
334
|
# TODO: delivery id is the right one if package was receive by workgroup
|
|
315
|
-
endpoint=
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
entry_xml=api_v3.call({operation: 'GET',subpath: "#{endpoint}/#{delivid}",headers: {'Accept'=>'application/xml'}})[:http].body
|
|
321
|
-
package_entry=XmlSimple.xml_in(entry_xml, {'ForceArray' => true})
|
|
322
|
-
pkg_id_uri=[{id: delivid,uri: self.class.get_fasp_uri_from_entry(package_entry)}]
|
|
335
|
+
endpoint =
|
|
336
|
+
case options.get_option(:box,is_type: :mandatory)
|
|
337
|
+
when :inbox,:archive then'received'
|
|
338
|
+
when :sent then 'sent'
|
|
339
|
+
end
|
|
340
|
+
entry_xml = api_v3.call({operation: 'GET',subpath: "#{endpoint}/#{delivid}",headers: {'Accept' => 'application/xml'}})[:http].body
|
|
341
|
+
package_entry = XmlSimple.xml_in(entry_xml, {'ForceArray' => true})
|
|
342
|
+
pkg_id_uri = [{id: delivid,uri: self.class.get_fasp_uri_from_entry(package_entry)}]
|
|
323
343
|
end
|
|
324
344
|
when /^faspe:/
|
|
325
|
-
pkg_id_uri=[{id: 'package',uri: link_url}]
|
|
345
|
+
pkg_id_uri = [{id: 'package',uri: link_url}]
|
|
326
346
|
else
|
|
327
|
-
link_data=self.class.get_link_data(link_url)
|
|
347
|
+
link_data = self.class.get_link_data(link_url)
|
|
328
348
|
if !link_data[:subpath].start_with?(PUB_LINK_EXTERNAL_MATCH)
|
|
329
349
|
raise CliBadArgument,"Pub link is #{link_data[:subpath]}. Expecting #{PUB_LINK_EXTERNAL_MATCH}"
|
|
330
350
|
end
|
|
331
351
|
# Note: unauthenticated API (authorization is in url params)
|
|
332
|
-
api_public_link=Rest.new({base_url: link_data[:base_url]})
|
|
333
|
-
pkgdatares=api_public_link.call(
|
|
352
|
+
api_public_link = Rest.new({base_url: link_data[:base_url]})
|
|
353
|
+
pkgdatares = api_public_link.call(
|
|
354
|
+
operation: 'GET',
|
|
355
|
+
subpath: link_data[:subpath],
|
|
356
|
+
url_params: {passcode: link_data[:query]['passcode']},
|
|
357
|
+
headers: {'Accept' => 'application/xml'})
|
|
334
358
|
if !pkgdatares[:http].body.start_with?('<?xml ')
|
|
335
359
|
OpenApplication.instance.uri(link_url)
|
|
336
360
|
raise CliError, 'no such package'
|
|
337
361
|
end
|
|
338
|
-
package_entry=XmlSimple.xml_in(pkgdatares[:http].body, {'ForceArray' => false})
|
|
362
|
+
package_entry = XmlSimple.xml_in(pkgdatares[:http].body, {'ForceArray' => false})
|
|
339
363
|
Log.dump(:package_entry,package_entry)
|
|
340
|
-
transfer_uri=self.class.get_fasp_uri_from_entry(package_entry)
|
|
341
|
-
pkg_id_uri=[{id: package_entry['id'],uri: transfer_uri}]
|
|
364
|
+
transfer_uri = self.class.get_fasp_uri_from_entry(package_entry)
|
|
365
|
+
pkg_id_uri = [{id: package_entry['id'],uri: transfer_uri}]
|
|
342
366
|
end # public link
|
|
367
|
+
# prune packages already downloaded
|
|
368
|
+
# TODO : remove ids from skip not present in inbox to avoid growing too big
|
|
369
|
+
# skip_ids_data.select!{|id|pkg_id_uri.select{|p|p[:id].eql?(id)}}
|
|
370
|
+
pkg_id_uri.reject!{|i|skip_ids_data.include?(i[:id])}
|
|
343
371
|
Log.dump(:pkg_id_uri,pkg_id_uri)
|
|
344
|
-
return Main.result_status('no package') if pkg_id_uri.empty?
|
|
345
|
-
result_transfer=[]
|
|
372
|
+
return Main.result_status('no new package') if pkg_id_uri.empty?
|
|
373
|
+
result_transfer = []
|
|
346
374
|
pkg_id_uri.each do |id_uri|
|
|
347
375
|
if id_uri[:uri].nil?
|
|
348
376
|
# skip package with no link: empty or content deleted
|
|
349
|
-
statuses=[:success]
|
|
377
|
+
statuses = [:success]
|
|
350
378
|
else
|
|
351
|
-
transfer_spec=Fasp::Uri.new(id_uri[:uri]).transfer_spec
|
|
379
|
+
transfer_spec = Fasp::Uri.new(id_uri[:uri]).transfer_spec
|
|
352
380
|
# NOTE: only external users have token in faspe: link !
|
|
353
381
|
if !transfer_spec.has_key?('token')
|
|
354
|
-
sanitized=id_uri[:uri].gsub('&','&')
|
|
355
|
-
xmlpayload
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
382
|
+
sanitized = id_uri[:uri].gsub('&','&')
|
|
383
|
+
xmlpayload =
|
|
384
|
+
%Q(<?xml version="1.0" encoding="UTF-8"?><url-list xmlns="http://schemas.asperasoft.com/xml/url-list"><url href="#{sanitized}"/></url-list>)
|
|
385
|
+
transfer_spec['token'] = api_v3.call({
|
|
386
|
+
operation: 'POST',
|
|
387
|
+
subpath: 'issue-token?direction=down',
|
|
388
|
+
headers: {'Accept' => 'text/plain','Content-Type' => 'application/vnd.aspera.url-list+xml'},
|
|
360
389
|
text_body_params: xmlpayload})[:http].body
|
|
361
390
|
end
|
|
362
|
-
transfer_spec['direction']=Fasp::TransferSpec::DIRECTION_RECEIVE
|
|
363
|
-
statuses=transfer.start(transfer_spec,{src: :node_gen3})
|
|
391
|
+
transfer_spec['direction'] = Fasp::TransferSpec::DIRECTION_RECEIVE
|
|
392
|
+
statuses = transfer.start(transfer_spec,{src: :node_gen3})
|
|
364
393
|
end
|
|
365
|
-
result_transfer.push({'package'=>id_uri[:id],Main::STATUS_FIELD=>statuses})
|
|
394
|
+
result_transfer.push({'package' => id_uri[:id],Main::STATUS_FIELD => statuses})
|
|
366
395
|
# skip only if all sessions completed
|
|
367
396
|
skip_ids_data.push(id_uri[:id]) if TransferAgent.session_status(statuses).eql?(:success)
|
|
368
397
|
end
|
|
369
|
-
skip_ids_persistency
|
|
398
|
+
skip_ids_persistency&.save
|
|
370
399
|
return Main.result_transfer_multiple(result_transfer)
|
|
371
400
|
end
|
|
372
401
|
when :source
|
|
373
|
-
command_source=options.get_next_command([
|
|
374
|
-
source_list=api_v3.call({operation: 'GET',subpath: 'source_shares',headers: {'Accept'=>'application/json'}})[:data]['items']
|
|
402
|
+
command_source = options.get_next_command(%i[list id name])
|
|
403
|
+
source_list = api_v3.call({operation: 'GET',subpath: 'source_shares',headers: {'Accept' => 'application/json'}})[:data]['items']
|
|
375
404
|
case command_source
|
|
376
405
|
when :list
|
|
377
406
|
return {type: :object_list,data: source_list}
|
|
378
407
|
else # :id or :name
|
|
379
|
-
source_match_val=options.get_next_argument('source id or name')
|
|
380
|
-
source_ids=source_list.select { |i| i[command_source.to_s].to_s.eql?(source_match_val) }
|
|
408
|
+
source_match_val = options.get_next_argument('source id or name')
|
|
409
|
+
source_ids = source_list.select { |i| i[command_source.to_s].to_s.eql?(source_match_val) }
|
|
381
410
|
if source_ids.empty?
|
|
382
411
|
raise CliError,"No such Faspex source #{command_source}: #{source_match_val} in [#{source_list.map{|i| i[command_source.to_s]}.join(', ')}]"
|
|
383
412
|
end
|
|
384
413
|
# get id and name
|
|
385
|
-
source_name=source_ids.first['name']
|
|
414
|
+
source_name = source_ids.first['name']
|
|
386
415
|
#source_id=source_ids.first['id']
|
|
387
|
-
source_hash=options.get_option(:storage
|
|
416
|
+
source_hash = options.get_option(:storage,is_type: :mandatory)
|
|
388
417
|
# check value of option
|
|
389
418
|
raise CliError,'storage option must be a Hash' unless source_hash.is_a?(Hash)
|
|
390
419
|
source_hash.each do |name,storage|
|
|
@@ -396,64 +425,69 @@ module Aspera
|
|
|
396
425
|
if !source_hash.has_key?(source_name)
|
|
397
426
|
raise CliError,"No such storage in config file: \"#{source_name}\" in [#{source_hash.keys.join(', ')}]"
|
|
398
427
|
end
|
|
399
|
-
source_info=source_hash[source_name]
|
|
428
|
+
source_info = source_hash[source_name]
|
|
400
429
|
Log.log.debug("source_info: #{source_info}")
|
|
401
|
-
command_node=options.get_next_command([
|
|
430
|
+
command_node = options.get_next_command(%i[info node])
|
|
402
431
|
case command_node
|
|
403
432
|
when :info
|
|
404
433
|
return {data: source_info,type: :single_object}
|
|
405
434
|
when :node
|
|
406
|
-
node_config=ExtendedValue.instance.evaluate(source_info[KEY_NODE])
|
|
435
|
+
node_config = ExtendedValue.instance.evaluate(source_info[KEY_NODE])
|
|
407
436
|
raise CliError,"bad type for: \"#{source_info[KEY_NODE]}\"" unless node_config.is_a?(Hash)
|
|
408
437
|
Log.log.debug("node=#{node_config}")
|
|
409
|
-
api_node=Rest.new({
|
|
410
|
-
base_url:
|
|
411
|
-
auth:
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
command=options.get_next_command(Node::COMMON_ACTIONS)
|
|
438
|
+
api_node = Rest.new({
|
|
439
|
+
base_url: node_config['url'],
|
|
440
|
+
auth: {
|
|
441
|
+
type: :basic,
|
|
442
|
+
username: node_config['username'],
|
|
443
|
+
password: node_config['password']}})
|
|
444
|
+
command = options.get_next_command(Node::COMMON_ACTIONS)
|
|
416
445
|
return Node.new(@agents.merge(skip_basic_auth_options: true, node_api: api_node)).execute_action(command,source_info[KEY_PATH])
|
|
417
446
|
end
|
|
418
447
|
end
|
|
419
448
|
when :me
|
|
420
|
-
my_info=api_v3.call({operation: 'GET',subpath: 'me',headers: {'Accept'=>'application/json'}})[:data]
|
|
449
|
+
my_info = api_v3.call({operation: 'GET',subpath: 'me',headers: {'Accept' => 'application/json'}})[:data]
|
|
421
450
|
return {data: my_info, type: :single_object}
|
|
422
451
|
when :dropbox
|
|
423
|
-
command_pkg=options.get_next_command([:list])
|
|
452
|
+
command_pkg = options.get_next_command([:list])
|
|
424
453
|
case command_pkg
|
|
425
454
|
when :list
|
|
426
|
-
dropbox_list=api_v3.call({operation: 'GET',subpath: 'dropboxes',headers: {'Accept'=>'application/json'}})[:data]
|
|
427
|
-
return {type: :object_list, data: dropbox_list['items'], fields: [
|
|
455
|
+
dropbox_list = api_v3.call({operation: 'GET',subpath: 'dropboxes',headers: {'Accept' => 'application/json'}})[:data]
|
|
456
|
+
return {type: :object_list, data: dropbox_list['items'], fields: %w[name id description can_read can_write]}
|
|
428
457
|
end
|
|
429
458
|
when :v4
|
|
430
|
-
command=options.get_next_command([
|
|
459
|
+
command = options.get_next_command(%i[package dropbox dmembership workgroup wmembership user metadata_profile])
|
|
431
460
|
case command
|
|
432
461
|
when :dropbox
|
|
433
|
-
return entity_action(api_v4,'admin/dropboxes',display_fields: [
|
|
462
|
+
return entity_action(api_v4,'admin/dropboxes',display_fields: %w[id e_wg_name e_wg_desc created_at])
|
|
434
463
|
when :dmembership
|
|
435
464
|
return entity_action(api_v4,'dropbox_memberships')
|
|
436
465
|
when :workgroup
|
|
437
|
-
return entity_action(api_v4,'admin/workgroups',display_fields: [
|
|
466
|
+
return entity_action(api_v4,'admin/workgroups',display_fields: %w[id e_wg_name e_wg_desc created_at])
|
|
438
467
|
when :wmembership
|
|
439
468
|
return entity_action(api_v4,'workgroup_memberships')
|
|
440
469
|
when :user
|
|
441
|
-
return entity_action(api_v4,'users',display_fields: [
|
|
470
|
+
return entity_action(api_v4,'users',display_fields: %w[id name first_name last_name])
|
|
442
471
|
when :metadata_profile
|
|
443
472
|
return entity_action(api_v4,'metadata_profiles')
|
|
444
473
|
when :package
|
|
445
|
-
pkg_box_type=options.get_next_command([:users])
|
|
446
|
-
pkg_box_id=instance_identifier
|
|
474
|
+
pkg_box_type = options.get_next_command([:users])
|
|
475
|
+
pkg_box_id = instance_identifier
|
|
447
476
|
return entity_action(api_v4,"#{pkg_box_type}/#{pkg_box_id}/packages")
|
|
448
477
|
end
|
|
449
478
|
when :address_book
|
|
450
|
-
result=api_v3.call(
|
|
479
|
+
result = api_v3.call(
|
|
480
|
+
operation: 'GET',
|
|
481
|
+
subpath: 'address-book',
|
|
482
|
+
headers: {'Accept' => 'application/json'},
|
|
483
|
+
url_params: {'format' => 'json','count' => 100_000}
|
|
484
|
+
)[:data]
|
|
451
485
|
self.format.display_status("users: #{result['itemsPerPage']}/#{result['totalResults']}, start:#{result['startIndex']}")
|
|
452
|
-
users=result['entry']
|
|
486
|
+
users = result['entry']
|
|
453
487
|
# add missing entries
|
|
454
488
|
users.each do |u|
|
|
455
489
|
unless u['emails'].nil?
|
|
456
|
-
email=u['emails'].find{|i|i['primary'].eql?('true')}
|
|
490
|
+
email = u['emails'].find{|i|i['primary'].eql?('true')}
|
|
457
491
|
u['email'] = email['value'] unless email.nil?
|
|
458
492
|
end
|
|
459
493
|
if u['email'].nil?
|
|
@@ -461,12 +495,12 @@ module Aspera
|
|
|
461
495
|
next
|
|
462
496
|
end
|
|
463
497
|
u['first_name'],u['last_name'] = u['displayName'].split(' ',2)
|
|
464
|
-
u['x']=true
|
|
498
|
+
u['x'] = true
|
|
465
499
|
end
|
|
466
500
|
return {type: :object_list,data: users}
|
|
467
501
|
when :login_methods
|
|
468
|
-
login_meths=api_v3.call({operation: 'GET',subpath: 'login/new',headers: {'Accept'=>'application/xrds+xml'}})[:http].body
|
|
469
|
-
login_methods=XmlSimple.xml_in(login_meths, {'ForceArray' => false})
|
|
502
|
+
login_meths = api_v3.call({operation: 'GET',subpath: 'login/new',headers: {'Accept' => 'application/xrds+xml'}})[:http].body
|
|
503
|
+
login_methods = XmlSimple.xml_in(login_meths, {'ForceArray' => false})
|
|
470
504
|
return {type: :object_list, data: login_methods['XRD']['Service']}
|
|
471
505
|
end # command
|
|
472
506
|
end
|