December 1, 2015

Exporting a list with details of the volumes of a server/computer in xml format to replicate the volumes in other machine

We were doing a massive datacenter migration, and it included lots of databases (And the respective applications).
As the approach we used was migration the C drive using a migration tool, and then restoring the datafiles to the new servers, we had to create all the mountpoints manually. Doing the preparation of the mountpoints manually would be totally crazy (As we have more than 150 databases, and some of them have up to 40 datafiles), I created the following script that generates some XMLs, and a report with the necessary information and instructions to automate the preparation of the disks.




#region Arguments definition
Param(
 [string]
 $OutputFilePath
    )
#endregion Arguments definiton

#region Local function definition
Function Get-ScriptPath {
  <#
   .SYNOPSIS
    This functions returns the actual running script directory.

   .EXAMPLE
    $MyPath = Get-ScriptPath
    Assigns the actual path of the script to MyPath variable as a string.

   .OUTPUTS
    System.String
  #>
  [CmdletBinding()]
  [OutputType([System.String])]
  param(
  )
  begin {
    if (($PSVersionTable.PSVersion.Major) -ne 2){
     Write-Error "This Function Is Only Compatible with Powershell version 2"
     break
    }
  }
  process {
   try {
    $Invocation = Split-Path -Parent $MyInvocation.ScriptName
    Return $Invocation
   }
   catch {
    Write-Error "Path cannot be obtained."
    break
   }
  }
  end {
  }
 }
#endregion Local function definition

#region Output file path validation
If ($OutputFilePath -eq ""){ #If OutputFilePath argument was not provided then
  try {
   $OutputFilePath = Read-Host -ErrorAction Stop -Prompt "Output file path: "
   if ($OutputFilePath -eq ""){
    Throw [System.Exception]
    }
   } catch {
   Write-Error "File path must be specified"
   break
   }
  }
  IF (Test-Path $OutputFilePath -PathType 'Container'){
   Write-Error "The complete file path must be specified, not only its directory"
   break
  }
  IF (-NOT (Test-Path (Split-Path -Parent $OutputFilePath) -PathType 'Container'))
        {
   #If parent folder does not exists
            Write-Error "$(Split-Path -Parent $OutputFilePath) is not a valid folder"
   break
        }
  If (Test-Path $OutputFilePath)
  {
   #If file already exists
   Write-Error "File already exists"
   break
  }
  Else
  {
        try{
             New-Item -Path $OutputFilePath -ItemType "file" -Value "Test" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue| Out-Null
    ($target = Get-Item $OutputFilePath -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) | Out-Null
             $writestream = $target.Openwrite()
             $writestream.Close() | Out-Null
             Remove-Variable -Name writestream
    Remove-Variable -Name target
    Remove-Item -Path $OutputFilePath -Force
         }
         catch
   {
             Write-Error "File cannot be written to the specified directory"
    break
         }
  }
#endregion Output file path validation

#region Variables definition and initialization
$scriptpath = Get-ScriptPath
$DisksInfo = New-Object System.Xml.XmlDocument
$XmlDisks =$DisksInfo.CreateElement("Disks")
$XmlDisk = $DisksInfo.CreateElement("Disk")
$XmlVolumes =$DisksInfo.CreateElement("Volumes") #Xml element that stores the volumes of each XmlDisk
$XmlVolume = $DisksInfo.CreateElement("Volume") #Xml element to store the volume, anyways it is created each time a new volume is being processed from the output.
$ActualVolume="" #Variable used to store the volume being processed
$Disk = "" #Variable used to store the disk number
$DisksArray = @() #This array is used to add all the XmlDisk's to this array and sort it before apprend it to XmlDisks
$VolumesToExport = (gwmi -Query "Select * from Win32_Volume Where DriveType=3" | SELECT -ExpandProperty DeviceID) #The intention of loading all the data from the WMI into a variable is to obtain a "shot" of the actual data at the start of the script to avoid querying different information during the execution of the script.
$DisksToExport = (gwmi -Query "Select * from Win32_DiskDrive") #The intention of loading all the data from the WMI into a variable is to obtain a "shot" of the actual data at the start of the script to avoid querying different information during the execution of the script.
#endregion Variables definition and initialization

#region Obtain data
$cmdOutput = & $scriptpath"\diskext.exe" 2>&1 #Gets the output of the diskext to fill the XML data. This tool was used as there is still no method to match a volume with a disk using WMIs.
foreach ( $line in $cmdOutput ) {
 If ($line.Replace(" ", "").Length -ne 0){ #Condinitional that avoids processing blank lines
   If ($line.Contains("Volume:") -and $line.Substring(8) -ne $ActualVolume -and $VolumesToExport -contains $line.Substring(8))  { #This conditional detects that a new volume was read from the output
   $XmlVolume = $DisksInfo.CreateElement("Volume") # Create a new element to store the volume being processed and its attributes.
   $ActualVolume=$line.Substring(8) # This stores the volume id into ActualVolume to be used after for conditionals.
   $Disk = "" #Cleans the disk number, as otherwise the conditional created below for the spanned disks will not work.
   #region This lines are used to add the attributes of the volume to the element.
   $XmlVolume.SetAttribute("DeviceID", $ActualVolume) | Out-Null
   $XmlVolume.SetAttribute("Label", (gwmi -Query ([string]::Concat("Select * From Win32_Volume Where DeviceID='",$ActualVolume.Replace("\", "\\"),"'")) | SELECT -ExpandProperty Label -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) | Out-Null
   $XmlVolume.SetAttribute("Path", (gwmi -Query ([string]::Concat("Select * From Win32_Volume Where DeviceID='",$ActualVolume.Replace("\", "\\"),"'")) | SELECT -ExpandProperty Name -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) | Out-Null
   $XmlVolume.SetAttribute("Cluster", (gwmi -Query ([string]::Concat("Select * From Win32_Volume Where DeviceID='",$ActualVolume.Replace("\", "\\"),"'")) | SELECT -ExpandProperty BlockSize -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) | Out-Null
   $XmlVolume.SetAttribute("Size", (gwmi -Query ([string]::Concat("Select * From Win32_Volume Where DeviceID='",$ActualVolume.Replace("\", "\\"),"'")) | SELECT -ExpandProperty Capacity -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) | Out-Null
   $XmlVolume.SetAttribute("Compressed", (gwmi -Query ([string]::Concat("Select * From Win32_Volume Where DeviceID='",$ActualVolume.Replace("\", "\\"),"'")) | SELECT -ExpandProperty Compressed -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) | Out-Null
   $XmlVolume.SetAttribute("Spanned", $false) | Out-Null # By default the attribue Spanned is set to false, it is turn to true if a second disk is detected.
   #endregion This lines are used to add the attributes of the volume to the element.
   }
   If ($ActualVolume -ne "" -and $line.Contains("       Disk:   ") -and $Disk -eq "") { #So, a disk was detected after a volume
    $XmlDisk = $DisksInfo.CreateElement("Disk") # A new element to store this disk was created
    $XmlVolumes =$DisksInfo.CreateElement("Volumes") #As this disk will contain volumes, then a volumes element is created
    $Disk = $line.Substring(15) #The string containing the disk number is filtered
    #region This lines are used to add the attributes of the disk to the element.
    $XmlDisk.SetAttribute("Number", $Disk) | Out-Null
    $XmlDisk.SetAttribute("DeviceID", (gwmi -Query ([string]::Concat("Select * From Win32_DiskDrive Where Index='",$Disk,"'")) | SELECT -ExpandProperty DeviceID) ) | Out-Null
    $XmlDisk.SetAttribute("Size", (gwmi -Query ([string]::Concat("Select * From Win32_DiskDrive Where Index='",$Disk,"'")) | SELECT -ExpandProperty Size)) | Out-Null
    #endregion This lines are used to add the attributes of the disk to the element.
    $XmlVolumes.AppendChild($XmlVolume) | Out-Null
    $XmlDisk.AppendChild($XmlVolumes) | Out-Null
    $DisksArray += $XmlDisk
   } Elseif ($ActualVolume -ne "" -and $line.Contains("       Disk:   ") -and $Disk -ne "") { #So, as a second disk was detected (Because the $disk was not cleaned)
    $XmlDisk = $DisksInfo.CreateElement("Disk") # A new disk element is created to store the newly detected disk
    $XmlVolumes =$DisksInfo.CreateElement("Volumes") # and its volumes
    $Disk = $line.Substring(15) #The string containing the disk number is filtered
    #region This lines are used to add the attributes of the disk to the element.
    $XmlDisk.SetAttribute("Number", $Disk) | Out-Null
    $XmlDisk.SetAttribute("DeviceID", (gwmi -Query ([string]::Concat("Select * From Win32_DiskDrive Where Index='",$Disk,"'")) | SELECT -ExpandProperty DeviceID) ) | Out-Null
    $XmlDisk.SetAttribute("Size", (gwmi -Query ([string]::Concat("Select * From Win32_DiskDrive Where Index='",$Disk,"'")) | SELECT -ExpandProperty Size)) | Out-Null
    #endregion This lines are used to add the attributes of the disk to the element.
    $XmlVolume.SetAttribute("Spanned", $true) | Out-Null #Sets spanned to true before clonning the element.
    $XmlVolumes.AppendChild($XmlVolume.CloneNode($true)) | Out-Null #Added the .CloneNode as otherwise the node is moved instead of being copied.
    $XmlDisk.AppendChild($XmlVolumes) | Out-Null
    #$XmlDisks.AppendChild($XmlDisk) # Commented as now it is added to an array to be sorted and then added with a foreach loop
    $DisksArray += $XmlDisk
    }
 }
}
#endregion Obtain data

#region Sorting the array of disks being ordered by the disk number
$DisksArray = $DisksArray | Sort-Object { [int]$_.Number }
foreach ($XmlDisk in $DisksArray){ # This loop is the one that adds each disk to the $DiskInfo XML
 $XmlDisks.AppendChild($XmlDisk)
}
#endregion Sorting the array of disks being ordered by the disk number

#region Part of code used to combine Disk duplicated nodes
$NewXmlDisks = $DisksInfo.CreateElement("Disks")
foreach ($DiskNumber in ($XmlDisks.Disk | Select -ExpandProperty Number -Unique)){
 If (($XmlDisks.Disk | Select -ExpandProperty Number | Where-Object {$_ -eq $DiskNumber}).Count -gt 1) {
  $NewXmlDisk = $DisksInfo.CreateElement("Disk")
  $NewXmlVolumes = $DisksInfo.CreateElement("Volumes")
  $NewXmlDisk.AppendChild($NewXmlVolumes)
  $NewXmlDisk.SetAttribute("Number", ($XmlDisks.Disk | Where-Object {$_.Number -eq $DiskNumber})[0].Number) | Out-Null
  $NewXmlDisk.SetAttribute("DeviceID", ($XmlDisks.Disk | Where-Object {$_.Number -eq $DiskNumber})[0].DeviceID) | Out-Null
  $NewXmlDisk.SetAttribute("Size", ($XmlDisks.Disk | Where-Object {$_.Number -eq $DiskNumber})[0].Size) | Out-Null
  foreach ($XmlDisk in ($XmlDisks.Disk | Where-Object {$_.Number -eq $DiskNumber})) {
   $NewXmlVolumes.AppendChild($XmlDisk.Volumes.Volume)
  }
 $NewXmlDisk.AppendChild($NewXmlVolumes)
 $NewXmlDisks.AppendChild($NewXmlDisk)
 } else {
  $NewXmlDisks.AppendChild(($XmlDisks.Disk | Where-Object {$_.Number -eq $DiskNumber}))
 }
}
#endregion Part of code used to combine Disk duplicated nodes

$DisksInfo.AppendChild($NewXmlDisks)  | Out-Null # Appends the disks already combined
$DisksInfo.Save($OutputFilePath)  | Out-Null # Saves the XML file.

break

]]