DSCResources/MSFT_xExchJetstressCleanup/MSFT_xExchJetstressCleanup.psm1

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

function Get-TargetResource
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")]
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $JetstressPath,

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

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

        [Parameter()]
        [System.Boolean]
        $DeleteAssociatedMountPoints,

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

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

        [Parameter()]
        [System.Boolean]
        $RemoveBinaries
    )

    Write-FunctionEntry -Parameters @{'JetstressPath' = $JetstressPath} -Verbose:$VerbosePreference

    $returnValue = @{
        JetstressPath = [System.String] $JetstressPath
    }

    $returnValue
}

function Set-TargetResource
{
    # Suppressing this rule because $global:DSCMachineStatus is used to trigger a reboot.
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')]
    <#
        Suppressing this rule because $global:DSCMachineStatus is only set,
        never used (by design of Desired State Configuration).
    #>

    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Scope='Function', Target='DSCMachineStatus')]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $JetstressPath,

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

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

        [Parameter()]
        [System.Boolean]
        $DeleteAssociatedMountPoints,

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

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

        [Parameter()]
        [System.Boolean]
        $RemoveBinaries
    )

    Write-FunctionEntry -Parameters @{'JetstressPath' = $JetstressPath} -Verbose:$VerbosePreference

    Assert-ParametersValidForJetstress @PSBoundParameters

    $jetstressInstalled = Get-IsJetstressInstalled

    if ($jetstressInstalled)
    {
        throw 'Jetstress must be uninstalled before using the xExchJetstressCleanup resource'
    }

    # If a config file was specified, pull the database and log paths out and put them into $DatabasePaths and $LogPaths
    if ($PSBoundParameters.ContainsKey('ConfigFilePath'))
    {
        [xml] $configFile = Get-JetstressConfigXmlContent -ConfigFilePath "$($ConfigFilePath)"

        [System.String[]] $DatabasePaths = $configFile.configuration.ExchangeProfile.EseInstances.EseInstance.DatabasePaths.Path
        [System.String[]] $LogPaths = $configFile.configuration.ExchangeProfile.EseInstances.EseInstance.LogPath
    }

    [System.String[]] $FoldersToRemove = $DatabasePaths + $LogPaths

    # Now delete the specified directories
    # Variable Only used if $DeleteAssociatedMountPoints is $true
    [Hashtable] $ParentFoldersToRemove = @{}

    foreach ($path in $FoldersToRemove)
    {
        # Get the parent folder for the specified path
        $parent = Get-ParentFolderFromPathString -Folder "$($path)"

        if (([System.String]::IsNullOrEmpty($parent) -eq $false -and $ParentFoldersToRemove.ContainsKey($parent) -eq $false))
        {
            $ParentFoldersToRemove.Add($parent, $null)
        }

        Remove-FolderAndContent -Path "$($path)"
    }

    # Delete associated mount points if requested
    if ($DeleteAssociatedMountPoints -eq $true -and $ParentFoldersToRemove.Count -gt 0)
    {
        $diskInfo = Get-DiskInfo

        foreach ($parent in $ParentFoldersToRemove.Keys)
        {
            if ($null -eq (Get-ChildItem -LiteralPath "$($parent)" -ErrorAction SilentlyContinue))
            {
                $volNum = Get-MountPointVolumeNumber -Path "$($parent)" -DiskInfo $diskInfo

                if ($volNum -ge 0)
                {
                    Start-DiskPart -Commands "select volume $($volNum)", "remove mount=`"$($parent)`"" -Verbose:$VerbosePreference | Out-Null

                    Remove-FolderAndContent -Path "$($parent)"
                }
                else
                {
                    Write-Warning "Folder '$($parent)' does not have an associated mount point."
                }
            }
            else
            {
                Write-Warning "Folder '$($parent)' still has child items. Skipping removing mount point."
            }
        }
    }

    # Clean up binaries if requested
    if ($RemoveBinaries -eq $true -and (Test-Path -LiteralPath "$($JetstressPath)") -eq $true)
    {
        # Move output files if requested
        if ([System.String]::IsNullOrEmpty($OutputSaveLocation) -eq $false)
        {
            if ((Test-Path -LiteralPath "$($OutputSaveLocation)") -eq $false)
            {
                New-Item -ItemType Directory -Path "$($OutputSaveLocation)"
            }

            $outputFiles = Get-ChildItem -LiteralPath "$($JetstressPath)" | Where-Object -FilterScript {
                $_.Name -like 'Performance*' -or `
                $_.Name -like 'Stress*' -or `
                $_.Name -like 'DBChecksum*' -or `
                $_.Name -like 'XmlConfig*' -or `
                $_.Name -like '*.evt' -or `
                $_.Name -like '*.log'
            }

            $outputFiles | Move-Item -Destination "$($OutputSaveLocation)" -Confirm:$false -Force
        }

        # Now remove the Jetstress folder

        # If the config file is in the Jetstress directory, remove everything but the config file, or else running Test-TargetResource after removing the directory will fail
        if ((Get-FolderPathWithNoTrailingSlash -Folder "$($JetstressPath)") -like (Get-ParentFolderFromPathString -Folder "$($ConfigFilePath)"))
        {
            Get-ChildItem -LiteralPath "$($JetstressPath)" | Where-Object {$_.FullName -notlike "$($ConfigFilePath)"} | Remove-Item -Recurse -Confirm:$false -Force
        }
        else # No config file in this directory. Remove the whole thing
        {
            Remove-FolderAndContent -Path "$($JetstressPath)"
        }
    }

    # Test if we successfully cleaned up Jetstress. If so, flag or initiate a reboot
    $cleanedUp = Test-TargetResource @PSBoundParameters

    if ($cleanedUp -eq $true)
    {
        Write-Verbose -Message 'Jetstress was successfully cleaned up. A reboot must occur to finish the cleanup.'

        $global:DSCMachineStatus = 1
    }
}

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $JetstressPath,

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

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

        [Parameter()]
        [System.Boolean]
        $DeleteAssociatedMountPoints,

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

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

        [Parameter()]
        [System.Boolean]
        $RemoveBinaries
    )

    Write-FunctionEntry -Parameters @{'JetstressPath' = $JetstressPath} -Verbose:$VerbosePreference

    Assert-ParametersValidForJetstress @PSBoundParameters

    $jetstressInstalled = Get-IsJetstressInstalled

    $testResults = $true

    if ($jetstressInstalled)
    {
        Write-Verbose -Message 'Jetstress is still installed'
        $testResults = $false
    }
    else
    {
        # If a config file was specified, pull the database and log paths out and put them into $DatabasePaths and $LogPaths
        if ($PSBoundParameters.ContainsKey('ConfigFilePath'))
        {
            [xml] $configFile = Get-JetstressConfigXmlContent -ConfigFilePath "$($ConfigFilePath)"

            [System.String[]] $FoldersToRemove = $configFile.configuration.ExchangeProfile.EseInstances.EseInstance.DatabasePaths.Path + $configFile.configuration.ExchangeProfile.EseInstances.EseInstance.LogPath
        }
        else
        {
            [System.String[]] $FoldersToRemove = $DatabasePaths + $LogPaths
        }

        # First make sure DB and log folders were cleaned up
        $diskInfo = Get-DiskInfo

        foreach ($folder in $FoldersToRemove)
        {
            # If DeleteAssociatedMountPoints was requested, make sure the parent folder doesn't have a mount point
            if ($DeleteAssociatedMountPoints -eq $true)
            {
                $parent = Get-ParentFolderFromPathString -Folder "$($folder)"

                if ((Get-MountPointVolumeNumber -Path "$($parent)" -DiskInfo $diskInfo) -ge 0)
                {
                    Write-Verbose -Message "Folder '$($parent)' still has a mount point associated with it."
                    $testResults = $false
                }
            }

            # Now check the folder itself
            if ((Test-Path -LiteralPath "$($folder)") -eq $true)
            {
                Write-Verbose -Message "Folder '$($folder)' still exists."
                $testResults = $false
            }
        }

        # Now check for binaries
        if ($RemoveBinaries -eq $true -and (Test-Path -LiteralPath "$($JetstressPath)") -eq $true)
        {
            if ((Get-FolderPathWithNoTrailingSlash -Folder "$($JetstressPath)") -like (Get-ParentFolderFromPathString -Folder "$($ConfigFilePath)"))
            {
                $items = Get-ChildItem -LiteralPath "$($JetstressPath)" | Where-Object {$_.FullName -notlike "$($ConfigFilePath)"}

                if ($null -ne $items -or $items.Count -gt 0)
                {
                    Write-Verbose -Message "Folder '$($JetstressPath)' still exists and contains items that are not the config file."
                    $testResults = $false
                }
            }
            else
            {
                Write-Verbose -Message "Folder '$($JetstressPath)' still exists."
                $testResults = $false
            }
        }
    }

    if ($testResults)
    {
        Write-Verbose -Message 'Jetstress has been successfully cleaned up.'
    }

    return $testResults
}

# Verifies that parameters for Jetstress were passed in correctly. Throws an exception if not.
function Assert-ParametersValidForJetstress
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $JetstressPath,

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

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

        [Parameter()]
        [System.Boolean]
        $DeleteAssociatedMountPoints,

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

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

        [Parameter()]
        [System.Boolean]
        $RemoveBinaries
    )

    if ($PSBoundParameters.ContainsKey('ConfigFilePath') -eq $false -and `
       ($PSBoundParameters.ContainsKey('DatabasePaths') -eq $false -or `
        $PSBoundParameters.ContainsKey('LogPaths') -eq $false))
    {
        throw 'Either the ConfigFilePath parameter must be specified, or DatabasePaths and LogPaths must be specified.'
    }

    if ($PSBoundParameters.ContainsKey('ConfigFilePath') -eq $true)
    {
        if ([System.String]::IsNullOrEmpty($ConfigFilePath) -or ((Test-Path -LiteralPath "$($ConfigFilePath)") -eq $false))
        {
            throw "The path specified for ConfigFilePath, '$($ConfigFilePath)', is either invalid or inaccessible"
        }
    }
    else
    {
        if ($null -eq $DatabasePaths -or $DatabasePaths.Count -eq 0)
        {
            throw 'No paths were specified in the DatabasePaths parameter'
        }

        if ($null -eq $LogPaths -or $LogPaths.Count -eq 0)
        {
            throw 'No paths were specified in the LogPaths parameter'
        }
    }
}

# Get a string for a folder without the trailing slash
function Get-FolderPathWithNoTrailingSlash
{
    param
    (
        [Parameter()]
        [System.String]
        $Folder
    )

    if ($Folder.EndsWith('\'))
    {
        $Folder = $Folder.Substring(0, $Folder.Length - 1)
    }

    return $Folder
}

# Simple string parsing method to determine a folder's parent
function Get-ParentFolderFromPathString
{
    param
    (
        [Parameter()]
        [System.String]
        $Folder
    )

    $Folder = Get-FolderPathWithNoTrailingSlash -Folder "$($Folder)"

    $parent = $Folder.Substring(0, $Folder.LastIndexOf('\'))

    return $parent
}

# Removes the specified folder, if it exists, and all subdirectories
function Remove-FolderAndContent
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [System.String]
        $Path
    )

    if ((Test-Path -LiteralPath "$($Path)") -eq $true)
    {
        Write-Verbose -Message "Attempting to remove folder '$($Path)' and all subfolders"
        Remove-Item -LiteralPath "$($Path)" -Recurse -Confirm:$false -Force
    }
    else
    {
        Write-Verbose -Message "Folder '$($Path)' does not exist. Skipping."
    }
}

# Loads the specified JetstressConfig.xml file and puts it into an [xml] variable
function Get-JetstressConfigXmlContent
{
    [CmdletBinding()]
    [OutputType([Xml])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $ConfigFilePath
    )

    [xml] $configFile = Get-Content -LiteralPath "$($ConfigFilePath)"

    if ($null -ne $configFile)
    {
        [System.String[]] $DatabasePaths = $configFile.configuration.ExchangeProfile.EseInstances.EseInstance.DatabasePaths.Path
        [System.String[]] $LogPaths = $configFile.configuration.ExchangeProfile.EseInstances.EseInstance.LogPath

        if ($null -eq $DatabasePaths -or $DatabasePaths.Count -eq 0)
        {
            throw "Failed to read any database paths out of config file '$($ConfigFilePath)'"
        }
        elseif ($null -eq $LogPaths -or $LogPaths.Count -eq 0)
        {
            throw "Failed to read any log paths out of config file '$($ConfigFilePath)'"
        }
    }
    else
    {
        throw "Failed to read config file at '$($ConfigFilePath)'"
    }

    return $configFile
}

# Checks whether Jetstress is installed by looking for Jetstress 2013's Product GUID
function Get-IsJetstressInstalled
{
    return ($null -ne (Get-Item 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{75189587-0D84-4404-8F02-79C39728FA64}' -ErrorAction SilentlyContinue))
}

Export-ModuleMember -Function *-TargetResource