DSCResources/DSC_ExchAutoMountPoint/DSC_ExchAutoMountPoint.psm1

Import-Module "$((Get-Item -LiteralPath "$($PSScriptRoot)").Parent.Parent.FullName)\Modules\ExchangeDscDiskPart\ExchangeDscDiskPart.psd1" -Force

<#
    .SYNOPSIS
        Gets DSC resource configuration.
 
    .PARAMETER Identity
        The name of the server. Not actually used for anything.
 
    .PARAMETER AutoDagDatabasesRootFolderPath
        The parent folder for Exchange database mount point folders.
 
    .PARAMETER AutoDagVolumesRootFolderPath
        The parent folder for Exchange volume mount point folders.
 
    .PARAMETER DiskToDBMap
        An array of strings containing the databases for each disk. Databases
        on the same disk should be in the same string, and comma separated.
        Example: 'DB1,DB2','DB3,DB4'. This puts DB1 and DB2 on one disk, and
        DB3 and DB4 on another.
 
    .PARAMETER SpareVolumeCount
        How many spare volumes will be available.
 
    .PARAMETER EnsureExchangeVolumeMountPointIsLast
        Whether the EXVOL mount point should be moved to be the last mount
        point listed on each disk. Defaults to $false.
 
    .PARAMETER CreateSubfolders
        If $true, specifies that DBNAME.db and DBNAME.log subfolders should be
        automatically created underneath the ExchangeDatabase mount points.
        Defaults to $false.
 
    .PARAMETER FileSystem
        The file system to use when formatting the volume. Defaults to NTFS.
 
    .PARAMETER MinDiskSize
        The minimum size of a disk to consider using. Defaults to none. Should
        be in a format like '1024MB' or '1TB'.
 
    .PARAMETER PartitioningScheme
        The partitioning scheme for the volume. Defaults to GPT.
 
    .PARAMETER UnitSize
        The unit size to use when formatting the disk. Defaults to 64k.
 
    .PARAMETER VolumePrefix
        The prefix to give to Exchange Volume folders. Defaults to EXVOL.
#>

function Get-TargetResource
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")]
    [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,

        [Parameter()]
        [System.Boolean]
        $EnsureExchangeVolumeMountPointIsLast = $false,

        [Parameter()]
        [System.Boolean]
        $CreateSubfolders = $false,

        [Parameter()]
        [ValidateSet('NTFS', 'REFS')]
        [System.String]
        $FileSystem = 'NTFS',

        [Parameter()]
        [System.String]
        $MinDiskSize = '',

        [Parameter()]
        [ValidateSet('MBR', 'GPT')]
        [System.String]
        $PartitioningScheme = 'GPT',

        [Parameter()]
        [System.String]
        $UnitSize = '64K',

        [Parameter()]
        [System.String]
        $VolumePrefix = 'EXVOL'
    )

    Write-FunctionEntry -Verbose:$VerbosePreference

    $diskInfo = Get-DiskInfo

    $dbMap = Get-DiskToDBMap -AutoDagDatabasesRootFolderPath $AutoDagDatabasesRootFolderPath -DiskInfo $diskInfo

    $returnValue = @{
        Identity                       = [System.String] $Identity
        DiskToDBMap                    = [System.String[]] $dbMap
        SpareVolumeCount               = [System.UInt32] $SpareVolumeCount
        AutoDagDatabasesRootFolderPath = [System.String] $AutoDagDatabasesRootFolderPath
        AutoDagVolumesRootFolderPath   = [System.String] $AutoDagVolumesRootFolderPath
        VolumePrefix                   = [System.String] $VolumePrefix
        MinDiskSize                    = [System.String] $MinDiskSize
        UnitSize                       = [System.String] $UnitSize
        PartitioningScheme             = [System.String] $PartitioningScheme
        FileSystem                     = [System.String] $FileSystem
    }

    $returnValue
}

<#
    .SYNOPSIS
        Configures settings defined DSC resource configuration.
 
    .PARAMETER Identity
        The name of the server. Not actually used for anything.
 
    .PARAMETER AutoDagDatabasesRootFolderPath
        The parent folder for Exchange database mount point folders.
 
    .PARAMETER AutoDagVolumesRootFolderPath
        The parent folder for Exchange volume mount point folders.
 
    .PARAMETER DiskToDBMap
        An array of strings containing the databases for each disk. Databases
        on the same disk should be in the same string, and comma separated.
        Example: 'DB1,DB2','DB3,DB4'. This puts DB1 and DB2 on one disk, and
        DB3 and DB4 on another.
 
    .PARAMETER SpareVolumeCount
        How many spare volumes will be available.
 
    .PARAMETER EnsureExchangeVolumeMountPointIsLast
        Whether the EXVOL mount point should be moved to be the last mount
        point listed on each disk. Defaults to $false.
 
    .PARAMETER CreateSubfolders
        If $true, specifies that DBNAME.db and DBNAME.log subfolders should be
        automatically created underneath the ExchangeDatabase mount points.
        Defaults to $false.
 
    .PARAMETER FileSystem
        The file system to use when formatting the volume. Defaults to NTFS.
 
    .PARAMETER MinDiskSize
        The minimum size of a disk to consider using. Defaults to none. Should
        be in a format like '1024MB' or '1TB'.
 
    .PARAMETER PartitioningScheme
        The partitioning scheme for the volume. Defaults to GPT.
 
    .PARAMETER UnitSize
        The unit size to use when formatting the disk. Defaults to 64k.
 
    .PARAMETER VolumePrefix
        The prefix to give to Exchange Volume folders. Defaults to EXVOL.
#>

function Set-TargetResource
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")]
    [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,

        [Parameter()]
        [System.Boolean]
        $EnsureExchangeVolumeMountPointIsLast = $false,

        [Parameter()]
        [System.Boolean]
        $CreateSubfolders = $false,

        [Parameter()]
        [ValidateSet('NTFS', 'REFS')]
        [System.String]
        $FileSystem = 'NTFS',

        [Parameter()]
        [System.String]
        $MinDiskSize = '',

        [Parameter()]
        [ValidateSet('MBR', 'GPT')]
        [System.String]
        $PartitioningScheme = 'GPT',

        [Parameter()]
        [System.String]
        $UnitSize = '64K',

        [Parameter()]
        [System.String]
        $VolumePrefix = 'EXVOL'
    )

    Write-FunctionEntry -Verbose:$VerbosePreference

    # First see if we need to assign any disks to ExVol's
    $diskInfo = Get-DiskInfo

    $exVolCount = Get-InUseMountPointCount -RootFolder $AutoDagVolumesRootFolderPath -DiskInfo $diskInfo
    $requiredVolCount = $DiskToDBMap.Count + $SpareVolumeCount

    if ($exVolCount -lt $requiredVolCount)
    {
        New-ExVolumesWhereMissing @PSBoundParameters -CurrentVolCount $exVolCount -RequiredVolCount $requiredVolCount
    }

    # Now see if we need any DB mount points
    $diskInfo = Get-DiskInfo

    $exDbCount = Get-InUseMountPointCount -RootFolder $AutoDagDatabasesRootFolderPath -DiskInfo $diskInfo
    $requiredDbCount = Get-DesiredDatabaseCount -DiskToDBMap $DiskToDBMap

    if ($exDbCount -lt $requiredDbCount)
    {
        New-ExDatabaseMountPointsWhereMissing @PSBoundParameters
    }

    # Now see if any Mount Points are ordered incorrectly. Jetstress wants ExchangeDatabase mount points to be listed before ExchangeVolume mount points
    $diskInfo = Get-DiskInfo

    if ($EnsureExchangeVolumeMountPointIsLast -eq $true)
    {
        while ($true)
        {
            $volNum = Get-VolumeNumberWhereMountPointNotLastInList -AutoDagVolumesRootFolderPath $AutoDagVolumesRootFolderPath -DiskInfo $diskInfo

            if ($volNum -ne -1)
            {
                Move-VolumeMountPointToEndOfList -AutoDagVolumesRootFolderPath $AutoDagVolumesRootFolderPath -VolumeNumber $volNum -DiskInfo $diskInfo

                # Update DiskInfo for next iteration
                $diskInfo = Get-DiskInfo
            }
            else
            {
                break
            }
        }
    }
}

<#
    .SYNOPSIS
        Tests whether settings defined DSC resource configuration are in the
        expected state.
 
    .PARAMETER Identity
        The name of the server. Not actually used for anything.
 
    .PARAMETER AutoDagDatabasesRootFolderPath
        The parent folder for Exchange database mount point folders.
 
    .PARAMETER AutoDagVolumesRootFolderPath
        The parent folder for Exchange volume mount point folders.
 
    .PARAMETER DiskToDBMap
        An array of strings containing the databases for each disk. Databases
        on the same disk should be in the same string, and comma separated.
        Example: 'DB1,DB2','DB3,DB4'. This puts DB1 and DB2 on one disk, and
        DB3 and DB4 on another.
 
    .PARAMETER SpareVolumeCount
        How many spare volumes will be available.
 
    .PARAMETER EnsureExchangeVolumeMountPointIsLast
        Whether the EXVOL mount point should be moved to be the last mount
        point listed on each disk. Defaults to $false.
 
    .PARAMETER CreateSubfolders
        If $true, specifies that DBNAME.db and DBNAME.log subfolders should be
        automatically created underneath the ExchangeDatabase mount points.
        Defaults to $false.
 
    .PARAMETER FileSystem
        The file system to use when formatting the volume. Defaults to NTFS.
 
    .PARAMETER MinDiskSize
        The minimum size of a disk to consider using. Defaults to none. Should
        be in a format like '1024MB' or '1TB'.
 
    .PARAMETER PartitioningScheme
        The partitioning scheme for the volume. Defaults to GPT.
 
    .PARAMETER UnitSize
        The unit size to use when formatting the disk. Defaults to 64k.
 
    .PARAMETER VolumePrefix
        The prefix to give to Exchange Volume folders. Defaults to EXVOL.
#>

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,

        [Parameter()]
        [System.Boolean]
        $EnsureExchangeVolumeMountPointIsLast = $false,

        [Parameter()]
        [System.Boolean]
        $CreateSubfolders = $false,

        [Parameter()]
        [ValidateSet('NTFS', 'REFS')]
        [System.String]
        $FileSystem = 'NTFS',

        [Parameter()]
        [System.String]
        $MinDiskSize = '',

        [Parameter()]
        [ValidateSet('MBR', 'GPT')]
        [System.String]
        $PartitioningScheme = 'GPT',

        [Parameter()]
        [System.String]
        $UnitSize = '64K',

        [Parameter()]
        [System.String]
        $VolumePrefix = 'EXVOL'
    )

    Write-FunctionEntry -Verbose:$VerbosePreference

    $diskInfo = Get-DiskInfo

    # Check if the number of assigned EXVOL's is less than the requested number of DB disks plus spares
    $mountPointCount = Get-InUseMountPointCount -RootFolder $AutoDagVolumesRootFolderPath -DiskInfo $diskInfo

    $testResults = $true

    if ($mountPointCount -lt ($DiskToDBMap.Count + $SpareVolumeCount))
    {
        Write-InvalidSettingVerbose -SettingName 'MountPointCount' -ExpectedValue ($DiskToDBMap.Count + $SpareVolumeCount) -ActualValue $mountPointCount -Verbose:$VerbosePreference
        $testResults = $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 ((Test-DBHasMountPoint -AutoDagDatabasesRootFolderPath $AutoDagDatabasesRootFolderPath -Database $db -DiskInfo $diskInfo) -eq $false)
                {
                    Write-InvalidSettingVerbose -SettingName "DB '$db' Has Mount Point" -ExpectedValue $true -ActualValue $false -Verbose:$VerbosePreference
                    $testResults = $false
                }
            }
        }
    }

    # Now check if any ExchangeVolume mount points are higher ordered than ExchangeDatabase mount points. ExchangeDatabase MP's must be listed first for logical disk counters to function properly
    if ($EnsureExchangeVolumeMountPointIsLast -eq $true -and (Get-VolumeNumberWhereMountPointNotLastInList -AutoDagVolumesRootFolderPath $AutoDagVolumesRootFolderPath -DiskInfo $diskInfo) -ne -1)
    {
        Write-Verbose -Message "One or more volumes have an $($AutoDagVolumesRootFolderPath) mount point ordered before a $($AutoDagDatabasesRootFolderPath) mount point"
        $testResults = $false
    }

    return $testResults
}

<#
    .SYNOPSIS
        Creates Exchange Volume mount points for any disks which should have
        them, but do not.
 
    .PARAMETER Identity
        The name of the server. Not actually used for anything.
 
    .PARAMETER AutoDagDatabasesRootFolderPath
        The parent folder for Exchange database mount point folders.
 
    .PARAMETER AutoDagVolumesRootFolderPath
        The parent folder for Exchange volume mount point folders.
 
    .PARAMETER DiskToDBMap
        An array of strings containing the databases for each disk. Databases
        on the same disk should be in the same string, and comma separated.
        Example: 'DB1,DB2','DB3,DB4'. This puts DB1 and DB2 on one disk, and
        DB3 and DB4 on another.
 
    .PARAMETER SpareVolumeCount
        How many spare volumes will be available.
 
    .PARAMETER EnsureExchangeVolumeMountPointIsLast
        Whether the EXVOL mount point should be moved to be the last mount
        point listed on each disk. Defaults to $false.
 
    .PARAMETER CreateSubfolders
        If $true, specifies that DBNAME.db and DBNAME.log subfolders should be
        automatically created underneath the ExchangeDatabase mount points.
        Defaults to $false.
 
    .PARAMETER FileSystem
        The file system to use when formatting the volume. Defaults to NTFS.
 
    .PARAMETER MinDiskSize
        The minimum size of a disk to consider using. Defaults to none. Should
        be in a format like '1024MB' or '1TB'.
 
    .PARAMETER PartitioningScheme
        The partitioning scheme for the volume. Defaults to GPT.
 
    .PARAMETER UnitSize
        The unit size to use when formatting the disk. Defaults to 64k.
 
    .PARAMETER VolumePrefix
        The prefix to give to Exchange Volume folders. Defaults to EXVOL.
 
    .PARAMETER CurrentVolCount
        The current number of Exchange Volumes that have been created.
 
    .PARAMETER RequiredVolCount
        The expected final number of Exchange Volumes.
#>

function New-ExVolumesWhereMissing
{
    [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,

        [Parameter()]
        [System.Boolean]
        $EnsureExchangeVolumeMountPointIsLast = $false,

        [Parameter()]
        [System.Boolean]
        $CreateSubfolders = $false,

        [Parameter()]
        [ValidateSet('NTFS', 'REFS')]
        [System.String]
        $FileSystem = 'NTFS',

        [Parameter()]
        [System.String]
        $MinDiskSize = '',

        [Parameter()]
        [ValidateSet('MBR', 'GPT')]
        [System.String]
        $PartitioningScheme = 'GPT',

        [Parameter()]
        [System.String]
        $UnitSize = '64K',

        [Parameter()]
        [System.String]
        $VolumePrefix = 'EXVOL',

        [Parameter(Mandatory = $true)]
        [System.Int32]
        $CurrentVolCount,

        [Parameter(Mandatory = $true)]
        [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
        {
            $diskInfo = Get-DiskInfo
        }

        $firstDisk = Get-FirstAvailableDiskNumber -MinDiskSize $MinDiskSize -DiskInfo $diskInfo

        if ($firstDisk -ne -1)
        {
            $firstVolume = Get-FirstAvailableVolumeNumber -AutoDagVolumesRootFolderPath $AutoDagVolumesRootFolderPath -VolumePrefix $VolumePrefix

            if ($firstVolume -ne -1)
            {
                $volPath = Join-Path -Path "$($AutoDagVolumesRootFolderPath)" -ChildPath "$($VolumePrefix)$($firstVolume)"

                Initialize-ExchangeVolume -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'
        }
    }
}

<#
    .SYNOPSIS
        Looks for databases that have never had a mount point created, and adds
        a mount point for them on an appropriate Exchange Volume.
 
    .PARAMETER Identity
        The name of the server. Not actually used for anything.
 
    .PARAMETER AutoDagDatabasesRootFolderPath
        The parent folder for Exchange database mount point folders.
 
    .PARAMETER AutoDagVolumesRootFolderPath
        The parent folder for Exchange volume mount point folders.
 
    .PARAMETER DiskToDBMap
        An array of strings containing the databases for each disk. Databases
        on the same disk should be in the same string, and comma separated.
        Example: 'DB1,DB2','DB3,DB4'. This puts DB1 and DB2 on one disk, and
        DB3 and DB4 on another.
 
    .PARAMETER SpareVolumeCount
        How many spare volumes will be available.
 
    .PARAMETER EnsureExchangeVolumeMountPointIsLast
        Whether the EXVOL mount point should be moved to be the last mount
        point listed on each disk. Defaults to $false.
 
    .PARAMETER CreateSubfolders
        If $true, specifies that DBNAME.db and DBNAME.log subfolders should be
        automatically created underneath the ExchangeDatabase mount points.
        Defaults to $false.
 
    .PARAMETER FileSystem
        The file system to use when formatting the volume. Defaults to NTFS.
 
    .PARAMETER MinDiskSize
        The minimum size of a disk to consider using. Defaults to none. Should
        be in a format like '1024MB' or '1TB'.
 
    .PARAMETER PartitioningScheme
        The partitioning scheme for the volume. Defaults to GPT.
 
    .PARAMETER UnitSize
        The unit size to use when formatting the disk. Defaults to 64k.
 
    .PARAMETER VolumePrefix
        The prefix to give to Exchange Volume folders. Defaults to EXVOL.
#>

function New-ExDatabaseMountPointsWhereMissing
{
    [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,

        [Parameter()]
        [System.Boolean]
        $EnsureExchangeVolumeMountPointIsLast = $false,

        [Parameter()]
        [System.Boolean]
        $CreateSubfolders = $false,

        [Parameter()]
        [ValidateSet('NTFS', 'REFS')]
        [System.String]
        $FileSystem = 'NTFS',

        [Parameter()]
        [System.String]
        $MinDiskSize = '',

        [Parameter()]
        [ValidateSet('MBR', 'GPT')]
        [System.String]
        $PartitioningScheme = 'GPT',

        [Parameter()]
        [System.String]
        $UnitSize = '64K',

        [Parameter()]
        [System.String]
        $VolumePrefix = 'EXVOL'
    )

    for ($i = 0; $i -lt $DiskToDBMap.Count; $i++)
    {
        if ($i -gt 0) # Need to refresh current disk info
        {
            $diskInfo = Get-DiskInfo
        }

        $dbsNeedingMountPoints = @()

        $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 ((Get-MountPointVolumeNumber -Path $path) -eq -1)
                {
                    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 = Get-ExchangeVolumeNumberForMountPoint -AutoDagDatabasesRootFolderPath $AutoDagDatabasesRootFolderPath -AutoDagVolumesRootFolderPath $AutoDagVolumesRootFolderPath -DBsPerDisk $allDBsRequestedForDisk.Count -VolumePrefix $VolumePrefix -DiskInfo $diskInfo
        }
        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 = Get-ExchangeVolumeNumberForMountPoint -AutoDagDatabasesRootFolderPath $AutoDagDatabasesRootFolderPath -AutoDagVolumesRootFolderPath $AutoDagVolumesRootFolderPath -ExistingDB $existingDB -DBsPerDisk $allDBsRequestedForDisk.Count -DBsToCreate $dbsNeedingMountPoints.Count -VolumePrefix $VolumePrefix -DiskInfo $diskInfo
            }
        }
        else # All DB's requested for this disk are good. Just continue on in the loop
        {
            continue
        }

        if ($null -ne $targetVolume)
        {
            if ($targetVolume -ne -1)
            {
                foreach ($db in $dbsNeedingMountPoints)
                {
                    $path = Join-Path -Path "$($AutoDagDatabasesRootFolderPath)" -ChildPath "$($db)"

                    Add-ExchangeMountPoint -VolumeNumber $targetVolume -Folder $path

                    if ($CreateSubfolders -eq $true)
                    {
                        $dbFolder = Join-Path -Path "$($path)" -ChildPath "$($db).db"
                        $logFolder = Join-Path -Path "$($path)" -ChildPath "$($db).log"

                        if ((Test-Path -LiteralPath "$($dbFolder)") -eq $false)
                        {
                            New-Item -ItemType Directory -Path "$($dbFolder)"
                        }

                        if ((Test-Path -LiteralPath "$($logFolder)") -eq $false)
                        {
                            New-Item -ItemType Directory -Path "$($logFolder)"
                        }
                    }
                }
            }
            else
            {
                throw "Unable to find a volume to place mount points for the following databases: '$($dbsNeedingMountPoints)'"
            }
        }
    }
}

<#
    .SYNOPSIS
        Builds a map of the DBs that already exist on disk.
 
    .PARAMETER AutoDagDatabasesRootFolderPath
        The parent folder for Exchange database mount point folders.
 
    .PARAMETER DiskInfo
        Information on the disks and volumes that already exist on the system.
#>

function Get-DiskToDBMap
{
    [CmdletBinding()]
    [OutputType([System.String[]])]
    param
    (
        [Parameter()]
        [System.String]
        $AutoDagDatabasesRootFolderPath,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DiskInfo
    )

    # Get the DB path to a point where we know there will be a trailing \
    $dbpath = Join-Path -Path "$($AutoDagDatabasesRootFolderPath)" -ChildPath ""

    # Will be the return value for DiskToDBMap
    [System.String[]] $dbMap = @()

    # Loop through all existing mount points and figure out which ones are for DB's
    foreach ($key in $DiskInfo.VolumeToMountPointMap.Keys)
    {
        [System.String] $mountPoints = ''

        foreach ($mountPoint in $DiskInfo.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
}

<#
    .SYNOPSIS
        Looks for a volume where an Exchange Volume or Database mount point can
        be added.
 
    .PARAMETER AutoDagDatabasesRootFolderPath
        The parent folder for Exchange database mount point folders.
 
    .PARAMETER AutoDagVolumesRootFolderPath
        The parent folder for Exchange volume mount point folders.
 
    .PARAMETER ExistingDB
        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.
 
    .PARAMETER DBsPerDisk
        The number of databases that are allowed per disk.
 
    .PARAMETER DBsToCreate
        The number of databases to create on the discovered disk.
 
    .PARAMETER VolumePrefix
        The prefix to give to Exchange Volume folders. Defaults to EXVOL.
 
    .PARAMETER DiskInfo
        Information on the disks and volumes that already exist on the system.
#>

function Get-ExchangeVolumeNumberForMountPoint
{
    [CmdletBinding()]
    [OutputType([System.Int32])]
    param
    (
        [Parameter()]
        [System.String]
        $AutoDagDatabasesRootFolderPath,

        [Parameter()]
        [System.String]
        $AutoDagVolumesRootFolderPath,

        [Parameter()]
        [System.String]
        $ExistingDB = '',

        [Parameter()]
        [Uint32]
        $DBsPerDisk,

        [Parameter()]
        [Uint32]
        $DBsToCreate,

        [Parameter()]
        [System.String]
        $VolumePrefix = 'EXVOL',

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DiskInfo
    )

    $targetVol = -1 # Our return variable

    [object[]] $keysSorted = Get-ExchangeVolumeKeysSorted -AutoDagDatabasesRootFolderPath $AutoDagDatabasesRootFolderPath -AutoDagVolumesRootFolderPath $AutoDagVolumesRootFolderPath -VolumePrefix $VolumePrefix -DiskInfo $DiskInfo

    # Loop through every volume
    foreach ($key in $keysSorted)
    {
        [int] $intKey = $key

        # Get mount points for this volume
        [System.String[]] $mountPoints = $DiskInfo.VolumeToMountPointMap[$intKey]

        $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 = $intKey
                break
            }
        }
        else
        {
            if ($hasExVol -eq $true -and $hasExistingDB -eq $true)
            {
                if (($mountPoints.Count + $DBsToCreate) -le ($DBsPerDisk + 1))
                {
                    $targetVol = $intKey
                }

                break
            }
        }
    }

    return $targetVol
}

<#
    .SYNOPSIS
        Finds the names of all existing EXVOL mount points, and returns a
        sorted array of all the EXVOL volume numbers.
 
    .PARAMETER AutoDagDatabasesRootFolderPath
        The parent folder for Exchange database mount point folders.
 
    .PARAMETER AutoDagVolumesRootFolderPath
        The parent folder for Exchange volume mount point folders.
 
    .PARAMETER VolumePrefix
        The prefix to give to Exchange Volume folders. Defaults to EXVOL.
 
    .PARAMETER DiskInfo
        Information on the disks and volumes that already exist on the system.
#>

function Get-ExchangeVolumeKeysSorted
{
    [CmdletBinding()]
    [OutputType([System.String[]])]
    param
    (
        [Parameter()]
        [System.String]
        $AutoDagDatabasesRootFolderPath,

        [Parameter()]
        [System.String]
        $AutoDagVolumesRootFolderPath,

        [Parameter()]
        [System.String]
        $VolumePrefix = 'EXVOL',

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DiskInfo
    )

    [System.String[]] $sortedKeys = @() # The return value

    [System.String] $pathBeforeVolumeNumber = Join-Path -Path $AutoDagVolumesRootFolderPath -ChildPath $VolumePrefix

    # First extract the actual volume number as an Int from the volume path, then add it to a new hashtable with the same key value
    [System.Collections.Hashtable] $tempVolumeToMountPointMap = @{}

    foreach ($key in $DiskInfo.VolumeToMountPointMap.Keys)
    {
        $volPath = ''

        # Loop through each mount point on this volume and find the EXVOL mount point
        foreach ($value in $DiskInfo.VolumeToMountPointMap[$key])
        {
            if ($value.StartsWith($pathBeforeVolumeNumber))
            {
                $volPath = $value
                break
            }
        }

        if ($volPath.StartsWith($pathBeforeVolumeNumber))
        {
            if ($volPath.EndsWith('\') -or $volPath.EndsWith('/'))
            {
                [System.String] $exVolNumberStr = $volPath.Substring($pathBeforeVolumeNumber.Length, ($volPath.Length - $pathBeforeVolumeNumber.Length - 1))
            }
            else
            {
                [System.String] $exVolNumberStr = $volPath.Substring($pathBeforeVolumeNumber.Length, ($volPath.Length - $pathBeforeVolumeNumber.Length))
            }

            [int] $exVolNumber = [int]::Parse($exVolNumberStr)
            $tempVolumeToMountPointMap.Add($key, $exVolNumber)
        }
    }

    # Now go through the volume numbers, and add the keys to the return array in sorted value order
    while ($tempVolumeToMountPointMap.Count -gt 0)
    {
        [object[]] $keys = $tempVolumeToMountPointMap.Keys
        [int] $lowestKey = $keys[0]
        [int] $lowestValue = $tempVolumeToMountPointMap[$keys[0]]

        for ($i = 1; $i -lt $tempVolumeToMountPointMap.Count; $i++)
        {
            [int] $currentValue = $tempVolumeToMountPointMap[$keys[$i]]

            if ($currentValue -lt $lowestValue)
            {
                $lowestKey = $keys[$i]
                $lowestValue = $currentValue
            }
        }

        $sortedKeys += $lowestKey
        $tempVolumeToMountPointMap.Remove($lowestKey)
    }

    return $sortedKeys
}

<#
    .SYNOPSIS
        Finds the lowest disk number that doesn't have any volumes associated,
        and is larger than the requested size.
 
    .PARAMETER MinDiskSize
        The minimum disk size to consider when looking for available disks.
 
    .PARAMETER DiskInfo
        Information on the disks and volumes that already exist on the system.
#>

function Get-FirstAvailableDiskNumber
{
    [CmdletBinding()]
    [OutputType([System.UInt32])]
    param
    (
        [Parameter()]
        [System.String]
        $MinDiskSize = '',

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DiskInfo
    )

    $diskNum = -1

    foreach ($key in $DiskInfo.DiskToVolumeMap.Keys)
    {
        if ($DiskInfo.DiskToVolumeMap[$key].Count -eq 0 -and ($key -lt $diskNum -or $diskNum -eq -1))
        {
            if ($MinDiskSize -ne '')
            {
                [Uint64] $minSize = 0 + $MinDiskSize.Replace(' ', '')
                [Uint64] $actualSize = 0 + $DiskInfo.DiskSizeMap[$key].Replace(' ', '')

                if ($actualSize -gt $minSize)
                {
                    $diskNum = $key
                }
            }
            else
            {
                $diskNum = $key
            }
        }
    }

    return $diskNum
}

<#
    .SYNOPSIS
        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.
 
    .PARAMETER AutoDagVolumesRootFolderPath
        The parent folder for Exchange volume mount point folders.
 
    .PARAMETER VolumePrefix
        The prefix to give to Exchange Volume folders. Defaults to EXVOL.
#>

function Get-FirstAvailableVolumeNumber
{
    [CmdletBinding()]
    [OutputType([System.Int32])]
    param
    (
        [Parameter()]
        [System.String]
        $AutoDagVolumesRootFolderPath,

        [Parameter()]
        [System.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-Object { $_.GetType().Name -eq 'DirectoryInfo' } |
        Sort-Object

    for ($i = 1; $i -lt 999; $i++)
    {
        $existing = $null
        $existing = $currentFolders |
            Where-Object { $_.Name -eq "$($VolumePrefix)$($i)" }

        if ($null -eq $existing)
        {
            return $i
        }
    }

    return -1
}

<#
    .SYNOPSIS
        Counts and returns the number of DB's in the input DiskToDBMap.
 
    .PARAMETER DiskToDBMap
        An array of strings containing the databases for each disk. Databases
        on the same disk should be in the same string, and comma separated.
        Example: 'DB1,DB2','DB3,DB4'. This puts DB1 and DB2 on one disk, and
        DB3 and DB4 on another.
#>

function Get-DesiredDatabaseCount
{
    [CmdletBinding()]
    [OutputType([System.Int32])]
    param
    (
        [Parameter()]
        [System.String[]]
        $DiskToDBMap
    )

    $count = 0

    foreach ($value in $DiskToDBMap)
    {
        $count += $value.Split(',').Count
    }

    return $count
}

<#
    .SYNOPSIS
        Checks if a database already has a mountpoint created.
 
    .PARAMETER AutoDagDatabasesRootFolderPath
        The parent folder for Exchange database mount point folders.
 
    .PARAMETER Database
        The name of the Database to check for.
 
    .PARAMETER DiskInfo
        Information on the disks and volumes that already exist on the system.
#>

function Test-DBHasMountPoint
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter()]
        [System.String]
        $AutoDagDatabasesRootFolderPath,

        [Parameter()]
        [System.String]
        $Database,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DiskInfo
    )

    $dbPath = Join-Path -Path "$($AutoDagDatabasesRootFolderPath)" -ChildPath "$($Database)"

    foreach ($key in $DiskInfo.VolumeToMountPointMap.Keys)
    {
        foreach ($mountPoint in $DiskInfo.VolumeToMountPointMap[$key])
        {
            if ($mountPoint.StartsWith($dbPath))
            {
                return $true
            }
        }
    }

    return $false
}

<#
    .SYNOPSIS
        Gets the count of in use mount points matching the given critera.
 
    .PARAMETER RootFolder
        The folder to count Mount Points within.
 
    .PARAMETER DiskInfo
        Information on the disks and volumes that already exist on the system.
#>

function Get-InUseMountPointCount
{
    [CmdletBinding()]
    [OutputType([System.Int32])]
    param
    (
        [Parameter()]
        [System.String]
        $RootFolder,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DiskInfo
    )

    $count = 0

    foreach ($key in $DiskInfo.VolumeToMountPointMap.Keys)
    {
        foreach ($mountPoint in $DiskInfo.VolumeToMountPointMap[$key])
        {
            if ($mountPoint.StartsWith($RootFolder))
            {
                $count++
            }
        }
    }

    return $count
}

<#
    .SYNOPSIS
        Checks all volumes, and sees if any of them have ExchangeVolume mount
        points that show up before other (like ExchangeDatabase) mount points.
        If so, it returns the volume number. If not, it returns -1.
 
    .PARAMETER AutoDagVolumesRootFolderPath
        The parent folder for Exchange volume mount point folders.
 
    .PARAMETER DiskInfo
        Information on the disks and volumes that already exist on the system.
#>

function Get-VolumeNumberWhereMountPointNotLastInList
{
    [CmdletBinding()]
    [OutputType([System.Int32])]
    param
    (
        [Parameter()]
        [System.String]
        $AutoDagVolumesRootFolderPath,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DiskInfo
    )

    foreach ($key in $DiskInfo.VolumeToMountPointMap.Keys)
    {
        $values = $DiskInfo.VolumeToMountPointMap[$key]

        if ($null -ne $values)
        {
            for ($i = 0; $i -lt $values.Count; $i++)
            {
                if ($values[$i].StartsWith($AutoDagVolumesRootFolderPath) -eq $true -and $i -lt ($values.Count - 1))
                {
                    return $key
                }
            }
        }
    }

    return -1
}

<#
    .SYNOPSIS
        For volumes that have multiple mount points including an ExchangeVolum
         mount point, sends removes and re-adds the ExchangeVolume mount point
         so that it is at the end of the list of mount points.
 
    .PARAMETER AutoDagVolumesRootFolderPath
        The parent folder for Exchange volume mount point folders.
 
    .PARAMETER VolumeNumber
        The number of the volume to modify.
 
    .PARAMETER DiskInfo
        Information on the disks and volumes that already exist on the system.
#>

function Move-VolumeMountPointToEndOfList
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $AutoDagVolumesRootFolderPath,

        [Parameter()]
        [System.Int32]
        $VolumeNumber,

        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DiskInfo
    )

    $values = $DiskInfo.VolumeToMountPointMap[$VolumeNumber]

    foreach ($folderName in $values)
    {
        if ($folderName.StartsWith($AutoDagVolumesRootFolderPath))
        {
            if ($folderName.EndsWith('\'))
            {
                $folderName = $folderName.Substring(0, $folderName.Length - 1)
            }

            Start-DiskPart -Commands "select volume $($VolumeNumber)", "remove mount=`"$($folderName)`"", "assign mount=`"$($folderName)`"" -Verbose:$VerbosePreference | Out-Null
            break
        }
    }
}

<#
    .SYNOPSIS
        Takes an empty disk, initalizes and formats it, and gives it an
        ExchangeVolume mount point.
 
    .PARAMETER Folder
        The folder to assign the Exchange Volume mount point to.
 
    .PARAMETER FileSystem
        The file system to use when formatting the volume. Defaults to NTFS.
 
    .PARAMETER Label
        The label to assign to the formatted volume.
 
    .PARAMETER PartitioningScheme
        The partitioning scheme for the volume. Defaults to GPT.
 
    .PARAMETER UnitSize
        The unit size to use when formatting the disk. Defaults to 64k.
#>

function Initialize-ExchangeVolume
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [int]
        $DiskNumber,

        [Parameter()]
        [System.String]
        $Folder,

        [Parameter()]
        [ValidateSet('NTFS', 'REFS')]
        [System.String]
        $FileSystem = 'NTFS',

        [Parameter()]
        [System.String]
        $Label,

        [Parameter()]
        [System.String]
        $PartitioningScheme,

        [Parameter()]
        [System.String]
        $UnitSize
    )

    # Initialize the disk and put in MBR format
    Start-DiskPart -Commands "select disk $($DiskNumber)", 'clean' -Verbose:$VerbosePreference | Out-Null
    Start-DiskPart -Commands "select disk $($DiskNumber)", 'online disk' -Verbose:$VerbosePreference | Out-Null
    Start-DiskPart -Commands "select disk $($DiskNumber)", 'attributes disk clear readonly', 'convert MBR' -Verbose:$VerbosePreference | Out-Null
    Start-DiskPart -Commands "select disk $($DiskNumber)", 'offline disk' -Verbose:$VerbosePreference | Out-Null

    # Online the disk
    Start-DiskPart -Commands "select disk $($DiskNumber)", 'attributes disk clear readonly', 'online disk' -Verbose:$VerbosePreference | Out-Null

    # Convert to GPT if requested
    if ($PartitioningScheme -eq 'GPT')
    {
        Start-DiskPart -Commands "select disk $($DiskNumber)", 'convert GPT noerr' -Verbose:$VerbosePreference | Out-Null
    }

    # Create the directory if it doesn't exist
    if ((Test-Path $Folder) -eq $False)
    {
        New-Item -ItemType Directory -Path "$($Folder)" | Out-Null
    }

    # Create the partition and format the drive
    if ($FileSystem -eq 'NTFS')
    {
        $formatString = "Format FS=$($FileSystem) UNIT=$($UnitSize) Label=$($Label) QUICK"

        Start-DiskPart -Commands "select disk $($DiskNumber)", "create partition primary", "$($formatString)", "assign mount=`"$($Folder)`"" -Verbose:$VerbosePreference | Out-Null
    }
    else # If ($FileSystem -eq "REFS")
    {
        Start-DiskPart -Commands "select disk $($DiskNumber)", "create partition primary" -Verbose:$VerbosePreference | Out-Null

        if ($UnitSize.ToLower().EndsWith('k'))
        {
            $UnitSizeBytes = [UInt64]::Parse($UnitSize.Substring(0, $UnitSize.Length - 1)) * 1024
        }
        else
        {
            $UnitSizeBytes = $UnitSize
        }

        Write-Verbose -Message 'Sleeping for 15 seconds after partition creation.'

        Start-Sleep -Seconds 15

        Get-Partition -DiskNumber $DiskNumber -PartitionNumber 2 | Format-Volume -AllocationUnitSize $UnitSizeBytes -FileSystem REFS -NewFileSystemLabel $Label -SetIntegrityStreams:$false -Confirm:$false
        Add-PartitionAccessPath -DiskNumber $DiskNumber -PartitionNumber 2 -AccessPath $Folder -PassThru | Set-Partition -NoDefaultDriveLetter $true
    }
}

<#
    .SYNOPSIS
        Adds a mount point to an existing volume.
 
    .PARAMETER VolumeNumber
        The number of the volume to assign a mount point to.
 
    .PARAMETER Folder
        The folder to assign the Exchange Volume mount point to.
#>

function Add-ExchangeMountPoint
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [int]
        $VolumeNumber,

        [Parameter()]
        [System.String]
        $Folder
    )

    # Create the directory if it doesn't exist
    if ((Test-Path $Folder) -eq $False)
    {
        New-Item -ItemType Directory -Path "$($Folder)" | Out-Null
    }

    Start-DiskPart -Commands "select volume $($VolumeNumber)", "assign mount=`"$($Folder)`"" -Verbose:$VerbosePreference | Out-Null
}

Export-ModuleMember -Function *-TargetResource