idrac 0.7.1 → 0.7.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.
data/lib/idrac/storage.rb CHANGED
@@ -3,32 +3,6 @@ require 'colorize'
3
3
 
4
4
  module IDRAC
5
5
  module Storage
6
- # Get storage controllers information
7
- def controller
8
- # Use the controllers method to get all controllers
9
- controller_list = controllers
10
-
11
- puts "Controllers".green
12
- controller_list.each { |c| puts "#{c["name"]} > #{c["drives_count"]}" }
13
-
14
- puts "Drives".green
15
- controller_list.each do |c|
16
- puts "Storage: #{c["name"]} > #{c["status"]} > #{c["drives_count"]}"
17
- end
18
-
19
- # Find the controller with the most drives (usually the PERC)
20
- controller_info = controller_list.max_by { |c| c["drives_count"] }
21
-
22
- if controller_info["name"] =~ /PERC/
23
- puts "Found #{controller_info["name"]}".green
24
- else
25
- puts "Found #{controller_info["name"]} but continuing...".yellow
26
- end
27
-
28
- # Return the raw controller data
29
- controller_info["raw"]
30
- end
31
-
32
6
  # Get all storage controllers and return them as an array
33
7
  def controllers
34
8
  response = authenticated_request(:get, '/redfish/v1/Systems/System.Embedded.1/Storage?$expand=*($levels=1)')
@@ -50,7 +24,6 @@ module IDRAC
50
24
  "controller_type" => controller.dig("Oem", "Dell", "DellController", "ControllerType"),
51
25
  "pci_slot" => controller.dig("Oem", "Dell", "DellController", "PCISlot"),
52
26
  "raw" => controller,
53
- "volumes_odata_id" => controller.dig("Volumes", "@odata.id"),
54
27
  "@odata.id" => controller["@odata.id"]
55
28
  }
56
29
  end
@@ -64,17 +37,73 @@ module IDRAC
64
37
  end
65
38
  end
66
39
 
40
+ # Find the best controller based on preference flags
41
+ # @param name_pattern [String] Regex pattern to match controller name (defaults to "PERC")
42
+ # @param prefer_most_drives_by_count [Boolean] Prefer controllers with more drives
43
+ # @param prefer_most_drives_by_size [Boolean] Prefer controllers with larger total drive capacity
44
+ # @return [Hash] The selected controller
45
+ def find_controller(name_pattern: "PERC", prefer_most_drives_by_count: false, prefer_most_drives_by_size: false)
46
+ all_controllers = controllers
47
+ return nil if all_controllers.empty?
48
+
49
+ # Filter by name pattern if provided
50
+ if name_pattern
51
+ pattern_matches = all_controllers.select { |c| c["name"] && c["name"].include?(name_pattern) }
52
+ return pattern_matches.first if pattern_matches.any?
53
+ end
54
+
55
+ selected_controller = nil
56
+
57
+ # If we prefer controllers by drive count
58
+ if prefer_most_drives_by_count
59
+ selected_controller = all_controllers.max_by { |c| c["drives_count"] || 0 }
60
+ end
61
+
62
+ # If we prefer controllers by total drive size
63
+ if prefer_most_drives_by_size && !selected_controller
64
+ # We need to calculate total drive size for each controller
65
+ controller_with_most_capacity = nil
66
+ max_capacity = -1
67
+
68
+ all_controllers.each do |controller|
69
+ # Get the drives for this controller
70
+ controller_drives = begin
71
+ drives(controller["@odata.id"])
72
+ rescue
73
+ [] # If we can't get drives, assume empty
74
+ end
75
+
76
+ # Calculate total capacity
77
+ total_capacity = controller_drives.sum { |d| d["capacity_bytes"] || 0 }
78
+
79
+ if total_capacity > max_capacity
80
+ max_capacity = total_capacity
81
+ controller_with_most_capacity = controller
82
+ end
83
+ end
84
+
85
+ selected_controller = controller_with_most_capacity if controller_with_most_capacity
86
+ end
87
+
88
+ # Default to first controller if no preferences matched
89
+ selected_controller || all_controllers.first
90
+ end
91
+
67
92
  # Get information about physical drives
68
- def drives(controller)
69
- raise Error, "Controller not provided" unless controller
93
+ def drives(controller_id) # expects @odata.id as string
94
+ raise Error, "Controller ID not provided" unless controller_id
95
+ raise Error, "Expected controller ID string, got #{controller_id.class}" unless controller_id.is_a?(String)
70
96
 
71
- odata_id_path = controller["@odata.id"] || controller["odata_id"]
72
- controller_path = odata_id_path.split("v1/").last
97
+ controller_path = controller_id.split("v1/").last
73
98
  response = authenticated_request(:get, "/redfish/v1/#{controller_path}?$expand=*($levels=1)")
74
99
 
75
100
  if response.status == 200
76
101
  begin
77
102
  data = JSON.parse(response.body)
103
+
104
+ # Debug dump of drive data - this happens with -vv or -vvv
105
+ dump_drive_data(data["Drives"])
106
+
78
107
  drives = data["Drives"].map do |body|
79
108
  serial = body["SerialNumber"]
80
109
  serial = body["Identifiers"].first["DurableName"] if serial.blank?
@@ -107,21 +136,51 @@ module IDRAC
107
136
  end
108
137
  end
109
138
 
139
+ # Helper method to display drive data in raw format
140
+ def dump_drive_data(drives)
141
+
142
+ self.debug "\n===== RAW DRIVE API DATA =====".green.bold
143
+ drives.each_with_index do |drive, index|
144
+ self.debug "\nDrive #{index + 1}: #{drive["Name"]}".cyan.bold
145
+ self.debug "PredictedMediaLifeLeftPercent: #{drive["PredictedMediaLifeLeftPercent"].inspect}".yellow
146
+
147
+ # Show other wear-related fields if they exist
148
+ wear_fields = drive.keys.select { |k| k.to_s =~ /wear|life|health|predict/i }
149
+ wear_fields.each do |field|
150
+ self.debug "#{field}: #{drive[field].inspect}".yellow unless field == "PredictedMediaLifeLeftPercent"
151
+ end
152
+
153
+ # Show all data for full debug (verbosity level 3 / -vvv)
154
+ self.debug "\nAll Drive Data:".light_magenta.bold
155
+ self.debug JSON.pretty_generate(drive)
156
+ end
157
+ self.debug "\n===== END RAW DRIVE DATA =====\n".green.bold
158
+ end
159
+
110
160
  # Get information about virtual disk volumes
111
- def volumes(controller)
112
- raise Error, "Controller not provided" unless controller
161
+ def volumes(controller_id) # expects @odata.id as string
162
+ raise Error, "Controller ID not provided" unless controller_id
163
+ raise Error, "Expected controller ID string, got #{controller_id.class}" unless controller_id.is_a?(String)
113
164
 
114
165
  puts "Volumes (e.g. Arrays)".green
115
166
 
116
- odata_id_path = controller["volumes_odata_id"]
117
- if odata_id_path.nil?
118
- raise Error, "No volumes_odata_id found in controller data. Make sure the controller is properly initialized."
119
- end
167
+ odata_id_path = controller_id + "/Volumes"
120
168
  response = authenticated_request(:get, "#{odata_id_path}?$expand=*($levels=1)")
121
169
 
122
170
  if response.status == 200
123
171
  begin
124
172
  data = JSON.parse(response.body)
173
+
174
+ # Check if we need SCP data (older firmware)
175
+ scp_data = nil
176
+ controller_fqdd = controller_id.split("/").last
177
+
178
+ # Get SCP data if needed (older firmware won't have these OEM attributes)
179
+ if data["Members"].any? &&
180
+ data["Members"].first&.dig("Oem", "Dell", "DellVirtualDisk", "WriteCachePolicy").nil?
181
+ scp_data = get_system_configuration_profile(target: "RAID")
182
+ end
183
+
125
184
  volumes = data["Members"].map do |vol|
126
185
  drives = vol["Links"]["Drives"]
127
186
  volume_data = {
@@ -129,15 +188,52 @@ module IDRAC
129
188
  "capacity_bytes" => vol["CapacityBytes"],
130
189
  "volume_type" => vol["VolumeType"],
131
190
  "drives" => drives,
132
- "write_cache_policy" => vol.dig("Oem", "Dell", "DellVirtualDisk", "WriteCachePolicy"),
133
- "read_cache_policy" => vol.dig("Oem", "Dell", "DellVirtualDisk", "ReadCachePolicy"),
134
- "stripe_size" => vol.dig("Oem", "Dell", "DellVirtualDisk", "StripeSize"),
135
191
  "raid_level" => vol["RAIDType"],
136
192
  "encrypted" => vol["Encrypted"],
137
- "lock_status" => vol.dig("Oem", "Dell", "DellVirtualDisk", "LockStatus"),
138
193
  "@odata.id" => vol["@odata.id"]
139
194
  }
140
195
 
196
+ # Try to get cache policies from OEM data first (newer firmware)
197
+ volume_data["write_cache_policy"] = vol.dig("Oem", "Dell", "DellVirtualDisk", "WriteCachePolicy")
198
+ volume_data["read_cache_policy"] = vol.dig("Oem", "Dell", "DellVirtualDisk", "ReadCachePolicy")
199
+ volume_data["stripe_size"] = vol.dig("Oem", "Dell", "DellVirtualDisk", "StripeSize")
200
+ volume_data["lock_status"] = vol.dig("Oem", "Dell", "DellVirtualDisk", "LockStatus")
201
+
202
+ # If we have SCP data and missing some policies, look them up from SCP
203
+ if scp_data && (volume_data["write_cache_policy"].nil? ||
204
+ volume_data["read_cache_policy"].nil? ||
205
+ volume_data["stripe_size"].nil?)
206
+
207
+ # Find controller component in SCP
208
+ controller_comp = scp_data.dig("SystemConfiguration", "Components")&.find do |comp|
209
+ comp["FQDD"] == controller_fqdd
210
+ end
211
+
212
+ if controller_comp
213
+ # Try to find the matching virtual disk
214
+ # Format is typically "Disk.Virtual.X:RAID...."
215
+ vd_name = vol["Id"] || vol["Name"]
216
+ vd_comp = controller_comp["Components"]&.find do |comp|
217
+ comp["FQDD"] =~ /Disk\.Virtual\.\d+:#{controller_fqdd}/
218
+ end
219
+
220
+ if vd_comp && vd_comp["Attributes"]
221
+ # Extract values from SCP
222
+ write_policy = vd_comp["Attributes"].find { |a| a["Name"] == "RAIDdefaultWritePolicy" }
223
+ read_policy = vd_comp["Attributes"].find { |a| a["Name"] == "RAIDdefaultReadPolicy" }
224
+ stripe = vd_comp["Attributes"].find { |a| a["Name"] == "StripeSize" }
225
+ lock_status = vd_comp["Attributes"].find { |a| a["Name"] == "LockStatus" }
226
+ raid_level = vd_comp["Attributes"].find { |a| a["Name"] == "RAIDTypes" }
227
+
228
+ volume_data["write_cache_policy"] ||= write_policy&.dig("Value")
229
+ volume_data["read_cache_policy"] ||= read_policy&.dig("Value")
230
+ volume_data["stripe_size"] ||= stripe&.dig("Value")
231
+ volume_data["lock_status"] ||= lock_status&.dig("Value")
232
+ volume_data["raid_level"] ||= raid_level&.dig("Value")
233
+ end
234
+ end
235
+ end
236
+
141
237
  # Check FastPath settings
142
238
  volume_data["fastpath"] = fastpath_good?(volume_data)
143
239
 
@@ -156,7 +252,6 @@ module IDRAC
156
252
  volume_data["message"] = nil
157
253
  end
158
254
 
159
- # Return the hash directly
160
255
  volume_data
161
256
  end
162
257
 
@@ -172,11 +267,18 @@ module IDRAC
172
267
  # Check if FastPath is properly configured for a volume
173
268
  def fastpath_good?(volume)
174
269
  return "disabled" unless volume
175
-
176
- # Modern firmware check handled by caller
270
+
271
+ # Note for older firmware, the stripe size is misreported as 128KB when it is actually 64KB (seen through the DELL Web UI), so ignore that:
272
+ firmware_version = get_firmware_version.split(".")[0,2].join.to_i
273
+ if firmware_version < 440
274
+ stripe_size = "64KB"
275
+ else
276
+ stripe_size = volume["stripe_size"]
277
+ end
278
+
177
279
  if volume["write_cache_policy"] == "WriteThrough" &&
178
280
  volume["read_cache_policy"] == "NoReadAhead" &&
179
- volume["stripe_size"] == "64KB"
281
+ stripe_size == "64KB"
180
282
  return "enabled"
181
283
  else
182
284
  return "disabled"
@@ -189,101 +291,159 @@ module IDRAC
189
291
  puts "Deleting volume: #{path}"
190
292
 
191
293
  response = authenticated_request(:delete, "/redfish/v1/#{path}")
192
-
193
- if response.status.between?(200, 299)
194
- puts "Delete volume request sent".green
195
-
196
- # Check if we need to wait for a job
197
- if response.headers["location"]
198
- job_id = response.headers["location"].split("/").last
199
- wait_for_job(job_id)
200
- end
201
-
202
- return true
203
- else
204
- error_message = "Failed to delete volume. Status code: #{response.status}"
205
-
206
- begin
207
- error_data = JSON.parse(response.body)
208
- error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
209
- rescue
210
- # Ignore JSON parsing errors
211
- end
212
-
213
- raise Error, error_message
214
- end
294
+
295
+ handle_response(response)
215
296
  end
216
297
 
217
298
  # Create a new virtual disk with RAID5 and FastPath optimizations
218
- def create_virtual_disk(controller_id, drives, name: "vssd0", raid_type: "RAID5")
219
- # Check firmware version to determine which API to use
299
+ def create_virtual_disk(controller_id:, drives:, name: "vssd0", raid_type: "RAID5", encrypt: true)
300
+ raise "Drives must be an array of @odata.id strings" unless drives.all? { |d| d.is_a?(String) }
301
+
302
+ # Get firmware version to determine approach
220
303
  firmware_version = get_firmware_version.split(".")[0,2].join.to_i
221
304
 
305
+ # For older iDRAC firmware, use SCP method instead of API
306
+ if firmware_version < 440
307
+ return create_virtual_disk_scp(
308
+ controller_id: controller_id,
309
+ drives: drives,
310
+ name: name,
311
+ raid_type: raid_type,
312
+ encrypt: encrypt
313
+ )
314
+ end
315
+
316
+ # For newer firmware, use Redfish API
317
+ drive_refs = drives.map { |d| { "@odata.id" => d.to_s } }
318
+
222
319
  # [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)
223
320
  payload = {
224
- "Drives": drives.map { |d| { "@odata.id": d["@odata.id"] } },
225
- "Name": name,
226
- "OptimumIOSizeBytes": 64 * 1024,
227
- "Oem": { "Dell": { "DellVolume": { "DiskCachePolicy": "Enabled" } } },
228
- "ReadCachePolicy": "Off", # "NoReadAhead"
229
- "WriteCachePolicy": "WriteThrough"
321
+ "Links" => { "Drives" => drive_refs },
322
+ "Name" => name,
323
+ "OptimumIOSizeBytes" => 64 * 1024,
324
+ "Oem" => { "Dell" => { "DellVolume" => { "DiskCachePolicy" => "Enabled" } } },
325
+ "ReadCachePolicy" => "Off", # "NoReadAhead"
326
+ "WriteCachePolicy" => "WriteThrough"
230
327
  }
231
328
 
232
- # If the firmware < 440, we need a different approach
233
- if firmware_version >= 440
234
- # For modern firmware
235
- if drives.size < 3 && raid_type == "RAID5"
236
- puts "*************************************************".red
237
- puts "* WARNING: Less than 3 drives. Selecting RAID0. *".red
238
- puts "*************************************************".red
239
- payload["RAIDType"] = "RAID0"
240
- else
241
- payload["RAIDType"] = raid_type
242
- end
329
+ # For modern firmware
330
+ if drives.size < 3 && raid_type == "RAID5"
331
+ debug "Less than 3 drives. Selecting RAID0.", 1, :red
332
+ payload["RAIDType"] = "RAID0"
243
333
  else
244
- # For older firmware
245
- payload["VolumeType"] = "StripedWithParity" if raid_type == "RAID5"
246
- payload["VolumeType"] = "SpannedDisks" if raid_type == "RAID0"
334
+ payload["RAIDType"] = raid_type
247
335
  end
248
336
 
249
- url = "Systems/System.Embedded.1/Storage/#{controller_id}/Volumes"
337
+ payload["Encrypted"] = true if encrypt
338
+
250
339
  response = authenticated_request(
251
340
  :post,
252
- "/redfish/v1/#{url}",
341
+ "#{controller_id}/Volumes",
253
342
  body: payload.to_json,
254
- headers: { 'Content-Type': 'application/json' }
343
+ headers: { 'Content-Type' => 'application/json' }
255
344
  )
256
345
 
257
- if response.status.between?(200, 299)
258
- puts "Virtual disk creation started".green
259
-
260
- # Check if we need to wait for a job
261
- if response.headers["location"]
262
- job_id = response.headers["location"].split("/").last
263
- wait_for_job(job_id)
346
+ handle_response(response)
347
+ end
348
+
349
+
350
+ ########################################################
351
+ # System Configuration Profile - based VSSD0
352
+ # This is required for older DELL iDRAC that
353
+ # doesn't support the POST method with cache policies
354
+ # nor encryption.
355
+ # When we remove 630/730's, we can remove this.
356
+ ########################################################
357
+ # We want one volume -- vssd0, RAID5, NO READ AHEAD, WRITE THROUGH, 64K STRIPE, ALL DISKS
358
+ # All we are doing here is manually setting WriteThrough. The rest is set correctly from
359
+ # the create_vssd0_post method.
360
+ # [FastPath](https://www.dell.com/support/manuals/en-us/poweredge-r7525/perc11_ug/fastpath?guid=guid-a9e90946-a41f-48ab-88f1-9ce514b4c414&lang=en-us)
361
+ # The PERC 11 series of cards support FastPath. To enable FastPath on a virtual disk, the
362
+ # cache policies of the RAID controller must be set to **write-through and no read ahead**.
363
+ # This enables FastPath to use the proper data path through the controller based on command
364
+ # (read/write), I/O size, and RAID type. For optimal solid-state drive performance,
365
+ # create virtual disks with **strip size of 64 KB**.
366
+ # Rest from:
367
+ # https://github.com/dell/iDRAC-Redfish-Scripting/blob/cc88a3db1bfb6cb5c6eea938ea6da67a84fb1dad/Redfish%20Python/CreateVirtualDiskREDFISH.py
368
+ # Create a RAID virtual disk using SCP for older iDRAC firmware
369
+ def create_virtual_disk_scp(controller_id:, drives:, name: "vssd0", raid_type: "RAID5", encrypt: true)
370
+ # Extract the controller FQDD from controller_id
371
+ controller_fqdd = controller_id.split("/").last
372
+
373
+ # Get drive IDs in the required format
374
+ drive_ids = drives.map do |drive_path|
375
+ # Extract the disk FQDD from @odata.id
376
+ drive_id = drive_path.split("/").last
377
+ if drive_id.include?(":") # Already in FQDD format
378
+ drive_id
379
+ else
380
+ # Need to convert to FQDD format
381
+ "Disk.Bay.#{drive_id}:#{controller_fqdd}"
264
382
  end
265
-
266
- return true
383
+ end
384
+
385
+ debugger
386
+ # Map RAID type to proper format
387
+ raid_level = case raid_type
388
+ when "RAID0" then "0"
389
+ when "RAID1" then "1"
390
+ when "RAID5" then "5"
391
+ when "RAID6" then "6"
392
+ when "RAID10" then "10"
393
+ else raid_type.gsub("RAID", "")
394
+ end
395
+
396
+ # Create the virtual disk component
397
+ vd_component = {
398
+ "FQDD" => "Disk.Virtual.0:#{controller_fqdd}",
399
+ "Attributes" => [
400
+ { "Name" => "RAIDaction", "Value" => "Create", "Set On Import" => "True" },
401
+ { "Name" => "Name", "Value" => name, "Set On Import" => "True" },
402
+ { "Name" => "RAIDTypes", "Value" => "RAID #{raid_level}", "Set On Import" => "True" },
403
+ { "Name" => "StripeSize", "Value" => "64KB", "Set On Import" => "True" }, # 64KB needed for FastPath
404
+ { "Name" => "RAIDdefaultWritePolicy", "Value" => "WriteThrough", "Set On Import" => "True" },
405
+ { "Name" => "RAIDdefaultReadPolicy", "Value" => "NoReadAhead", "Set On Import" => "True" },
406
+ { "Name" => "DiskCachePolicy", "Value" => "Enabled", "Set On Import" => "True" }
407
+ ]
408
+ }
409
+
410
+ # Add encryption if requested
411
+ if encrypt
412
+ vd_component["Attributes"] << { "Name" => "LockStatus", "Value" => "Unlocked", "Set On Import" => "True" }
413
+ end
414
+
415
+ # Add the include physical disks
416
+ drive_ids.each do |disk_id|
417
+ vd_component["Attributes"] << {
418
+ "Name" => "IncludedPhysicalDiskID",
419
+ "Value" => disk_id,
420
+ "Set On Import" => "True"
421
+ }
422
+ end
423
+
424
+ # Create an SCP with the controller component that contains the VD component
425
+ controller_component = {
426
+ "FQDD" => controller_fqdd,
427
+ "Components" => [vd_component]
428
+ }
429
+
430
+ # Apply the SCP
431
+ scp = { "SystemConfiguration" => { "Components" => [controller_component] } }
432
+ result = set_system_configuration_profile(scp, target: "RAID", reboot: false)
433
+
434
+ if result[:status] == :success
435
+ return { status: :success, job_id: result[:job_id] }
267
436
  else
268
- error_message = "Failed to create virtual disk. Status code: #{response.status}"
269
-
270
- begin
271
- error_data = JSON.parse(response.body)
272
- error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message']
273
- rescue
274
- # Ignore JSON parsing errors
275
- end
276
-
277
- raise Error, error_message
437
+ raise Error, "Failed to create virtual disk: #{result[:error] || 'Unknown error'}"
278
438
  end
279
439
  end
280
440
 
281
441
  # Enable Self-Encrypting Drive support on controller
282
- def enable_local_key_management(controller_id, passphrase: "Secure123!", keyid: "RAID-Key-2023")
442
+ def enable_local_key_management(controller_id:, passphrase: "Secure123!", key_id: "RAID-Key-2023")
283
443
  payload = {
284
444
  "TargetFQDD": controller_id,
285
445
  "Key": passphrase,
286
- "Keyid": keyid
446
+ "Keyid": key_id
287
447
  }
288
448
 
289
449
  response = authenticated_request(
@@ -400,4 +560,4 @@ module IDRAC
400
560
  controller.dig("encryption_mode") =~ /localkey/i
401
561
  end
402
562
  end
403
- end
563
+ end