DSCResources/MSFT_xExchAutoMountPoint/MSFT_xExchAutoMountPoint.psm1
function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [parameter(Mandatory = $true)] [System.String] $Identity, [parameter(Mandatory = $true)] [System.String] $AutoDagDatabasesRootFolderPath, [parameter(Mandatory = $true)] [System.String] $AutoDagVolumesRootFolderPath, [parameter(Mandatory = $true)] [System.String[]] $DiskToDBMap, [parameter(Mandatory = $true)] [System.UInt32] $SpareVolumeCount, [ValidateSet("NTFS","REFS")] [System.String] $FileSystem = "NTFS", [System.String] $MinDiskSize = "", [ValidateSet("MBR","GPT")] [System.String] $PartitioningScheme = "GPT", [System.String] $UnitSize = "64K", [System.String] $VolumePrefix = "EXVOL" ) #Load helper module Import-Module "$((Get-Item -LiteralPath "$($PSScriptRoot)").Parent.Parent.FullName)\Misc\xExchangeCommon.psm1" -Verbose:0 LogFunctionEntry -VerbosePreference $VerbosePreference GetDiskInfo $dbMap = GetDiskToDBMap -AutoDagDatabasesRootFolderPath $AutoDagDatabasesRootFolderPath $returnValue = @{ Identity = $Identity DiskToDBMap = $dbMap SpareVolumeCount = $SpareVolumeCount AutoDagDatabasesRootFolderPath = $AutoDagDatabasesRootFolderPath AutoDagVolumesRootFolderPath = $AutoDagVolumesRootFolderPath VolumePrefix = $VolumePrefix MinDiskSize = $MinDiskSize UnitSize = $UnitSize PartitioningScheme = $PartitioningScheme FileSystem = $FileSystem } $returnValue } function Set-TargetResource { [CmdletBinding()] param ( [parameter(Mandatory = $true)] [System.String] $Identity, [parameter(Mandatory = $true)] [System.String] $AutoDagDatabasesRootFolderPath, [parameter(Mandatory = $true)] [System.String] $AutoDagVolumesRootFolderPath, [parameter(Mandatory = $true)] [System.String[]] $DiskToDBMap, [parameter(Mandatory = $true)] [System.UInt32] $SpareVolumeCount, [ValidateSet("NTFS","REFS")] [System.String] $FileSystem = "NTFS", [System.String] $MinDiskSize = "", [ValidateSet("MBR","GPT")] [System.String] $PartitioningScheme = "GPT", [System.String] $UnitSize = "64K", [System.String] $VolumePrefix = "EXVOL" ) #Load helper module Import-Module "$((Get-Item -LiteralPath "$($PSScriptRoot)").Parent.Parent.FullName)\Misc\xExchangeCommon.psm1" -Verbose:0 LogFunctionEntry -VerbosePreference $VerbosePreference #First see if we need to assign any disks to ExVol's GetDiskInfo $exVolCount = GetInUseMountPointCount -RootFolder $AutoDagVolumesRootFolderPath $requiredVolCount = $DiskToDBMap.Count + $SpareVolumeCount if ($exVolCount -lt $requiredVolCount) { CreateMissingExVolumes @PSBoundParameters -CurrentVolCount $exVolCount -RequiredVolCount $requiredVolCount } #Now see if we need any DB mount points GetDiskInfo $exDbCount = GetInUseMountPointCount -RootFolder $AutoDagDatabasesRootFolderPath $requiredDbCount = GetDesiredDatabaseCount -DiskToDBMap $DiskToDBMap if ($exDbCount -lt $requiredDbCount) { CreateMissingExDatabases @PSBoundParameters } } function Test-TargetResource { [CmdletBinding()] [OutputType([System.Boolean])] param ( [parameter(Mandatory = $true)] [System.String] $Identity, [parameter(Mandatory = $true)] [System.String] $AutoDagDatabasesRootFolderPath, [parameter(Mandatory = $true)] [System.String] $AutoDagVolumesRootFolderPath, [parameter(Mandatory = $true)] [System.String[]] $DiskToDBMap, [parameter(Mandatory = $true)] [System.UInt32] $SpareVolumeCount, [ValidateSet("NTFS","REFS")] [System.String] $FileSystem = "NTFS", [System.String] $MinDiskSize = "", [ValidateSet("MBR","GPT")] [System.String] $PartitioningScheme = "GPT", [System.String] $UnitSize = "64K", [System.String] $VolumePrefix = "EXVOL" ) #Load helper module Import-Module "$((Get-Item -LiteralPath "$($PSScriptRoot)").Parent.Parent.FullName)\Misc\xExchangeCommon.psm1" -Verbose:0 LogFunctionEntry -VerbosePreference $VerbosePreference GetDiskInfo #Check if the number of assigned EXVOL's is less than the requested number of DB disks plus spares $mountPointCount = GetInUseMountPointCount -RootFolder $AutoDagVolumesRootFolderPath if ($mountPointCount -lt ($DiskToDBMap.Count + $SpareVolumeCount)) { ReportBadSetting -SettingName "MountPointCount" -ExpectedValue ($DiskToDBMap.Count + $SpareVolumeCount) -ActualValue $mountPointCount -VerbosePreference $VerbosePreference return $false } else #Loop through all requested DB's and see if they have a mount point yet { foreach ($value in $DiskToDBMap) { foreach ($db in $value.Split(',')) { if ((DBHasMountPoint -AutoDagDatabasesRootFolderPath $AutoDagDatabasesRootFolderPath -DB $db) -eq $false) { ReportBadSetting -SettingName "DB '$($db)' Has Mount Point" -ExpectedValue $true -ActualValue $false -VerbosePreference $VerbosePreference return $false } } } } return $true } #Creates mount points for any Exchange Volumes we are missing function CreateMissingExVolumes { [CmdletBinding()] param ( [parameter(Mandatory = $true)] [System.String] $Identity, [parameter(Mandatory = $true)] [System.String] $AutoDagDatabasesRootFolderPath, [parameter(Mandatory = $true)] [System.String] $AutoDagVolumesRootFolderPath, [parameter(Mandatory = $true)] [System.String[]] $DiskToDBMap, [parameter(Mandatory = $true)] [System.UInt32] $SpareVolumeCount, [ValidateSet("NTFS","REFS")] [System.String] $FileSystem = "NTFS", [System.String] $MinDiskSize = "", [ValidateSet("MBR","GPT")] [System.String] $PartitioningScheme = "GPT", [System.String] $UnitSize = "64K", [System.String] $VolumePrefix = "EXVOL", [System.Int32] $CurrentVolCount, [System.Int32] $RequiredVolCount ) for ($i = $CurrentVolCount; $i -lt $RequiredVolCount; $i++) { if ($i -ne $CurrentVolCount) #Need to update disk info if we've gone through the loop already { GetDiskInfo } $firstDisk = FindFirstAvailableDisk -MinDiskSize $MinDiskSize if ($firstDisk -ne -1) { $firstVolume = FindFirstAvailableVolumeNumber -AutoDagVolumesRootFolderPath $AutoDagVolumesRootFolderPath -VolumePrefix $VolumePrefix if ($firstVolume -ne -1) { $volPath = Join-Path -Path "$($AutoDagVolumesRootFolderPath)" -ChildPath "$($VolumePrefix)$($firstVolume)" PrepareVolume -DiskNumber $firstDisk -Folder $volPath -FileSystem $FileSystem -UnitSize $UnitSize -PartitioningScheme $PartitioningScheme -Label "$($VolumePrefix)$($firstVolume)" } else { throw "Unable to find a free volume number to use when naming the volume folder" } } else { throw "No available disks to assign an Exchange Volume mount point to" } } } #Looks for databases that have never had a mount point created, and gives them a mount point function CreateMissingExDatabases { [CmdletBinding()] param ( [parameter(Mandatory = $true)] [System.String] $Identity, [parameter(Mandatory = $true)] [System.String] $AutoDagDatabasesRootFolderPath, [parameter(Mandatory = $true)] [System.String] $AutoDagVolumesRootFolderPath, [parameter(Mandatory = $true)] [System.String[]] $DiskToDBMap, [parameter(Mandatory = $true)] [System.UInt32] $SpareVolumeCount, [ValidateSet("NTFS","REFS")] [System.String] $FileSystem = "NTFS", [System.String] $MinDiskSize = "", [ValidateSet("MBR","GPT")] [System.String] $PartitioningScheme = "GPT", [System.String] $UnitSize = "64K", [System.String] $VolumePrefix = "EXVOL" ) for ($i = 0; $i -lt $DiskToDBMap.Count; $i++) { if ($i -gt 0) #Need to refresh current disk info { GetDiskInfo } [string[]]$dbsNeedingMountPoints = @() [string[]]$allDBsRequestedForDisk = $DiskToDBMap[$i].Split(',') for ($j = 0; $j -lt $allDBsRequestedForDisk.Count; $j++) { $current = $allDBsRequestedForDisk[$j] $path = Join-Path -Path "$($AutoDagDatabasesRootFolderPath)" -ChildPath "$($current)" #We only want to touch datases who have never had a mount point created. After that, AutoReseed will handle it. if ((Test-Path -Path "$($path)") -eq $false) { $dbsNeedingMountPoints += $current } else #Since the folder already exists, need to check and error if the mount point doesn't { if ((MountPointExists -Path $path) -eq $false) { throw "Database '$($current)' already has a folder on disk at '$($path)', but does not have a mount point. This must be manually corrected for xAutoMountPoint to proceed." } } } if ($dbsNeedingMountPoints.Count -eq $allDBsRequestedForDisk.Count) #No DB mount points for this disk have been created yet { $targetVolume = GetExchangeVolume -AutoDagDatabasesRootFolderPath $AutoDagDatabasesRootFolderPath -AutoDagVolumesRootFolderPath $AutoDagVolumesRootFolderPath -DBsPerDisk $allDBsRequestedForDisk.Count } elseif ($dbsNeedingMountPoints.Count -gt 0) #We just need to create some mount points { $existingDB = "" #Find a DB that's already had its mount point created foreach ($db in $allDBsRequestedForDisk) { if (($dbsNeedingMountPoints.Contains($db) -eq $false)) { $existingDB = $db break } } if ($existingDB -ne "") { $targetVolume = GetExchangeVolume -AutoDagDatabasesRootFolderPath $AutoDagDatabasesRootFolderPath -AutoDagVolumesRootFolderPath $AutoDagVolumesRootFolderPath -ExistingDB $existingDB -DBsPerDisk $allDBsRequestedForDisk.Count -DBsToCreate $dbsNeedingMountPoints.Count } } else #All DB's requested for this disk are good. Just continue on in the loop { continue } if ($targetVolume -ne $null) { if ($targetVolume -ne -1) { foreach ($db in $dbsNeedingMountPoints) { $path = Join-Path -Path "$($AutoDagDatabasesRootFolderPath)" -ChildPath "$($db)" AddMountPoint -VolumeNumber $targetVolume -Folder $path } } else { throw "Unable to find a volume to place mount points for the following databases: '$($dbsNeedingMountPoints)'" } } } } #Builds a map of the DBs that already exist on disk function GetDiskToDBMap { param([string]$AutoDagDatabasesRootFolderPath) #Get the DB path to a point where we know there will be a trailing \ $dbpath = Join-Path -Path "$($AutoDagDatabasesRootFolderPath)" -ChildPath "" #Keep track of a disk number for putting in the map $i = 0 #Will be the return value for DiskToDBMap [string[]]$dbMap = @() #Loop through all existing mount points and figure out which ones are for DB's foreach ($key in $global:VolumeToMountPointMap.Keys) { [string]$mountPoints = "" foreach ($mountPoint in $global:VolumeToMountPointMap[$key]) { if ($mountPoint.StartsWith($dbpath)) { $startIndex = $dbpath.Length $endIndex = $mountPoint.IndexOf("\", $startIndex) $dbName = $mountPoint.Substring($startIndex, $endIndex - $startIndex) if ($mountPoints -eq "") { $mountPoints = $dbName } else { $mountPoints += ",$($dbName)" } } } if ($mountPoints.Length -gt 0) { $dbMap += $mountPoints } } return $dbMap } #Looks for a volume where an Exchange Volume or Database mount point can be added. #If ExistingDB is not specified, looks for a spare volume that has no mount points yet. #If ExistingDB is specified, finds the volume number where that DB exists, only if there is room to #create the requested database mount points. function GetExchangeVolume { param([string]$AutoDagDatabasesRootFolderPath, [string]$AutoDagVolumesRootFolderPath, [string]$ExistingDB = "", [Uint32]$DBsPerDisk, [Uint32]$DBsToCreate) $targetVol = -1 #Our return variable $keysSorted = $global:VolumeToMountPointMap.Keys | Sort-Object #Loop through every volume foreach ($key in $keysSorted) { #Get mount points for this volume [string[]]$mountPoints = $global:VolumeToMountPointMap[$key] $hasExVol = $false #Whether any ExVol mount points exist on this disk $hasExDb = $false #Whether any ExDB mount points exist on this disk $hasExistingDB = $false #Whether $ExistingDB exists as a mount point on this disk #Inspect each individual mount point foreach($mountPoint in $mountPoints) { if ($mountPoint.StartsWith($AutoDagVolumesRootFolderPath)) { $hasExVol = $true } elseif ($mountPoint.StartsWith($AutoDagDatabasesRootFolderPath)) { $hasExDb = $true $path = Join-Path -Path "$($AutoDagDatabasesRootFolderPath)" -ChildPath "$($ExistingDB)" if ($mountPoint.StartsWith($path)) { $hasExistingDB = $true } } } if ($ExistingDB -eq "") { if ($hasExVol -eq $true -and $hasExDb -eq $false) { $targetVol = $key break } } else { if ($hasExVol -eq $true -and $hasExistingDB -eq $true) { if (($mountPoints.Count + $DBsToCreate) -le ($DBsPerDisk + 1)) { $targetVol = $key } break } } } return $targetVol } #Finds the lowest disk number that doesn't have any volumes associated, and is larger than the requested size function FindFirstAvailableDisk { param([string]$MinDiskSize = "") $diskNum = -1 foreach ($key in $global:DiskToVolumeMap.Keys) { if ($global:DiskToVolumeMap[$key].Count -eq 0 -and ($key -lt $diskNum -or $diskNum -eq -1)) { if ($MinDiskSize -ne "") { [Uint64]$minSize = 0 + $MinDiskSize.Replace(" ", "") [Uint64]$actualSize = 0 + $global:DiskSizeMap[$key].Replace(" ", "") if ($actualSize -gt $minSize) { $diskNum = $key } } else { $diskNum = $key } } } return $diskNum } #Looks in the volumes root folder and finds the first number we can give to a volume folder #based off of what folders have already been created function FindFirstAvailableVolumeNumber { param([string]$AutoDagVolumesRootFolderPath, [string]$VolumePrefix) if((Test-Path -LiteralPath "$($AutoDagVolumesRootFolderPath)") -eq $false) #If the ExVol folder doesn't already exist, then we can start with 1 { return 1 } $currentFolders = Get-ChildItem -LiteralPath "$($AutoDagVolumesRootFolderPath)" | where {$_.GetType().Name -eq "DirectoryInfo"} | Sort-Object for ($i = 1; $i -lt 999; $i++) { $existing = $null $existing = $currentFolders | where {$_.Name -eq "$($VolumePrefix)$($i)"} if ($existing -eq $null) { return $i } } return -1 } #Counts and returns the number of DB's in the disk to db map function GetDesiredDatabaseCount { param([string[]]$DiskToDBMap) $count = 0 foreach ($value in $DiskToDBMap) { $count += $value.Split(',').Count } return $count } #Checks if a database already has a mountpoint created function DBHasMountPoint { param([string]$AutoDagDatabasesRootFolderPath, [string]$DB) $dbPath = Join-Path -Path "$($AutoDagDatabasesRootFolderPath)" -ChildPath "$($DB)" foreach ($key in $global:VolumeToMountPointMap.Keys) { foreach ($mountPoint in $global:VolumeToMountPointMap[$key]) { if ($mountPoint.StartsWith($dbPath)) { return $true } } } return $false } #Gets the count of in use mount points matching the given critera function GetInUseMountPointCount { param([string]$RootFolder) $count = 0 foreach ($key in $global:VolumeToMountPointMap.Keys) { foreach ($mountPoint in $global:VolumeToMountPointMap[$key]) { if ($mountPoint.StartsWith($RootFolder)) { $count++ } } } return $count } #Checks whether the mount point specified in the given path already exists as a mount point function MountPointExists { param([string]$Path) foreach ($key in $global:VolumeToMountPointMap.Keys) { foreach ($value in $global:VolumeToMountPointMap[$key]) { #Make sure both paths end with the same character if (($value.EndsWith("\")) -eq $false) { $value += "\" } if (($Path.EndsWith("\")) -eq $false) { $Path += "\" } #Do the comparison if ($value -like $Path) { return $true } } } return $false } #Adds the array of commands to a single temp file, and has disk part execute the temp file function Run-Diskpart { [CmdletBinding()] [OutputType([System.String])] Param ([Array]$Commands, [Boolean]$ShowOutput = $true) $Tempfile = [System.IO.Path]::GetTempFileName() foreach ($Com in $Commands) { $CMDLine = $CMDLine + $Com + ", " Add-Content $Tempfile $Com } $Output = DiskPart /s $Tempfile if ($ShowOutput) { Write-Verbose "Executed Diskpart commands: $(StringArrayToCommaSeparatedString -Array $Commands). Result:" Write-Verbose "$($Output)" } Remove-Item $Tempfile return $Output } function StringArrayToCommaSeparatedString { param([string[]]$Array) $string = "" if ($Array -ne $null -and $Array.Count -gt 0) { $string = $Array[0] for ($i = 1; $i -lt $Array.Count; $i++) { $string += ",$($Array[$i])" } } return $string } #Uses diskpart to obtain information on the disks and volumes that already exist on the system function GetDiskInfo { [Hashtable]$global:DiskToVolumeMap = @{} [Hashtable]$global:VolumeToMountPointMap = @{} [Hashtable]$global:DiskSizeMap = @{} [int[]]$diskNums = @() $diskList = Run-Diskpart -Commands "List Disk" -ShowOutput $false $foundDisks = $false #First parse out the list of disks foreach ($line in $diskList) { if ($foundDisks -eq $true) { if ($line.Contains("Disk ")) { #First find the disk number $startIndex = " Disk ".Length $endIndex = " -------- ".Length $diskNumStr = $line.Substring($startIndex, $endIndex - $startIndex).Trim() if ($diskNumStr.Length -gt 0) { $diskNum = [int]::Parse($diskNumStr) $diskNums += $diskNum } #Now find the disk size $startIndex = " -------- ------------- ".Length $endIndex = " -------- ------------- ------- ".Length $diskSize = $line.Substring($startIndex, $endIndex - $startIndex).Trim() if ($diskSize.Length -gt 0 -and $diskNum -ne $null) { $DiskSizeMap.Add($diskNum, $diskSize) } } } elseif ($line.Contains("-------- ------------- ------- ------- --- ---")) #Scroll forward until we find the where the list of disks starts { $foundDisks = $true } } #Now get info on the disks foreach ($diskNum in $diskNums) { $diskDetails = Run-Diskpart -Commands "Select Disk $($diskNum)","Detail Disk" -ShowOutput $false $foundVolumes = $false for ($i = 0; $i -lt $diskDetails.Count; $i++) { $line = $diskDetails[$i] if ($foundVolumes -eq $true) { if ($line.StartsWith(" Volume ")) { #First find the volume number $volStart = " Volume ".Length $volEnd = " ---------- ".Length $volStr = $line.Substring($volStart, $volEnd - $volStart).Trim() if ($volStr.Length -gt 0) { $volNum = [int]::Parse($volStr) AddObjectToMapOfObjectArrays -Map $DiskToVolumeMap -Key $diskNum -Value $volNum #Now parse out the drive letter if it's set $letterStart = " ---------- ".Length $letterEnd = $line.IndexOf(" ---------- --- ") + " ---------- --- ".Length $letter = $line.Substring($letterStart, $letterEnd - $letterStart).Trim() if ($letter.Length -eq 1) { AddObjectToMapOfObjectArrays -Map $VolumeToMountPointMap -Key $volNum -Value $letter } #Now find all the mount points do { $line = $diskDetails[++$i] if ($line -eq $null -or $line.StartsWith(" Volume ") -or $line.Trim().Length -eq 0) #We've hit the next volume, or the end of all info { $i-- #Move $i back one as we may have overrun the start of the next volume info break } else { $mountPoint = $line.Trim() AddObjectToMapOfObjectArrays -Map $VolumeToMountPointMap -Key $volNum -Value $mountPoint } } while ($i -lt $diskDetails.Count) } } } elseif ($line.Contains("There are no volumes.")) { [string[]]$emptyArray = @() $DiskToVolumeMap[$diskNum] = $emptyArray break } elseif ($line.Contains("---------- --- ----------- ----- ---------- ------- --------- --------")) { $foundVolumes = $true } } } } #Takes an empty disk, initalizes and formats it, and gives it an ExchangeVolume mount point function PrepareVolume { param([int]$DiskNumber, [string]$Folder, [string]$FileSystem, [string]$UnitSize, [string]$PartitioningScheme, [string]$Label) $formatString = "Format FS=$($FileSystem) UNIT=$($UnitSize) Label=$($Label) QUICK" #Initialize the disk and put in MBR format Run-Diskpart -Commands "select disk $($DiskNumber)","clean" | Out-Null Run-Diskpart -Commands "select disk $($DiskNumber)","online disk" | Out-Null Run-Diskpart -Commands "select disk $($DiskNumber)","attributes disk clear readonly","convert MBR" | Out-Null Run-Diskpart -Commands "select disk $($DiskNumber)","offline disk" | Out-Null #Online the disk Run-Diskpart -Commands "select disk $($DiskNumber)","attributes disk clear readonly","online disk" | Out-Null #Convert to GPT if requested if ($PartitioningScheme -eq "GPT") { Run-Diskpart -Commands "select disk $($DiskNumber)","convert GPT noerr" | Out-Null } #Create the directory if it doesn't exist if ((Test-Path $Folder) -eq $False) { mkdir -Path "$($Folder)" | Out-Null } #Create the partition and format the drive Run-Diskpart -Commands "select disk $($DiskNumber)","create partition primary","$($formatString)","assign mount=`"$($Folder)`"" | Out-Null } #Adds a mount point to an existing volume function AddMountPoint { param([int]$VolumeNumber, [string]$Folder) #Create the directory if it doesn't exist if ((Test-Path $Folder) -eq $False) { mkdir -Path "$($Folder)" | Out-Null } Run-Diskpart -Commands "select volume $($VolumeNumber)","assign mount=`"$($Folder)`"" | Out-Null } #Takes a hashtable, and adds the given key and value. function AddObjectToMapOfObjectArrays { Param([Hashtable]$Map, $Key, $Value) if ($Map.ContainsKey($Key)) { $Map[$Key] += $Value } else { [object[]]$Array = $Value $Map[$Key] = $Array } } Export-ModuleMember -Function *-TargetResource |