Module: IDRAC::Storage
- Included in:
- Client
- Defined in:
- lib/idrac/storage.rb
Instance Method Summary collapse
-
#all_seds?(drives) ⇒ Boolean
Check if all physical disks are Self-Encrypting Drives.
-
#controller_encryption_capable?(controller) ⇒ Boolean
Check if the controller is capable of encryption.
-
#controller_encryption_enabled?(controller) ⇒ Boolean
Check if controller encryption is enabled.
-
#controllers ⇒ Object
Get all storage controllers and return them as an array.
-
#create_virtual_disk(controller_id:, drives:, name: "vssd0", raid_type: "RAID5", encrypt: true) ⇒ Object
Create a new virtual disk with RAID5 and FastPath optimizations.
-
#create_virtual_disk_scp(controller_id:, drives:, name: "vssd0", raid_type: "RAID5", encrypt: true) ⇒ Object
System Configuration Profile - based VSSD0 This is required for older DELL iDRAC that doesn’t support the POST method with cache policies nor encryption.
-
#delete_volume(odata_id) ⇒ Object
Delete a volume.
-
#disable_local_key_management(controller_id) ⇒ Object
Disable Self-Encrypting Drive support on controller.
-
#drives(controller_id) ⇒ Object
Get information about physical drives.
-
#dump_drive_data(drives) ⇒ Object
Helper method to display drive data in raw format.
-
#enable_local_key_management(controller_id:, passphrase: "Secure123!", key_id: "RAID-Key-2023") ⇒ Object
Enable Self-Encrypting Drive support on controller.
-
#fastpath_good?(volume) ⇒ Boolean
Check if FastPath is properly configured for a volume.
-
#find_controller(name_pattern: "PERC", prefer_most_drives_by_count: false, prefer_most_drives_by_size: false) ⇒ Hash
Find the best controller based on preference flags.
-
#get_firmware_version ⇒ Object
Get firmware version.
-
#sed_ready?(controller, drives) ⇒ Boolean
Check if the system is ready for SED operations.
-
#volumes(controller_id) ⇒ Object
Get information about virtual disk volumes.
Instance Method Details
#all_seds?(drives) ⇒ Boolean
Check if all physical disks are Self-Encrypting Drives
516 517 518 |
# File 'lib/idrac/storage.rb', line 516 def all_seds?(drives) drives.all? { |d| d["encryption_ability"] == "SelfEncryptingDrive" } end |
#controller_encryption_capable?(controller) ⇒ Boolean
Check if the controller is capable of encryption
554 555 556 |
# File 'lib/idrac/storage.rb', line 554 def controller_encryption_capable?(controller) controller.dig("encryption_capability") =~ /localkey/i end |
#controller_encryption_enabled?(controller) ⇒ Boolean
Check if controller encryption is enabled
559 560 561 |
# File 'lib/idrac/storage.rb', line 559 def controller_encryption_enabled?(controller) controller.dig("encryption_mode") =~ /localkey/i end |
#controllers ⇒ Object
Get all storage controllers and return them as an array
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 |
# File 'lib/idrac/storage.rb', line 7 def controllers response = authenticated_request(:get, '/redfish/v1/Systems/System.Embedded.1/Storage?$expand=*($levels=1)') if response.status == 200 begin data = JSON.parse(response.body) # Transform and return all controllers as an array of hashes with string keys controllers = data["Members"].map do |controller| { "name" => controller["Name"], "model" => controller["Model"], "drives_count" => controller["Drives"].size, "status" => controller.dig("Status", "Health") || "N/A", "firmware_version" => controller.dig("StorageControllers", 0, "FirmwareVersion"), "encryption_mode" => controller.dig("Oem", "Dell", "DellController", "EncryptionMode"), "encryption_capability" => controller.dig("Oem", "Dell", "DellController", "EncryptionCapability"), "controller_type" => controller.dig("Oem", "Dell", "DellController", "ControllerType"), "pci_slot" => controller.dig("Oem", "Dell", "DellController", "PCISlot"), "raw" => controller, "@odata.id" => controller["@odata.id"] } end return controllers.sort_by { |c| c["name"] } rescue JSON::ParserError raise Error, "Failed to parse controllers response: #{response.body}" end else raise Error, "Failed to get controllers. Status code: #{response.status}" end end |
#create_virtual_disk(controller_id:, drives:, name: "vssd0", raid_type: "RAID5", encrypt: true) ⇒ Object
Create a new virtual disk with RAID5 and FastPath optimizations
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 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 |
# File 'lib/idrac/storage.rb', line 299 def create_virtual_disk(controller_id:, drives:, name: "vssd0", raid_type: "RAID5", encrypt: true) raise "Drives must be an array of @odata.id strings" unless drives.all? { |d| d.is_a?(String) } # Get firmware version to determine approach firmware_version = get_firmware_version.split(".")[0,2].join.to_i # For older iDRAC firmware, use SCP method instead of API if firmware_version < 440 return create_virtual_disk_scp( controller_id: controller_id, drives: drives, name: name, raid_type: raid_type, encrypt: encrypt ) end # For newer firmware, use Redfish API drive_refs = drives.map { |d| { "@odata.id" => d.to_s } } # [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) payload = { "Links" => { "Drives" => drive_refs }, "Name" => name, "OptimumIOSizeBytes" => 64 * 1024, "Oem" => { "Dell" => { "DellVolume" => { "DiskCachePolicy" => "Enabled" } } }, "ReadCachePolicy" => "Off", # "NoReadAhead" "WriteCachePolicy" => "WriteThrough" } # For modern firmware if drives.size < 3 && raid_type == "RAID5" debug "Less than 3 drives. Selecting RAID0.", 1, :red payload["RAIDType"] = "RAID0" else payload["RAIDType"] = raid_type end payload["Encrypted"] = true if encrypt response = authenticated_request( :post, "#{controller_id}/Volumes", body: payload.to_json, headers: { 'Content-Type' => 'application/json' } ) handle_response(response) end |
#create_virtual_disk_scp(controller_id:, drives:, name: "vssd0", raid_type: "RAID5", encrypt: true) ⇒ Object
System Configuration Profile - based VSSD0
This is required for older DELL iDRAC that
doesn't support the POST method with cache policies
nor encryption.
When we remove 630/730's, we can remove this.
We want one volume – vssd0, RAID5, NO READ AHEAD, WRITE THROUGH, 64K STRIPE, ALL DISKS All we are doing here is manually setting WriteThrough. The rest is set correctly from the create_vssd0_post method. [FastPath](www.dell.com/support/manuals/en-us/poweredge-r7525/perc11_ug/fastpath?guid=guid-a9e90946-a41f-48ab-88f1-9ce514b4c414&lang=en-us) The PERC 11 series of cards support FastPath. To enable FastPath on a virtual disk, the cache policies of the RAID controller must be set to **write-through and no read ahead**. This enables FastPath to use the proper data path through the controller based on command (read/write), I/O size, and RAID type. For optimal solid-state drive performance, create virtual disks with **strip size of 64 KB**. Rest from: github.com/dell/iDRAC-Redfish-Scripting/blob/cc88a3db1bfb6cb5c6eea938ea6da67a84fb1dad/Redfish%20Python/CreateVirtualDiskREDFISH.py Create a RAID virtual disk using SCP for older iDRAC firmware
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'lib/idrac/storage.rb', line 369 def create_virtual_disk_scp(controller_id:, drives:, name: "vssd0", raid_type: "RAID5", encrypt: true) # Extract the controller FQDD from controller_id controller_fqdd = controller_id.split("/").last # Get drive IDs in the required format drive_ids = drives.map do |drive_path| # Extract the disk FQDD from @odata.id drive_id = drive_path.split("/").last if drive_id.include?(":") # Already in FQDD format drive_id else # Need to convert to FQDD format "Disk.Bay.#{drive_id}:#{controller_fqdd}" end end debugger # Map RAID type to proper format raid_level = case raid_type when "RAID0" then "0" when "RAID1" then "1" when "RAID5" then "5" when "RAID6" then "6" when "RAID10" then "10" else raid_type.gsub("RAID", "") end # Create the virtual disk component vd_component = { "FQDD" => "Disk.Virtual.0:#{controller_fqdd}", "Attributes" => [ { "Name" => "RAIDaction", "Value" => "Create", "Set On Import" => "True" }, { "Name" => "Name", "Value" => name, "Set On Import" => "True" }, { "Name" => "RAIDTypes", "Value" => "RAID #{raid_level}", "Set On Import" => "True" }, { "Name" => "StripeSize", "Value" => "64KB", "Set On Import" => "True" }, # 64KB needed for FastPath { "Name" => "RAIDdefaultWritePolicy", "Value" => "WriteThrough", "Set On Import" => "True" }, { "Name" => "RAIDdefaultReadPolicy", "Value" => "NoReadAhead", "Set On Import" => "True" }, { "Name" => "DiskCachePolicy", "Value" => "Enabled", "Set On Import" => "True" } ] } # Add encryption if requested if encrypt vd_component["Attributes"] << { "Name" => "LockStatus", "Value" => "Unlocked", "Set On Import" => "True" } end # Add the include physical disks drive_ids.each do |disk_id| vd_component["Attributes"] << { "Name" => "IncludedPhysicalDiskID", "Value" => disk_id, "Set On Import" => "True" } end # Create an SCP with the controller component that contains the VD component controller_component = { "FQDD" => controller_fqdd, "Components" => [vd_component] } # Apply the SCP scp = { "SystemConfiguration" => { "Components" => [controller_component] } } result = set_system_configuration_profile(scp, target: "RAID", reboot: false) if result[:status] == :success return { status: :success, job_id: result[:job_id] } else raise Error, "Failed to create virtual disk: #{result[:error] || 'Unknown error'}" end end |
#delete_volume(odata_id) ⇒ Object
Delete a volume
289 290 291 292 293 294 295 296 |
# File 'lib/idrac/storage.rb', line 289 def delete_volume(odata_id) path = odata_id.split("v1/").last puts "Deleting volume: #{path}" response = authenticated_request(:delete, "/redfish/v1/#{path}") handle_response(response) end |
#disable_local_key_management(controller_id) ⇒ Object
Disable Self-Encrypting Drive support on controller
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 |
# File 'lib/idrac/storage.rb', line 481 def disable_local_key_management(controller_id) payload = { "TargetFQDD": controller_id } response = authenticated_request( :post, "/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.RemoveControllerKey", body: payload.to_json, headers: { 'Content-Type': 'application/json' } ) if response.status == 202 puts "Controller encryption disabled".green # Check if we need to wait for a job if response.headers["location"] job_id = response.headers["location"].split("/").last wait_for_job(job_id) end return true else = "Failed to disable controller encryption. Status code: #{response.status}" begin error_data = JSON.parse(response.body) += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message'] rescue # Ignore JSON parsing errors end raise Error, end end |
#drives(controller_id) ⇒ Object
Get information about physical drives
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 |
# File 'lib/idrac/storage.rb', line 93 def drives(controller_id) # expects @odata.id as string raise Error, "Controller ID not provided" unless controller_id raise Error, "Expected controller ID string, got #{controller_id.class}" unless controller_id.is_a?(String) controller_path = controller_id.split("v1/").last response = authenticated_request(:get, "/redfish/v1/#{controller_path}?$expand=*($levels=1)") if response.status == 200 begin data = JSON.parse(response.body) # Debug dump of drive data - this happens with -vv or -vvv dump_drive_data(data["Drives"]) drives = data["Drives"].map do |body| serial = body["SerialNumber"] serial = body["Identifiers"].first["DurableName"] if serial.blank? { "serial" => serial, "model" => body["Model"], "name" => body["Name"], "capacity_bytes" => body["CapacityBytes"], "health" => body.dig("Status", "Health") || "N/A", "speed_gbp" => body["CapableSpeedGbs"], "manufacturer" => body["Manufacturer"], "media_type" => body["MediaType"], "failure_predicted" => body["FailurePredicted"], "life_left_percent" => body["PredictedMediaLifeLeftPercent"], "certified" => body.dig("Oem", "Dell", "DellPhysicalDisk", "Certified"), "raid_status" => body.dig("Oem", "Dell", "DellPhysicalDisk", "RaidStatus"), "operation_name" => body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationName"), "operation_progress" => body.dig("Oem", "Dell", "DellPhysicalDisk", "OperationPercentCompletePercent"), "encryption_ability" => body["EncryptionAbility"], "@odata.id" => body["@odata.id"] } end return drives.sort_by { |d| d["name"] } rescue JSON::ParserError raise Error, "Failed to parse drives response: #{response.body}" end else raise Error, "Failed to get drives. Status code: #{response.status}" end end |
#dump_drive_data(drives) ⇒ Object
Helper method to display drive data in raw format
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/idrac/storage.rb', line 140 def dump_drive_data(drives) self.debug "\n===== RAW DRIVE API DATA =====".green.bold drives.each_with_index do |drive, index| self.debug "\nDrive #{index + 1}: #{drive["Name"]}".cyan.bold self.debug "PredictedMediaLifeLeftPercent: #{drive["PredictedMediaLifeLeftPercent"].inspect}".yellow # Show other wear-related fields if they exist wear_fields = drive.keys.select { |k| k.to_s =~ /wear|life|health|predict/i } wear_fields.each do |field| self.debug "#{field}: #{drive[field].inspect}".yellow unless field == "PredictedMediaLifeLeftPercent" end # Show all data for full debug (verbosity level 3 / -vvv) self.debug "\nAll Drive Data:".light_magenta.bold self.debug JSON.pretty_generate(drive) end self.debug "\n===== END RAW DRIVE DATA =====\n".green.bold end |
#enable_local_key_management(controller_id:, passphrase: "Secure123!", key_id: "RAID-Key-2023") ⇒ Object
Enable Self-Encrypting Drive support on controller
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 |
# File 'lib/idrac/storage.rb', line 442 def enable_local_key_management(controller_id:, passphrase: "Secure123!", key_id: "RAID-Key-2023") payload = { "TargetFQDD": controller_id, "Key": passphrase, "Keyid": key_id } response = authenticated_request( :post, "/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.SetControllerKey", body: payload.to_json, headers: { 'Content-Type': 'application/json' } ) if response.status == 202 puts "Controller encryption enabled".green # Check if we need to wait for a job if response.headers["location"] job_id = response.headers["location"].split("/").last wait_for_job(job_id) end return true else = "Failed to enable controller encryption. Status code: #{response.status}" begin error_data = JSON.parse(response.body) += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message'] rescue # Ignore JSON parsing errors end raise Error, end end |
#fastpath_good?(volume) ⇒ Boolean
Check if FastPath is properly configured for a volume
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/idrac/storage.rb', line 268 def fastpath_good?(volume) return "disabled" unless volume # 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: firmware_version = get_firmware_version.split(".")[0,2].join.to_i if firmware_version < 440 stripe_size = "64KB" else stripe_size = volume["stripe_size"] end if volume["write_cache_policy"] == "WriteThrough" && volume["read_cache_policy"] == "NoReadAhead" && stripe_size == "64KB" return "enabled" else return "disabled" end end |
#find_controller(name_pattern: "PERC", prefer_most_drives_by_count: false, prefer_most_drives_by_size: false) ⇒ Hash
Find the best controller based on preference flags
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 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/idrac/storage.rb', line 45 def find_controller(name_pattern: "PERC", prefer_most_drives_by_count: false, prefer_most_drives_by_size: false) all_controllers = controllers return nil if all_controllers.empty? # Filter by name pattern if provided if name_pattern pattern_matches = all_controllers.select { |c| c["name"] && c["name"].include?(name_pattern) } return pattern_matches.first if pattern_matches.any? end selected_controller = nil # If we prefer controllers by drive count if prefer_most_drives_by_count selected_controller = all_controllers.max_by { |c| c["drives_count"] || 0 } end # If we prefer controllers by total drive size if prefer_most_drives_by_size && !selected_controller # We need to calculate total drive size for each controller controller_with_most_capacity = nil max_capacity = -1 all_controllers.each do |controller| # Get the drives for this controller controller_drives = begin drives(controller["@odata.id"]) rescue [] # If we can't get drives, assume empty end # Calculate total capacity total_capacity = controller_drives.sum { |d| d["capacity_bytes"] || 0 } if total_capacity > max_capacity max_capacity = total_capacity controller_with_most_capacity = controller end end selected_controller = controller_with_most_capacity if controller_with_most_capacity end # Default to first controller if no preferences matched selected_controller || all_controllers.first end |
#get_firmware_version ⇒ Object
Get firmware version
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 |
# File 'lib/idrac/storage.rb', line 526 def get_firmware_version response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1?$select=FirmwareVersion") if response.status == 200 begin data = JSON.parse(response.body) return data["FirmwareVersion"] rescue JSON::ParserError raise Error, "Failed to parse firmware version response: #{response.body}" end else # Try again without the $select parameter for older firmware response = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1") if response.status == 200 begin data = JSON.parse(response.body) return data["FirmwareVersion"] rescue JSON::ParserError raise Error, "Failed to parse firmware version response: #{response.body}" end else raise Error, "Failed to get firmware version. Status code: #{response.status}" end end end |
#sed_ready?(controller, drives) ⇒ Boolean
Check if the system is ready for SED operations
521 522 523 |
# File 'lib/idrac/storage.rb', line 521 def sed_ready?(controller, drives) all_seds?(drives) && controller_encryption_capable?(controller) && controller_encryption_enabled?(controller) end |
#volumes(controller_id) ⇒ Object
Get information about virtual disk volumes
161 162 163 164 165 166 167 168 169 170 171 172 173 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 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 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 |
# File 'lib/idrac/storage.rb', line 161 def volumes(controller_id) # expects @odata.id as string raise Error, "Controller ID not provided" unless controller_id raise Error, "Expected controller ID string, got #{controller_id.class}" unless controller_id.is_a?(String) puts "Volumes (e.g. Arrays)".green odata_id_path = controller_id + "/Volumes" response = authenticated_request(:get, "#{odata_id_path}?$expand=*($levels=1)") if response.status == 200 begin data = JSON.parse(response.body) # Check if we need SCP data (older firmware) scp_data = nil controller_fqdd = controller_id.split("/").last # Get SCP data if needed (older firmware won't have these OEM attributes) if data["Members"].any? && data["Members"].first&.dig("Oem", "Dell", "DellVirtualDisk", "WriteCachePolicy").nil? scp_data = get_system_configuration_profile(target: "RAID") end volumes = data["Members"].map do |vol| drives = vol["Links"]["Drives"] volume_data = { "name" => vol["Name"], "capacity_bytes" => vol["CapacityBytes"], "volume_type" => vol["VolumeType"], "drives" => drives, "raid_level" => vol["RAIDType"], "encrypted" => vol["Encrypted"], "@odata.id" => vol["@odata.id"] } # Try to get cache policies from OEM data first (newer firmware) volume_data["write_cache_policy"] = vol.dig("Oem", "Dell", "DellVirtualDisk", "WriteCachePolicy") volume_data["read_cache_policy"] = vol.dig("Oem", "Dell", "DellVirtualDisk", "ReadCachePolicy") volume_data["stripe_size"] = vol.dig("Oem", "Dell", "DellVirtualDisk", "StripeSize") volume_data["lock_status"] = vol.dig("Oem", "Dell", "DellVirtualDisk", "LockStatus") # If we have SCP data and missing some policies, look them up from SCP if scp_data && (volume_data["write_cache_policy"].nil? || volume_data["read_cache_policy"].nil? || volume_data["stripe_size"].nil?) # Find controller component in SCP controller_comp = scp_data.dig("SystemConfiguration", "Components")&.find do |comp| comp["FQDD"] == controller_fqdd end if controller_comp # Try to find the matching virtual disk # Format is typically "Disk.Virtual.X:RAID...." vd_name = vol["Id"] || vol["Name"] vd_comp = controller_comp["Components"]&.find do |comp| comp["FQDD"] =~ /Disk\.Virtual\.\d+:#{controller_fqdd}/ end if vd_comp && vd_comp["Attributes"] # Extract values from SCP write_policy = vd_comp["Attributes"].find { |a| a["Name"] == "RAIDdefaultWritePolicy" } read_policy = vd_comp["Attributes"].find { |a| a["Name"] == "RAIDdefaultReadPolicy" } stripe = vd_comp["Attributes"].find { |a| a["Name"] == "StripeSize" } lock_status = vd_comp["Attributes"].find { |a| a["Name"] == "LockStatus" } raid_level = vd_comp["Attributes"].find { |a| a["Name"] == "RAIDTypes" } volume_data["write_cache_policy"] ||= write_policy&.dig("Value") volume_data["read_cache_policy"] ||= read_policy&.dig("Value") volume_data["stripe_size"] ||= stripe&.dig("Value") volume_data["lock_status"] ||= lock_status&.dig("Value") volume_data["raid_level"] ||= raid_level&.dig("Value") end end end # Check FastPath settings volume_data["fastpath"] = fastpath_good?(volume_data) # Handle volume operations and status if vol["Operations"].any? volume_data["health"] = vol.dig("Status", "Health") || "N/A" volume_data["progress"] = vol["Operations"].first["PercentageComplete"] volume_data["message"] = vol["Operations"].first["OperationName"] elsif vol.dig("Status", "Health") == "OK" volume_data["health"] = "OK" volume_data["progress"] = nil volume_data["message"] = nil else volume_data["health"] = "?" volume_data["progress"] = nil volume_data["message"] = nil end volume_data end return volumes.sort_by { |d| d["name"] } rescue JSON::ParserError raise Error, "Failed to parse volumes response: #{response.body}" end else raise Error, "Failed to get volumes. Status code: #{response.status}" end end |