Class: Radfish::Client

Inherits:
Object
  • Object
show all
Includes:
Debuggable
Defined in:
lib/radfish/client.rb

Constant Summary collapse

BOOT_PROGRESS_ORDER =

Canonical Redfish BootProgress order (normalized to snake_case), earliest to latest. Used to decide when a host has reached OR passed a requested state.

%i[
  none
  primary_processor_initialization_started
  bus_initialization_started
  memory_initialization_started
  secondary_processor_initialization_started
  pci_resource_config_started
  system_hardware_initialization_complete
  setup_entered
  os_boot_started
  os_running
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Debuggable

#debug

Constructor Details

#initialize(host:, username:, password:, vendor: nil, **options) ⇒ Client

Returns a new instance of Client.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/radfish/client.rb', line 10

def initialize(host:, username:, password:, vendor: nil, **options)
  @verbosity = options[:verbosity] || 0
  
  # Auto-detect vendor if not specified
  if vendor.nil?
    detector = VendorDetector.new(
      host: host,
      username: username,
      password: password,
      port: options[:port] || 443,
      use_ssl: options.fetch(:use_ssl, true),
      verify_ssl: options.fetch(:verify_ssl, false),
      host_header: options[:host_header]
    )
    detector.verbosity = @verbosity
    @vendor = detector.detect
    
    if @vendor.nil?
      raise UnsupportedVendorError, "Could not detect vendor for #{host}:#{options[:port] || 443}. Please check: 1) The host is reachable, 2) Credentials are correct (#{username}), 3) The BMC supports Redfish API"
    end
    
    debug "Auto-detected vendor: #{@vendor}", 1, :green
  else
    @vendor = vendor.to_s.downcase
    debug "Using specified vendor: #{@vendor}", 1, :cyan
  end
  
  # Get the adapter class for this vendor
  adapter_class = Radfish.get_adapter(@vendor)
  
  if adapter_class.nil?
    # Try to load the adapter gem dynamically
    begin
      require "radfish/#{@vendor}_adapter"
      adapter_class = Radfish.get_adapter(@vendor)
    rescue LoadError
      # Adapter gem not installed
    end
  end
  
  if adapter_class.nil?
    raise UnsupportedVendorError, "No adapter available for vendor: #{@vendor}. " \
      "Please install the radfish-#{@vendor} gem or use a supported vendor."
  end
  
  # Create the adapter instance
  @adapter = adapter_class.new(
    host: host,
    username: username,
    password: password,
    **options
  )
  
  # Pass verbosity to adapter
  @adapter.verbosity = @verbosity if @adapter.respond_to?(:verbosity=)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, **kwargs, &block) ⇒ Object

Delegate all method calls to the adapter



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/radfish/client.rb', line 83

def method_missing(method, *args, **kwargs, &block)
  if @adapter.respond_to?(method)
    if kwargs.empty?
      @adapter.send(method, *args, &block)
    else
      @adapter.send(method, *args, **kwargs, &block)
    end
  else
    super
  end
end

Instance Attribute Details

#adapterObject (readonly)

Returns the value of attribute adapter.



7
8
9
# File 'lib/radfish/client.rb', line 7

def adapter
  @adapter
end

#vendorObject (readonly)

Returns the value of attribute vendor.



7
8
9
# File 'lib/radfish/client.rb', line 7

def vendor
  @vendor
end

#verbosityObject

Returns the value of attribute verbosity.



8
9
10
# File 'lib/radfish/client.rb', line 8

def verbosity
  @verbosity
end

Class Method Details

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



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/radfish/client.rb', line 67

def self.connect(host:, username:, password:, vendor: nil, **options)
  client = new(host: host, username: username, password: password, vendor: vendor, **options)
  
  if block_given?
    begin
      client.
      yield client
    ensure
      client.logout
    end
  else
    client
  end
end

Instance Method Details

#adapter_classObject



113
114
115
# File 'lib/radfish/client.rb', line 113

def adapter_class
  @adapter.class
end

#bmcObject



123
124
125
# File 'lib/radfish/client.rb', line 123

def bmc
  @bmc ||= BmcInfo.new(self)
end

#bootObject

Boot facade: client.boot.stale_uefi_entries / client.boot.disable_entries(match:).



132
133
134
# File 'lib/radfish/client.rb', line 132

def boot
  @boot ||= BootInfo.new(self)
end

#boot_progressObject

Normalized last BootProgress state (a snake_case symbol), or nil when the BMC omits BootProgress entirely (iDRAC8). nil means "cannot observe", never "not running".



159
160
161
162
163
# File 'lib/radfish/client.rb', line 159

def boot_progress
  return nil unless @adapter.respond_to?(:boot_progress)

  @adapter.boot_progress
end

#controllersObject

Storage convenience API Return normalized Controller objects while keeping adapter APIs intact.



233
234
235
236
# File 'lib/radfish/client.rb', line 233

def controllers
  raw = @adapter.storage_controllers
  Array(raw).map { |c| build_controller(c) }
end

#create_virtual_disk(**options) ⇒ Object



269
270
271
272
273
274
275
# File 'lib/radfish/client.rb', line 269

def create_virtual_disk(**options)
  if @adapter.respond_to?(:create_virtual_disk)
    @adapter.create_virtual_disk(**options)
  else
    raise NotImplementedError, "Virtual disk creation not supported by #{@vendor} adapter"
  end
end

#delete_volume(volume_odata_id) ⇒ Object



277
278
279
280
281
282
283
# File 'lib/radfish/client.rb', line 277

def delete_volume(volume_odata_id)
  if @adapter.respond_to?(:delete_volume)
    @adapter.delete_volume(volume_odata_id)
  else
    raise NotImplementedError, "Volume deletion not supported by #{@vendor} adapter"
  end
end

#disable_local_key_management(controller_id:) ⇒ Object



261
262
263
264
265
266
267
# File 'lib/radfish/client.rb', line 261

def disable_local_key_management(controller_id:)
  if @adapter.respond_to?(:disable_local_key_management)
    @adapter.disable_local_key_management(controller_id: controller_id)
  else
    raise NotImplementedError, "Local key management not supported by #{@vendor} adapter"
  end
end

#drives(controller) ⇒ Object

Public storage API (not backward-compatible): require a Controller object.

Raises:

  • (ArgumentError)


239
240
241
242
# File 'lib/radfish/client.rb', line 239

def drives(controller)
  raise ArgumentError, "Controller required" unless controller.is_a?(Controller)
  @adapter.drives(controller)
end

#enable_local_key_management(controller_id:, passphrase:, key_id:) ⇒ Object

Storage management methods - pass through to adapter These are Dell-specific for now but will be generalized when other vendors add support



253
254
255
256
257
258
259
# File 'lib/radfish/client.rb', line 253

def enable_local_key_management(controller_id:, passphrase:, key_id:)
  if @adapter.respond_to?(:enable_local_key_management)
    @adapter.enable_local_key_management(controller_id: controller_id, passphrase: passphrase, key_id: key_id)
  else
    raise NotImplementedError, "Local key management not supported by #{@vendor} adapter"
  end
end

#infoObject



221
222
223
224
225
226
227
228
229
# File 'lib/radfish/client.rb', line 221

def info
  {
    vendor: @vendor,
    adapter: adapter_class.name,
    features: supported_features,
    host: @adapter.host,
    base_url: @adapter.base_url
  }
end

#loginObject

Core methods that should always be available



101
102
103
# File 'lib/radfish/client.rb', line 101

def 
  @adapter.
end

#logoutObject



105
106
107
# File 'lib/radfish/client.rb', line 105

def logout
  @adapter.logout
end

#pciObject



195
196
197
# File 'lib/radfish/client.rb', line 195

def pci
  @pci ||= PciInfo.new(self)
end

#powerObject



127
128
129
# File 'lib/radfish/client.rb', line 127

def power
  @power ||= PowerInfo.new(self)
end

#power_stateObject

Live BMC power state (e.g. "On"/"Off"), read straight from the adapter. Replaces callers reaching through the adapter to the vendor client's own power-state call.



138
139
140
# File 'lib/radfish/client.rb', line 138

def power_state
  @adapter.power_status
end

#respond_to_missing?(method, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


95
96
97
# File 'lib/radfish/client.rb', line 95

def respond_to_missing?(method, include_private = false)
  @adapter.respond_to?(method, include_private) || super
end

#service_tagObject



199
200
201
202
# File 'lib/radfish/client.rb', line 199

def service_tag
  # Get service_tag from system info
  system.service_tag
end

#supported_featuresObject



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/radfish/client.rb', line 204

def supported_features
  # Return a list of features this adapter supports
  features = []
  
  # Check which modules are included
  features << :power if @adapter.respond_to?(:power_status)
  features << :system if @adapter.respond_to?(:system_info)
  features << :storage if @adapter.respond_to?(:storage_controllers)
  features << :virtual_media if @adapter.respond_to?(:virtual_media)
  features << :boot if @adapter.respond_to?(:boot_options)
  features << :jobs if @adapter.respond_to?(:jobs)
  features << :utility if @adapter.respond_to?(:sel_log)
  features << :network if @adapter.respond_to?(:get_bmc_network) || @adapter.respond_to?(:set_bmc_network)
  
  features
end

#systemObject

Lazy-loading API methods that return structured data



119
120
121
# File 'lib/radfish/client.rb', line 119

def system
  @system ||= SystemInfo.new(self)
end

#thermalObject



191
192
193
# File 'lib/radfish/client.rb', line 191

def thermal
  @thermal ||= ThermalInfo.new(self)
end

#vendor_nameObject



109
110
111
# File 'lib/radfish/client.rb', line 109

def vendor_name
  @vendor
end

#volumes(controller) ⇒ Object

Raises:

  • (ArgumentError)


244
245
246
247
248
# File 'lib/radfish/client.rb', line 244

def volumes(controller)
  raise ArgumentError, "Controller required" unless controller.is_a?(Controller)
  raw = @adapter.volumes(controller)
  Array(raw).map { |v| build_volume(v, controller) }
end

#wait_for_boot_progress(target, timeout: nil, poll: 20) ⇒ Object

Wait until the host reaches (or passes) target BootProgress, bounded by timeout or, when not given, the adapter's per-model POST ceiling. Returns the observed state on success, or nil when the BMC does not report BootProgress (nothing to wait on -- the caller uses other signals). Raises Radfish::BootProgressTimeout on a stall.



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/radfish/client.rb', line 169

def wait_for_boot_progress(target, timeout: nil, poll: 20)
  target = target.to_sym
  ceiling = timeout || (@adapter.respond_to?(:boot_progress_ceiling) ? @adapter.boot_progress_ceiling(target) : 900)
  deadline = Time.now + ceiling
  target_idx = BOOT_PROGRESS_ORDER.index(target)

  loop do
    state = boot_progress
    return nil if state.nil? # BMC omits BootProgress (iDRAC8): unobservable, not a failure
    return state if state == target

    state_idx = BOOT_PROGRESS_ORDER.index(state)
    return state if target_idx && state_idx && state_idx >= target_idx

    if Time.now > deadline
      raise BootProgressTimeout,
            "BootProgress did not reach #{target.inspect} within #{ceiling}s (last: #{state.inspect})"
    end
    sleep poll
  end
end