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:



161
162
163
164
165
166
167
168
169
170
171
# File 'lib/idrac/system_config.rb', line 161

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"])
  raise(Error, "Failed exporting SCP, taskstate: #{scp["TaskState"]}, taskstatus: #{scp["TaskStatus"]}") unless scp["SystemConfiguration"]
  return scp
end

#handle_location(location) ⇒ Object

Handle location header and determine whether to use wait_for_job or wait_for_task



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

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

#hash_to_scp(hash) ⇒ Object

Convert an SCP hash back to array format



338
339
340
341
342
343
# File 'lib/idrac/system_config.rb', line 338

def hash_to_scp(hash)
  hash.inject([]) do |acc, (fqdd, attributes)|
    acc << { "FQDD" => fqdd, "Attributes" => attributes }
    acc
  end
end

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

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



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

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(scp1, scp2) ⇒ Object

Merge two SCPs together



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/idrac/system_config.rb', line 346

def merge_scp(scp1, scp2)
  return scp1 || scp2 unless scp1 && scp2 # Return the one that's not nil if either is nil
  
  # Make them both arrays in case they aren't
  scp1_array = scp1.is_a?(Array) ? scp1 : [scp1]
  scp2_array = scp2.is_a?(Array) ? scp2 : [scp2]
  
  # Convert to hashes for merging
  hash1 = scp_to_hash(scp1_array)
  hash2 = scp_to_hash(scp2_array)
  
  # Perform deep merge
  merged = deep_merge(hash1, hash2)
  
  # Convert back to SCP array format
  hash_to_scp(merged)
end

#normalize_enabled_value(v) ⇒ Object

Helper method to normalize enabled/disabled values

Raises:



225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/idrac/system_config.rb', line 225

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



330
331
332
333
334
335
# File 'lib/idrac/system_config.rb', line 330

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") ⇒ 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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/idrac/system_config.rb', line 7

def set_idrac_ip(new_ip:, new_gw:, new_nm:, vnc_password: "calvin")
  scp = get_system_configuration_profile(target: "iDRAC")
  pp scp
  ## We want to access the iDRAC web server even when IPs don't match (and they won't when we port forward local host):
  set_scp_attribute(scp, "WebServer.1#HostHeaderCheck", "Disabled")
  ## We want VirtualMedia to be enabled so we can mount ISOs: set_scp_attribute(scp, "VirtualMedia.1#Enable", "Enabled")
  set_scp_attribute(scp, "VirtualMedia.1#EncryptEnable", "Disabled")
  ## We want to access VNC Server on 5901 for screenshots and without SSL:
  set_scp_attribute(scp, "VNCServer.1#Enable", "Enabled")
  set_scp_attribute(scp, "VNCServer.1#Port", "5901")
  set_scp_attribute(scp, "VNCServer.1#SSLEncryptionBitLength", "Disabled")
  # And password calvin
  set_scp_attribute(scp, "VNCServer.1#Password", vnc_password)
  # Disable DHCP on management NIC
  set_scp_attribute(scp, "IPv4.1#DHCPEnable", "Disabled")
  if drac_license_version.to_i == 8
    # We want to use HTML for the virtual console
    set_scp_attribute(scp, "VirtualConsole.1#PluginType", "HTML5")
    # We want static IP for the iDRAC
    set_scp_attribute(scp, "IPv4.1#Address", new_ip)
    set_scp_attribute(scp, "IPv4.1#Gateway", new_gw)
    set_scp_attribute(scp, "IPv4.1#Netmask", new_nm)
  elsif drac_license_version.to_i == 9
    # We want static IP for the iDRAC
    set_scp_attribute(scp, "IPv4Static.1#Address", new_ip)
    set_scp_attribute(scp, "IPv4Static.1#Gateway", new_gw)
    set_scp_attribute(scp, "IPv4Static.1#Netmask", new_nm)
    # {"Name"=>"SerialCapture.1#Enable", "Value"=>"Disabled", "Set On Import"=>"True", "Comment"=>"Read and Write"},
  else
    raise "Unknown iDRAC version"
  end
  while true
    res = self.post(path: "Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager.ImportSystemConfiguration", params: {"ImportBuffer": scp.to_json, "ShareParameters": {"Target": "iDRAC"}})
    # A successful JOB will have a location header with a job id.
    # We can get a busy message instead if we've sent too many iDRAC jobs back-to-back, so we check for that here.
    if res[:headers]["location"].present?
      # We have a job id, so we're good to go.
      break
    else
      # Depending on iDRAC version content-length may be present or not.
      # res[:headers]["content-length"].blank?
      msg = res['body']['error']['@Message.ExtendedInfo'].first['Message']
      details = res['body']['error']['@Message.ExtendedInfo'].first['Resolution']
      # msg     => "A job operation is already running. Retry the operation after the existing job is completed."
      # details => "Wait until the running job is completed or delete the scheduled job and retry the operation."
      if details =~ /Wait until the running job is completed/
        sleep 10
      else
        Rails.logger.warn msg+details
        raise "failed configuring static ip, message: #{msg}, details: #{details}"
      end
    end
  end
  
  # Allow some time for the iDRAC to prepare before checking the task status
  sleep 3
  
  # Use handle_location to monitor task progress
  result = handle_location(res[:headers]["location"])
  
  # Check if the operation succeeded
  if result[:status] != :success
    # Extract error details if available
    message = result[:messages].first rescue "N/A" 
    error = result[:error] || "Unknown error"
    raise "Failed configuring static IP: #{message} - #{error}"
  end
  
  # Finally, let's update our configuration to reflect the new port:
  self.idrac
  return true
end

#set_scp_attribute(scp, name, value) ⇒ Object

Set an attribute in a system configuration profile



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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
# File 'lib/idrac/system_config.rb', line 174

def set_scp_attribute(scp, name, value)
  # Make a deep copy to avoid modifying the original
  scp_copy = JSON.parse(scp.to_json)
  
  # Clear unrelated attributes for quicker transfer
  scp_copy["SystemConfiguration"].delete("Comments")
  scp_copy["SystemConfiguration"].delete("TimeStamp")
  scp_copy["SystemConfiguration"].delete("ServiceTag")
  scp_copy["SystemConfiguration"].delete("Model")

  # Skip these attribute groups to make the transfer faster
  excluded_prefixes = [
    "User", "Telemetry", "SecurityCertificate", "AutoUpdate", "PCIe", "LDAP", "ADGroup", "ActiveDirectory",
    "IPMILan", "EmailAlert", "SNMP", "IPBlocking", "IPMI", "Security", "RFS", "OS-BMC", "SupportAssist",
    "Redfish", "RedfishEventing", "Autodiscovery", "SEKM-LKC", "Telco-EdgeServer", "8021XSecurity", "SPDM",
    "InventoryHash", "RSASecurID2FA", "USB", "NIC", "IPv6", "NTP", "Logging", "IOIDOpt", "SSHCrypto",
    "RemoteHosts", "SysLog", "Time", "SmartCard", "ACME", "ServiceModule", "Lockdown",
    "DefaultCredentialMitigation", "AutoOSLockGroup", "LocalSecurity", "IntegratedDatacenter",
    "SecureDefaultPassword.1#ForceChangePassword", "SwitchConnectionView.1#Enable", "GroupManager.1",
    "ASRConfig.1#Enable", "SerialCapture.1#Enable", "CertificateManagement.1",
    "Update", "SSH", "SysInfo", "GUI"
  ]
  
  # Remove excluded attribute groups
  if scp_copy["SystemConfiguration"]["Components"] && 
     scp_copy["SystemConfiguration"]["Components"][0] && 
     scp_copy["SystemConfiguration"]["Components"][0]["Attributes"]
    
    attrs = scp_copy["SystemConfiguration"]["Components"][0]["Attributes"]
    
    attrs.reject! do |attr|
      excluded_prefixes.any? { |prefix| attr["Name"] =~ /\A#{prefix}/ }
    end
    
    # Update or add the specified attribute
    if attrs.find { |a| a["Name"] == name }.nil?
      # Attribute doesn't exist, create it
      attrs << { "Name" => name, "Value" => value, "Set On Import" => "True" }
    else
      # Update existing attribute
      attrs.find { |a| a["Name"] == name }["Value"] = value
      attrs.find { |a| a["Name"] == name }["Set On Import"] = "True"
    end
    
    scp_copy["SystemConfiguration"]["Components"][0]["Attributes"] = attrs
  end
  
  return scp_copy
end

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

Apply a system configuration profile to the iDRAC



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

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

  # Create the import parameters
  params = { 
    "ImportBuffer" => JSON.pretty_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)}", 3
  
  # 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
    return { status: :failed, error: "Failed importing SCP: #{response.body}" }
  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.



279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/idrac/system_config.rb', line 279

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

#wait_for_task(task_id) ⇒ Object

Wait for a task to complete



81
82
83
84
85
86
87
88
89
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 81

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"
      debugger
      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