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.
@@ -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