Class: IDRAC::Client

Inherits:
Object
  • Object
show all
Includes:
Boot, Debuggable, Jobs, License, Lifecycle, Network, Power, Storage, System, SystemConfig, Utility, VirtualMedia
Defined in:
lib/idrac/client.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Network

#get_bmc_network, #get_idrac_version_info, #set_bmc_dhcp, #set_bmc_network

Methods included from Utility

#accept_supportassist_eula, #download_tsr_from_location, #generate_and_download_tsr, #generate_tsr_logs, #parse_error_response, #reset!, #supportassist_eula_status, #tsr_status, #wait_for_job_with_location

Methods included from Debuggable

#debug

Methods included from SystemConfig

#get_system_configuration_profile, #handle_location_with_ip_change, #hash_to_scp, #make_scp, #merge_scp, #normalize_enabled_value, #scp_to_hash, #set_idrac_ip, #set_system_configuration_profile, #usable_scp

Methods included from License

#clear_license_version_cache, #license_info, #license_version

Methods included from Boot

#bios_error_prompt_disabled?, #bios_hdd_placeholder_enabled?, #bios_os_power_control_enabled?, #boot, #boot_config, #boot_options, #boot_raw, #boot_to_bios_setup, #boot_to_cd, #boot_to_disk, #boot_to_pxe, #boot_to_usb, #clear_boot_override, #configure_bios_settings, #create_scp_for_bios, #ensure_uefi_boot, #get_bios_boot_options, #get_boot_devices, #get_idrac_version, #import_system_configuration, #override_boot_source, #scp_boot_mode_uefi, #set_bios, #set_bios_ignore_errors, #set_bios_os_power_control, #set_boot_order, #set_boot_order_hd_first, #set_boot_override, #set_uefi_boot_cd_once_then_hd

Methods included from VirtualMedia

#eject_virtual_media, #insert_virtual_media, #set_one_time_virtual_media_boot, #virtual_media

Methods included from System

#clear_system_event_logs, #cpus, #fans, #get_basic_system_info, #get_system_config, #get_system_summary, #idrac_interface, #idrac_network, #memory, #nics, #nics_to_pci, #pci_devices, #psus, #sel_summary, #system_event_logs, #system_health, #system_info, #total_memory_human

Methods included from Storage

#all_seds?, #controller_encryption_capable?, #controller_encryption_enabled?, #controllers, #create_virtual_disk, #create_virtual_disk_scp, #delete_volume, #disable_local_key_management, #drives, #dump_drive_data, #enable_local_key_management, #fastpath_good?, #find_controller, #sed_ready?, #volumes

Methods included from Lifecycle

#clear_lifecycle!, #clear_system_event_logs!, #ensure_lifecycle_controller!, #get_lifecycle_status, #get_lifecycle_status_from_registry, #get_lifecycle_status_from_scp, #get_lifecycle_status_modern_firmware, #get_system_event_logs, #set_lifecycle_status, #update_status_message

Methods included from Jobs

#clear_jobs!, #force_clear_jobs!, #jobs, #jobs_detail, #tasks, #wait_for_job

Methods included from Power

#get_power_state, #get_power_usage_watts, #power_off, #power_on, #reboot

Constructor Details

#initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, retry_count: 3, retry_delay: 1, host_header: nil) ⇒ Client

Returns a new instance of Client.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/idrac/client.rb', line 28

def initialize(host:, username:, password:, port: 443, use_ssl: true, verify_ssl: false, direct_mode: false, retry_count: 3, retry_delay: 1, host_header: nil)
  @host = host
  @username = username
  @password = password
  @port = port
  @use_ssl = use_ssl
  @verify_ssl = verify_ssl
  @direct_mode = direct_mode
  @host_header = host_header
  @verbosity = 0
  @retry_count = retry_count
  @retry_delay = retry_delay
  
  # Initialize the session and web classes
  @session = Session.new(self)
  @web = Web.new(self)
  
  # Add finalizer to ensure sessions are cleaned up
  ObjectSpace.define_finalizer(self, self.class.finalizer(@session, @web))
end

Instance Attribute Details

#direct_modeObject

Returns the value of attribute direct_mode.



13
14
15
# File 'lib/idrac/client.rb', line 13

def direct_mode
  @direct_mode
end

#hostObject (readonly)

Returns the value of attribute host.



12
13
14
# File 'lib/idrac/client.rb', line 12

def host
  @host
end

#host_headerObject (readonly)

Returns the value of attribute host_header.



12
13
14
# File 'lib/idrac/client.rb', line 12

def host_header
  @host_header
end

#passwordObject (readonly)

Returns the value of attribute password.



12
13
14
# File 'lib/idrac/client.rb', line 12

def password
  @password
end

#portObject (readonly)

Returns the value of attribute port.



12
13
14
# File 'lib/idrac/client.rb', line 12

def port
  @port
end

#retry_countObject

Returns the value of attribute retry_count.



13
14
15
# File 'lib/idrac/client.rb', line 13

def retry_count
  @retry_count
end

#retry_delayObject

Returns the value of attribute retry_delay.



13
14
15
# File 'lib/idrac/client.rb', line 13

def retry_delay
  @retry_delay
end

#sessionObject (readonly)

Returns the value of attribute session.



12
13
14
# File 'lib/idrac/client.rb', line 12

def session
  @session
end

#use_sslObject (readonly)

Returns the value of attribute use_ssl.



12
13
14
# File 'lib/idrac/client.rb', line 12

def use_ssl
  @use_ssl
end

#usernameObject (readonly)

Returns the value of attribute username.



12
13
14
# File 'lib/idrac/client.rb', line 12

def username
  @username
end

#verbosityObject

Returns the value of attribute verbosity.



13
14
15
# File 'lib/idrac/client.rb', line 13

def verbosity
  @verbosity
end

#verify_sslObject (readonly)

Returns the value of attribute verify_ssl.



12
13
14
# File 'lib/idrac/client.rb', line 12

def verify_ssl
  @verify_ssl
end

#webObject (readonly)

Returns the value of attribute web.



12
13
14
# File 'lib/idrac/client.rb', line 12

def web
  @web
end

Class Method Details

.connect(host:, username:, password:, **options) ⇒ Object

Primary interface - block-based API that ensures session cleanup



62
63
64
65
66
67
68
69
70
71
72
# File 'lib/idrac/client.rb', line 62

def self.connect(host:, username:, password:, **options)
  client = new(host: host, username: username, password: password, **options)
  return client unless block_given?
  
  begin
    client.
    yield client
  ensure
    client.logout
  end
end

.finalizer(session, web) ⇒ Object

Finalizer to clean up sessions when object is garbage collected



50
51
52
53
54
55
56
57
58
59
# File 'lib/idrac/client.rb', line 50

def self.finalizer(session, web)
  proc do
    begin
      session.delete if session.x_auth_token
      web.logout if web.session_id
    rescue
      # Ignore errors during cleanup
    end
  end
end

Instance Method Details

#authenticated_request(method, path, body: nil, headers: {}, timeout: nil, open_timeout: nil, **options, &block) ⇒ Object

Send an authenticated request to the iDRAC

Returns the full HTTParty::Response object by default, which allows access to:

  • response.status (HTTP status code)

  • response.body (response body as string)

  • response.headers (response headers)

Automatically handles retry for 503 ServiceTemporarilyUnavailable errors. For error status codes (4xx, 5xx), handle_response is called to raise appropriate errors.

You can provide a block for custom response handling:

authenticated_request(:post, path) { |response| custom_logic(response) }


128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/idrac/client.rb', line 128

def authenticated_request(method, path, body: nil, headers: {}, timeout: nil, open_timeout: nil, **options, &block)
  # Automatically set Content-Type for JSON requests if not already set
  if body && body.is_a?(String) && !headers.key?('Content-Type') && !headers.key?(:content_type)
    headers = headers.merge('Content-Type' => 'application/json')
  end

  # Build options hash with all parameters
  request_options = {
    body: body,
    headers: headers,
    timeout: timeout,
    open_timeout: open_timeout
  }.merge(options).compact

  with_retries do
    response = _perform_authenticated_request(method, path, request_options)

    # If a block is provided, use it for custom response handling
    if block_given?
      yield response
    else
      # Call handle_response only for error status codes to enable retry logic
      # This allows 503 errors to be caught and retried by with_retries
      handle_response(response) if response.status >= 400
      response  # Return full response object for backward compatibility
    end
  end
end

#base_urlObject



338
339
340
341
# File 'lib/idrac/client.rb', line 338

def base_url
  protocol = use_ssl ? 'https' : 'http'
  "#{protocol}://#{host}:#{port}"
end

#connectionObject



74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/idrac/client.rb', line 74

def connection
  @connection ||= Faraday.new(url: base_url, ssl: { verify: verify_ssl }) do |faraday|
    faraday.request :multipart
    faraday.request :url_encoded
    faraday.adapter Faraday.default_adapter
    # Add request/response logging based on verbosity
    if @verbosity > 0
      faraday.response :logger, Logger.new(STDOUT), bodies: @verbosity >= 2 do |logger|
        logger.filter(/(Authorization: Basic )([^,\n]+)/, '\1[FILTERED]')
        logger.filter(/(Password"=>"?)([^,"]+)/, '\1[FILTERED]')
      end
    end
  end
end

#get(path:, headers: {}) ⇒ Object



157
158
159
160
161
# File 'lib/idrac/client.rb', line 157

def get(path:, headers: {})
  with_retries do
    _perform_get(path: path, headers: headers)
  end
end

#get_firmware_versionObject



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/idrac/client.rb', line 353

def get_firmware_version
  response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1?$select=FirmwareVersion")
  
  if response.status == 200
    begin
      data = JSON.parse(response.body)
      return data["FirmwareVersion"]
    rescue JSON::ParserError
      raise Error, "Failed to parse firmware version response: #{response.body}"
    end
  else
    # Try again without the $select parameter for older firmware
    response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1")
    
    if response.status == 200
      begin
        data = JSON.parse(response.body)
        return data["FirmwareVersion"]
      rescue JSON::ParserError
        raise Error, "Failed to parse firmware version response: #{response.body}"
      end
    else
      raise Error, "Failed to get firmware version. Status code: #{response.status}"
    end
  end
end

#handle_location(location) ⇒ Object

Handle location header and determine whether to use wait_for_job or wait_for_task



544
545
546
547
548
549
550
551
552
553
554
555
556
557
# File 'lib/idrac/client.rb', line 544

def handle_location(location)
  return nil if location.nil? || location.empty?
  
  # Extract the ID from the location
  id = location.split("/").last
  
  # Determine if it's a task or job based on the URL pattern
  if location.include?("/TaskService/Tasks/")
    wait_for_task(id)
  else
    # Assuming it's a job
    wait_for_job(id)
  end
end

#handle_response(response) ⇒ Object



482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# File 'lib/idrac/client.rb', line 482

def handle_response(response)
  # First see if there is a location header
  if response.headers["location"]
    return handle_location(response.headers["location"])
  end

  # If there is no location header, check the status code
  if response.status.between?(200, 299)
    return response.body
  else
    # Enhanced error handling with ExtendedInfo support
    error_message = "Failed with status #{response.status}"
    
    begin
      error_data = JSON.parse(response.body)
      
      # Check for standard error message
      if error_data['error'] && error_data['error']['message']
        error_message += ": #{error_data['error']['message']}"
      end
      
      # Check for ExtendedInfo which contains detailed error information
      if error_data['error'] && error_data['error']['@Message.ExtendedInfo']
        extended_info = error_data['error']['@Message.ExtendedInfo']
        if extended_info.is_a?(Array) && extended_info.any?
          error_message += "\nExtendedInfo:"
          retry_delay = nil
          extended_info.each_with_index do |info, index|
            error_message += "\n  #{index + 1}. #{info['Message']}" if info['Message']
            error_message += " (#{info['MessageId']})" if info['MessageId']
            error_message += " - Resolution: #{info['Resolution']}" if info['Resolution']
            
            # Check for ServiceTemporarilyUnavailable with retry delay
            if info['MessageId'] == 'Base.1.12.ServiceTemporarilyUnavailable'
              # Extract retry delay from MessageArgs (usually first argument)
              if info['MessageArgs'] && info['MessageArgs'].any?
                retry_delay = info['MessageArgs'].first.to_i
                debug "🕒 iDRAC ServiceTemporarilyUnavailable detected - will wait #{retry_delay} seconds as requested", 1, :yellow
              end
            end
          end
          
          # If we detected a ServiceTemporarilyUnavailable error, raise a special exception
          if retry_delay
            raise ServiceTemporarilyUnavailableError.new(error_message, retry_delay)
          end
        end
      end
      
      # Also add the full response body for debugging
      debug "Full error response: #{response.body}", 1, :red if @verbosity && @verbosity > 0
      
    rescue JSON::ParserError => e
      error_message += " - Raw response: #{response.body}"
      debug "Failed to parse JSON error response: #{e.message}", 1, :yellow if @verbosity && @verbosity > 0
    end
    
    raise Error, error_message
  end
end

#loginObject

Login to iDRAC



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/idrac/client.rb', line 90

def 
  # If we're in direct mode, skip login attempts
  if @direct_mode
    debug "Using direct mode (Basic Auth) for all requests", 1, :light_yellow
    return true
  end
  
  # Try to create a Redfish session
  if session.create
    debug "Successfully logged in to iDRAC using Redfish session", 1, :green
    return true
  else
    debug "Failed to create Redfish session, falling back to direct mode", 1, :light_yellow
    @direct_mode = true
    return true
  end
end

#logoutObject

Logout from iDRAC



109
110
111
112
113
114
# File 'lib/idrac/client.rb', line 109

def logout
  session.delete if session.x_auth_token
  web.logout if web.session_id
  debug "Logged out from iDRAC", 1, :green
  return true
end

#redfish_versionObject



343
344
345
346
347
348
349
350
351
# File 'lib/idrac/client.rb', line 343

def redfish_version
  response = authenticated_request(:get, "/redfish/v1")
  if response.status == 200
    data = JSON.parse(response.body)
    data["RedfishVersion"]
  else
    raise Error, "Failed to get Redfish version: #{response.status} - #{response.body}"
  end
end

#screenshotObject



334
335
336
# File 'lib/idrac/client.rb', line 334

def screenshot
  web.capture_screenshot
end

#wait_for_task(task_id) ⇒ Object

Wait for a task to complete



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'lib/idrac/client.rb', line 420

def wait_for_task(task_id)
  task = nil
  
  begin
    loop do
      task_response = authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{task_id}")
      
      case task_response.status
        # 200-299
      when 200..299
        task = JSON.parse(task_response.body)

        if task["TaskState"] != "Running"
          break
        end
        
        # Extract percentage complete if available
        percent_complete = nil
        if task["Oem"] && task["Oem"]["Dell"] && task["Oem"]["Dell"]["PercentComplete"]
          percent_complete = task["Oem"]["Dell"]["PercentComplete"]
          debug "Task progress: #{percent_complete}% complete", 1
        end
        
        debug "Waiting for task to complete...: #{task["TaskState"]} #{task["TaskStatus"]}", 1
        sleep 5
      else
        return { 
          status: :failed, 
          error: "Failed to check task status: #{task_response.status} - #{task_response.body}" 
        }
      end
    end
    
    # Check final task state
    if task["TaskState"] == "Completed" && task["TaskStatus"] == "OK"
      return { status: :success }
    elsif task["SystemConfiguration"] # SystemConfigurationProfile requests yield a 202 with a SystemConfiguration key
      return task
    else
      # For debugging purposes
      debug task.inspect, 1, :yellow
      
      # Extract any messages from the response
      messages = []
      if task["Messages"] && task["Messages"].is_a?(Array)
        messages = task["Messages"].map { |m| m["Message"] }.compact
      end
      
      return { 
        status: :failed, 
        task_state: task["TaskState"], 
        task_status: task["TaskStatus"],
        messages: messages,
        error: messages.first || "Task failed with state: #{task["TaskState"]}"
      }
    end
  rescue => e
    debugger
    return { status: :error, error: "Exception monitoring task: #{e.message}" }
  end
end

#with_retries(max_retries = nil, initial_delay = nil, error_classes = nil) { ... } ⇒ Object

Execute a block with automatic retries

Parameters:

  • max_retries (Integer) (defaults to: nil)

    Maximum number of retry attempts

  • initial_delay (Integer) (defaults to: nil)

    Initial delay in seconds between retries (increases exponentially)

  • error_classes (Array) (defaults to: nil)

    Array of error classes to catch and retry

Yields:

  • The block to execute with retries

Returns:

  • (Object)

    The result of the block



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/idrac/client.rb', line 386

def with_retries(max_retries = nil, initial_delay = nil, error_classes = nil)
  # Use instance variables if not specified
  max_retries ||= @retry_count
  initial_delay ||= @retry_delay
  error_classes ||= [StandardError]
  
  retries = 0
  begin
    yield
  rescue ServiceTemporarilyUnavailableError => e
    retries += 1
    if retries <= max_retries
      delay = e.retry_delay  # Use the delay specified by iDRAC
      debug "🕒 IDRAC REQUESTED RETRY: ServiceTemporarilyUnavailable - Attempt #{retries}/#{max_retries}, waiting #{delay}s as instructed by iDRAC", 1, :cyan
      sleep delay
      retry
    else
      debug "MAX RETRIES REACHED: #{e.message} after #{max_retries} attempts", 1, :red
      raise e
    end
  rescue *error_classes => e
    retries += 1
    if retries <= max_retries
      delay = initial_delay * (retries ** 1.5).to_i  # Exponential backoff
      debug "RETRY: #{e.message} - Attempt #{retries}/#{max_retries}, waiting #{delay}s", 1, :yellow
      sleep delay
      retry
    else
      debug "MAX RETRIES REACHED: #{e.message} after #{max_retries} attempts", 1, :red
      raise e
    end
  end
end