Module: IDRAC::SystemConfig

Included in:
Client
Defined in:
lib/idrac/system_config.rb

Instance Method Summary collapse

Instance Method Details

#get_system_configuration_profile(target: "RAID") ⇒ Object

Get the system configuration profile for a given target (e.g. “RAID”)

Raises:



59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/idrac/system_config.rb', line 59

def get_system_configuration_profile(target: "RAID")
  debug "Exporting System Configuration..."
  response = authenticated_request(:post, 
    "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ExportSystemConfiguration", 
    body: {"ExportFormat": "JSON", "ShareParameters":{"Target": target}}.to_json,
    headers: {"Content-Type" => "application/json"}
  )
  scp = handle_location(response.headers["location"]) 
  # We experienced this with older iDRACs, so let's give a enriched error to help debug.
  raise(Error, "Failed exporting SCP, no location header found in response. Response: #{response.inspect}") if scp.nil?
  raise(Error, "Failed exporting SCP, taskstate: #{scp["TaskState"]}, taskstatus: #{scp["TaskStatus"]}") unless scp["SystemConfiguration"]
  return scp
end

#handle_location_with_ip_change(location, new_ip, timeout: 300) ⇒ Object

Handle location header for IP change operations. Monitors old IP until it fails, then monitors job completion at new IP with proper task polling.



292
293
294
295
296
297
298
299
300
301
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
332
333
334
335
336
337
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
367
368
369
370
371
372
# File 'lib/idrac/system_config.rb', line 292

def handle_location_with_ip_change(location, new_ip, timeout: 300)
  return nil if location.nil? || location.empty?
  
  # Extract job ID from location header
  job_id = location.split("/").last
  debug "Extracted job ID: #{job_id}", 1, :cyan
  
  old_ip = @host
  start_time = Time.now
  old_ip_failed = false
  task = nil
  tries = 0
  
  debug "Monitoring IP change with job tracking: #{old_ip} → #{new_ip}", 1, :blue
  
  while Time.now - start_time < timeout
    # Try old IP until it fails, then focus on new IP with job monitoring
    [
      old_ip_failed ? nil : [self, old_ip, "Old IP failed"],
      [create_temp_client(new_ip), new_ip, old_ip_failed ? "New IP not ready" : "Cannot reach new IP"]
    ].compact.each do |client, ip, error_prefix|
      
      begin
        client. if ip == new_ip
        
        # Test basic connectivity first
        client.authenticated_request(:get, "/redfish/v1", timeout: 2, open_timeout: 1)
        
        if ip == new_ip
          # Once we can reach the new IP, check the job status
          debug "✅ New IP reachable, checking job status...", 1, :green
          begin
            task_response = client.authenticated_request(:get, "/redfish/v1/TaskService/Tasks/#{job_id}", timeout: 10)
            task = JSON.parse(task_response.body)
            
            debug "Job status: #{task['TaskState']} / #{task['TaskStatus']}", 1, :cyan
            
            if task["TaskState"] == "Completed"
              if task["TaskStatus"] == "OK"
                debug "✅ Job completed successfully!", 1, :green
                @host = new_ip
                return { status: :success, ip: new_ip, job_status: task }
              else
                # Job completed but with error
                msg = task['Messages']&.first&.dig('Message') rescue "N/A"
                attr = task['Messages']&.first&.dig('Oem', 'Dell', 'Name') rescue "N/A"
                error_msg = "Job failed: #{msg} : #{attr}, TaskState: #{task['TaskState']}, TaskStatus: #{task['TaskStatus']}"
                debug "❌ #{error_msg}", 1, :red
                return { status: :error, error: error_msg, job_status: task }
              end
            elsif task["TaskState"] == "Running"
              debug "⏳ Job still running, continuing to wait...", 2, :yellow
              # Continue monitoring
            else
              debug "⚠️  Unexpected job state: #{task['TaskState']}", 1, :yellow
              # Continue monitoring
            end
          rescue => job_error
            debug "Failed to check job status: #{job_error.message}", 2, :yellow
            # Continue monitoring - job might not be ready yet
          end
        else
          # Still on old IP, just test connectivity
          return { status: :success, ip: old_ip }
        end
      rescue => e
        debug "#{error_prefix}: #{e.message}", ip == new_ip ? 2 : 1, ip == new_ip ? :gray : :yellow
        old_ip_failed = true if ip == old_ip
      end
    end
    
    tries += 1
    if tries > 20
      return { status: :timeout, error: "Job monitoring exceeded maximum retries (#{tries})" }
    end
    
    sleep old_ip_failed ? 6 : 5  # Wait longer during IP change
  end
  
  { status: :timeout, error: "IP change timed out after #{timeout}s. Old IP failed: #{old_ip_failed}" }
end

#hash_to_scp(hash) ⇒ Object

Convert an SCP hash back to array format



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/idrac/system_config.rb', line 205

def hash_to_scp(hash)
  hash.inject([]) do |acc, (fqdd, attributes)|
    # Convert hash attributes to Dell SCP format (array of Name/Value/Set On Import objects)
    scp_attributes = case attributes
    when Hash
      attributes.map do |name, value|
        {
          "Name" => name.to_s,
          "Value" => value.to_s,
          "Set On Import" => "True"
        }
      end
    when Array
      attributes # Already in correct format
    else
      []
    end
    
    acc << { "FQDD" => fqdd, "Attributes" => scp_attributes }
    acc
  end
end

#make_scp(fqdd:, components: [], attributes: {}) ⇒ Object

Helper method to create an SCP component with the specified FQDD and attributes



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/idrac/system_config.rb', line 160

def make_scp(fqdd:, components: [], attributes: {})
  com = []
  att = []
  
  # Process components
  components.each do |component|
    com << component
  end
  
  # Process attributes
  attributes.each do |k, v|
    if v.is_a?(Array)
      v.each do |value|
        att << { "Name" => k, "Value" => value, "Set On Import" => "True" }
      end
    elsif v.is_a?(Integer)
      # Convert integers to strings
      att << { "Name" => k, "Value" => v.to_s, "Set On Import" => "True" }
    elsif v.is_a?(Hash)
      # Handle nested components
      v.each do |kk, vv|
        com += make_scp(fqdd: kk, attributes: vv)
      end
    else
      att << { "Name" => k, "Value" => v, "Set On Import" => "True" }
    end
  end
  
  # Build the final component
  bundle = { "FQDD" => fqdd }
  bundle["Components"] = com if com.any?
  bundle["Attributes"] = att if att.any?
  
  return bundle
end

#merge_scp(*scps) ⇒ Object

Merge multiple SCP configurations together Takes multiple arguments - each can be an SCP hash, array of components, or full SCP structure



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/idrac/system_config.rb', line 230

def merge_scp(*scps)
  merged_components = {}
  
  # Get iDRAC version for version-specific handling
  version = begin
    license_version.to_i
  rescue
    9 # Default to iDRAC 9 behavior if version detection fails
  end
  
  scps.compact.each do |scp|
    components = extract_components(scp)
    components.each do |component|
      fqdd = component["FQDD"]
      if merged_components[fqdd]
        # Merge attributes for the same FQDD
        existing_attrs = merged_components[fqdd]["Attributes"] || []
        new_attrs = component["Attributes"] || []
        
        # Build hash of existing attributes by name for easy lookup
        attr_hash = {}
        
        # Handle different attribute structures between iDRAC versions
        existing_attrs.each do |attr|
          case attr
          when Hash
            # iDRAC 8 style: {"Name" => "Users.3#IpmiLanPrivilege", "Value" => "Administrator"}
            attr_hash[attr["Name"]] = attr if attr["Name"]
          when String
            # iDRAC 9 style: strings or different structure - preserve as-is
            # For strings, use the string itself as both key and value
            attr_hash[attr] = attr
          else
            # Unknown structure, preserve as-is with a generated key
            attr_hash["attr_#{attr_hash.size}"] = attr
          end
        end
        
        # Add/overwrite with new attributes
        new_attrs.each do |attr|
          case attr
          when Hash
            attr_hash[attr["Name"]] = attr if attr["Name"]
          when String
            attr_hash[attr] = attr
          else
            attr_hash["attr_#{attr_hash.size}"] = attr
          end
        end
        
        merged_components[fqdd]["Attributes"] = attr_hash.values
      else
        merged_components[fqdd] = component.dup
      end
    end
  end
  
  merged_components.values
end

#normalize_enabled_value(v) ⇒ Object

Helper method to normalize enabled/disabled values

Raises:



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

def normalize_enabled_value(v)
  return "Disabled" if v.nil? || v == false
  return "Enabled"  if v == true
  
  raise Error, "Invalid value for normalize_enabled_value: #{v}" unless v.is_a?(String)
  
  if v.strip.downcase == "enabled"
    return "Enabled"
  else
    return "Disabled"
  end
end

#scp_to_hash(scp) ⇒ Object

Convert an SCP array to a hash for easier manipulation



197
198
199
200
201
202
# File 'lib/idrac/system_config.rb', line 197

def scp_to_hash(scp)
  scp.inject({}) do |acc, component|
    acc[component["FQDD"]] = component["Attributes"]
    acc
  end
end

#set_idrac_ip(new_ip:, new_gw:, new_nm:, vnc_password: "calvin", vnc_port: 5901) ⇒ Object

This assigns the iDRAC IP to be a STATIC IP.



7
8
9
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
# File 'lib/idrac/system_config.rb', line 7

def set_idrac_ip(new_ip:, new_gw:, new_nm:, vnc_password: "calvin", vnc_port: 5901)
  # Cache license version to avoid multiple iDRAC calls
  version = license_version.to_i
  
  case version
  when 8
    ipv4_prefix = "IPv4"
    settings = { "VirtualConsole.1#PluginType" => "HTML5" }
  when 9
    ipv4_prefix = "IPv4Static"
    settings = {}
  else
    raise Error, "Unsupported iDRAC version: #{version}. Supported versions: 8, 9"
  end
  
  # Build base settings for all versions
  settings.merge!({
    "WebServer.1#HostHeaderCheck" => "Disabled",
    "VirtualMedia.1#EncryptEnable" => "Disabled", 
    "VNCServer.1#Enable" => "Enabled",
    "VNCServer.1#Port" => vnc_port.to_s,
    "VNCServer.1#SSLEncryptionBitLength" => "Disabled",
    "VNCServer.1#Password" => vnc_password,
    "IPv4.1#DHCPEnable" => "Disabled", # only applies to iDRAC 8
    "#{ipv4_prefix}.1#Address" => new_ip, # only applies to iDRAC 9
    "#{ipv4_prefix}.1#Gateway" => new_gw,
    "#{ipv4_prefix}.1#Netmask" => new_nm
  })
  
  # Build SCP from scratch instead of getting full profile
  scp_component = make_scp(fqdd: "iDRAC.Embedded.1", attributes: settings)
  scp = { "SystemConfiguration" => { "Components" => [scp_component] } }
  
  # Submit configuration with job availability handling
  res = wait_for_job_availability do
    authenticated_request(:post, 
      "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration",
      body: {"ImportBuffer": scp.to_json, "ShareParameters": {"Target": "iDRAC"}}.to_json,
      headers: {"Content-Type" => "application/json"}
    )
  end
  
  sleep 3  # Allow iDRAC to prepare
  result = handle_location_with_ip_change(res.headers["location"], new_ip)
  
  raise "Failed configuring static IP: #{result[:messages]&.first || result[:error] || "Unknown error"}" if result[:status] != :success
  true
end

#set_system_configuration_profile(scp, target: "ALL", reboot: false, retry_count: 0) ⇒ Object

Apply a system configuration profile to the iDRAC



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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
140
141
142
# File 'lib/idrac/system_config.rb', line 90

def set_system_configuration_profile(scp, target: "ALL", reboot: false, retry_count: 0)
  # Ensure scp has the proper structure with SystemConfiguration wrapper
  scp_to_apply = if scp.is_a?(Hash) && scp["SystemConfiguration"]
    scp
  else
    # Ensure scp is an array of components
    components = scp.is_a?(Array) ? scp : [scp]
    { "SystemConfiguration" => { "Components" => components } }
  end
  
  # Validate the SCP structure before sending
  unless scp_to_apply.is_a?(Hash) && scp_to_apply["SystemConfiguration"] && scp_to_apply["SystemConfiguration"]["Components"]
    raise ArgumentError, "Invalid SCP structure: must contain SystemConfiguration.Components"
  end

  # Create the import parameters
  # Use compact JSON generation to avoid formatting issues with Dell iDRAC
  params = { 
    "ImportBuffer" => JSON.generate(scp_to_apply),
    "ShareParameters" => {"Target" => target},
    "ShutdownType" => "Forced",
    "HostPowerState" => reboot ? "On" : "Off"
  }
  
  debug "Importing System Configuration...", 1, :blue
  debug "Configuration: #{JSON.pretty_generate(scp_to_apply)}", 1, :cyan
  debug "ImportBuffer content: #{params['ImportBuffer']}", 1, :yellow
  
  # Make the API request
  response = authenticated_request(
    :post, 
    "/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration",
    body: params.to_json,
    headers: {"Content-Type" => "application/json"}
  )
  
  # Check for immediate errors
  if response.headers["content-length"].to_i > 0
    debug response.inspect, 1, :red
    error_message = "Failed importing SCP: #{response.body}"
    
    # Check for specific schema validation errors
    if response.body.include?("invalid characters") || response.body.include?("invalid token")
      error_message += "\nThis may be due to JSON formatting issues. The SCP structure might contain characters not accepted by Dell iDRAC."
    elsif response.body.include?("not compliant with configuration schema")
      error_message += "\nThe SCP structure does not match Dell's expected schema format."
    end
    
    return { status: :failed, error: error_message }
  end
  
  return handle_location(response.headers["location"])
end

#usable_scp(scp) ⇒ Object

This puts the SCP into a format that can be used by reasonable Ruby code. It’s a hash of FQDDs to attributes.



146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/idrac/system_config.rb', line 146

def usable_scp(scp)
  # { "FQDD1" => { "Name" => "Value" }, "FQDD2" => { "Name" => "Value" } }
  scp.dig("SystemConfiguration", "Components").inject({}) do |acc, component|
    fqdd = component["FQDD"]
    attributes = component["Attributes"]
    acc[fqdd] = attributes.inject({}) do |attr_acc, attr|
      attr_acc[attr["Name"]] = attr["Value"]
      attr_acc
    end
    acc
  end
end