DSCResources/MSFT_xSQLServerAvailabilityGroupListener/MSFT_xSQLServerAvailabilityGroupListener.psm1

$ErrorActionPreference = "Stop"

$script:currentPath = Split-Path -Parent $MyInvocation.MyCommand.Path
Import-Module $currentPath\..\..\xSQLServerHelper.psm1 -ErrorAction Stop

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $InstanceName = "DEFAULT",

        [Parameter(Mandatory = $true)]
        [System.String]
        $NodeName,

        [Parameter(Mandatory = $true)]
        [ValidateLength(1,15)]
        [System.String]
        $Name,

        [Parameter(Mandatory = $true)]
        [System.String]
        $AvailabilityGroup
    )

    try {
        $listener = Get-SQLAlwaysOnAvailabilityGroupListener -Name $Name -AvailabilityGroup $AvailabilityGroup -NodeName $NodeName -InstanceName $InstanceName
        
        if( $null -ne $listener ) {
            New-VerboseMessage -Message "Listener $Name already exist"

            $ensure = "Present"
            
            $port = [uint16]( $listener | Select-Object -ExpandProperty PortNumber )

            $presentIpAddress = $listener.AvailabilityGroupListenerIPAddresses

            $dhcp = [bool]( $presentIpAddress | Select-Object -First 1 -ExpandProperty IsDHCP )

            $ipAddress = @()
            foreach( $currentIpAddress in $presentIpAddress ) {
                $ipAddress += "$($currentIpAddress.IPAddress)/$($currentIpAddress.SubnetMask)"
            } 
        } else {
            New-VerboseMessage -Message "Listener $Name does not exist"

            $ensure = "Absent"
            $port = 0
            $dhcp = $false
            $ipAddress = $null
        }
    } catch {
        throw New-TerminatingError -ErrorType AvailabilityGroupListenerNotFound -FormatArgs @($Name) -ErrorCategory ObjectNotFound -InnerException $_.Exception
    }

    $returnValue = @{
        InstanceName = [System.String] $InstanceName
        NodeName = [System.String] $NodeName
        Name = [System.String] $Name
        Ensure = [System.String] $ensure
        AvailabilityGroup = [System.String] $AvailabilityGroup
        IpAddress = [System.String[]] $ipAddress
        Port = [System.UInt16] $port
        DHCP = [System.Boolean] $dhcp
    }

    return $returnValue
}

function Set-TargetResource
{
    [CmdletBinding(SupportsShouldProcess)]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $InstanceName = "DEFAULT",

        [Parameter(Mandatory = $true)]
        [System.String]
        $NodeName,

        [Parameter(Mandatory = $true)]
        [ValidateLength(1,15)]
        [System.String]
        $Name,

        [ValidateSet("Present","Absent")]
        [System.String]
        $Ensure,

        [Parameter(Mandatory = $true)]
        [System.String]
        $AvailabilityGroup,

        [System.String[]]
        $IpAddress,

        [System.UInt16]
        $Port,

        [System.Boolean]
        $DHCP
    )
   
    $parameters = @{
        InstanceName = [System.String] $InstanceName
        NodeName = [System.String] $NodeName
        Name = [System.String] $Name
        AvailabilityGroup = [System.String] $AvailabilityGroup
    }
    
    $listenerState = Get-TargetResource @parameters 
    if( $null -ne $listenerState ) {
        if( $Ensure -ne "" -and $listenerState.Ensure -ne $Ensure ) {
            $InstanceName = Get-SQLPSInstanceName -InstanceName $InstanceName
            
            if( $Ensure -eq "Present") {
                if( ( $PSCmdlet.ShouldProcess( $Name, "Create listener on $AvailabilityGroup" ) ) ) {
                    $newListenerParams = @{
                        Name = $Name
                        Path = "SQLSERVER:\SQL\$NodeName\$InstanceName\AvailabilityGroups\$AvailabilityGroup"
                    }

                    if( $Port ) {
                        New-VerboseMessage -Message "Listener port set to $Port"
                        $newListenerParams += @{
                            Port = $Port
                        }
                    }

                    if( $DHCP -and $IpAddress.Count -gt 0 ) {
                        New-VerboseMessage -Message "Listener set to DHCP with subnet $IpAddress"
                        $newListenerParams += @{
                            DhcpSubnet = [string]$IpAddress
                        }
                    } elseif ( -not $DHCP -and $IpAddress.Count -gt 0 ) {
                        New-VerboseMessage -Message "Listener set to static IP-address(es); $($IpAddress -join ', ')"
                        $newListenerParams += @{
                            StaticIp = $IpAddress
                        }
                    } else {
                        New-VerboseMessage -Message "Listener using DHCP with server default subnet"
                    }
                                        
                    New-SqlAvailabilityGroupListener @newListenerParams -Verbose:$False | Out-Null   # Suppressing Verbose because it prints the entire T-SQL statement otherwise
                }
            } else {
                if( ( $PSCmdlet.ShouldProcess( $Name, "Remove listener from $AvailabilityGroup" ) ) ) {
                    Remove-Item "SQLSERVER:\SQL\$NodeName\$InstanceName\AvailabilityGroups\$AvailabilityGroup\AvailabilityGroupListeners\$Name"
                }
            }
        } else {
            if( $Ensure -ne "" ) { New-VerboseMessage -Message "State is already $Ensure" }
            
            if( $listenerState.Ensure -eq "Present") {
                if( -not $DHCP -and $listenerState.IpAddress.Count -lt $IpAddress.Count ) { # Only able to add a new IP-address, not change existing ones.
                    New-VerboseMessage -Message "Found at least one new IP-address."
                    $ipAddressEqual = $False
                } else {
                    # No new IP-address
                    if( $null -eq $IpAddress -or -not ( Compare-Object -ReferenceObject $IpAddress -DifferenceObject $listenerState.IpAddress ) ) { 
                       $ipAddressEqual = $True
                    } else {
                        throw New-TerminatingError -ErrorType AvailabilityGroupListenerIPChangeError -FormatArgs @($($IpAddress -join ', '),$($listenerState.IpAddress -join ', ')) -ErrorCategory InvalidOperation
                    }
                }

                if( $($PSBoundParameters.ContainsKey('DHCP')) -and $listenerState.DHCP -ne $DHCP ) {
                    throw New-TerminatingError -ErrorType AvailabilityGroupListenerDHCPChangeError -FormatArgs @( $DHCP, $($listenerState.DHCP) ) -ErrorCategory InvalidOperation
                }
                
                if( $listenerState.Port -ne $Port -or -not $ipAddressEqual ) {
                    New-VerboseMessage -Message "Listener differ in configuration."

                    if( $listenerState.Port -ne $Port ) {
                        if( ( $PSCmdlet.ShouldProcess( $Name, "Changing port configuration" ) ) ) {
                            $InstanceName = Get-SQLPSInstanceName -InstanceName $InstanceName
                            
                            $setListenerParams = @{
                                Path = "SQLSERVER:\SQL\$NodeName\$InstanceName\AvailabilityGroups\$AvailabilityGroup\AvailabilityGroupListeners\$Name"
                                Port = $Port
                            }

                            Set-SqlAvailabilityGroupListener @setListenerParams -Verbose:$False | Out-Null # Suppressing Verbose because it prints the entire T-SQL statement otherwise
                        }
                    }

                    if( -not $ipAddressEqual ) {
                        if( ( $PSCmdlet.ShouldProcess( $Name, "Adding IP-address(es)" ) ) ) {
                            $InstanceName = Get-SQLPSInstanceName -InstanceName $InstanceName
                            
                            $newIpAddress = @()
                            
                            foreach( $currentIpAddress in $IpAddress ) {
                                if( -not ( $listenerState.IpAddress -contains $currentIpAddress ) ) {
                                    $newIpAddress += $currentIpAddress
                                }
                            }
                            
                            $setListenerParams = @{
                                Path = "SQLSERVER:\SQL\$NodeName\$InstanceName\AvailabilityGroups\$AvailabilityGroup\AvailabilityGroupListeners\$Name"
                                StaticIp = $newIpAddress
                            }

                            Add-SqlAvailabilityGroupListenerStaticIp @setListenerParams -Verbose:$False | Out-Null # Suppressing Verbose because it prints the entire T-SQL statement otherwise
                        }
                    }

                } else {
                    New-VerboseMessage -Message "Listener configuration is already correct."
                }
            } else {
                throw New-TerminatingError -ErrorType AvailabilityGroupListenerNotFound -ErrorCategory ObjectNotFound
            }
        }
    } else {
        throw New-TerminatingError -ErrorType UnexpectedErrorFromGet -ErrorCategory InvalidResult
    }
}

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

        [Parameter(Mandatory = $true)]
        [System.String]
        $NodeName,

        [Parameter(Mandatory = $true)]
        [ValidateLength(1,15)]
        [System.String]
        $Name,

        [ValidateSet("Present","Absent")]
        [System.String]
        $Ensure,

        [Parameter(Mandatory = $true)]
        [System.String]
        $AvailabilityGroup,

        [System.String[]]
        $IpAddress,

        [System.UInt16]
        $Port,

        [System.Boolean]
        $DHCP
    )

    $parameters = @{
        InstanceName = [System.String] $InstanceName
        NodeName = [System.String] $NodeName
        Name = [System.String] $Name
        AvailabilityGroup = [System.String] $AvailabilityGroup
    }
    
    New-VerboseMessage -Message "Testing state of listener $Name"
    
    $listenerState = Get-TargetResource @parameters 
    if( $null -ne $listenerState ) {
        if( $null -eq $IpAddress -or ($null -ne $listenerState.IpAddress -and -not ( Compare-Object -ReferenceObject $IpAddress -DifferenceObject $listenerState.IpAddress ) ) ) { 
            $ipAddressEqual = $true
        } else {
            $ipAddressEqual = $false
        }
        
        [System.Boolean] $result = $false
        if( $listenerState.Ensure -eq $Ensure)  {
            if( $Ensure -eq 'Absent' ) {
                $result = $true
            }
        }

        if( -not $($PSBoundParameters.ContainsKey('Ensure')) -or $Ensure -eq "Present" ) { 
            if( ( $Port -eq "" -or $listenerState.Port -eq $Port) -and 
                $ipAddressEqual -and 
                ( -not $($PSBoundParameters.ContainsKey('DHCP')) -or $listenerState.DHCP -eq $DHCP ) ) 
            {
                $result = $true
            }
        }

    } else {
        throw New-TerminatingError -ErrorType UnexpectedErrorFromGet -ErrorCategory InvalidResult
    }

    return $result
}

function Get-SQLAlwaysOnAvailabilityGroupListener
{
    [CmdletBinding()]
    [OutputType()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter(Mandatory = $true)]
        [System.String]
        $AvailabilityGroup,

        [Parameter(Mandatory = $true)]
        [System.String]
        $InstanceName,

        [Parameter(Mandatory = $true)]
        [System.String]
        $NodeName 
    )

    $instance = Get-SQLPSInstance -InstanceName $InstanceName -NodeName $NodeName
    $Path = "$($instance.PSPath)\AvailabilityGroups\$AvailabilityGroup\AvailabilityGroupListeners"

    Write-Debug "Connecting to $Path as $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
    
    [String[]] $presentListener = Get-ChildItem $Path
    if( $presentListener.Count -ne 0 -and $presentListener.Contains("[$Name]") ) {
        Write-Debug "Connecting to availability group $Name as $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
        $listener = Get-Item "$Path\$Name"
    } else {
        $listener = $null
    }    

    return $listener
}

Export-ModuleMember -Function *-TargetResource