ExchangeNodeMaintenanceMode.psm1

## Pre-Loaded Module code ##

#region PreLoad
<#
    Placeholder
#>

#EndRegion PreLoad

## PRIVATE MODULE FUNCTIONS AND DATA ##

function Get-CallerPreference 
{
    <#
            .Synopsis
            Fetches "Preference" variable values from the caller's scope.
 
            .DESCRIPTION
            Script module functions do not automatically inherit their caller's variables,
            but they can be obtained through the $PSCmdlet variable in Advanced Functions.
            This function is a helper function for any script module Advanced Function;
            by passing in the values of $ExecutionContext.SessionState and $PSCmdlet,
            Get-CallerPreference will set the caller's preference variables locally.
 
            .PARAMETER Cmdlet
            The $PSCmdlet object from a script module Advanced Function.
 
            .PARAMETER SessionState
            The $ExecutionContext.SessionState object from a script module Advanced Function.
            This is how the Get-CallerPreference function sets variables in its callers' scope,
            even if that caller is in a different script module.
 
            .PARAMETER Name
            Optional array of parameter names to retrieve from the caller's scope.
            Default is to retrieve all Preference variables as defined in the
            about_Preference_Variables help file (as of PowerShell 4.0) This parameter may also
            specify names of variables that are not in the about_Preference_Variables help file,
            and the function will retrieve and set those as well.
 
            .EXAMPLE
            # Imports the default PowerShell preference variables from the caller into the local scope.
            PS> Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
            .EXAMPLE
            # Imports only the ErrorActionPreference and SomeOtherVariable variables into the local scope.
            PS> Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -Name 'ErrorActionPreference','SomeOtherVariable'
 
            .EXAMPLE
            + Same as Example 2, but sends variable names to the Name parameter via pipeline input.
            PS> 'ErrorActionPreference','SomeOtherVariable' | Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
            .INPUTS
            String
 
            .OUTPUTS
            None. This function does not produce pipeline output.
 
            .LINK
            about_Preference_Variables
    #>


    [CmdletBinding(DefaultParameterSetName = 'AllVariables')]
    param (
        [Parameter(Mandatory = $true,HelpMessage='Add help message for user')]
        [ValidateScript({
                    $_.GetType().FullName -eq 'System.Management.Automation.PSScriptCmdlet'
        })]
        $Cmdlet,

        [Parameter(Mandatory = $true,HelpMessage='Add help message for user')]
        [Management.Automation.SessionState]$SessionState,

        [Parameter(ParameterSetName = 'Filtered', ValueFromPipeline = $true)]
        [string[]]$Name
    )

    BEGINN
    {
        $filterHash = @{}
    }

    PROCESS
    {
        if ($null -ne $Name)
        {
            foreach ($string in $Name)
            {
                $filterHash[$string] = $true
            }
        }
    }

    END
    {
        # List of preference variables taken from the about_Preference_Variables help file in PowerShell version 4.0
        $vars = @{
            'ErrorView'                   = $null
            'FormatEnumerationLimit'      = $null
            'LogCommandHealthEvent'       = $null
            'LogCommandLifecycleEvent'    = $null
            'LogEngineHealthEvent'        = $null
            'LogEngineLifecycleEvent'     = $null
            'LogProviderHealthEvent'      = $null
            'LogProviderLifecycleEvent'   = $null
            'MaximumAliasCount'           = $null
            'MaximumDriveCount'           = $null
            'MaximumErrorCount'           = $null
            'MaximumFunctionCount'        = $null
            'MaximumHistoryCount'         = $null
            'MaximumVariableCount'        = $null
            'OFS'                         = $null
            'OutputEncoding'              = $null
            'ProgressPreference'          = $null
            'PSDefaultParameterValues'    = $null
            'PSEmailServer'               = $null
            'PSModuleAutoLoadingPreference' = $null
            'PSSessionApplicationName'    = $null
            'PSSessionConfigurationName'  = $null
            'PSSessionOption'             = $null
            'ErrorActionPreference'       = 'ErrorAction'
            'DebugPreference'             = 'Debug'
            'ConfirmPreference'           = 'Confirm'
            'WhatIfPreference'            = 'WhatIf'
            'VerbosePreference'           = 'Verbose'
            'WarningPreference'           = 'WarningAction'
        }

        foreach ($entry in $vars.GetEnumerator()) 
        {
            if (([string]::IsNullOrEmpty($entry.Value) -or -not $Cmdlet.MyInvocation.BoundParameters.ContainsKey($entry.Value)) -and ($PSCmdlet.ParameterSetName -eq 'AllVariables' -or $filterHash.ContainsKey($entry.Name))) 
            {
                $variable = $Cmdlet.SessionState.PSVariable.Get($entry.Key)

                if ($null -ne $variable) 
                {
                    if ($SessionState -eq $ExecutionContext.SessionState) 
                    {
                        Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
                    }
                    else 
                    {
                        $SessionState.PSVariable.Set($variable.Name, $variable.Value)
                    }
                }
            }
        }

        if ($PSCmdlet.ParameterSetName -eq 'Filtered') 
        {
            foreach ($varName in $filterHash.Keys) 
            {
                if (-not $vars.ContainsKey($varName)) 
                {
                    $variable = $Cmdlet.SessionState.PSVariable.Get($varName)

                    if ($null -ne $variable)
                    {
                        if ($SessionState -eq $ExecutionContext.SessionState)
                        {
                            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
                        }
                        else
                        {
                            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
                        }
                    }
                }
            }
        }
    }
}


## PUBLIC MODULE FUNCTIONS AND DATA ##

function Invoke-ApplyExchangeCumulativeUpdate
{
    <#
    .EXTERNALHELP ExchangeNodeMaintenanceMode-help.xml
    .LINK
        https://github.com/jhochwald/ExchangeNodeMaintenanceMode/tree/master/release/1.0.0.11/docs/Functions/Invoke-ApplyExchangeCumulativeUpdate.md
    #>

    
    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
        Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Source = 'E:\',
        [Parameter(ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
        Position = 2)]
        [switch]
        $Prepare = $true,
        [Parameter(ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
        Position = 3)]
        [switch]
        $UMLangHandling = $null,
        [Parameter(ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
        Position = 4)]
        [string]
        $UMLangSource = 'F:\',
        [Parameter(ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
        Position = 5)]
        [string]
        $UMLanguages = 'de-DE,en-GB,en-US'
    )
    
    BEGIN
    {
        # Check if the given Directory conains the setup
        
        if ($UMLangHandling)
        {
            # Check if the given directory exists
        }
        
        if ($Prepare)
        {
            # Change to the Installer location
            Push-Location -Path $Source
            
            # Start the Setup
            .\Setup.exe /PrepareSchema /IAcceptExchangeServerLicenseTerms
            .\Setup.exe /PrepareAD /IAcceptExchangeServerLicenseTerms
            .\Setup.exe /PrepareDomain /IAcceptExchangeServerLicenseTerms
            
            # Return
            Pop-Location
        }
    }
    
    PROCESS
    {
        if ($UMLangHandling)
        {
            # Remove the old UM Languages
            # Change to the Installer location
            Push-Location -Path $Source
            
            # Start the Setup
            .\Setup.exe /RemoveUMLanguagePack:$UMLanguages
            
            # Return
            Pop-Location
        }
        
        # Default installation
        # Change to the Installer location
        Push-Location -Path $Source
            
        # Start the Setup
        .\Setup.exe /PrepareSchema /IAcceptExchangeServerLicenseTerms
        .\Setup.exe /PrepareAD /IAcceptExchangeServerLicenseTerms
        .\Setup.exe /PrepareDomain /IAcceptExchangeServerLicenseTerms
            
        # Return
        Pop-Location
        
        if ($UMLangHandling)
        {
            #
            # Change to the Installer location
            Push-Location -Path $Source
            
            # Start the Setup
            .\Setup.exe /AddUMLanguagePack:$UMLanguages /s:$UMLangSource /IAcceptExchangeServerLicenseTerms
            
            # Return
            Pop-Location
        }
    }
    
    END
    {
        # Cleanup
    }
}



function Invoke-Exchange2016Workaround
{
    <#
    .EXTERNALHELP ExchangeNodeMaintenanceMode-help.xml
    .LINK
        https://github.com/jhochwald/ExchangeNodeMaintenanceMode/tree/master/release/1.0.0.11/docs/Functions/Invoke-Exchange2016Workaround.md
    #>

    
    $paramGetCommand = @{
        Name          = 'Get-MailboxDatabaseCopyStatus'
        ErrorAction   = 'SilentlyContinue'
        WarningAction = 'SilentlyContinue'
    }
    if (-not (Get-Command @paramGetCommand )) 
    {
        try 
        {
            $paramAddPSSnapin = @{
                Name = 'Microsoft.Exchange.Management.PowerShell.SnapIn'
            }
            Add-PSSnapin @paramAddPSSnapin
        }
        catch 
        {
            $paramWriteError = @{
                Message       = 'Sure that this is a Exchange Server?'
                ErrorAction   = 'Stop'
                WarningAction = 'SilentlyContinue'
            }
            Write-Error @paramWriteError 
        }
    }
}



function Set-ExchangeNodeMaintenanceModeOff
{
    <#
    .EXTERNALHELP ExchangeNodeMaintenanceMode-help.xml
    .LINK
        https://github.com/jhochwald/ExchangeNodeMaintenanceMode/tree/master/release/1.0.0.11/docs/Functions/Set-ExchangeNodeMaintenanceModeOff.md
    #>

    
    [OutputType([bool])]
    param
    (
        [Parameter(ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
        Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ComputerName = $env:COMPUTERNAME
    )
    
    Begin
    {
        # Workaround for Exchange 2016 on Windows Server 2016
        Invoke-Exchange2016Workaround
    }
    
    Process
    {
        try
        {
            # Activate the components
            $paramSetServerComponentState = @{
                identity  = $ComputerName
                Component = 'ServerWideOffline'
                State     = 'Active'
                Requester = 'Maintenance'
            }
            $null = (Set-ServerComponentState @paramSetServerComponentState)
            
            # Activate the Cluster Node
            $paramResumeClusterNode = @{
                Name          = $ComputerName
                ErrorAction   = 'Stop'
                WarningAction = 'SilentlyContinue'
            }
            $null = (Resume-ClusterNode @paramResumeClusterNode)
            
            # Activate the Databases
            $paramSetMailboxServer = @{
                identity                                 = $ComputerName
                DatabaseCopyAutoActivationPolicy         = 'Unrestricted'
                DatabaseCopyActivationDisabledAndMoveNow = $false
                ErrorAction                              = 'Stop'
                WarningAction                            = 'SilentlyContinue'
            }
            $null = (Set-MailboxServer @paramSetMailboxServer)
        }
        catch
        {
            return $false
        }

        # Default
        return $true
    }
}


function Set-ExchangeNodeMaintenanceModeOn
{
    <#
    .EXTERNALHELP ExchangeNodeMaintenanceMode-help.xml
    .LINK
        https://github.com/jhochwald/ExchangeNodeMaintenanceMode/tree/master/release/1.0.0.11/docs/Functions/Set-ExchangeNodeMaintenanceModeOn.md
    #>

    
    param
    (
        [Parameter(ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
        Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ComputerName = $env:COMPUTERNAME
    )
    
    Begin
    {
        # Workaround for Exchange 2016 on Windows Server 2016
        Invoke-Exchange2016Workaround
    }
    
    Process
    {
        # Draining the server
        $paramSetServerComponentState = @{
            identity  = $ComputerName
            Component = 'ServerWideOffline'
            State     = 'Draining'
            Requester = 'Maintenance'
        }
        Set-ServerComponentState @paramSetServerComponentState
        
        # Restart of the Sertvices enforces the draining
        $paramRestartService = @{
            Name          = 'MSExchangeTransport'
            Force         = $true
            ErrorAction   = 'SilentlyContinue'
            WarningAction = 'SilentlyContinue'
            Confirm       = $false
        }
        $null = (Restart-Service @paramRestartService)
        
        $paramRestartService = @{
            Name          = 'MSExchangeFrontEndTransport'
            Force         = $true
            ErrorAction   = 'SilentlyContinue'
            WarningAction = 'SilentlyContinue'
            Confirm       = $false
        }
        $null = (Restart-Service @paramRestartService)
        
        # Suspend the cluster node
        $paramSuspendClusterNode = @{
            Name    = $ComputerName
            Confirm = $false
        }
        $null = (Suspend-ClusterNode @paramSuspendClusterNode)
        
        # Move all databases to the other servers
        $paramSetMailboxServer = @{
            identity                                 = $ComputerName
            DatabaseCopyActivationDisabledAndMoveNow = $true
        }
        $null = (Set-MailboxServer @paramSetMailboxServer)
        
        # Get the Cluster Twin
        $PartnerNode = (Get-ClusterNode | Where-Object -FilterScript {
                $_.Name -ne $ComputerName
        } | Select-Object -ExpandProperty name)
        
        $paramResolveDnsName = @{
            Name = $PartnerNode
            Type = 'A'
        }
        $PartnerNodeFQDN = ((Resolve-DnsName @paramResolveDnsName).name)
        
        $paramSetServerComponentState = @{
            identity  = $ComputerName
            Component = 'ServerWideOffline'
            State     = 'Inactive'
            Requester = 'Maintenance'
        }
        $null = (Set-ServerComponentState @paramSetServerComponentState)
        
        $paramRedirectMessage = @{
            Server  = $ComputerName
            Target  = $PartnerNodeFQDN
            Confirm = $false
        }
        $null = (Redirect-Message @paramRedirectMessage)
    }
}


function Test-ExchangeNodeMaintenanceMode
{
    <#
    .EXTERNALHELP ExchangeNodeMaintenanceMode-help.xml
    .LINK
        https://github.com/jhochwald/ExchangeNodeMaintenanceMode/tree/master/release/1.0.0.11/docs/Functions/Test-ExchangeNodeMaintenanceMode.md
    #>

    
    [OutputType([bool])]
    param
    (
        [Parameter(ValueFromPipeline = $true,
                ValueFromPipelineByPropertyName = $true,
        Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ComputerName = $env:COMPUTERNAME
    )
    
    Begin
    {
        # Workaround for Exchange 2016 on Windows Server 2016
        Invoke-Exchange2016Workaround
        
        # Set the Default
        $IsFalse = $false
    }
    
    Process
    {
        # Wait until all databases are moved
        $paramGetMailboxDatabaseCopyStatus = @{
            server        = $ComputerName
            ErrorAction   = 'Stop'
            WarningAction = 'SilentlyContinue'
        }
        try
        {
            $ActiveDBs = $null
            $ActiveDBs = (Get-MailboxDatabaseCopyStatus @paramGetMailboxDatabaseCopyStatus | Where-Object -FilterScript {
                    $_.Status -eq 'Mounted'
            })
        }
        catch
        {
            $IsFalse = $true
        }
        
        if ($ActiveDBs)
        {
            $IsFalse = $false
        }
        else
        {
            # Build the URL to check
            $URL = 'https://' + $ComputerName + '/owa/healthcheck.htm'
            
            # Ignore certificate warning (Will not match anyway)
            try 
            {
                Add-Type -TypeDefinition @'
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
    public bool CheckValidationResult(
        ServicePoint srvPoint, X509Certificate certificate,
        WebRequest request, int certificateProblem) {
        return true;
    }
}
'@

            }
            catch
            {
                Write-Verbose -Message 'Unable to add new Type'
            }
            
            $paramNewObject = @{
                TypeName      = 'TrustAllCertsPolicy'
                ErrorAction   = 'SilentlyContinue'
                WarningAction = 'SilentlyContinue'
            }
            
            [Net.ServicePointManager]::CertificatePolicy = (New-Object @paramNewObject)
            
            try
            {
                $result = $null
                
                # Get the result
                $paramInvokeWebRequest = @{
                    Uri           = $URL
                    ErrorAction   = 'Stop'
                    WarningAction = 'SilentlyContinue'
                }
                $result = $null
                $result = (Invoke-WebRequest @paramInvokeWebRequest)
            }
            catch
            {
                $IsFalse = $true
            }
            
            # Check the result
            if ($result.StatusCode -eq '200')
            {
                $IsFalse = $false
            }
        }
    }
    
    End
    {
        # Default
        return $IsFalse
    }
}


## Post-Load Module code ##

<#
    Use this variable for any path-sepecific actions (like loading dlls and such) to ensure it
    will work in testing and after being built
#>

$MyModulePath = $(
    Function Get-ScriptPath {
        $Invocation = (Get-Variable -Name MyInvocation -Scope 1).Value
        if($Invocation.PSScriptRoot) {
            $Invocation.PSScriptRoot
        }
        Elseif($Invocation.MyCommand.Path) {
            Split-Path -Path $Invocation.MyCommand.Path
        }
        elseif ($Invocation.InvocationName.Length -eq 0) {
            (Get-Location).Path
        }
        else {
            $Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf('\'))

        }
    }

    Get-ScriptPath
)

#region Module Cleanup
$ExecutionContext.SessionState.Module.OnRemove = {
    # Action to take if the module is removed
}

$null = Register-EngineEvent -SourceIdentifier ( [System.Management.Automation.PsEngineEvent]::Exiting ) -Action {
    # Action to take if the whole pssession is killed
}
#endregion Module Cleanup

# Non-function exported public module members might go here.
#Export-ModuleMember -Variable SomeVariable -Function *

# This file cannot be completely empty. Even leaving this comment is good enough.