functions/exchangeschema/Invoke-FMExchangeSchema.ps1

function Invoke-FMExchangeSchema {
    <#
    .SYNOPSIS
        Applies the desired Exchange version to the tareted Forest.
     
    .DESCRIPTION
        Applies the desired Exchange version to the tareted Forest.
        Requires Schema Admin & Enterprise Admin privileges.
     
    .PARAMETER InputObject
        Test results provided by the associated test command.
        Only the provided changes will be executed, unless none were specified, in which ALL pending changes will be executed.
     
    .PARAMETER Server
        The server / domain to work with.
         
    .PARAMETER Credential
        The credentials to use for this operation.
     
    .PARAMETER EnableException
        This parameters disables user-friendly warnings and enables the throwing of exceptions.
        This is less user friendly, but allows catching exceptions in calling scripts.
     
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
     
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
     
    .EXAMPLE
        PS C:\> Invoke-FMExchangeSchema -Server contoso.com
     
        Applies the desired Exchange version to the contoso.com Forest.
#>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '')]
    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject,
        
        [PSFComputer]
        $Server,
        
        [PSCredential]
        $Credential,
        
        [switch]
        $EnableException
    )
    
    begin {
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential
        $parameters['Debug'] = $false
        Assert-ADConnection @parameters -Cmdlet $PSCmdlet
        Invoke-Callback @parameters -Cmdlet $PSCmdlet
        Assert-Configuration -Type ExchangeSchema -Cmdlet $PSCmdlet
        Set-FMDomainContext @parameters
        $forestObject = Get-ADForest @parameters
        
        $psParameter = $PSBoundParameters | ConvertTo-PSFHashtable -Include Credential
        $psParameter.ComputerName = $Server
        
        try { $session = New-PSSession @psParameter -ErrorAction Stop }
        catch {
            Stop-PSFFunction -String 'Invoke-FMExchangeSchema.WinRM.Failed' -StringValues $computerName -ErrorRecord $_ -EnableException $EnableException -Cmdlet $PSCmdlet -Target $computerName
            return
        }
        
        #region Functions
        function Test-ExchangeIsoPath {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
            [CmdletBinding()]
            param (
                [System.Management.Automation.Runspaces.PSSession]
                $Session,
                
                [string]
                $Path
            )
            
            Invoke-Command -Session $Session -ScriptBlock {
                Test-Path -Path $using:Path
            }
        }
        
        function Test-ExchangeSite {
            [CmdletBinding()]
            param (
                [hashtable]
                $Parameters,

                $Forest
            )

            $currentServer = Get-ADDomainController @parameters
            $schemaMaster = Get-ADDomainController @parameters -Identity $Forest.SchemaMaster

            $currentServer.Site -eq $schemaMaster.Site
        }

        function Invoke-ExchangeSchemaUpdate {
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")]
            [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
            [CmdletBinding()]
            param (
                [System.Management.Automation.Runspaces.PSSession]
                $Session,
                
                [string]
                $Path,
                
                [string]
                $OrganizationName,
                
                [switch]
                $SchemaOnly,

                [ValidateSet('InstallSchema', 'UpdateSchema', 'Install', 'Update', 'EnableSplitP', 'DisableSplitP')]
                [string]
                $Mode,

                [bool]
                $SplitPermission,

                [bool]
                $AllDomains,

                [hashtable]
                $Parameters
            )
            
            $result = Invoke-Command -Session $Session -ScriptBlock {
                param (
                    $Parameters
                )
                $exchangeIsoPath = Resolve-Path -Path $Parameters.Path
                
                # Mount Volume
                $diskImage = Mount-DiskImage -ImagePath $exchangeIsoPath -PassThru
                $volume = Get-Volume -DiskImage $diskImage
                $limit = (Get-Date).AddMinutes(1)
                while (-not $volume.DriveLetter) {
                    $volume = Get-Volume -DiskImage $diskImage
                    if ($volume.DriveLetter) { break }
                    if ((Get-Date) -gt $limit) {
                        try { Dismount-DiskImage -ImagePath $exchangeIsoPath }
                        catch { }
                        throw "Timeout waiting for volume drive letter!"
                    }
                    Start-Sleep -Milliseconds 250
                }
                $installPath = "$($volume.DriveLetter):\setup.exe"
                
                #region Perform Installation
                $resultText = switch ($Parameters.Mode) {
                    'InstallSchema' { & $installPath /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF /PrepareSchema 2>&1 }
                    'UpdateSchema' { & $installPath /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF /PrepareSchema 2>&1 }
                    'Install' {
                        if (-not $Parameters.SplitPermission) { & $installPath /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF /PrepareAD /OrganizationName:$($Parameters.OrganizationName) 2>&1 }
                        else { & $installPath /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF /PrepareAD /ActiveDirectorySplitPermissions:true /OrganizationName:$($Parameters.OrganizationName) 2>&1 }
                    }
                    'Update' { & $installPath /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF /PrepareAD 2>&1 }
                    'EnableSplitP' {
                        & $installPath /PrepareAD /ActiveDirectorySplitPermissions:true /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF 2>&1
                        if (-not $Parameters.AllDomains) { & $installPath /PrepareDomain /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF 2>&1 }
                        else { & $installPath /PrepareAllDomains /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF 2>&1 }
                    }
                    'DisableSplitP' {
                        & $installPath /PrepareAD /ActiveDirectorySplitPermissions:false /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF 2>&1
                        if (-not $Parameters.AllDomains) { & $installPath /PrepareDomain /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF 2>&1 }
                        else { & $installPath /PrepareAllDomains /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF 2>&1 }
                    }
                }
                $results = [pscustomobject]@{
                    Success = $LASTEXITCODE -lt 1
                    Message = $resultText -join "`n"
                }
                #endregion Perform Installation
                
                # Dismount Volume
                try { Dismount-DiskImage -ImagePath $exchangeIsoPath }
                catch { }
                
                # Report result
                $results
            } -ArgumentList ($PSBoundParameters | ConvertTo-PSFHashtable -Exclude Session)
            Write-PSFMessage -Message ($result.Message -join "`n") -Tag exchange, result -Target $Parameters.Server
            if (-not $result.Success) {
                throw "Error applying exchange update: $($result.Message)"
            }

            # Test Message validation (Text parsing is bad, but the method below is less reliable)
            if ($result.Message -match 'The Exchange Server setup operation completed successfully') { return }

            # Exchange's setup.exe is not always reliable in its exit codes, thus we need to retest
            # This is not guaranteed to work 100%, as replication delay may lead to false errors
            $testResult = Test-FMExchangeSchema @Parameters
            if (-not $testResult) { return }
            if ($testResult.Type -contains $Mode) {
                throw "Exchange Update probably failed! Success could not be verified, but replication delays might lead to a wrong alert here. This was the return from the exchange installer:`n$($result.Message)"
            }
        }
        #endregion Functions
    }
    process {
        if (Test-PSFFunctionInterrupt) { return }

        if (-not $InputObject) {
            $InputObject = Test-FMExchangeSchema @parameters
        }

        foreach ($testItem in $InputObject) {
            $commonParam = @{
                Session          = $session
                Path             = $testItem.Configuration.LocalImagePath
                OrganizationName = $testItem.Configuration.OrganizationName
                Parameters       = $parameters
                ErrorAction      = 'Stop'
            }
            #region Apply Updates if needed
            switch ($testItem.Type) {
                #region Install Exchange Schema
                'CreateSchema' {
                    if (-not (Test-ExchangeIsoPath -Session $session -Path $testItem.Configuration.LocalImagePath)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.IsoPath.Missing' -StringValues $testItem.Configuration.LocalImagePath -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    if (-not (Test-ExchangeSite -Parameters $parameters -Forest $forest)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.SchemaMaster.WrongSite' -StringValues $parameters.Server, $forest.SchemaMaster -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    Invoke-PSFProtectedCommand -ActionString 'Invoke-FMExchangeSchema.Installing' -ActionStringValues $testItem.Configuration -Target $forestObject -ScriptBlock {
                        Invoke-ExchangeSchemaUpdate @commonParam -Mode InstallSchema -SchemaOnly
                    } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                }
                #endregion Install Exchange Schema
                
                #region Update Exchange Schema
                'UpdateSchema' {
                    if (-not (Test-ExchangeIsoPath -Session $session -Path $testItem.Configuration.LocalImagePath)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.IsoPath.Missing' -StringValues $testItem.Configuration.LocalImagePath -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    if (-not (Test-ExchangeSite -Parameters $parameters -Forest $forest)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.SchemaMaster.WrongSite' -StringValues $parameters.Server, $forest.SchemaMaster -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    Invoke-PSFProtectedCommand -ActionString 'Invoke-FMExchangeSchema.Updating' -ActionStringValues $testItem.ADObject, $testItem.Configuration -Target $forestObject -ScriptBlock {
                        Invoke-ExchangeSchemaUpdate @commonParam -Mode UpdateSchema -SchemaOnly
                    } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                }
                #endregion Update Exchange Schema
                
                #region Install Exchange Schema & AD Objects
                'Create' {
                    if (-not (Test-ExchangeIsoPath -Session $session -Path $testItem.Configuration.LocalImagePath)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.IsoPath.Missing' -StringValues $testItem.Configuration.LocalImagePath -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    if (-not (Test-ExchangeSite -Parameters $parameters -Forest $forest)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.SchemaMaster.WrongSite' -StringValues $parameters.Server, $forest.SchemaMaster -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    Invoke-PSFProtectedCommand -ActionString 'Invoke-FMExchangeSchema.Installing' -ActionStringValues $testItem.Configuration -Target $forestObject -ScriptBlock {
                        Invoke-ExchangeSchemaUpdate @commonParam -Mode Install
                    } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                }
                #endregion Install Exchange Schema & AD Objects
                
                #region Update Exchange Schema & AD Objects
                'Update' {
                    if (-not (Test-ExchangeIsoPath -Session $session -Path $testItem.Configuration.LocalImagePath)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.IsoPath.Missing' -StringValues $testItem.Configuration.LocalImagePath -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    if (-not (Test-ExchangeSite -Parameters $parameters -Forest $forest)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.SchemaMaster.WrongSite' -StringValues $parameters.Server, $forest.SchemaMaster -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    Invoke-PSFProtectedCommand -ActionString 'Invoke-FMExchangeSchema.Updating' -ActionStringValues $testItem.ADObject, $testItem.Configuration -Target $forestObject -ScriptBlock {
                        Invoke-ExchangeSchemaUpdate @commonParam -Mode Update
                    } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                }
                #endregion Update Exchange Schema & AD Objects

                'DisableSplitP' {
                    if (-not (Test-ExchangeIsoPath -Session $session -Path $testItem.Configuration.LocalImagePath)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.IsoPath.Missing' -StringValues $testItem.Configuration.LocalImagePath -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    if (-not (Test-ExchangeSite -Parameters $parameters -Forest $forest)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.SchemaMaster.WrongSite' -StringValues $parameters.Server, $forest.SchemaMaster -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    Invoke-PSFProtectedCommand -ActionString 'Invoke-FMExchangeSchema.DisablingSplitPermissions' -ActionStringValues $testItem.Configuration -Target $Server -ScriptBlock {
                        Invoke-ExchangeSchemaUpdate @commonParam -Mode DisableSplitP -AllDomains $testItem.Configuration.AllDomains
                    } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                }
                'EnableSplitP' {
                    if (-not (Test-ExchangeIsoPath -Session $session -Path $testItem.Configuration.LocalImagePath)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.IsoPath.Missing' -StringValues $testItem.Configuration.LocalImagePath -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    if (-not (Test-ExchangeSite -Parameters $parameters -Forest $forest)) {
                        Stop-PSFFunction -String 'Invoke-FMExchangeSchema.SchemaMaster.WrongSite' -StringValues $parameters.Server, $forest.SchemaMaster -EnableException $EnableException -Continue -Category ResourceUnavailable -Target $Server
                    }
                    Invoke-PSFProtectedCommand -ActionString 'Invoke-FMExchangeSchema.EnablingSplitPermissions' -ActionStringValues $testItem.Configuration -Target $Server -ScriptBlock {
                        Invoke-ExchangeSchemaUpdate @commonParam -Mode EnableSplitP -AllDomains $testItem.Configuration.AllDomains
                    } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
                }
            }
            #endregion Apply Updates if needed
        }
    }
    end {
        if ($session) { Remove-PSSession -Session $session -ErrorAction Ignore -Confirm:$false -WhatIf:$false }
    }
}