opennebula-cli 7.2.0 → 7.2.1
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
- data/bin/onevm +1 -1
- data/lib/command_parser.rb +134 -125
- data/lib/load_opennebula_paths.rb +5 -0
- data/lib/ods_helper.rb +541 -0
- data/lib/one_helper/oneacct_helper.rb +116 -108
- data/lib/one_helper/onedatastore_helper.rb +86 -79
- data/lib/one_helper/onemarket_helper.rb +58 -57
- data/lib/one_helper/onequota_helper.rb +239 -189
- data/lib/one_helper/onesecgroup_helper.rb +86 -84
- data/lib/one_helper/onetemplate_helper.rb +63 -63
- data/lib/one_helper/onevdc_helper.rb +44 -44
- data/lib/one_helper/onevm_helper.rb +5 -7
- data/lib/one_helper/onevmgroup_helper.rb +64 -62
- data/lib/one_helper/onevntemplate_helper.rb +42 -39
- data/lib/one_helper/onevrouter_helper.rb +85 -86
- data/lib/one_helper/onezone_helper.rb +97 -100
- data/lib/one_helper.rb +88 -69
- data/share/schemas/xsd/acct.xsd +3 -104
- data/share/schemas/xsd/cluster.xsd +4 -21
- data/share/schemas/xsd/datastore.xsd +4 -29
- data/share/schemas/xsd/document.xsd +3 -25
- data/share/schemas/xsd/group.xsd +2 -14
- data/share/schemas/xsd/group_pool.xsd +2 -14
- data/share/schemas/xsd/hook.xsd +2 -0
- data/share/schemas/xsd/host.xsd +5 -7
- data/share/schemas/xsd/image.xsd +2 -25
- data/share/schemas/xsd/marketplace.xsd +3 -22
- data/share/schemas/xsd/marketplaceapp.xsd +3 -25
- data/share/schemas/xsd/opennebula_configuration.xsd +1 -0
- data/share/schemas/xsd/requirements.xsd +3 -21
- data/share/schemas/xsd/security_group.xsd +6 -43
- data/share/schemas/xsd/shared.xsd +3 -3
- data/share/schemas/xsd/vdc.xsd +2 -7
- data/share/schemas/xsd/vm_group.xsd +3 -25
- data/share/schemas/xsd/vm_pool.xsd +2 -0
- data/share/schemas/xsd/vmtemplate.xsd +3 -25
- data/share/schemas/xsd/vnet.xsd +9 -67
- data/share/schemas/xsd/vnet_pool.xsd +8 -57
- data/share/schemas/xsd/vntemplate.xsd +3 -25
- data/share/schemas/xsd/vrouter.xsd +4 -32
- metadata +6 -4
data/lib/ods_helper.rb
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# -------------------------------------------------------------------------- #
|
|
2
|
+
# Copyright 2002-2026, OpenNebula Project, OpenNebula Systems #
|
|
3
|
+
# #
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
|
5
|
+
# not use this file except in compliance with the License. You may obtain #
|
|
6
|
+
# a copy of the License at #
|
|
7
|
+
# #
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0 #
|
|
9
|
+
# #
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software #
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, #
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
|
13
|
+
# See the License for the specific language governing permissions and #
|
|
14
|
+
# limitations under the License. #
|
|
15
|
+
#--------------------------------------------------------------------------- #
|
|
16
|
+
require 'tempfile'
|
|
17
|
+
require 'json'
|
|
18
|
+
require 'yaml'
|
|
19
|
+
|
|
20
|
+
require 'one_helper'
|
|
21
|
+
require 'cloud/CloudClient'
|
|
22
|
+
|
|
23
|
+
# Generic CLI helper for ODS-based services
|
|
24
|
+
class ODSHelper < OpenNebulaHelper::OneHelper
|
|
25
|
+
|
|
26
|
+
# Configuration file name used by the helper
|
|
27
|
+
def self.conf_file
|
|
28
|
+
raise NotImplementedError, "#{name}.conf_file must be implemented"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.template_tag
|
|
32
|
+
raise NotImplementedError, "#{name}.template_tag must be implemented"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# ODS client class used by the helper
|
|
36
|
+
def self.client_class(options = {})
|
|
37
|
+
raise NotImplementedError, "#{name}.client_class must be implemented"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def client(options = {})
|
|
41
|
+
self.class.client_class.new(
|
|
42
|
+
:username => options[:username],
|
|
43
|
+
:password => options[:password],
|
|
44
|
+
:endpoint => options[:endpoint] || options[:server],
|
|
45
|
+
:opts => {
|
|
46
|
+
:version => options[:api_version],
|
|
47
|
+
:user_agent => USER_AGENT
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#------------------------------------------------------
|
|
53
|
+
# Operations wrappers
|
|
54
|
+
#------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
# Generic list flow
|
|
57
|
+
# @param client [ODSClient]
|
|
58
|
+
# @param list_method [Symbol]
|
|
59
|
+
# @param options [Hash]
|
|
60
|
+
# @yield [response] Custom renderer for normal output mode
|
|
61
|
+
# @return [Integer, Array]
|
|
62
|
+
def list_resources(client, list_method, options = {})
|
|
63
|
+
response = client.public_send(list_method, options)
|
|
64
|
+
render_response(response, options) {|data| yield(data) if block_given? }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Generic show flow
|
|
68
|
+
# @param client [ODSClient]
|
|
69
|
+
# @param resource_id [Integer, String]
|
|
70
|
+
# @param get_method [Symbol]
|
|
71
|
+
# @param options [Hash]
|
|
72
|
+
# @yield [response] Custom renderer for normal output mode
|
|
73
|
+
# @return [Integer, Array]
|
|
74
|
+
def show_resource(client, get_method, resource_id, options = {})
|
|
75
|
+
response = client.public_send(get_method, resource_id, options)
|
|
76
|
+
render_response(response, options) {|data| yield(data) if block_given? }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Generic continuous loop for top-like views.
|
|
80
|
+
# @param delay [Integer, Float, nil]
|
|
81
|
+
# @yield Body to execute in each refresh
|
|
82
|
+
# @return [Integer]
|
|
83
|
+
def top_resources(delay = nil)
|
|
84
|
+
delay ||= 5
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
loop do
|
|
88
|
+
CLIHelper.scr_cls
|
|
89
|
+
CLIHelper.scr_move(0, 0)
|
|
90
|
+
|
|
91
|
+
yield
|
|
92
|
+
|
|
93
|
+
sleep delay
|
|
94
|
+
end
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
STDERR.puts e.message
|
|
97
|
+
exit(-1)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
0
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Generic update flow for JSON resources.
|
|
104
|
+
# If no file is provided, current resource body is fetched, dumped into a
|
|
105
|
+
# tempfile, opened in the editor, and then sent back using update_method.
|
|
106
|
+
# @param client [ODSClient]
|
|
107
|
+
# @param resource_id [Integer, String]
|
|
108
|
+
# @param get_method [Symbol]
|
|
109
|
+
# @param update_method [Symbol]
|
|
110
|
+
# @param file_path [String, nil]
|
|
111
|
+
# @return [Integer, Array]
|
|
112
|
+
def update_resource_from_editor(client, resource_id, get_method, update_method, file_path)
|
|
113
|
+
path =
|
|
114
|
+
if file_path
|
|
115
|
+
file_path
|
|
116
|
+
else
|
|
117
|
+
response = client.public_send(get_method, resource_id)
|
|
118
|
+
|
|
119
|
+
if CloudClient.is_error?(response)
|
|
120
|
+
return [response[:err_code], response[:message]]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
body = response.dig(:TEMPLATE, self.class.template_tag)
|
|
124
|
+
prefix = self.class.client_class.name.split('::').first.downcase
|
|
125
|
+
|
|
126
|
+
self.class.open_json_editor(
|
|
127
|
+
"#{prefix}_#{resource_id}_tmp",
|
|
128
|
+
body
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
response = client.public_send(update_method, resource_id, File.read(path))
|
|
133
|
+
|
|
134
|
+
if CloudClient.is_error?(response)
|
|
135
|
+
[response[:err_code], response[:message]]
|
|
136
|
+
else
|
|
137
|
+
0
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
#------------------------------------------------------
|
|
142
|
+
# Inputs
|
|
143
|
+
#------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def ask_required_value(label)
|
|
146
|
+
loop do
|
|
147
|
+
prompt = "> #{label}: "
|
|
148
|
+
print prompt
|
|
149
|
+
|
|
150
|
+
value = STDIN.readline.strip
|
|
151
|
+
return value unless value.to_s.empty?
|
|
152
|
+
|
|
153
|
+
puts ' A value is required.'
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def ask_required_integer(label)
|
|
158
|
+
loop do
|
|
159
|
+
prompt = "> #{label}: "
|
|
160
|
+
print prompt
|
|
161
|
+
|
|
162
|
+
raw = STDIN.readline.strip
|
|
163
|
+
return raw.to_i if raw.match?(/\A-?\d+\z/)
|
|
164
|
+
|
|
165
|
+
puts " #{label} must be an integer."
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def select_by_index(items)
|
|
170
|
+
loop do
|
|
171
|
+
print ' Select an option by number: '
|
|
172
|
+
input = STDIN.readline.strip
|
|
173
|
+
|
|
174
|
+
if input =~ /\A\d+\z/
|
|
175
|
+
index = input.to_i
|
|
176
|
+
return items[index] if index >= 0 && index < items.size
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
puts ' Invalid selection, please try again.'
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Ask user values for a list of user inputs.
|
|
184
|
+
# @param user_inputs [Array<Hash>, nil]
|
|
185
|
+
# @return [Hash, nil]
|
|
186
|
+
def get_user_values(user_inputs)
|
|
187
|
+
return if user_inputs.nil? || user_inputs.empty?
|
|
188
|
+
|
|
189
|
+
ask_user_inputs(user_inputs)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Prompt interactively for input values
|
|
193
|
+
# @param inputs [Array<Hash>]
|
|
194
|
+
# @return [Hash]
|
|
195
|
+
def ask_user_inputs(inputs)
|
|
196
|
+
puts 'There are some parameters that require user input.'
|
|
197
|
+
|
|
198
|
+
answers = {}
|
|
199
|
+
|
|
200
|
+
inputs.each do |input|
|
|
201
|
+
name = input[:name]
|
|
202
|
+
description = input[:description] || ''
|
|
203
|
+
type = normalize_input_type(input[:type])
|
|
204
|
+
default = input[:default]
|
|
205
|
+
match = input[:match]
|
|
206
|
+
|
|
207
|
+
puts " * (#{name}) #{description} [type: #{input[:type]}]"
|
|
208
|
+
|
|
209
|
+
header = ' '
|
|
210
|
+
header += "Press enter for default (#{default}). " if default
|
|
211
|
+
|
|
212
|
+
answer = case type
|
|
213
|
+
when 'string'
|
|
214
|
+
ask_string_input(header, default, match)
|
|
215
|
+
when 'number'
|
|
216
|
+
ask_number_input(header, default, match)
|
|
217
|
+
when 'list'
|
|
218
|
+
ask_list_input(header, default, match)
|
|
219
|
+
when 'map'
|
|
220
|
+
ask_map_input(header, default)
|
|
221
|
+
else
|
|
222
|
+
STDERR.puts "Unknown input type '#{input[:type]}' for '#{name}'"
|
|
223
|
+
exit(-1)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
answers[name] = answer
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
answers
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Read and parse JSON input from a file or STDIN.
|
|
233
|
+
#
|
|
234
|
+
# If a file path is provided, the content is read from that file.
|
|
235
|
+
# Otherwise, STDIN is used. If no input is available, nil is returned.
|
|
236
|
+
#
|
|
237
|
+
# @param file [String, nil] path to the JSON input file
|
|
238
|
+
# @return [Hash, Array, nil]
|
|
239
|
+
def self.read_json_input(file = nil)
|
|
240
|
+
content = nil
|
|
241
|
+
|
|
242
|
+
if file
|
|
243
|
+
begin
|
|
244
|
+
content = File.read(file)
|
|
245
|
+
rescue Errno::ENOENT
|
|
246
|
+
STDERR.puts "File not found: #{file}"
|
|
247
|
+
exit(-1)
|
|
248
|
+
end
|
|
249
|
+
else
|
|
250
|
+
stdin = OpenNebulaHelper.read_stdin
|
|
251
|
+
content = stdin unless stdin.empty?
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
return if content.nil? || content.strip.empty?
|
|
255
|
+
|
|
256
|
+
JSON.parse(content, :symbolize_names => true)
|
|
257
|
+
rescue JSON::ParserError => e
|
|
258
|
+
STDERR.puts "Invalid JSON - #{e.message}"
|
|
259
|
+
exit(-1)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Open the editor with JSON content and return the edited file path.
|
|
263
|
+
# @param prefix [String]
|
|
264
|
+
# @param content [Object]
|
|
265
|
+
# @return [String]
|
|
266
|
+
def self.open_json_editor(prefix, content)
|
|
267
|
+
tmp = Tempfile.new(prefix)
|
|
268
|
+
path = tmp.path
|
|
269
|
+
|
|
270
|
+
tmp.write(JSON.pretty_generate(content))
|
|
271
|
+
tmp.flush
|
|
272
|
+
|
|
273
|
+
editor_path = ENV['EDITOR'] || OpenNebulaHelper::EDITOR_PATH
|
|
274
|
+
system("#{editor_path} #{path}")
|
|
275
|
+
|
|
276
|
+
unless $CHILD_STATUS.exitstatus.zero?
|
|
277
|
+
STDERR.puts 'Editor not defined'
|
|
278
|
+
exit(-1)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
tmp.close
|
|
282
|
+
|
|
283
|
+
path
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def self.update_from_editor(client, resource_id, get_method)
|
|
287
|
+
response = client.public_send(get_method, resource_id)
|
|
288
|
+
return [response[:err_code], response[:message]] if CloudClient.is_error?(response)
|
|
289
|
+
|
|
290
|
+
body = response.dig(:TEMPLATE, template_tag)
|
|
291
|
+
prefix = client_class.name.split('::').first.downcase
|
|
292
|
+
path = open_json_editor("#{prefix}_#{resource_id}_tmp", body)
|
|
293
|
+
|
|
294
|
+
read_json_input(path)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Prompt for string input
|
|
298
|
+
# @param header [String]
|
|
299
|
+
# @param default [Object]
|
|
300
|
+
# @param match [Hash, nil]
|
|
301
|
+
# @return [String]
|
|
302
|
+
def ask_string_input(header, default, match)
|
|
303
|
+
if match&.dig(:type) == 'list'
|
|
304
|
+
options = match[:values] || []
|
|
305
|
+
|
|
306
|
+
options.each_with_index {|opt, i| puts " #{i}: #{opt}" }
|
|
307
|
+
puts
|
|
308
|
+
|
|
309
|
+
loop do
|
|
310
|
+
print "#{header}Please type the selection number: "
|
|
311
|
+
raw = STDIN.readline.strip
|
|
312
|
+
|
|
313
|
+
if raw.empty?
|
|
314
|
+
answer = default
|
|
315
|
+
return answer if options.include?(answer)
|
|
316
|
+
else
|
|
317
|
+
index = Integer(raw, :exception => false)
|
|
318
|
+
answer = options[index] if index && index >= 0
|
|
319
|
+
return answer if answer
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
puts ' Invalid selection, please try again.'
|
|
323
|
+
end
|
|
324
|
+
else
|
|
325
|
+
print header
|
|
326
|
+
answer = STDIN.readline.strip
|
|
327
|
+
answer = OpenNebulaHelper.editor_input if answer == '<<EDITOR>>'
|
|
328
|
+
answer = default if answer.empty?
|
|
329
|
+
answer
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Prompt for numeric input
|
|
334
|
+
# @param header [String]
|
|
335
|
+
# @param default [Object]
|
|
336
|
+
# @param match [Hash, nil]
|
|
337
|
+
# @return [Integer, Float]
|
|
338
|
+
def ask_number_input(header, default, match)
|
|
339
|
+
min = match&.dig(:values, :min)
|
|
340
|
+
max = match&.dig(:values, :max)
|
|
341
|
+
|
|
342
|
+
begin
|
|
343
|
+
range_msg = min && max ? " (#{min} to #{max})" : ''
|
|
344
|
+
print "#{header}Enter a number#{range_msg}: "
|
|
345
|
+
|
|
346
|
+
raw = STDIN.readline.strip
|
|
347
|
+
raw = default.to_s if raw.empty?
|
|
348
|
+
|
|
349
|
+
if raw.match?(/\A-?\d+\z/)
|
|
350
|
+
answer = raw.to_i
|
|
351
|
+
elsif raw.match?(/\A-?\d+\.\d+\z/)
|
|
352
|
+
answer = raw.to_f
|
|
353
|
+
else
|
|
354
|
+
puts 'Not a valid number'
|
|
355
|
+
raise ArgumentError
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
raise ArgumentError if min && answer < min
|
|
359
|
+
raise ArgumentError if max && answer > max
|
|
360
|
+
|
|
361
|
+
answer
|
|
362
|
+
rescue StandardError
|
|
363
|
+
puts ' Invalid number, please try again.'
|
|
364
|
+
retry
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Prompt for list input
|
|
369
|
+
# @param header [String]
|
|
370
|
+
# @param default [Object]
|
|
371
|
+
# @param match [Hash, nil]
|
|
372
|
+
# @return [Array]
|
|
373
|
+
def ask_list_input(header, default, match)
|
|
374
|
+
loop do
|
|
375
|
+
print "#{header}Enter comma-separated values: "
|
|
376
|
+
raw = STDIN.readline.strip
|
|
377
|
+
|
|
378
|
+
if raw.empty?
|
|
379
|
+
if default.is_a?(Array)
|
|
380
|
+
return default
|
|
381
|
+
else
|
|
382
|
+
puts ' No default available.'
|
|
383
|
+
next
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
answer = raw.split(',').map(&:strip).reject(&:empty?)
|
|
388
|
+
|
|
389
|
+
if match&.dig(:type) == 'list'
|
|
390
|
+
invalid = answer - Array(match[:values])
|
|
391
|
+
|
|
392
|
+
if invalid.any?
|
|
393
|
+
puts " Invalid values: #{invalid.join(', ')}"
|
|
394
|
+
puts " Allowed: #{Array(match[:values]).join(', ')}"
|
|
395
|
+
next
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
return answer
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Prompt for map input
|
|
404
|
+
# @param header [String]
|
|
405
|
+
# @param default [Object]
|
|
406
|
+
# @return [Hash]
|
|
407
|
+
def ask_map_input(header, default)
|
|
408
|
+
loop do
|
|
409
|
+
print "#{header}Enter KEY=VALUE pairs separated by commas: "
|
|
410
|
+
raw = STDIN.readline.strip
|
|
411
|
+
|
|
412
|
+
if raw.empty?
|
|
413
|
+
if default.is_a?(Hash)
|
|
414
|
+
return default
|
|
415
|
+
else
|
|
416
|
+
puts ' No default available.'
|
|
417
|
+
next
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
begin
|
|
422
|
+
answer = {}
|
|
423
|
+
|
|
424
|
+
raw.split(',').each do |pair|
|
|
425
|
+
key, value = pair.split('=', 2)
|
|
426
|
+
|
|
427
|
+
raise ArgumentError if key.nil? || value.nil?
|
|
428
|
+
raise ArgumentError if key.strip.empty? || value.strip.empty?
|
|
429
|
+
|
|
430
|
+
answer[key.strip] = value.strip
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
return answer
|
|
434
|
+
rescue StandardError
|
|
435
|
+
puts ' Invalid map format. Expected KEY=VALUE,...'
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Normalize typed user input definitions
|
|
441
|
+
# @param type [String]
|
|
442
|
+
# @return [String]
|
|
443
|
+
def normalize_input_type(type)
|
|
444
|
+
case type
|
|
445
|
+
when /\Amap\(/
|
|
446
|
+
'map'
|
|
447
|
+
when /\Alist\(/
|
|
448
|
+
'list'
|
|
449
|
+
else
|
|
450
|
+
type
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
#------------------------------------------------------
|
|
455
|
+
# Utilities
|
|
456
|
+
#------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
# Parse a JSON string or KEY=VALUE string to a hash
|
|
459
|
+
def self.parse_values_option(raw)
|
|
460
|
+
value = raw.to_s.strip
|
|
461
|
+
return [0, {}] if value.empty?
|
|
462
|
+
|
|
463
|
+
begin
|
|
464
|
+
if value.start_with?('{')
|
|
465
|
+
parsed = JSON.parse(value)
|
|
466
|
+
return [0, parsed.transform_keys(&:to_s)] if parsed.is_a?(Hash)
|
|
467
|
+
end
|
|
468
|
+
rescue JSON::ParserError
|
|
469
|
+
nil
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
parsed = {}
|
|
473
|
+
|
|
474
|
+
begin
|
|
475
|
+
value.split(',').each do |pair|
|
|
476
|
+
key, item = pair.split('=', 2)
|
|
477
|
+
|
|
478
|
+
raise ArgumentError if key.nil? || item.nil?
|
|
479
|
+
|
|
480
|
+
key = key.strip
|
|
481
|
+
item = item.strip
|
|
482
|
+
|
|
483
|
+
raise ArgumentError if key.empty?
|
|
484
|
+
|
|
485
|
+
parsed[key] = item
|
|
486
|
+
end
|
|
487
|
+
rescue ArgumentError
|
|
488
|
+
return [-1, 'Invalid --values format. Use KEY=VALUE[,KEY=VALUE...] or a JSON object']
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
[0, parsed]
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Checks whether a helper hook returned a CLI error tuple.
|
|
495
|
+
# @param value [Object]
|
|
496
|
+
# @return [Boolean]
|
|
497
|
+
def is_error?(value)
|
|
498
|
+
value.is_a?(Array) && value.size == 2 && value[0].is_a?(Integer)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Render a response in JSON, YAML or custom formatted output
|
|
502
|
+
# @param response [Object]
|
|
503
|
+
# @param options [Hash]
|
|
504
|
+
# @yield [response] Custom rendering block for table/plain output
|
|
505
|
+
# @return [Integer, Array]
|
|
506
|
+
def render_response(response, options = {})
|
|
507
|
+
if CloudClient.is_error?(response)
|
|
508
|
+
[response[:err_code], response[:message]]
|
|
509
|
+
elsif options[:json]
|
|
510
|
+
[0, JSON.pretty_generate(response)]
|
|
511
|
+
elsif options[:yaml]
|
|
512
|
+
[0, response.to_yaml(:indent => 4)]
|
|
513
|
+
else
|
|
514
|
+
yield(response) if block_given?
|
|
515
|
+
0
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def format_template(template, indent = 6)
|
|
520
|
+
return 'N/A' unless template
|
|
521
|
+
|
|
522
|
+
template.map do |k, v|
|
|
523
|
+
value =
|
|
524
|
+
if v.is_a?(Hash)
|
|
525
|
+
v.map {|k2, v2| ' ' * indent + "#{k}: #{k2}=#{v2}" }
|
|
526
|
+
elsif v.is_a?(Array)
|
|
527
|
+
v.map do |elem|
|
|
528
|
+
if elem.is_a?(Hash)
|
|
529
|
+
elem.map {|k2, v2| ' ' * indent + "#{k}: #{k2}=#{v2}" }
|
|
530
|
+
else
|
|
531
|
+
' ' * indent + "#{k}: #{elem}"
|
|
532
|
+
end
|
|
533
|
+
end.flatten
|
|
534
|
+
else
|
|
535
|
+
' ' * indent + "#{k}: #{v}"
|
|
536
|
+
end
|
|
537
|
+
value.is_a?(Array) ? value.join("\n") : value
|
|
538
|
+
end.join("\n")
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
end
|