DeploymentReadinessChecker.psm1

#Requires -Version 4
#Requires -Modules 'Pester'

Function Test-DeploymentReadiness {

<#
.SYNOPSIS
    Validates that one or more computers meet the prerequisites for a software deployment/upgrade.
 
.DESCRIPTION
    Validates that one or more computers meet the prerequisites for a software deployment/upgrade.
    It generates a NUnit-style test result file for each computer and creates a visual, dynamic HTLM report encompassing data from all the test results.
    The list of computers to check is specified via the ComputerName parameter.
     
    The deployment or upgrade prerequisites are specified in a Pester-based validation script located in the sub-directory \ReadinessValidationScript.
    All the prerequisites tests should be in a single validation script, so there should be only one file named *.Tests.ps1 in the ReadinessValidationScript sub-directory.
 
.PARAMETER ComputerName
    To specify one or more computers against which the prerequisites checks will be performed.
 
    If the validation script has a ComputerName parameter, the function passes one computer at a time to its ComputerName parameter, via the Script parameter of Invoke-Pester.
 
.PARAMETER Credential
    To specify the credentials to connect remotely to the target computers.
 
    If the validation script has a Credential parameter, the function passes the value of its own Credential parameter to the validation script, via the Script parameter of Invoke-Pester.
 
.PARAMETER OutputPath
    To specify in which directory the output test results files and the summary report should be located.
    If the directory doesn't exist, it will be created.
    If not specified, the defaut output path is the current directory.
 
.PARAMETER TestParameters
    If the test script used to validate the prerequisites take parameters, their names and values can be specified as a hashtable via this parameter.
    Then, the function will pass these into the Script parameter of Invoke-Pester, when calling the validation script.
    To see the format of the hashtable for this parameter, please refer to the examples by running : Get-Help Test-DeploymentReadiness -Examples
 
.PARAMETER Tag
    If the Pester validation script contains Describe blocks with tags, only the tests in Describe blocks with the specified Tag parameter value(s) are run.
    Wildcard characters and Tag values that include spaces or whitespace characters are not supported.
 
.PARAMETER ExcludeTag
    If the Pester validation script contains Describe blocks with tags, tests in Describe blocks with the specified Tag parameter values are omitted.
    Wildcard characters and Tag values that include spaces or whitespace characters are not supported.
 
    Just like the ExcludeTag parameter of Invoke-Pester, when you specify multiple ExcludeTag values, this omits tests that have any of the listed tags (it ORs the tags).
     
.EXAMPLE
    Test-DeploymentReadiness -ComputerName (Get-Content .\Computers_List.txt) -Credential (Get-Credential) -OutputPath $env:USERPROFILE\Desktop\DeploymentReadinessReport
 
    Validates that all the computers with the name listed in the file Computers_list.txt meet the prerequisites specified in a validation script located in the sub-directory \ReadinessValidationScript.
 
.EXAMPLE
    $TestParams = @{ DeploymentServerName = $DeploymentServerName; ManagementServerName = $ManagementServerName }
    Test-DeploymentReadiness -ComputerName 'Server1','Server2' -Credential (Get-Credential) -TestParameters $TestParams
 
    Validates that all the computers with the name listed in the file Computers_list.txt meet the prerequisites specified in a validation script located in the sub-directory \ReadinessValidationScript.
    It uses a hashtable ($TestParams) to pass parameter names and values to the validation script.
 
.EXAMPLE
    'Computer1','Computer2','Computer3' | Test-DeploymentReadiness -Credential (Get-Credential) -OutputPath $env:USERPROFILE\Desktop\DeploymentReadinessReport
     
    Validates that all the computers specified via pipeline input meet the prerequisites specified in a validation script located in the sub-directory \ReadinessValidationScript.
     
.NOTES
    Author : Mathieu Buisson
     
.LINK
    https://github.com/MathieuBuisson/DeploymentReadinessChecker
#>


[cmdletbinding()]
    param(
        [Parameter(ValueFromPipeline=$True, Mandatory=$True, Position=0)]
        [string[]]$ComputerName,

        [Parameter(Position=1)]
        [pscredential]$Credential,

        [Parameter(Position=2)]
        [string]$OutputPath = $($pwd.ProviderPath),

        [Parameter(Position=3)]
        [hashtable]$TestParameters, #= @{ DeploymentServerName = $DeploymentServerName
                                   # ManagementServerName = $ManagementServerName
                                   # }
        [Parameter(Position=4)]
        [Alias('Tags')]
        [string[]]$Tag,

        [Parameter(Position=5)]
        [Alias('ExcludeTags')]
        [string[]]$ExcludeTag
    )

    Begin {
        
        If ( -not(Test-Path -Path $OutputPath) ) {
            New-Item -ItemType Directory -Path $OutputPath -Force
        }

        # Checking if the validation script has a ComputerName parameter
        $ValidationScriptFile = (Get-ChildItem -Path "$PSScriptRoot\ReadinessValidationScript\" -Recurse -Filter '*.Tests.ps1').FullName
        If ( $ValidationScriptFile.Count -gt 1 ) {
            Throw "Having more than 1 file named *.Tests.ps1 in the 'ReadinessValidationScript' directory is not supported."
        }
        Write-Verbose "The detected validation script file is : $ValidationScriptFile"
        
        $ScriptInfo = Get-Command $ValidationScriptFile
        [System.Boolean]$HasComputerNameParameter = $ScriptInfo.Parameters.Keys -contains 'ComputerName'
        Write-Verbose "Does the Pester validation script have a ComputerName parameter ? $($HasComputerNameParameter)."

        # Checking if credentials to connect to target computers were specified
        If ( $PSBoundParameters.ContainsKey('Credential') ) {
            $CredentialSpecified = $True
        }

        # Checking if the validation script has a Credential parameter
        [System.Boolean]$HasCredentialParameter = $ScriptInfo.Parameters.Keys -contains 'Credential'
        Write-Verbose "Does the Pester validation script have a Credential parameter ? $($HasCredentialParameter)."

        # Setting tag filtering parameters to pass to Invoke-Pester if the Tag or ExcludeTag parameter is specified
        If ( $PSBoundParameters.ContainsKey('Tag') -or $PSBoundParameters.ContainsKey('ExcludeTag') ) {
            
            [hashtable]$TagFilteringParameters = @{}
            Write-Verbose 'Tag filtering is ON.'

            If ( $PSBoundParameters.ContainsKey('Tag') ) {
                $TagFilteringParameters.Add('Tag', $Tag)
            }
            If ( $PSBoundParameters.ContainsKey('ExcludeTag') ) {
                $TagFilteringParameters.Add('ExcludeTag', $ExcludeTag)
            }
        }
    }
    Process {
        
        # If the validation script has a Credential parameter, the function passes the value of
        # its own Credential parameter to the validation script, via the Script parameter of Invoke-Pester.
        If ( $CredentialSpecified -and $HasCredentialParameter ) {

            If ( $PSBoundParameters.ContainsKey('TestParameters') ) {
                If ( $TestParameters.Credential ) {
                    $TestParameters.Credential = $Credential
                }
                Else {
                    $TestParameters.Add('Credential', $Credential)
                }
            }
            Else {
                $TestParameters = @{ Credential = $Credential }
            }
        }
        
        Foreach ( $Computer in $ComputerName ) {
            
            # If the validation script has a ComputerName parameter, the function passes one computer at a
            # time to the validation script's ComputerName parameter, via the Script parameter of Invoke-Pester.
            If ( $HasComputerNameParameter ) {

                If ( $TestParameters ) {
                    If ( $TestParameters.ComputerName ) {
                        $TestParameters.ComputerName = $Computer
                    }
                    Else {
                        $TestParameters.Add('ComputerName', $Computer)
                    }
                }
                Else {
                    $TestParameters = @{ ComputerName = $Computer }
                }
            }

            # Building the hashtable to pass parameters to the Pester validation script via the Script parameter of Invoke-Pester
            If ( $TestParameters ) {
                Foreach ( $Key in $TestParameters.Keys ) {
                    Write-Verbose "Parameter passed to the validation script. Key : $Key, Value : $($TestParameters.$Key)"
                }

                $ScriptParameters = @{
                    Path = "$PSScriptRoot\ReadinessValidationScript\*"
                    Parameters = $TestParameters
                }
            }
            Else {
                $ScriptParameters = @{
                    Path = "$PSScriptRoot\ReadinessValidationScript\*"
                }
            }

            Write-Verbose "Running Pester validation script against computer : $Computer"
            If ( $TagFilteringParameters ) {
                Invoke-Pester -Script $ScriptParameters -OutputFile "$OutputPath\$Computer.xml" -OutputFormat NUnitXml @TagFilteringParameters
            }
            Else {
                Invoke-Pester -Script $ScriptParameters -OutputFile "$OutputPath\$Computer.xml" -OutputFormat NUnitXml
            }
        }
    }
    End {
        Invoke-ReportUnit -OutputPath $OutputPath
    }
}

# Wrapper function to call ReportUnit.exe, this is mainly to be able to mock ReportUnit calls
Function Invoke-ReportUnit ($OutputPath) {
    
    $ReportUnitPath = "$PSScriptRoot\ReportUnit\ReportUnit.exe"
    $Null = & $ReportUnitPath $OutputPath
    If ( $LASTEXITCODE -eq 0 ) {
        Write-Host "`r`nThe deployment readiness report has been successfully created."
        Write-Host "To view the report, please open the following file : $OutputPath\Index.html"

        # It maybe be useful to output the file containing the overview report to the pipeline, in case the user wants to do something with it.
        Get-ChildItem -Path (Join-Path -Path $OutputPath -ChildPath 'Index.html')
    }
    Else {
        Write-Error "An error occurred when ReportUnit was generating HTML reports from the Pester test results. To troubleshoot this, try running '$PSScriptRoot\ReportUnit\ReportUnit.exe' manually to see the actual error message."
    }
}

New-Alias -Name 'tdr' -Value 'Test-DeploymentReadiness'
Export-ModuleMember -Function 'Test-DeploymentReadiness' -Alias 'tdr'