idrac 0.1.92 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/idrac +920 -1
- data/lib/idrac/boot.rb +357 -0
- data/lib/idrac/client.rb +163 -68
- data/lib/idrac/core_ext.rb +100 -0
- data/lib/idrac/jobs.rb +0 -4
- data/lib/idrac/session.rb +61 -12
- data/lib/idrac/storage.rb +399 -0
- data/lib/idrac/system.rb +383 -0
- data/lib/idrac/version.rb +1 -1
- data/lib/idrac/virtual_media.rb +285 -0
- data/lib/idrac.rb +15 -10
- metadata +7 -2
data/lib/idrac/system.rb
ADDED
@@ -0,0 +1,383 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module IDRAC
|
5
|
+
module SystemComponentMethods
|
6
|
+
# Get memory information
|
7
|
+
def memory
|
8
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/Memory?$expand=*($levels=1)")
|
9
|
+
|
10
|
+
if response.status == 200
|
11
|
+
begin
|
12
|
+
data = JSON.parse(response.body)
|
13
|
+
memory = data["Members"].map do |m|
|
14
|
+
dimm_name = m["Name"] # e.g. DIMM A1
|
15
|
+
bank, index = /DIMM ([A-Z])(\d+)/.match(dimm_name).captures rescue [nil, nil]
|
16
|
+
|
17
|
+
puts "DIMM: #{m["Model"]} #{m["Name"]} > #{m["CapacityMiB"]}MiB > #{m["Status"]["Health"]} > #{m["OperatingSpeedMhz"]}MHz > #{m["PartNumber"]} / #{m["SerialNumber"]}"
|
18
|
+
|
19
|
+
{
|
20
|
+
"model" => m["Model"],
|
21
|
+
"name" => m["Name"],
|
22
|
+
"capacity_bytes" => m["CapacityMiB"].to_i.megabyte,
|
23
|
+
"health" => m["Status"]["Health"] ? m["Status"]["Health"] : "N/A",
|
24
|
+
"speed_mhz" => m["OperatingSpeedMhz"],
|
25
|
+
"part_number" => m["PartNumber"],
|
26
|
+
"serial" => m["SerialNumber"],
|
27
|
+
"bank" => bank,
|
28
|
+
"index" => index.to_i
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
return memory.sort_by { |a| [a["bank"] || "Z", a["index"] || 999] }
|
33
|
+
rescue JSON::ParserError
|
34
|
+
raise Error, "Failed to parse memory response: #{response.body}"
|
35
|
+
end
|
36
|
+
else
|
37
|
+
raise Error, "Failed to get memory. Status code: #{response.status}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get power supply information
|
42
|
+
def psus
|
43
|
+
response = authenticated_request(:get, "/redfish/v1/Chassis/System.Embedded.1/Power")
|
44
|
+
|
45
|
+
if response.status == 200
|
46
|
+
begin
|
47
|
+
data = JSON.parse(response.body)
|
48
|
+
puts "Power Supplies".green
|
49
|
+
|
50
|
+
psus = data["PowerSupplies"].map do |psu|
|
51
|
+
puts "PSU: #{psu["Name"]} > #{psu["PowerInputWatts"]}W > #{psu["Status"]["Health"]}"
|
52
|
+
{
|
53
|
+
"name" => psu["Name"],
|
54
|
+
"voltage" => psu["LineInputVoltage"],
|
55
|
+
"voltage_human" => psu["LineInputVoltageType"], # AC240V
|
56
|
+
"watts" => psu["PowerInputWatts"],
|
57
|
+
"part" => psu["PartNumber"],
|
58
|
+
"model" => psu["Model"],
|
59
|
+
"serial" => psu["SerialNumber"],
|
60
|
+
"status" => psu["Status"]["Health"],
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
return psus
|
65
|
+
rescue JSON::ParserError
|
66
|
+
raise Error, "Failed to parse PSU response: #{response.body}"
|
67
|
+
end
|
68
|
+
else
|
69
|
+
raise Error, "Failed to get PSUs. Status code: #{response.status}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Get fan information
|
74
|
+
def fans
|
75
|
+
tries = 0
|
76
|
+
max_tries = 3
|
77
|
+
|
78
|
+
while tries < max_tries
|
79
|
+
begin
|
80
|
+
response = authenticated_request(:get, "/redfish/v1/Chassis/System.Embedded.1/Thermal?$expand=*($levels=1)")
|
81
|
+
|
82
|
+
if response.status == 200
|
83
|
+
data = JSON.parse(response.body)
|
84
|
+
|
85
|
+
fans = data["Fans"].map do |fan|
|
86
|
+
puts "Fan: #{fan["Name"]} > #{fan["Reading"]} > #{fan["Status"]["Health"]}"
|
87
|
+
{
|
88
|
+
"name" => fan["Name"],
|
89
|
+
"rpm" => fan["Reading"],
|
90
|
+
"serial" => fan["SerialNumber"],
|
91
|
+
"status" => fan["Status"]["Health"]
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
return fans
|
96
|
+
elsif response.status.between?(400, 499)
|
97
|
+
# Check if system is powered off
|
98
|
+
power_response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1?$select=PowerState")
|
99
|
+
if power_response.status == 200 && JSON.parse(power_response.body)["PowerState"] == "Off"
|
100
|
+
puts "WARN: System is off. Fans are not available.".yellow
|
101
|
+
return []
|
102
|
+
end
|
103
|
+
end
|
104
|
+
rescue => e
|
105
|
+
puts "WARN: Error getting fans: #{e.message}".yellow
|
106
|
+
end
|
107
|
+
|
108
|
+
tries += 1
|
109
|
+
puts "Failed to get fans. Retrying #{tries}/#{max_tries}.".red if tries < max_tries
|
110
|
+
sleep 10
|
111
|
+
end
|
112
|
+
|
113
|
+
puts "Failed to get fans after #{max_tries} tries".red
|
114
|
+
return []
|
115
|
+
end
|
116
|
+
|
117
|
+
# Get NIC information
|
118
|
+
def nics
|
119
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/NetworkAdapters?$expand=*($levels=1)")
|
120
|
+
|
121
|
+
if response.status == 200
|
122
|
+
begin
|
123
|
+
adapters_data = JSON.parse(response.body)
|
124
|
+
|
125
|
+
# Determine iDRAC version for different port paths
|
126
|
+
idrac_version_response = authenticated_request(:get, "/redfish/v1")
|
127
|
+
idrac_version_data = JSON.parse(idrac_version_response.body)
|
128
|
+
server = idrac_version_data["RedfishVersion"] || idrac_version_response.headers["server"]
|
129
|
+
|
130
|
+
is_idrac9 = case server.to_s.downcase
|
131
|
+
when /idrac\/9/
|
132
|
+
true
|
133
|
+
when /idrac\/8/
|
134
|
+
false
|
135
|
+
when /appweb\/4.5/
|
136
|
+
false
|
137
|
+
else
|
138
|
+
# Default to newer format for unknown versions
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
port_part = is_idrac9 ? 'Ports' : 'NetworkPorts'
|
143
|
+
|
144
|
+
nics = adapters_data["Members"].map do |adapter|
|
145
|
+
path = "#{adapter["@odata.id"].split("v1/").last}/#{port_part}?$expand=*($levels=1)"
|
146
|
+
ports_response = authenticated_request(:get, "/redfish/v1/#{path}")
|
147
|
+
|
148
|
+
if ports_response.status == 200
|
149
|
+
ports_data = JSON.parse(ports_response.body)
|
150
|
+
|
151
|
+
ports = ports_data["Members"].map do |nic|
|
152
|
+
if is_idrac9
|
153
|
+
link_speed_mbps = nic['CurrentSpeedGbps'].to_i * 1000
|
154
|
+
mac_addr = nic['Ethernet']['AssociatedMACAddresses'].first
|
155
|
+
port_num = nic['PortId']
|
156
|
+
network_technology = nic['LinkNetworkTechnology']
|
157
|
+
link_status = nic['LinkStatus'] =~ /up/i ? "Up" : "Down"
|
158
|
+
else # iDRAC 8
|
159
|
+
link_speed_mbps = nic["SupportedLinkCapabilities"].first["LinkSpeedMbps"]
|
160
|
+
mac_addr = nic["AssociatedNetworkAddresses"].first
|
161
|
+
port_num = nic["PhysicalPortNumber"]
|
162
|
+
network_technology = nic["SupportedLinkCapabilities"].first["LinkNetworkTechnology"]
|
163
|
+
link_status = nic['LinkStatus']
|
164
|
+
end
|
165
|
+
|
166
|
+
puts "NIC: #{nic["Id"]} > #{mac_addr} > #{link_status} > #{port_num} > #{link_speed_mbps}Mbps"
|
167
|
+
|
168
|
+
{
|
169
|
+
"name" => nic["Id"],
|
170
|
+
"status" => link_status,
|
171
|
+
"mac" => mac_addr,
|
172
|
+
"port" => port_num,
|
173
|
+
"speed_mbps" => link_speed_mbps,
|
174
|
+
"kind" => network_technology&.downcase
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
{
|
179
|
+
"name" => adapter["Id"],
|
180
|
+
"manufacturer" => adapter["Manufacturer"],
|
181
|
+
"model" => adapter["Model"],
|
182
|
+
"part_number" => adapter["PartNumber"],
|
183
|
+
"serial" => adapter["SerialNumber"],
|
184
|
+
"ports" => ports
|
185
|
+
}
|
186
|
+
else
|
187
|
+
# Return adapter info without ports if we can't get port details
|
188
|
+
{
|
189
|
+
"name" => adapter["Id"],
|
190
|
+
"manufacturer" => adapter["Manufacturer"],
|
191
|
+
"model" => adapter["Model"],
|
192
|
+
"part_number" => adapter["PartNumber"],
|
193
|
+
"serial" => adapter["SerialNumber"],
|
194
|
+
"ports" => []
|
195
|
+
}
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
return nics
|
200
|
+
rescue JSON::ParserError
|
201
|
+
raise Error, "Failed to parse NICs response: #{response.body}"
|
202
|
+
end
|
203
|
+
else
|
204
|
+
raise Error, "Failed to get NICs. Status code: #{response.status}"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Get iDRAC network information
|
209
|
+
def idrac_network
|
210
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/EthernetInterfaces/iDRAC.Embedded.1%23NIC.1")
|
211
|
+
|
212
|
+
if response.status == 200
|
213
|
+
begin
|
214
|
+
data = JSON.parse(response.body)
|
215
|
+
|
216
|
+
idrac = {
|
217
|
+
"name" => data["Id"],
|
218
|
+
"status" => data["Status"]["Health"] == 'OK' ? 'Up' : 'Down',
|
219
|
+
"mac" => data["MACAddress"],
|
220
|
+
"mask" => data["IPv4Addresses"].first["SubnetMask"],
|
221
|
+
"ipv4" => data["IPv4Addresses"].first["Address"],
|
222
|
+
"origin" => data["IPv4Addresses"].first["AddressOrigin"], # DHCP or Static
|
223
|
+
"port" => nil,
|
224
|
+
"speed_mbps" => data["SpeedMbps"],
|
225
|
+
"kind" => "ethernet"
|
226
|
+
}
|
227
|
+
|
228
|
+
return idrac
|
229
|
+
rescue JSON::ParserError
|
230
|
+
raise Error, "Failed to parse iDRAC network response: #{response.body}"
|
231
|
+
end
|
232
|
+
else
|
233
|
+
raise Error, "Failed to get iDRAC network. Status code: #{response.status}"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Get PCI device information
|
238
|
+
def pci_devices
|
239
|
+
response = authenticated_request(:get, "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices?$expand=*($levels=1)")
|
240
|
+
|
241
|
+
if response.status == 200
|
242
|
+
begin
|
243
|
+
data = JSON.parse(response.body)
|
244
|
+
|
245
|
+
pci = data["Members"].map do |stub|
|
246
|
+
manufacturer = stub["Manufacturer"]
|
247
|
+
|
248
|
+
# Get PCIe function details if available
|
249
|
+
pcie_function = nil
|
250
|
+
if stub.dig("Links", "PCIeFunctions", 0, "@odata.id")
|
251
|
+
pcie_function_path = stub.dig("Links", "PCIeFunctions", 0, "@odata.id").split("v1/").last
|
252
|
+
function_response = authenticated_request(:get, "/redfish/v1/#{pcie_function_path}")
|
253
|
+
|
254
|
+
if function_response.status == 200
|
255
|
+
pcie_function = JSON.parse(function_response.body)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Create device info with available data
|
260
|
+
device_info = {
|
261
|
+
device_class: pcie_function ? pcie_function["DeviceClass"] : nil,
|
262
|
+
manufacturer: manufacturer,
|
263
|
+
name: stub["Name"],
|
264
|
+
description: stub["Description"],
|
265
|
+
id: pcie_function ? pcie_function["Id"] : stub["Id"],
|
266
|
+
slot_type: pcie_function ? pcie_function.dig("Oem", "Dell", "DellPCIeFunction", "SlotType") : nil,
|
267
|
+
bus_width: pcie_function ? pcie_function.dig("Oem", "Dell", "DellPCIeFunction", "DataBusWidth") : nil,
|
268
|
+
nic: pcie_function ? pcie_function.dig("Links", "EthernetInterfaces", 0, "@odata.id") : nil
|
269
|
+
}
|
270
|
+
|
271
|
+
puts "PCI Device: #{device_info[:name]} > #{device_info[:manufacturer]} > #{device_info[:device_class]} > #{device_info[:description]} > #{device_info[:id]}"
|
272
|
+
|
273
|
+
device_info
|
274
|
+
end
|
275
|
+
|
276
|
+
return pci
|
277
|
+
rescue JSON::ParserError
|
278
|
+
raise Error, "Failed to parse PCI devices response: #{response.body}"
|
279
|
+
end
|
280
|
+
else
|
281
|
+
raise Error, "Failed to get PCI devices. Status code: #{response.status}"
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Map NICs to PCI bus IDs for Mellanox cards
|
286
|
+
def nics_to_pci(nics, pci_devices)
|
287
|
+
# Filter for Mellanox network controllers
|
288
|
+
mellanox_pci = pci_devices.select do |dev|
|
289
|
+
dev[:device_class] =~ /NetworkController/ && dev[:manufacturer] =~ /Mellanox/
|
290
|
+
end
|
291
|
+
|
292
|
+
# Create mapping of NIC names to PCI IDs
|
293
|
+
mapping = {}
|
294
|
+
mellanox_pci.each do |dev|
|
295
|
+
if dev[:nic] && dev[:nic] =~ /.*\/([^\/\-]+-\d+)/
|
296
|
+
nic = $1 # e.g. NIC.Slot.1-1
|
297
|
+
if dev[:id] =~ /^(\d+)-\d+-\d/
|
298
|
+
pci_bus = $1 # e.g. 59
|
299
|
+
mapping[nic] = pci_bus
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# Add PCI bus info to each NIC port
|
305
|
+
nics_with_pci = nics.map do |nic|
|
306
|
+
nic_with_pci = nic.dup
|
307
|
+
|
308
|
+
if nic_with_pci["ports"]
|
309
|
+
nic_with_pci["ports"] = nic_with_pci["ports"].map do |port|
|
310
|
+
port_with_pci = port.dup
|
311
|
+
pci_bus = mapping[port["name"]]
|
312
|
+
|
313
|
+
if pci_bus
|
314
|
+
port_with_pci["pci"] = pci_bus
|
315
|
+
port_with_pci["linux_device"] = "enp#{pci_bus}s0np0" # e.g. enp3s0np0
|
316
|
+
end
|
317
|
+
|
318
|
+
port_with_pci
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
nic_with_pci
|
323
|
+
end
|
324
|
+
|
325
|
+
return nics_with_pci
|
326
|
+
end
|
327
|
+
|
328
|
+
# Get system event logs
|
329
|
+
def system_event_logs
|
330
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/Logs/Sel?$expand=*($levels=1)")
|
331
|
+
|
332
|
+
if response.status == 200
|
333
|
+
begin
|
334
|
+
data = JSON.parse(response.body)
|
335
|
+
|
336
|
+
logs = data["Members"].map do |log|
|
337
|
+
puts "#{log['Id']} : #{log['Created']} : #{log['Message']} : #{log['Severity']}".yellow
|
338
|
+
log
|
339
|
+
end
|
340
|
+
|
341
|
+
# Sort by creation date, newest first
|
342
|
+
return logs.sort_by { |log| log['Created'] }.reverse
|
343
|
+
rescue JSON::ParserError
|
344
|
+
raise Error, "Failed to parse system event logs response: #{response.body}"
|
345
|
+
end
|
346
|
+
else
|
347
|
+
raise Error, "Failed to get system event logs. Status code: #{response.status}"
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# Clear system event logs
|
352
|
+
def clear_system_event_logs
|
353
|
+
response = authenticated_request(
|
354
|
+
:post,
|
355
|
+
"/redfish/v1/Managers/iDRAC.Embedded.1/LogServices/Sel/Actions/LogService.ClearLog",
|
356
|
+
body: {}.to_json,
|
357
|
+
headers: { 'Content-Type': 'application/json' }
|
358
|
+
)
|
359
|
+
|
360
|
+
if response.status.between?(200, 299)
|
361
|
+
puts "System Event Logs cleared".green
|
362
|
+
return true
|
363
|
+
else
|
364
|
+
error_message = "Failed to clear System Event Logs. Status code: #{response.status}"
|
365
|
+
|
366
|
+
begin
|
367
|
+
error_data = JSON.parse(response.body)
|
368
|
+
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
369
|
+
rescue
|
370
|
+
# Ignore JSON parsing errors
|
371
|
+
end
|
372
|
+
|
373
|
+
raise Error, error_message
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
# Get total memory in human-readable format
|
378
|
+
def total_memory_human(memory_data)
|
379
|
+
total_memory = memory_data.sum { |m| m['capacity_bytes'] }
|
380
|
+
"%0.2f GB" % (total_memory.to_f / 1.gigabyte)
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
data/lib/idrac/version.rb
CHANGED
@@ -0,0 +1,285 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module IDRAC
|
5
|
+
module VirtualMediaMethods
|
6
|
+
# Get current virtual media status
|
7
|
+
def virtual_media
|
8
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia?$expand=*($levels=1)")
|
9
|
+
|
10
|
+
if response.status == 200
|
11
|
+
begin
|
12
|
+
data = JSON.parse(response.body)
|
13
|
+
|
14
|
+
media = data["Members"].map do |m|
|
15
|
+
if m["Inserted"]
|
16
|
+
puts "#{m["Name"]} #{m["ConnectedVia"]} #{m["Image"]}".green
|
17
|
+
else
|
18
|
+
puts "#{m["Name"]} #{m["ConnectedVia"]}".yellow
|
19
|
+
end
|
20
|
+
|
21
|
+
action_path = m.dig("Actions", "#VirtualMedia.InsertMedia", "target")
|
22
|
+
|
23
|
+
{
|
24
|
+
device: m["Id"],
|
25
|
+
inserted: m["Inserted"],
|
26
|
+
image: m["Image"] || m["ConnectedVia"],
|
27
|
+
action_path: action_path
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
return media
|
32
|
+
rescue JSON::ParserError
|
33
|
+
raise Error, "Failed to parse virtual media response: #{response.body}"
|
34
|
+
end
|
35
|
+
else
|
36
|
+
raise Error, "Failed to get virtual media. Status code: #{response.status}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Eject virtual media from a device
|
41
|
+
def eject_virtual_media(device: "CD")
|
42
|
+
media_list = virtual_media
|
43
|
+
|
44
|
+
# Find the device to eject
|
45
|
+
media_to_eject = media_list.find { |m| m[:device] == device && m[:inserted] }
|
46
|
+
|
47
|
+
if media_to_eject.nil?
|
48
|
+
puts "No media #{device} to eject".yellow
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
puts "Ejecting #{media_to_eject[:device]} #{media_to_eject[:image]}".yellow
|
53
|
+
|
54
|
+
# Use the action path from the media object if available
|
55
|
+
path = if media_to_eject[:action_path]
|
56
|
+
media_to_eject[:action_path].sub(/^\/redfish\/v1\//, "").sub(/InsertMedia$/, "EjectMedia")
|
57
|
+
else
|
58
|
+
"Managers/iDRAC.Embedded.1/VirtualMedia/#{device}/Actions/VirtualMedia.EjectMedia"
|
59
|
+
end
|
60
|
+
|
61
|
+
response = authenticated_request(
|
62
|
+
:post,
|
63
|
+
"/redfish/v1/#{path}",
|
64
|
+
body: {}.to_json,
|
65
|
+
headers: { 'Content-Type': 'application/json' }
|
66
|
+
)
|
67
|
+
|
68
|
+
case response.status
|
69
|
+
when 200..299
|
70
|
+
sleep 5 # Wait for ejection to complete
|
71
|
+
puts "Ejected #{media_to_eject[:device]}".green
|
72
|
+
return true
|
73
|
+
when 500..599
|
74
|
+
# Check if the error is "No Virtual Media devices are currently connected"
|
75
|
+
begin
|
76
|
+
error_data = JSON.parse(response.body)
|
77
|
+
if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"] &&
|
78
|
+
error_data["error"]["@Message.ExtendedInfo"].any? { |m| m["Message"] =~ /No Virtual Media devices are currently connected/ }
|
79
|
+
puts "No Virtual Media devices are currently connected".yellow
|
80
|
+
return false
|
81
|
+
end
|
82
|
+
rescue JSON::ParserError
|
83
|
+
# Ignore parsing errors
|
84
|
+
end
|
85
|
+
|
86
|
+
puts "Failed to eject media: #{response.status}".red
|
87
|
+
return false
|
88
|
+
else
|
89
|
+
puts "Unexpected response code: #{response.status}".red
|
90
|
+
return false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Insert virtual media (ISO)
|
95
|
+
def insert_virtual_media(iso_url, device: "CD")
|
96
|
+
raise Error, "Device must be CD or RemovableDisk" unless ["CD", "RemovableDisk"].include?(device)
|
97
|
+
|
98
|
+
# First eject any inserted media
|
99
|
+
eject_virtual_media(device: device)
|
100
|
+
|
101
|
+
# Firmware version determines which API to use
|
102
|
+
firmware_version = get_firmware_version.split(".")[0,2].join.to_i
|
103
|
+
|
104
|
+
puts "Inserting media: #{iso_url}".yellow
|
105
|
+
|
106
|
+
tries = 0
|
107
|
+
max_tries = 10
|
108
|
+
|
109
|
+
while tries < max_tries
|
110
|
+
begin
|
111
|
+
# Different endpoint based on firmware version
|
112
|
+
path = if firmware_version >= 600
|
113
|
+
"Systems/System.Embedded.1/VirtualMedia/1/Actions/VirtualMedia.InsertMedia"
|
114
|
+
else
|
115
|
+
"Managers/iDRAC.Embedded.1/VirtualMedia/#{device}/Actions/VirtualMedia.InsertMedia"
|
116
|
+
end
|
117
|
+
|
118
|
+
response = authenticated_request(
|
119
|
+
:post,
|
120
|
+
"/redfish/v1/#{path}",
|
121
|
+
body: { "Image": iso_url, "Inserted": true, "WriteProtected": true }.to_json,
|
122
|
+
headers: { 'Content-Type': 'application/json' }
|
123
|
+
)
|
124
|
+
|
125
|
+
if response.status == 204 || response.status == 200
|
126
|
+
puts "Inserted media successfully".green
|
127
|
+
return true
|
128
|
+
end
|
129
|
+
|
130
|
+
# Handle error responses
|
131
|
+
error_message = "Failed to insert media. Status code: #{response.status}"
|
132
|
+
begin
|
133
|
+
error_data = JSON.parse(response.body)
|
134
|
+
if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
|
135
|
+
error_info = error_data["error"]["@Message.ExtendedInfo"].first
|
136
|
+
error_message += ", Message: #{error_info['Message']}"
|
137
|
+
end
|
138
|
+
rescue
|
139
|
+
# Ignore JSON parsing errors
|
140
|
+
end
|
141
|
+
|
142
|
+
puts "#{error_message}. Retrying (#{tries + 1}/#{max_tries})...".red
|
143
|
+
rescue => e
|
144
|
+
puts "Error during insert_virtual_media: #{e.message}. Retrying (#{tries + 1}/#{max_tries})...".red
|
145
|
+
end
|
146
|
+
|
147
|
+
tries += 1
|
148
|
+
sleep 60 # Wait before retry
|
149
|
+
end
|
150
|
+
|
151
|
+
raise Error, "Failed to insert virtual media after #{max_tries} attempts"
|
152
|
+
end
|
153
|
+
|
154
|
+
# Set boot to virtual media once, then boot from HD
|
155
|
+
def set_one_time_virtual_media_boot
|
156
|
+
# Check firmware version to determine which API to use
|
157
|
+
firmware_version = get_firmware_version.split(".")[0,2].join.to_i
|
158
|
+
|
159
|
+
if firmware_version >= 440 # Modern iDRAC
|
160
|
+
# Check current boot configuration
|
161
|
+
boot_response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
|
162
|
+
if boot_response.status == 200
|
163
|
+
boot_data = JSON.parse(boot_response.body)
|
164
|
+
enabled = boot_data['Boot']['BootSourceOverrideEnabled']
|
165
|
+
target = boot_data['Boot']['BootSourceOverrideTarget']
|
166
|
+
puts "Currently override is #{enabled} to boot from #{target}".yellow
|
167
|
+
end
|
168
|
+
|
169
|
+
# Set one-time boot to CD
|
170
|
+
response = authenticated_request(
|
171
|
+
:patch,
|
172
|
+
"/redfish/v1/Systems/System.Embedded.1",
|
173
|
+
body: { "Boot": { "BootSourceOverrideTarget": "Cd", "BootSourceOverrideEnabled": "Once" } }.to_json,
|
174
|
+
headers: { 'Content-Type': 'application/json' }
|
175
|
+
)
|
176
|
+
|
177
|
+
if response.status.between?(200, 299)
|
178
|
+
puts "One-time boot to virtual media configured".green
|
179
|
+
return true
|
180
|
+
else
|
181
|
+
error_message = "Failed to set one-time boot. Status code: #{response.status}"
|
182
|
+
begin
|
183
|
+
error_data = JSON.parse(response.body)
|
184
|
+
if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
|
185
|
+
error_info = error_data["error"]["@Message.ExtendedInfo"].first
|
186
|
+
error_message += ", Message: #{error_info['Message']}"
|
187
|
+
end
|
188
|
+
rescue
|
189
|
+
# Ignore JSON parsing errors
|
190
|
+
end
|
191
|
+
|
192
|
+
raise Error, error_message
|
193
|
+
end
|
194
|
+
else
|
195
|
+
# For older iDRAC, we need to use the iDRAC-specific method
|
196
|
+
payload = {
|
197
|
+
"ServerBoot.1#BootOnce": "Enabled",
|
198
|
+
"ServerBoot.1#FirstBootDevice": "VCD-DVD"
|
199
|
+
}
|
200
|
+
|
201
|
+
response = authenticated_request(
|
202
|
+
:patch,
|
203
|
+
"/redfish/v1/Managers/iDRAC.Embedded.1/Attributes",
|
204
|
+
body: payload.to_json,
|
205
|
+
headers: { 'Content-Type': 'application/json' }
|
206
|
+
)
|
207
|
+
|
208
|
+
if response.status.between?(200, 299)
|
209
|
+
puts "One-time boot to virtual media configured".green
|
210
|
+
return true
|
211
|
+
else
|
212
|
+
error_message = "Failed to set one-time boot. Status code: #{response.status}"
|
213
|
+
begin
|
214
|
+
error_data = JSON.parse(response.body)
|
215
|
+
if error_data["error"] && error_data["error"]["@Message.ExtendedInfo"]
|
216
|
+
error_info = error_data["error"]["@Message.ExtendedInfo"].first
|
217
|
+
error_message += ", Message: #{error_info['Message']}"
|
218
|
+
end
|
219
|
+
rescue
|
220
|
+
# Ignore JSON parsing errors
|
221
|
+
end
|
222
|
+
|
223
|
+
raise Error, error_message
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Get current boot source override settings
|
229
|
+
def get_boot_source_override
|
230
|
+
response = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1")
|
231
|
+
|
232
|
+
if response.status == 200
|
233
|
+
begin
|
234
|
+
data = JSON.parse(response.body)
|
235
|
+
boot = data["Boot"]
|
236
|
+
|
237
|
+
puts "Boot Source Override Configuration:".green
|
238
|
+
puts " Enabled: #{boot['BootSourceOverrideEnabled']}"
|
239
|
+
puts " Target: #{boot['BootSourceOverrideTarget']}"
|
240
|
+
puts " Mode: #{boot['BootSourceOverrideMode']}" if boot['BootSourceOverrideMode']
|
241
|
+
|
242
|
+
if boot["BootSourceOverrideEnabled"] != "Once" || boot["BootSourceOverrideTarget"] == "None"
|
243
|
+
return "None"
|
244
|
+
else
|
245
|
+
return "#{boot['BootSourceOverrideMode']} #{boot['BootSourceOverrideTarget']}"
|
246
|
+
end
|
247
|
+
rescue JSON::ParserError
|
248
|
+
raise Error, "Failed to parse boot source response: #{response.body}"
|
249
|
+
end
|
250
|
+
else
|
251
|
+
raise Error, "Failed to get boot source override. Status code: #{response.status}"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
private
|
256
|
+
|
257
|
+
# Get firmware version (helper method for virtual media operations)
|
258
|
+
def get_firmware_version
|
259
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1?$select=FirmwareVersion")
|
260
|
+
|
261
|
+
if response.status == 200
|
262
|
+
begin
|
263
|
+
data = JSON.parse(response.body)
|
264
|
+
return data["FirmwareVersion"]
|
265
|
+
rescue JSON::ParserError
|
266
|
+
raise Error, "Failed to parse firmware version response: #{response.body}"
|
267
|
+
end
|
268
|
+
else
|
269
|
+
# Try again without the $select parameter for older firmware
|
270
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1")
|
271
|
+
|
272
|
+
if response.status == 200
|
273
|
+
begin
|
274
|
+
data = JSON.parse(response.body)
|
275
|
+
return data["FirmwareVersion"]
|
276
|
+
rescue JSON::ParserError
|
277
|
+
raise Error, "Failed to parse firmware version response: #{response.body}"
|
278
|
+
end
|
279
|
+
else
|
280
|
+
raise Error, "Failed to get firmware version. Status code: #{response.status}"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|