idrac 0.1.91 → 0.3.1
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 +754 -4
- data/lib/idrac/boot.rb +357 -0
- data/lib/idrac/client.rb +167 -72
- data/lib/idrac/core_ext.rb +100 -0
- data/lib/idrac/lifecycle.rb +86 -189
- data/lib/idrac/session.rb +134 -85
- data/lib/idrac/storage.rb +374 -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 +16 -11
- metadata +7 -2
@@ -0,0 +1,374 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module IDRAC
|
5
|
+
module StorageMethods
|
6
|
+
# Get storage controllers information
|
7
|
+
def controller
|
8
|
+
response = authenticated_request(:get, '/redfish/v1/Systems/System.Embedded.1/Storage?$expand=*($levels=1)')
|
9
|
+
|
10
|
+
if response.status == 200
|
11
|
+
begin
|
12
|
+
data = JSON.parse(response.body)
|
13
|
+
|
14
|
+
puts "Controllers".green
|
15
|
+
data["Members"].each { |ctrlr| puts "#{ctrlr["Name"]} > #{ctrlr["Drives@odata.count"]}" }
|
16
|
+
|
17
|
+
puts "Drives".green
|
18
|
+
data["Members"].each do |m|
|
19
|
+
puts "Storage: #{m["Name"]} > #{m["Status"]["Health"] || "N/A"} > #{m["Drives"].size}"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Find the controller with the most drives (usually the PERC)
|
23
|
+
controller = data["Members"].sort_by { |ctrlr| ctrlr["Drives"].size }.last
|
24
|
+
|
25
|
+
if controller["Name"] =~ /PERC/
|
26
|
+
puts "Found #{controller["Name"]}".green
|
27
|
+
else
|
28
|
+
puts "Found #{controller["Name"]} but continuing...".yellow
|
29
|
+
end
|
30
|
+
|
31
|
+
return controller
|
32
|
+
rescue JSON::ParserError
|
33
|
+
raise Error, "Failed to parse controller response: #{response.body}"
|
34
|
+
end
|
35
|
+
else
|
36
|
+
raise Error, "Failed to get controller. Status code: #{response.status}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Check if controller supports encryption
|
41
|
+
def controller_encryption_capable?(controller)
|
42
|
+
return false unless controller
|
43
|
+
controller.dig("Oem", "Dell", "DellController", "EncryptionCapability") =~ /localkey/i
|
44
|
+
end
|
45
|
+
|
46
|
+
# Check if controller encryption is enabled
|
47
|
+
def controller_encryption_enabled?(controller)
|
48
|
+
return false unless controller
|
49
|
+
controller.dig("Oem", "Dell", "DellController", "EncryptionMode") =~ /localkey/i
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get information about physical drives
|
53
|
+
def drives(controller)
|
54
|
+
raise Error, "Controller not provided" unless controller
|
55
|
+
|
56
|
+
controller_path = controller["@odata.id"].split("v1/").last
|
57
|
+
response = authenticated_request(:get, "/redfish/v1/#{controller_path}?$expand=*($levels=1)")
|
58
|
+
|
59
|
+
if response.status == 200
|
60
|
+
begin
|
61
|
+
data = JSON.parse(response.body)
|
62
|
+
drives = data["Drives"].map do |body|
|
63
|
+
serial = body["SerialNumber"]
|
64
|
+
serial = body["Identifiers"].first["DurableName"] if serial.blank?
|
65
|
+
{
|
66
|
+
serial: serial,
|
67
|
+
model: body["Model"],
|
68
|
+
name: body["Name"],
|
69
|
+
capacity_bytes: body["CapacityBytes"],
|
70
|
+
health: body["Status"]["Health"] ? body["Status"]["Health"] : "N/A",
|
71
|
+
speed_gbp: body["CapableSpeedGbs"],
|
72
|
+
manufacturer: body["Manufacturer"],
|
73
|
+
media_type: body["MediaType"],
|
74
|
+
failure_predicted: body["FailurePredicted"],
|
75
|
+
life_left_percent: body["PredictedMediaLifeLeftPercent"],
|
76
|
+
certified: body.dig("Oem", "Dell", "DellPhysicalDisk", "Certified"),
|
77
|
+
raid_status: body.dig("Oem", "Dell", "DellPhysicalDisk", "RaidStatus"),
|
78
|
+
operation_name: body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationName"),
|
79
|
+
operation_progress: body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationPercentCompletePercent"),
|
80
|
+
encryption_ability: body["EncryptionAbility"],
|
81
|
+
"@odata.id": body["@odata.id"]
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
return drives.sort_by { |d| d[:name] }
|
86
|
+
rescue JSON::ParserError
|
87
|
+
raise Error, "Failed to parse drives response: #{response.body}"
|
88
|
+
end
|
89
|
+
else
|
90
|
+
raise Error, "Failed to get drives. Status code: #{response.status}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get information about virtual disk volumes
|
95
|
+
def volumes(controller)
|
96
|
+
raise Error, "Controller not provided" unless controller
|
97
|
+
|
98
|
+
puts "Volumes (e.g. Arrays)".green
|
99
|
+
|
100
|
+
v = controller["Volumes"]
|
101
|
+
path = v["@odata.id"].split("v1/").last
|
102
|
+
response = authenticated_request(:get, "/redfish/v1/#{path}?$expand=*($levels=1)")
|
103
|
+
|
104
|
+
if response.status == 200
|
105
|
+
begin
|
106
|
+
data = JSON.parse(response.body)
|
107
|
+
volumes = data["Members"].map do |vol|
|
108
|
+
drives = vol["Links"]["Drives"]
|
109
|
+
volume = {
|
110
|
+
name: vol["Name"],
|
111
|
+
capacity_bytes: vol["CapacityBytes"],
|
112
|
+
volume_type: vol["VolumeType"],
|
113
|
+
drives: drives,
|
114
|
+
write_cache_policy: vol.dig("Oem", "Dell", "DellVirtualDisk", "WriteCachePolicy"),
|
115
|
+
read_cache_policy: vol.dig("Oem", "Dell", "DellVirtualDisk", "ReadCachePolicy"),
|
116
|
+
stripe_size: vol.dig("Oem", "Dell", "DellVirtualDisk", "StripeSize"),
|
117
|
+
raid_level: vol["RAIDType"],
|
118
|
+
encrypted: vol["Encrypted"],
|
119
|
+
lock_status: vol.dig("Oem", "Dell", "DellVirtualDisk", "LockStatus"),
|
120
|
+
"@odata.id": vol["@odata.id"]
|
121
|
+
}
|
122
|
+
|
123
|
+
# Check FastPath settings
|
124
|
+
volume[:fastpath] = fastpath_good?(volume)
|
125
|
+
|
126
|
+
# Handle volume operations and status
|
127
|
+
if vol["Operations"].any?
|
128
|
+
volume[:health] = vol["Status"]["Health"] ? vol["Status"]["Health"] : "N/A"
|
129
|
+
volume[:progress] = vol["Operations"].first["PercentageComplete"]
|
130
|
+
volume[:message] = vol["Operations"].first["OperationName"]
|
131
|
+
elsif vol["Status"]["Health"] == "OK"
|
132
|
+
volume[:health] = "OK"
|
133
|
+
volume[:progress] = nil
|
134
|
+
volume[:message] = nil
|
135
|
+
else
|
136
|
+
volume[:health] = "?"
|
137
|
+
volume[:progress] = nil
|
138
|
+
volume[:message] = nil
|
139
|
+
end
|
140
|
+
|
141
|
+
volume
|
142
|
+
end
|
143
|
+
|
144
|
+
return volumes.sort_by { |d| d[:name] }
|
145
|
+
rescue JSON::ParserError
|
146
|
+
raise Error, "Failed to parse volumes response: #{response.body}"
|
147
|
+
end
|
148
|
+
else
|
149
|
+
raise Error, "Failed to get volumes. Status code: #{response.status}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Check if FastPath is properly configured for a volume
|
154
|
+
def fastpath_good?(volume)
|
155
|
+
return "disabled" unless volume
|
156
|
+
|
157
|
+
# Modern firmware check handled by caller
|
158
|
+
if volume[:write_cache_policy] == "WriteThrough" &&
|
159
|
+
volume[:read_cache_policy] == "NoReadAhead" &&
|
160
|
+
volume[:stripe_size] == "64KB"
|
161
|
+
return "enabled"
|
162
|
+
else
|
163
|
+
return "disabled"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Delete a volume
|
168
|
+
def delete_volume(odata_id)
|
169
|
+
path = odata_id.split("v1/").last
|
170
|
+
puts "Deleting volume: #{path}"
|
171
|
+
|
172
|
+
response = authenticated_request(:delete, "/redfish/v1/#{path}")
|
173
|
+
|
174
|
+
if response.status.between?(200, 299)
|
175
|
+
puts "Delete volume request sent".green
|
176
|
+
|
177
|
+
# Check if we need to wait for a job
|
178
|
+
if response.headers["location"]
|
179
|
+
job_id = response.headers["location"].split("/").last
|
180
|
+
wait_for_job(job_id)
|
181
|
+
end
|
182
|
+
|
183
|
+
return true
|
184
|
+
else
|
185
|
+
error_message = "Failed to delete volume. Status code: #{response.status}"
|
186
|
+
|
187
|
+
begin
|
188
|
+
error_data = JSON.parse(response.body)
|
189
|
+
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
190
|
+
rescue
|
191
|
+
# Ignore JSON parsing errors
|
192
|
+
end
|
193
|
+
|
194
|
+
raise Error, error_message
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Create a new virtual disk with RAID5 and FastPath optimizations
|
199
|
+
def create_virtual_disk(controller_id, drives, name: "vssd0", raid_type: "RAID5")
|
200
|
+
# Check firmware version to determine which API to use
|
201
|
+
firmware_version = get_firmware_version.split(".")[0,2].join.to_i
|
202
|
+
|
203
|
+
# [FastPath optimization for SSDs](https://www.dell.com/support/manuals/en-us/perc-h755/perc11_ug/fastpath?guid=guid-a9e90946-a41f-48ab-88f1-9ce514b4c414&lang=en-us)
|
204
|
+
payload = {
|
205
|
+
"Drives": drives.map { |d| { "@odata.id": d["@odata.id"] } },
|
206
|
+
"Name": name,
|
207
|
+
"OptimumIOSizeBytes": 64 * 1024,
|
208
|
+
"Oem": { "Dell": { "DellVolume": { "DiskCachePolicy": "Enabled" } } },
|
209
|
+
"ReadCachePolicy": "Off", # "NoReadAhead"
|
210
|
+
"WriteCachePolicy": "WriteThrough"
|
211
|
+
}
|
212
|
+
|
213
|
+
# If the firmware < 440, we need a different approach
|
214
|
+
if firmware_version >= 440
|
215
|
+
# For modern firmware
|
216
|
+
if drives.size < 3 && raid_type == "RAID5"
|
217
|
+
puts "*************************************************".red
|
218
|
+
puts "* WARNING: Less than 3 drives. Selecting RAID0. *".red
|
219
|
+
puts "*************************************************".red
|
220
|
+
payload["RAIDType"] = "RAID0"
|
221
|
+
else
|
222
|
+
payload["RAIDType"] = raid_type
|
223
|
+
end
|
224
|
+
else
|
225
|
+
# For older firmware
|
226
|
+
payload["VolumeType"] = "StripedWithParity" if raid_type == "RAID5"
|
227
|
+
payload["VolumeType"] = "SpannedDisks" if raid_type == "RAID0"
|
228
|
+
end
|
229
|
+
|
230
|
+
url = "Systems/System.Embedded.1/Storage/#{controller_id}/Volumes"
|
231
|
+
response = authenticated_request(
|
232
|
+
:post,
|
233
|
+
"/redfish/v1/#{url}",
|
234
|
+
body: payload.to_json,
|
235
|
+
headers: { 'Content-Type': 'application/json' }
|
236
|
+
)
|
237
|
+
|
238
|
+
if response.status.between?(200, 299)
|
239
|
+
puts "Virtual disk creation started".green
|
240
|
+
|
241
|
+
# Check if we need to wait for a job
|
242
|
+
if response.headers["location"]
|
243
|
+
job_id = response.headers["location"].split("/").last
|
244
|
+
wait_for_job(job_id)
|
245
|
+
end
|
246
|
+
|
247
|
+
return true
|
248
|
+
else
|
249
|
+
error_message = "Failed to create virtual disk. Status code: #{response.status}"
|
250
|
+
|
251
|
+
begin
|
252
|
+
error_data = JSON.parse(response.body)
|
253
|
+
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
254
|
+
rescue
|
255
|
+
# Ignore JSON parsing errors
|
256
|
+
end
|
257
|
+
|
258
|
+
raise Error, error_message
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Enable Self-Encrypting Drive support on controller
|
263
|
+
def enable_local_key_management(controller_id, passphrase: "Secure123!", keyid: "RAID-Key-2023")
|
264
|
+
payload = {
|
265
|
+
"TargetFQDD": controller_id,
|
266
|
+
"Key": passphrase,
|
267
|
+
"Keyid": keyid
|
268
|
+
}
|
269
|
+
|
270
|
+
response = authenticated_request(
|
271
|
+
:post,
|
272
|
+
"/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.SetControllerKey",
|
273
|
+
body: payload.to_json,
|
274
|
+
headers: { 'Content-Type': 'application/json' }
|
275
|
+
)
|
276
|
+
|
277
|
+
if response.status == 202
|
278
|
+
puts "Controller encryption enabled".green
|
279
|
+
|
280
|
+
# Check if we need to wait for a job
|
281
|
+
if response.headers["location"]
|
282
|
+
job_id = response.headers["location"].split("/").last
|
283
|
+
wait_for_job(job_id)
|
284
|
+
end
|
285
|
+
|
286
|
+
return true
|
287
|
+
else
|
288
|
+
error_message = "Failed to enable controller encryption. Status code: #{response.status}"
|
289
|
+
|
290
|
+
begin
|
291
|
+
error_data = JSON.parse(response.body)
|
292
|
+
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
293
|
+
rescue
|
294
|
+
# Ignore JSON parsing errors
|
295
|
+
end
|
296
|
+
|
297
|
+
raise Error, error_message
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Disable Self-Encrypting Drive support on controller
|
302
|
+
def disable_local_key_management(controller_id)
|
303
|
+
payload = { "TargetFQDD": controller_id }
|
304
|
+
|
305
|
+
response = authenticated_request(
|
306
|
+
:post,
|
307
|
+
"/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.RemoveControllerKey",
|
308
|
+
body: payload.to_json,
|
309
|
+
headers: { 'Content-Type': 'application/json' }
|
310
|
+
)
|
311
|
+
|
312
|
+
if response.status == 202
|
313
|
+
puts "Controller encryption disabled".green
|
314
|
+
|
315
|
+
# Check if we need to wait for a job
|
316
|
+
if response.headers["location"]
|
317
|
+
job_id = response.headers["location"].split("/").last
|
318
|
+
wait_for_job(job_id)
|
319
|
+
end
|
320
|
+
|
321
|
+
return true
|
322
|
+
else
|
323
|
+
error_message = "Failed to disable controller encryption. Status code: #{response.status}"
|
324
|
+
|
325
|
+
begin
|
326
|
+
error_data = JSON.parse(response.body)
|
327
|
+
error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
|
328
|
+
rescue
|
329
|
+
# Ignore JSON parsing errors
|
330
|
+
end
|
331
|
+
|
332
|
+
raise Error, error_message
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
# Check if all physical disks are Self-Encrypting Drives
|
337
|
+
def all_seds?(drives)
|
338
|
+
drives.all? { |d| d[:encryption_ability] == "SelfEncryptingDrive" }
|
339
|
+
end
|
340
|
+
|
341
|
+
# Check if the system is ready for SED operations
|
342
|
+
def sed_ready?(controller, drives)
|
343
|
+
all_seds?(drives) && controller_encryption_capable?(controller) && controller_encryption_enabled?(controller)
|
344
|
+
end
|
345
|
+
|
346
|
+
# Get firmware version
|
347
|
+
def get_firmware_version
|
348
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1?$select=FirmwareVersion")
|
349
|
+
|
350
|
+
if response.status == 200
|
351
|
+
begin
|
352
|
+
data = JSON.parse(response.body)
|
353
|
+
return data["FirmwareVersion"]
|
354
|
+
rescue JSON::ParserError
|
355
|
+
raise Error, "Failed to parse firmware version response: #{response.body}"
|
356
|
+
end
|
357
|
+
else
|
358
|
+
# Try again without the $select parameter for older firmware
|
359
|
+
response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1")
|
360
|
+
|
361
|
+
if response.status == 200
|
362
|
+
begin
|
363
|
+
data = JSON.parse(response.body)
|
364
|
+
return data["FirmwareVersion"]
|
365
|
+
rescue JSON::ParserError
|
366
|
+
raise Error, "Failed to parse firmware version response: #{response.body}"
|
367
|
+
end
|
368
|
+
else
|
369
|
+
raise Error, "Failed to get firmware version. Status code: #{response.status}"
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|