Class: ODSHelper

Inherits:
OpenNebulaHelper::OneHelper show all
Defined in:
lib/ods_helper.rb

Overview

Generic CLI helper for ODS-based services

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from OpenNebulaHelper::OneHelper

#backup_mode_valid?, #check_orphan, client, #create_resource, #filterflag_to_i, filterflag_to_i_desc, get_client, get_password, #group_name, #initialize, list_layout_help, #list_pool, #list_pool_format, #list_pool_table, #list_pool_top, #list_pool_xml, #list_to_id, list_to_id_desc, name_to_id, #perform_action, #perform_actions, #print_page, #retrieve_resource, #set_client, set_endpoint, set_password, set_user, #start_pager, #stop_pager, table_conf, template_input_help, #to_id, to_id_desc, #user_name

Constructor Details

This class inherits a constructor from OpenNebulaHelper::OneHelper

Class Method Details

.client_class(options = {}) ⇒ Object

ODS client class used by the helper

Raises:

  • (NotImplementedError)


36
37
38
# File 'lib/ods_helper.rb', line 36

def self.client_class(options = {})
    raise NotImplementedError, "#{name}.client_class must be implemented"
end

.conf_fileObject

Configuration file name used by the helper

Raises:

  • (NotImplementedError)


27
28
29
# File 'lib/ods_helper.rb', line 27

def self.conf_file
    raise NotImplementedError, "#{name}.conf_file must be implemented"
end

.open_json_editor(prefix, content) ⇒ String

Open the editor with JSON content and return the edited file path.

Parameters:

  • prefix (String)
  • content (Object)

Returns:



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/ods_helper.rb', line 266

def self.open_json_editor(prefix, content)
    tmp  = Tempfile.new(prefix)
    path = tmp.path

    tmp.write(JSON.pretty_generate(content))
    tmp.flush

    editor_path = ENV['EDITOR'] || OpenNebulaHelper::EDITOR_PATH
    system("#{editor_path} #{path}")

    unless $CHILD_STATUS.exitstatus.zero?
        STDERR.puts 'Editor not defined'
        exit(-1)
    end

    tmp.close

    path
end

.parse_values_option(raw) ⇒ Object

Parse a JSON string or KEY=VALUE string to a hash



459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# File 'lib/ods_helper.rb', line 459

def self.parse_values_option(raw)
    value = raw.to_s.strip
    return [0, {}] if value.empty?

    begin
        if value.start_with?('{')
            parsed = JSON.parse(value)
            return [0, parsed.transform_keys(&:to_s)] if parsed.is_a?(Hash)
        end
    rescue JSON::ParserError
        nil
    end

    parsed = {}

    begin
        value.split(',').each do |pair|
            key, item = pair.split('=', 2)

            raise ArgumentError if key.nil? || item.nil?

            key  = key.strip
            item = item.strip

            raise ArgumentError if key.empty?

            parsed[key] = item
        end
    rescue ArgumentError
        return [-1, 'Invalid --values format. Use KEY=VALUE[,KEY=VALUE...] or a JSON object']
    end

    [0, parsed]
end

.read_json_input(file = nil) ⇒ Hash, ...

Read and parse JSON input from a file or STDIN.

If a file path is provided, the content is read from that file. Otherwise, STDIN is used. If no input is available, nil is returned.

Parameters:

  • file (String, nil) (defaults to: nil)

    path to the JSON input file

Returns:

  • (Hash, Array, nil)


239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/ods_helper.rb', line 239

def self.read_json_input(file = nil)
    content = nil

    if file
        begin
            content = File.read(file)
        rescue Errno::ENOENT
            STDERR.puts "File not found: #{file}"
            exit(-1)
        end
    else
        stdin = OpenNebulaHelper.read_stdin
        content = stdin unless stdin.empty?
    end

    return if content.nil? || content.strip.empty?

    JSON.parse(content, :symbolize_names => true)
rescue JSON::ParserError => e
    STDERR.puts "Invalid JSON - #{e.message}"
    exit(-1)
end

.template_tagObject

Raises:

  • (NotImplementedError)


31
32
33
# File 'lib/ods_helper.rb', line 31

def self.template_tag
    raise NotImplementedError, "#{name}.template_tag must be implemented"
end

.update_from_editor(client, resource_id, get_method) ⇒ Object



286
287
288
289
290
291
292
293
294
295
# File 'lib/ods_helper.rb', line 286

def self.update_from_editor(client, resource_id, get_method)
    response = client.public_send(get_method, resource_id)
    return [response[:err_code], response[:message]] if CloudClient.is_error?(response)

    body   = response.dig(:TEMPLATE, template_tag)
    prefix = client_class.name.split('::').first.downcase
    path   = open_json_editor("#{prefix}_#{resource_id}_tmp", body)

    read_json_input(path)
end

Instance Method Details

#ask_list_input(header, default, match) ⇒ Array

Prompt for list input

Parameters:

  • header (String)
  • default (Object)
  • match (Hash, nil)

Returns:

  • (Array)


373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/ods_helper.rb', line 373

def ask_list_input(header, default, match)
    loop do
        print "#{header}Enter comma-separated values: "
        raw = STDIN.readline.strip

        if raw.empty?
            if default.is_a?(Array)
                return default
            else
                puts '    No default available.'
                next
            end
        end

        answer = raw.split(',').map(&:strip).reject(&:empty?)

        if match&.dig(:type) == 'list'
            invalid = answer - Array(match[:values])

            if invalid.any?
                puts "    Invalid values: #{invalid.join(', ')}"
                puts "    Allowed: #{Array(match[:values]).join(', ')}"
                next
            end
        end

        return answer
    end
end

#ask_map_input(header, default) ⇒ Hash

Prompt for map input

Parameters:

  • header (String)
  • default (Object)

Returns:

  • (Hash)


407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/ods_helper.rb', line 407

def ask_map_input(header, default)
    loop do
        print "#{header}Enter KEY=VALUE pairs separated by commas: "
        raw = STDIN.readline.strip

        if raw.empty?
            if default.is_a?(Hash)
                return default
            else
                puts '    No default available.'
                next
            end
        end

        begin
            answer = {}

            raw.split(',').each do |pair|
                key, value = pair.split('=', 2)

                raise ArgumentError if key.nil? || value.nil?
                raise ArgumentError if key.strip.empty? || value.strip.empty?

                answer[key.strip] = value.strip
            end

            return answer
        rescue StandardError
            puts '    Invalid map format. Expected KEY=VALUE,...'
        end
    end
end

#ask_number_input(header, default, match) ⇒ Integer, Float

Prompt for numeric input

Parameters:

  • header (String)
  • default (Object)
  • match (Hash, nil)

Returns:

  • (Integer, Float)


338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/ods_helper.rb', line 338

def ask_number_input(header, default, match)
    min = match&.dig(:values, :min)
    max = match&.dig(:values, :max)

    begin
        range_msg = min && max ? " (#{min} to #{max})" : ''
        print "#{header}Enter a number#{range_msg}: "

        raw = STDIN.readline.strip
        raw = default.to_s if raw.empty?

        if raw.match?(/\A-?\d+\z/)
            answer = raw.to_i
        elsif raw.match?(/\A-?\d+\.\d+\z/)
            answer = raw.to_f
        else
            puts 'Not a valid number'
            raise ArgumentError
        end

        raise ArgumentError if min && answer < min
        raise ArgumentError if max && answer > max

        answer
    rescue StandardError
        puts '    Invalid number, please try again.'
        retry
    end
end

#ask_required_integer(label) ⇒ Object



157
158
159
160
161
162
163
164
165
166
167
# File 'lib/ods_helper.rb', line 157

def ask_required_integer(label)
    loop do
        prompt = "> #{label}: "
        print prompt

        raw = STDIN.readline.strip
        return raw.to_i if raw.match?(/\A-?\d+\z/)

        puts "    #{label} must be an integer."
    end
end

#ask_required_value(label) ⇒ Object


Inputs




145
146
147
148
149
150
151
152
153
154
155
# File 'lib/ods_helper.rb', line 145

def ask_required_value(label)
    loop do
        prompt = "> #{label}: "
        print prompt

        value = STDIN.readline.strip
        return value unless value.to_s.empty?

        puts '    A value is required.'
    end
end

#ask_string_input(header, default, match) ⇒ String

Prompt for string input

Parameters:

  • header (String)
  • default (Object)
  • match (Hash, nil)

Returns:



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/ods_helper.rb', line 302

def ask_string_input(header, default, match)
    if match&.dig(:type) == 'list'
        options = match[:values] || []

        options.each_with_index {|opt, i| puts "    #{i}: #{opt}" }
        puts

        loop do
            print "#{header}Please type the selection number: "
            raw = STDIN.readline.strip

            if raw.empty?
                answer = default
                return answer if options.include?(answer)
            else
                index  = Integer(raw, :exception => false)
                answer = options[index] if index && index >= 0
                return answer if answer
            end

            puts '    Invalid selection, please try again.'
        end
    else
        print header
        answer = STDIN.readline.strip
        answer = OpenNebulaHelper.editor_input if answer == '<<EDITOR>>'
        answer = default if answer.empty?
        answer
    end
end

#ask_user_inputs(inputs) ⇒ Hash

Prompt interactively for input values

Parameters:

  • inputs (Array<Hash>)

Returns:

  • (Hash)


195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/ods_helper.rb', line 195

def ask_user_inputs(inputs)
    puts 'There are some parameters that require user input.'

    answers = {}

    inputs.each do |input|
        name        = input[:name]
        description = input[:description] || ''
        type        = normalize_input_type(input[:type])
        default     = input[:default]
        match       = input[:match]

        puts "  * (#{name}) #{description} [type: #{input[:type]}]"

        header = '    '
        header += "Press enter for default (#{default}). " if default

        answer = case type
                 when 'string'
                     ask_string_input(header, default, match)
                 when 'number'
                     ask_number_input(header, default, match)
                 when 'list'
                     ask_list_input(header, default, match)
                 when 'map'
                     ask_map_input(header, default)
                 else
                     STDERR.puts "Unknown input type '#{input[:type]}' for '#{name}'"
                     exit(-1)
                 end

        answers[name] = answer
    end

    answers
end

#client(options = {}) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
# File 'lib/ods_helper.rb', line 40

def client(options = {})
    self.class.client_class.new(
        :username => options[:username],
        :password => options[:password],
        :endpoint => options[:endpoint] || options[:server],
        :opts     => {
            :version    => options[:api_version],
            :user_agent => USER_AGENT
        }
    )
end

#format_template(template, indent = 6) ⇒ Object



519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/ods_helper.rb', line 519

def format_template(template, indent = 6)
    return 'N/A' unless template

    template.map do |k, v|
        value =
            if v.is_a?(Hash)
                v.map {|k2, v2| ' ' * indent + "#{k}: #{k2}=#{v2}" }
            elsif v.is_a?(Array)
                v.map do |elem|
                    if elem.is_a?(Hash)
                        elem.map {|k2, v2| ' ' * indent + "#{k}: #{k2}=#{v2}" }
                    else
                        ' ' * indent + "#{k}: #{elem}"
                    end
                end.flatten
            else
                ' ' * indent + "#{k}: #{v}"
            end
        value.is_a?(Array) ? value.join("\n") : value
    end.join("\n")
end

#get_user_values(user_inputs) ⇒ Hash?

Ask user values for a list of user inputs.

Parameters:

  • user_inputs (Array<Hash>, nil)

Returns:

  • (Hash, nil)


186
187
188
189
190
# File 'lib/ods_helper.rb', line 186

def get_user_values(user_inputs)
    return if user_inputs.nil? || user_inputs.empty?

    ask_user_inputs(user_inputs)
end

#is_error?(value) ⇒ Boolean

Checks whether a helper hook returned a CLI error tuple.

Parameters:

  • value (Object)

Returns:

  • (Boolean)


497
498
499
# File 'lib/ods_helper.rb', line 497

def is_error?(value)
    value.is_a?(Array) && value.size == 2 && value[0].is_a?(Integer)
end

#list_resources(client, list_method, options = {}) {|response| ... } ⇒ Integer, Array

Generic list flow

Parameters:

  • client (ODSClient)
  • list_method (Symbol)
  • options (Hash) (defaults to: {})

Yields:

  • (response)

    Custom renderer for normal output mode

Returns:

  • (Integer, Array)


62
63
64
65
# File 'lib/ods_helper.rb', line 62

def list_resources(client, list_method, options = {})
    response = client.public_send(list_method, options)
    render_response(response, options) {|data| yield(data) if block_given? }
end

#normalize_input_type(type) ⇒ String

Normalize typed user input definitions

Parameters:

Returns:



443
444
445
446
447
448
449
450
451
452
# File 'lib/ods_helper.rb', line 443

def normalize_input_type(type)
    case type
    when /\Amap\(/
        'map'
    when /\Alist\(/
        'list'
    else
        type
    end
end

#render_response(response, options = {}) {|response| ... } ⇒ Integer, Array

Render a response in JSON, YAML or custom formatted output

Parameters:

  • response (Object)
  • options (Hash) (defaults to: {})

Yields:

  • (response)

    Custom rendering block for table/plain output

Returns:

  • (Integer, Array)


506
507
508
509
510
511
512
513
514
515
516
517
# File 'lib/ods_helper.rb', line 506

def render_response(response, options = {})
    if CloudClient.is_error?(response)
        [response[:err_code], response[:message]]
    elsif options[:json]
        [0, JSON.pretty_generate(response)]
    elsif options[:yaml]
        [0, response.to_yaml(:indent => 4)]
    else
        yield(response) if block_given?
        0
    end
end

#select_by_index(items) ⇒ Object



169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/ods_helper.rb', line 169

def select_by_index(items)
    loop do
        print '    Select an option by number: '
        input = STDIN.readline.strip

        if input =~ /\A\d+\z/
            index = input.to_i
            return items[index] if index >= 0 && index < items.size
        end

        puts '    Invalid selection, please try again.'
    end
end

#show_resource(client, get_method, resource_id, options = {}) {|response| ... } ⇒ Integer, Array

Generic show flow

Parameters:

  • client (ODSClient)
  • resource_id (Integer, String)
  • get_method (Symbol)
  • options (Hash) (defaults to: {})

Yields:

  • (response)

    Custom renderer for normal output mode

Returns:

  • (Integer, Array)


74
75
76
77
# File 'lib/ods_helper.rb', line 74

def show_resource(client, get_method, resource_id, options = {})
    response = client.public_send(get_method, resource_id, options)
    render_response(response, options) {|data| yield(data) if block_given? }
end

#top_resources(delay = nil) { ... } ⇒ Integer

Generic continuous loop for top-like views.

Parameters:

  • delay (Integer, Float, nil) (defaults to: nil)

Yields:

  • Body to execute in each refresh

Returns:

  • (Integer)


83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/ods_helper.rb', line 83

def top_resources(delay = nil)
    delay ||= 5

    begin
        loop do
            CLIHelper.scr_cls
            CLIHelper.scr_move(0, 0)

            yield

            sleep delay
        end
    rescue StandardError => e
        STDERR.puts e.message
        exit(-1)
    end

    0
end

#update_resource_from_editor(client, resource_id, get_method, update_method, file_path) ⇒ Integer, Array

Generic update flow for JSON resources. If no file is provided, current resource body is fetched, dumped into a tempfile, opened in the editor, and then sent back using update_method.

Parameters:

  • client (ODSClient)
  • resource_id (Integer, String)
  • get_method (Symbol)
  • update_method (Symbol)
  • file_path (String, nil)

Returns:

  • (Integer, Array)


112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/ods_helper.rb', line 112

def update_resource_from_editor(client, resource_id, get_method, update_method, file_path)
    path =
        if file_path
            file_path
        else
            response = client.public_send(get_method, resource_id)

            if CloudClient.is_error?(response)
                return [response[:err_code], response[:message]]
            end

            body   = response.dig(:TEMPLATE, self.class.template_tag)
            prefix = self.class.client_class.name.split('::').first.downcase

            self.class.open_json_editor(
                "#{prefix}_#{resource_id}_tmp",
                body
            )
        end

    response = client.public_send(update_method, resource_id, File.read(path))

    if CloudClient.is_error?(response)
        [response[:err_code], response[:message]]
    else
        0
    end
end