DSCResources/MSFT_SqlServerMemory/MSFT_SqlServerMemory.psm1

Import-Module -Name (Join-Path -Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -ChildPath 'SqlServerDscHelper.psm1') -Force

<#
    .SYNOPSIS
        This function gets the value of the min and max memory server configuration option.
 
    .PARAMETER ServerName
        The host name of the SQL Server to be configured.
 
    .PARAMETER InstanceName
        The name of the SQL instance to be configured.
#>


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

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ServerName = $env:COMPUTERNAME
    )

    $sqlServerObject = Connect-SQL -SQLServer $ServerName -SQLInstanceName $InstanceName

    if ($sqlServerObject)
    {
        Write-Verbose -Message 'Getting the value for minimum and maximum SQL server memory.'
        $minMemory = $sqlServerObject.Configuration.MinServerMemory.ConfigValue
        $maxMemory = $sqlServerObject.Configuration.MaxServerMemory.ConfigValue

        # Is this node actively hosting the SQL instance?
        $isActiveNode = Test-ActiveNode -ServerObject $sqlServerObject
    }

    $returnValue = @{
        InstanceName = $InstanceName
        ServerName   = $ServerName
        MinMemory    = $minMemory
        MaxMemory    = $maxMemory
        IsActiveNode = $isActiveNode
    }

    $returnValue
}

<#
    .SYNOPSIS
        This function sets the value for the min and max memory server configuration option.
 
    .PARAMETER ServerName
        The host name of the SQL Server to be configured.
 
    .PARAMETER InstanceName
        The name of the SQL instance to be configured.
 
    .PARAMETER Ensure
        When set to 'Present' then min and max memory will be set to either the value in parameter MinMemory and MaxMemory or dynamically configured when parameter DynamicAlloc is set to $true.
        When set to 'Absent' min and max memory will be set to default values.
 
    .PARAMETER DynamicAlloc
        If set to $true then max memory will be dynamically configured.
        When this is set parameter is set to $true, the parameter MaxMemory must be set to $null or not be configured.
 
    .PARAMETER MinMemory
        This is the minimum amount of memory, in MB, in the buffer pool used by the instance of SQL Server.
 
    .PARAMETER MaxMemory
        This is the maximum amount of memory, in MB, in the buffer pool used by the instance of SQL Server.
 
    .PARAMETER ProcessOnlyOnActiveNode
        Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server instance.
        Not used in Set-TargetResource.
#>

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

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ServerName = $env:COMPUTERNAME,

        [Parameter()]
        [ValidateSet('Present', 'Absent')]
        [System.String]
        $Ensure = 'Present',

        [Parameter()]
        [System.Boolean]
        $DynamicAlloc = $false,

        [Parameter()]
        [System.Int32]
        $MinMemory,

        [Parameter()]
        [System.Int32]
        $MaxMemory,

        [Parameter()]
        [Boolean]
        $ProcessOnlyOnActiveNode
    )

    $sqlServerObject = Connect-SQL -SQLServer $ServerName -SQLInstanceName $InstanceName

    if ($sqlServerObject)
    {
        Write-Verbose -Message 'Setting the minimum and maximum memory used by the instance.'
        switch ($Ensure)
        {
            'Present'
            {
                if ($DynamicAlloc)
                {
                    if ($MaxMemory)
                    {
                        throw New-TerminatingError -ErrorType MaxMemoryParamMustBeNull `
                            -FormatArgs @( $ServerName, $InstanceName ) `
                            -ErrorCategory InvalidArgument
                    }

                    $MaxMemory = Get-SqlDscDynamicMaxMemory
                    New-VerboseMessage -Message "Dynamic maximum memory has been calculated to $($MaxMemory)MB."
                }
                else
                {
                    if (-not $MaxMemory)
                    {
                        throw New-TerminatingError -ErrorType MaxMemoryParamMustNotBeNull `
                            -FormatArgs @( $ServerName, $InstanceName ) `
                            -ErrorCategory InvalidArgument
                    }
                }

                $sqlServerObject.Configuration.MaxServerMemory.ConfigValue = $MaxMemory
                New-VerboseMessage -Message "Maximum memory used by the instance has been limited to $($MaxMemory)MB."
            }

            'Absent'
            {
                $sqlServerObject.Configuration.MaxServerMemory.ConfigValue = 2147483647
                $sqlServerObject.Configuration.MinServerMemory.ConfigValue = 0
                New-VerboseMessage -Message ('Ensure is set to absent. Minimum and maximum server memory' + `
                        'values used by the instance are reset to the default values.')
            }
        }

        try
        {
            if ($MinMemory)
            {
                $sqlServerObject.Configuration.MinServerMemory.ConfigValue = $MinMemory
                New-VerboseMessage -Message "Minimum memory used by the instance is set to $($MinMemory)MB."
            }

            $sqlServerObject.Alter()
        }
        catch
        {
            throw New-TerminatingError -ErrorType AlterServerMemoryFailed `
                -FormatArgs @($ServerName, $InstanceName) `
                -ErrorCategory InvalidOperation `
                -InnerException $_.Exception
        }
    }
}

<#
    .SYNOPSIS
        This function tests the value of the min and max memory server configuration option.
 
    .PARAMETER ServerName
        The host name of the SQL Server to be configured.
 
    .PARAMETER InstanceName
        The name of the SQL instance to be configured.
 
    .PARAMETER Ensure
        When set to 'Present' then min and max memory will be set to either the value in parameter MinMemory and MaxMemory or dynamically configured when parameter DynamicAlloc is set to $true.
        When set to 'Absent' min and max memory will be set to default values.
 
    .PARAMETER DynamicAlloc
        If set to $true then max memory will be dynamically configured.
        When this is set parameter is set to $true, the parameter MaxMemory must be set to $null or not be configured.
 
    .PARAMETER MinMemory
        This is the minimum amount of memory, in MB, in the buffer pool used by the instance of SQL Server.
 
    .PARAMETER MaxMemory
        This is the maximum amount of memory, in MB, in the buffer pool used by the instance of SQL Server.
 
    .PARAMETER ProcessOnlyOnActiveNode
        Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server instance.
#>

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

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ServerName = $env:COMPUTERNAME,

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

        [Parameter()]
        [System.Boolean]
        $DynamicAlloc = $false,

        [Parameter()]
        [System.Int32]
        $MinMemory,

        [Parameter()]
        [System.Int32]
        $MaxMemory,

        [Parameter()]
        [Boolean]
        $ProcessOnlyOnActiveNode
    )

    Write-Verbose -Message 'Testing the values of the minimum and maximum memory server configuration option set to be used by the instance.'

    $getTargetResourceParameters = @{
        InstanceName = $InstanceName
        ServerName   = $ServerName
    }

    $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters

    $currentMinMemory = $getTargetResourceResult.MinMemory
    $currentMaxMemory = $getTargetResourceResult.MaxMemory
    $isServerMemoryInDesiredState = $true

    <#
        If this is supposed to process only the active node, and this is not the
        active node, don't bother evaluating the test.
    #>

    if ( $ProcessOnlyOnActiveNode -and -not $getTargetResourceResult.IsActiveNode )
    {
        # Use localization if the resource has been converted
        New-VerboseMessage -Message ( 'The node "{0}" is not actively hosting the instance "{1}". Exiting the test.' -f $env:COMPUTERNAME, $InstanceName )
        return $isServerMemoryInDesiredState
    }

    switch ($Ensure)
    {
        'Absent'
        {
            if ($currentMaxMemory -ne 2147483647)
            {
                New-VerboseMessage -Message "Current maximum server memory used by the instance is $($currentMaxMemory)MB. Expected 2147483647MB."
                $isServerMemoryInDesiredState = $false
            }

            if ($currentMinMemory -ne 0)
            {
                New-VerboseMessage -Message "Current minimum server memory used by the instance is $($currentMinMemory)MB. Expected 0MB."
                $isServerMemoryInDesiredState = $false
            }
        }

        'Present'
        {
            if ($DynamicAlloc)
            {
                if ($MaxMemory)
                {
                    throw New-TerminatingError -ErrorType MaxMemoryParamMustBeNull `
                        -FormatArgs @( $ServerName, $InstanceName ) `
                        -ErrorCategory InvalidArgument
                }

                $MaxMemory = Get-SqlDscDynamicMaxMemory
                New-VerboseMessage -Message "Dynamic maximum memory has been calculated to $($MaxMemory)MB."
            }
            else
            {
                if (-not $MaxMemory)
                {
                    throw New-TerminatingError -ErrorType MaxMemoryParamMustNotBeNull `
                        -FormatArgs @( $ServerName, $InstanceName ) `
                        -ErrorCategory InvalidArgument
                }
            }

            if ($MaxMemory -ne $currentMaxMemory)
            {
                New-VerboseMessage -Message ("Current maximum server memory used by the instance " + `
                        "is $($currentMaxMemory)MB. Expected $($MaxMemory)MB.")
                $isServerMemoryInDesiredState = $false
            }

            if ($MinMemory)
            {
                if ($MinMemory -ne $currentMinMemory)
                {
                    New-VerboseMessage -Message ("Current minimum server memory used by the instance " + `
                            "is $($currentMinMemory)MB. Expected $($MinMemory)MB.")
                    $isServerMemoryInDesiredState = $false
                }
            }
        }
    }

    return $isServerMemoryInDesiredState
}

<#
    .SYNOPSIS
    This cmdlet is used to return the Dynamic MaxMemory of a SQL Instance
#>

function Get-SqlDscDynamicMaxMemory
{
    try
    {
        $physicalMemory = ((Get-CimInstance -ClassName Win32_PhysicalMemory).Capacity | Measure-Object -Sum).Sum
        $physicalMemoryInMegaBytes = [Math]::Round($physicalMemory / 1MB)

        # Find how much to save for OS: 20% of total ram for under 15GB / 12.5% for over 20GB
        if ($physicalMemoryInMegaBytes -ge 20480)
        {
            $reservedOperatingSystemMemory = [Math]::Round((0.125 * $physicalMemoryInMegaBytes))
        }
        else
        {
            $reservedOperatingSystemMemory = [Math]::Round((0.2 * $physicalMemoryInMegaBytes))
        }

        $numberOfCores = (Get-CimInstance -ClassName Win32_Processor | Measure-Object -Property NumberOfCores -Sum).Sum

        # Get the number of SQL threads.
        if ($numberOfCores -ge 4)
        {
            $numberOfSqlThreads = 256 + ($numberOfCores - 4) * 8
        }
        else
        {
            $numberOfSqlThreads = 0
        }

        $operatingSystemArchitecture = (Get-CimInstance -ClassName Win32_operatingsystem).OSArchitecture

        # Find threadStackSize 1MB x86/ 2MB x64/ 4MB IA64
        if ($operatingSystemArchitecture -eq '32-bit')
        {
            $threadStackSize = 1
        }
        elseif ($operatingSystemArchitecture -eq '64-bit')
        {
            $threadStackSize = 2
        }
        else
        {
            $threadStackSize = 4
        }

        $maxMemory = $physicalMemoryInMegaBytes - $reservedOperatingSystemMemory - ($numberOfSqlThreads * $threadStackSize) - (1024 * [System.Math]::Ceiling($numberOfCores / 4))
    }
    catch
    {
        throw New-TerminatingError -ErrorType ErrorGetDynamicMaxMemory `
            -ErrorCategory InvalidOperation `
            -InnerException $_.Exception
    }

    $maxMemory
}

Export-ModuleMember -Function *-TargetResource