module IDRAC::Storage
def all_seds?(drives)
def all_seds?(drives) drives.all? { |d| d["encryption_ability"] == "SelfEncryptingDrive" } end
def controller_encryption_capable?(controller)
def controller_encryption_capable?(controller) controller.dig("encryption_capability") =~ /localkey/i end
def controller_encryption_enabled?(controller)
def controller_encryption_enabled?(controller) controller.dig("encryption_mode") =~ /localkey/i end
def controllers
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
def create_virtual_disk(controller_id:, drives:, name: "vssd0", raid_type: "RAID5", encrypt: true)
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
def create_virtual_disk_scp(controller_id:, drives:, name: "vssd0", raid_type: "RAID5", encrypt: true)
https://github.com/dell/iDRAC-Redfish-Scripting/blob/cc88a3db1bfb6cb5c6eea938ea6da67a84fb1dad/Redfish%20Python/CreateVirtualDiskREDFISH.py
Rest from:
create virtual disks with **strip size of 64 KB**.
(read/write), I/O size, and RAID type. For optimal solid-state drive performance,
This enables FastPath to use the proper data path through the controller based on command
cache policies of the RAID controller must be set to **write-through and no read ahead**.
The PERC 11 series of cards support FastPath. To enable FastPath on a virtual disk, the
[FastPath](https://www.dell.com/support/manuals/en-us/poweredge-r7525/perc11_ug/fastpath?guid=guid-a9e90946-a41f-48ab-88f1-9ce514b4c414&lang=en-us)
the create_vssd0_post method.
All we are doing here is manually setting WriteThrough. The rest is set correctly from
We want one volume -- vssd0, RAID5, NO READ AHEAD, WRITE THROUGH, 64K STRIPE, ALL DISKS
#######################################################
When we remove 630/730's, we can remove this.
nor encryption.
doesn't support the POST method with cache policies
This is required for older DELL iDRAC that
System Configuration Profile - based VSSD0
#######################################################
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
def delete_volume(odata_id)
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
def disable_local_key_management(controller_id)
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 error_message = "Failed to disable controller encryption. Status code: #{response.status}" begin error_data = JSON.parse(response.body) error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message'] rescue # Ignore JSON parsing errors end raise Error, error_message end end
def drives(controller_id) # expects @odata.id as string
Get information about physical drives
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
def dump_drive_data(drives)
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
def enable_local_key_management(controller_id:, passphrase: "Secure123!", key_id: "RAID-Key-2023")
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 error_message = "Failed to enable controller encryption. Status code: #{response.status}" begin error_data = JSON.parse(response.body) error_message += ", Message: #{error_data['error']['message']}" if error_data['error'] && error_data['error']['message'] rescue # Ignore JSON parsing errors end raise Error, error_message end end
def fastpath_good?(volume)
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
def find_controller(name_pattern: "PERC", prefer_most_drives_by_count: false, prefer_most_drives_by_size: false)
-
(Hash)
- The selected controller
Parameters:
-
prefer_most_drives_by_size
(Boolean
) -- Prefer controllers with larger total drive capacity -
prefer_most_drives_by_count
(Boolean
) -- Prefer controllers with more drives -
name_pattern
(String
) -- Regex pattern to match controller name (defaults to "PERC")
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
def get_firmware_version
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
def sed_ready?(controller, drives)
def sed_ready?(controller, drives) all_seds?(drives) && controller_encryption_capable?(controller) && controller_encryption_enabled?(controller) end
def volumes(controller_id) # expects @odata.id as string
Get information about virtual disk volumes
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