08 January, 2015

Determine if a drive is SAN drive - OS

If you have servers connected to SAN you would think that you don't really have to worry about the physical representation of your volumes, in other words, you don't need to know how many physical drives (spindles) are behind your e.g. D: drive.
Life is not simple. There are times when you need to know if a volume is on a local hard disk or is on a SAN LUN. An example would be when you need to move the pagefile out of drive C:, you might not want to put that on the SAN disk (there can be several reasons, one is that the volume may be in a dynamic or clustered disk group and can fail over to another node or just simply because SAN disk space is expensive therefore using it for paging is not the best use of you dollars.)

Now we have a case: we have a drive letter and we want to decide if the volume is on a SAN disk or not. Of course we want to do this remotely and on 100+ servers.
The philosophical problem with this is that engineers spent so much effort in the last decades on creating storage systems and disk manager sub-systems to hide the complex details of a storage - including multipath fiber channels, SAN switches, disk arrays...etc.) from the OS and to make sure that the OS can see a volume and does not need to worry about how it's presented and what's behind it. So any API that could be used to track down SAN drives remotely are vendor specific (it's different for EMulex, Qlogic or whatever HBA driver). I need something universal.

I was poking around the WMI classes and noticed that bits of information are in several classes, so started connecting the dots - nothing scientific, just trial and error as usual. If you look long enough you can connect the drive letter to a PNPDeviceID which can tell you if the physical drive is local or SAN, here is an example:
  • Win32_LogicalDiskToPartition: D: -> Disk #0, Partition#0
  • Win32_DiskDriveToPartition: Disk #0, Partition#0 -> PHYSICALDRIVE2
  • Win32_DiskDrive: PHYSICALDRIVE2 -> MPIO\DISK&....
    If the PNPDeviceID starts with MPIO (which stands for MultiPath I/O) then it's a drive hosted on SAN Array which the host can see on multiple fiber channels.

Just need a bit of regex matching to walk through this in Powershell:









  1. Get the disk number where the volume is hosted (and the partition number as well):
    $DriveletterToDiskNumberQuery = gwmi -ComputerName $srv -Class Win32_LogicalDiskToPartition | ?{$_.Dependent -imatch "Win32_LogicalDisk\.DeviceID=`"$driveletter\:`""} | select -First 1 | %{$_.Antecedent}
  2. Need to parse the exact Disk # from the long text which is in the WMI instance:
    $DriveletterToDiskNumber = ([regex]::Match($DriveletterToDiskNumberQuery, "Disk #\d+, Partition #\d+")).Value
  3. Take the disk number and lookup the DeviceID:
    $DiskNumberToDevideIDQuery = gwmi -ComputerName $srv -Class Win32_DiskDriveToDiskPartition | ?{$_.Dependent -imatch $DriveletterToDiskNumber} | select -First 1 | %{$_.Antecedent}
  4. Parse the DeviceID from the long text:
    $DiskNumberToDevideID = ([regex]::Match($DiskNumberToDevideIDQuery, "PHYSICALDRIVE\d+")).Value
  5. Get the PNPDeviceID of the given device to see if it's MPIO or not:
    gwmi -ComputerName $srv -Class Win32_DiskDrive | ?{$_.DeviceID -imatch $DiskNumberToDevideID} | Select -First 1 | %{$_.PNPDeviceID}
A simple script which takes the list of hosts from the pipe and outputs and object with the hostname, the PNPDeviceID and the Drive Type would look like this (without handling errors e.g. when there's no drive D or the host is not accessible...etc.):
 $hostlist = @($input)  
 $driveletter = "D"  
   
 foreach($srv in $hostlist){  
    $obj = "" | Select ComputerName,DriveType,PNPDeviceID  
    $obj.ComputerName = $srv  
   
    # Get the list Disk # for the given volume  
    $DriveletterToDiskNumberQuery = gwmi -ComputerName $srv -Class Win32_LogicalDiskToPartition | ?{$_.Dependent -imatch "Win32_LogicalDisk\.DeviceID=`"$driveletter\:`""} | select -First 1 | %{$_.Antecedent}  
   
    # parse the Disk and partition # from the text  
    $DriveletterToDiskNumber = ([regex]::Match($DriveletterToDiskNumberQuery, "Disk #\d+, Partition #\d+")).Value  
   
    # Get the DeviceID of the given Disk #  
    $DiskNumberToDevideIDQuery = gwmi -ComputerName $srv -Class Win32_DiskDriveToDiskPartition | ?{$_.Dependent -imatch $DriveletterToDiskNumber} | select -First 1 | %{$_.Antecedent}  
      
    # parse the DeviceID from the text  
    $DiskNumberToDevideID = ([regex]::Match($DiskNumberToDevideIDQuery, "PHYSICALDRIVE\d+")).Value  
   
    # get the PNPDeviceID of the given Device  
    $obj.PNPDeviceID = gwmi -ComputerName $srv -Class Win32_DiskDrive | ?{$_.DeviceID -imatch $DiskNumberToDevideID} | Select -First 1 | %{$_.PNPDeviceID}  
   
    if($obj.PNPDeviceID -imatch "^mpio"){  
       $obj.DriveType = "SAN"  
    }  
    else{  
       $obj.DriveType = "Local"  
    }  
   
    $obj  
 }  


t

3 comments:

  1. Hey Tom,

    Thanks for your post. I was struggling with querying a few hundred Win2003 servers looking to find their WWN ids (if they had any) using MSFC_FibrePortHBAAttributes or MSFC_FCAdapterHBAAttributes but it turns out that those classes are almost always missing on Win2003 servers, even when they have an HBA installed. Your post helped me find out that only about 1 percent of my servers were actually SAN attached which made it a lot easier to manually log on to those 1 percent and retrieve their WWN ids.

    I am including a snippet of code which is a modification of yours in case it helps anybody else. My mod's loop through each drive to determine whether it is local or SAN attached.

    -------------------------------------------------------------------------------------------------

    strDriveAttachmentInfo = $null
    $strDriveletterToPartitionQuery= Get-WMIObject -ComputerName $strDeviceName -Credential $curCredential -Class Win32_LogicalDiskToPartition -ErrorAction Stop
    foreach ($strAssignedDriveLetter in $strDriveletterToPartitionQuery)
    {
    # parse the Disk and partition # from the text
    $strPartition = ([regex]::Match($strAssignedDriveLetter.Antecedent, "Disk #\d+, Partition #\d+")).Value
    $strDriveLetter = ([regex]::Match($strAssignedDriveLetter.Dependent, '(?<=")[^"]*(?=")')).Value

    # Get the DeviceID of the given Disk #
    $strPartitionToDeviceIDQuery = Get-WMIObject -ComputerName $strDeviceName -Credential $curCredential -Class Win32_DiskDriveToDiskPartition -ErrorAction Stop | ?{$_.Dependent -imatch $strPartition} | select -First 1 | %{$_.Antecedent}

    # parse the DeviceID from the text
    $strDeviceID = ([regex]::Match($strPartitionToDeviceIDQuery, "PHYSICALDRIVE\d+")).Value

    # get the PNPDeviceID of the given Device
    $strPNPDeviceID = Get-WMIObject -ComputerName $strDeviceName -Credential $curCredential -Class Win32_DiskDrive -ErrorAction Stop | ?{$_.DeviceID -imatch $strDeviceID} | %{$_.PNPDeviceID}

    if($strPNPDeviceID -imatch "^mpio") {$strDriveType = "SAN attached"}
    else{$strDriveType = "Local"}

    $strDriveAttachmentInfo = $strDriveAttachmentInfo + $strDriveLetter + " - " + $strPartition + " - " + $strDriveType + "`n"
    }
    $wksGetInfo.cells.item($intRow,$intSANAttachedColumn) = $strDriveAttachmentInfo.trim()
    if ($strDriveAttachmentInfo -match "SAN attached"){$wksGetInfo.cells.item($intRow,$intSANAttachedColumn).Interior.ColorIndex = 27}

    -------------------------------------------------------------------------------------------------

    DM

    ReplyDelete
  2. I'm guessing that the need to have "| Select -First 1" is because there is a possibility that Win32_LogicalDiskToPartition.Dependent will return multiple results for DeviceID="C:"
    How so, and how do you know the first is the right one?

    Thanks

    ReplyDelete
    Replies
    1. Correct, it's to make sure there's only one object in the pipe. I've seen duplicate WMI instances in some cases, so to make sure the script doesn't fail on them, I implemented this workaround. I don't know if the first one is always correct, but in the cases I've seen the duplicate entries were identical.

      Delete