DSCResources/MSFT_xSqlHAGroup/MSFT_xSqlHAGroup.psm1

#
# xSqlHAGroup: DSC resource to configure a Sql High Availability (HA) Group. If the HA Group does not exist, it will
# create one with given name on given sql instance, it also adds the database(s) to the group. If the HA group
# already exists, it will join sql instance to the group, replicate the database(s) in the group to local instance.
#

#
# The Get-TargetResource cmdlet.
#
function Get-TargetResource
{
    param
    (    
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [parameter(Mandatory)]
        [ValidateNotNull()]
        [string[]] $Database,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $ClusterName,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $DatabaseBackupPath,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $InstanceName,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $EndpointName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [PSCredential] $DomainCredential,
        
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [PSCredential] $SqlAdministratorCredential    
      )

    if ($Database.Count -lt 1)
    {
        throw "Parameter Database does not have any database"
    }

    $bConfigured = Test-TargetResource -Name $Name -Database $Database -ClusterName $ClusterName -DatabaseBackupPath $DatabaseBackupPath -InstanceName $InstanceName -EndpointName $EndpointName -DomainCredential $DomainCredential -SqlAdministratorCredential $SqlAdministratorCredential

    $returnValue = @{
 
        Database = $Database
        Name = $Name
        ClusterName = $ClusterName
        DatabaseBackupPath = $DatabaseBackupPath
        InstanceName = $InstanceName
        EndpointName = $EndpointName

        DomainCredential = $DomainCredential.UserName
        SqlAdministratorCredential = $SqlAdministratorCredential.UserName

        Configured = $bConfigured
    }

    $returnValue
}

#
# The Set-TargetResource cmdlet.
#
function Set-TargetResource
{
    param
    (    
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [parameter(Mandatory)]
        [ValidateNotNull()]
        [string[]] $Database,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $ClusterName,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $DatabaseBackupPath,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $InstanceName,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $EndpointName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [PSCredential] $DomainCredential,
        
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [PSCredential] $SqlAdministratorCredential    
      )

    if ($Database.Count -lt 1)
    {
        throw "Parameter Database does not have any database"
    }

    Write-Verbose -Message "Checking if SQL HAG $Name is present ..."
    Write-Verbose -Message "Cluster: $ClusterName, Database: $Database"

    $bHAGExist = $false
    $primaryReplica = $InstanceName

    $sa = $SqlAdministratorCredential.UserName
    $saPassword = $SqlAdministratorCredential.GetNetworkCredential().Password

    try
    {
        ($oldToken, $context, $newToken) = ImpersonateAs -cred $DomainCredential    
        $nodes = Get-ClusterNode -Cluster $ClusterName
    }
    finally
    {
        if ($context)
        {
            $context.Undo()
            $context.Dispose()

            CloseUserToken($newToken)
        }
    }

    foreach ($node in $nodes.Name)
    {
        $instance = Get-SQLInstanceName -node $node -InstanceName $InstanceName

        $bCheck = Check-SQLHAGroup -InstanceName $instance -Name $Name -sa $sa -saPassword $saPassword
        if ($bCheck)
        {
            Write-Verbose -Message "Found SQL HAG $Name on instance $instance"
            $bHAGExist = $true

            # check if it is the primary replica
            $bPrimaryCheck = Check-SQLHAGroupPrimaryReplica -InstanceName $instance -Name $Name -sa $sa -saPassword $saPassword
            if ($bPrimaryCheck)
            {
                $primaryReplica = $instance
            }
        }
    }

    if ($bHAGExist)
    {
        Write-Verbose -Message "Add instance $InstanceName to SQL HAG $Name"
        
        $bCheckPreviousInstance = Check-SQLHAGroupReplicaExist -InstanceName $InstanceName -Name $Name -PrimaryInstanceName $primaryReplica -sa $sa -saPassword $saPassword
        if ($bCheckPreviousInstance)
        {
            Write-Verbose -Message "SQLHAGroup $Name already has the instance $InstanceName, clean up first"
            $query = "alter availability group $Name `
                                        remove replica on '$InstanceName'"


            Write-Verbose -Message "Query: $query"
            osql -S $primaryReplica -U $sa -P $saPassword -Q $query
        }
   

        # Add this instance to HAG group on instance $primaryInstance
        $query = "alter availability group $Name `
                                        add replica on '$InstanceName' with `
                                        ( `
                                            EndPoint_URL = 'TCP://$EndpointName', `
                                            Availability_Mode = Synchronous_Commit, `
                                            Failover_Mode = Automatic, `
                                            Secondary_Role(Allow_connections = ALL) `
                                         ) "


        Write-Verbose -Message "Query: $query"
        osql -S $primaryReplica -U $sa -P $saPassword -Q $query

        # Add this node to HAG
        osql -S $InstanceName -U $sa -P $saPassword -Q "ALTER AVAILABILITY GROUP $Name JOIN"

        # restore database
        foreach($db in $Database)
        {
            $query = "restore database $db from disk = '$DatabaseBackupPath\$db.bak' with norecovery"
            Write-Verbose -Message "Instance $InstanceName Query: $query"
            osql -S $InstanceName -U $sa -P $saPassword -Q $query
        

            $query = "restore log $db from disk = '$DatabaseBackupPath\$db.log' with norecovery "
            Write-Verbose -Message "Query: $query"
            osql -S $InstanceName -U $sa -P $saPassword -Q $query

            # Add database to HAG
            osql -S $InstanceName -U $sa -P $saPassword -Q "ALTER DATABASE $db SET HADR AVAILABILITY GROUP = $Name"
        }
    }

    else # create
    {
        Write-Verbose -Message "Create SQL HAG $Name and primary instance $InstanceName"

        foreach($db in $Database)
        {
            Write-Verbose -Message "Create database $db ..."
            osql -S $InstanceName -U $sa -P $saPassword -Q "if not exists (select * from master.sys.databases where name = '$db') begin Create database $db end;"    

            Write-Verbose -Message "Backup to $DatabaseBackupPath .."
            osql -S $InstanceName -U $sa -P $saPassword -Q "backup database $db to disk = '$DatabaseBackupPath\$db.bak' with format"
        }

        $dblist = "$Database" -replace " ", ", "

        Write-Verbose -Message "AG: $Name "
        $query =  "Create Availability Group $Name `
                                    For Database $Database `
                                    Replica ON `
                                    '$InstanceName' with `
                                    ( `
                                        ENDPOINT_URL = 'TCP://$EndpointName', `
                                        Availability_Mode = Synchronous_Commit, `
                                        Failover_Mode = Automatic `
                                     )"


        Write-Verbose -Message "Create HAG : $query.."
        osql -S $InstanceName -U $sa -P $saPassword -Q $query

        foreach($db in $Database)
        {
            Write-Verbose -Message "Backup Log to $DatabaseBackupPath .."
            osql -S $InstanceName -U $sa -P $saPassword -Q "backup log $db to disk = '$DatabaseBackupPath\$db.log' with NOFormat"
        }
   }


}

#
# The Test-TargetResource cmdlet.
#
function Test-TargetResource
{
    param
    (    
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [parameter(Mandatory)]
        [ValidateNotNull()]
        [string[]] $Database,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $ClusterName,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $DatabaseBackupPath,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $InstanceName,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string] $EndpointName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [PSCredential] $DomainCredential,
        
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [PSCredential] $SqlAdministratorCredential    
      )

    if ($Database.Count -lt 1)
    {
        throw "Parameter Database does not have any database"
    }

    Write-Verbose -Message "Checking if SQL HA Group $Name on instance $InstanceName present ..."

    $sa = $SqlAdministratorCredential.UserName
    $saPassword = $SqlAdministratorCredential.GetNetworkCredential().Password

    $bFound = Check-SQLHAGroup -InstanceName $InstanceName -Name $Name -sa $sa -saPassword $saPassword
    if ($bFound)
    {
        Write-Verbose -Message "SQL HA Group $Name is present"
        $true
    }
    else
    {
        Write-Verbose -Message "SQL HA Group $Name not found"
        $false
    }
}


function Check-SQLHAGroup($InstanceName, $Name, $sa, $saPassword)
{
    Write-Verbose -Message "Check HAG $Name including instance $InstanceName ..."
    $query = OSQL -S $InstanceName -U $sa -P $saPassword -Q "select count(name) from master.sys.availability_groups where name = '$Name'" -h-1
    
    Write-Verbose -Message "SQL: $query"
    
    [bool] [int] ([String] $query[0]).Trim()
}


function Check-SQLHAGroupPrimaryReplica($InstanceName, $Name, $sa, $saPassword)
{
    $query = OSQL -S $InstanceName -U $sa -P $saPassword -Q "select count(replica_id) from sys.dm_hadr_availability_replica_states s `
                                        inner join sys.availability_groups g on g.group_id = s.group_id `
                                        where g.name = '$Name' and s.role_desc = 'PRIMARY' and s.is_local = 1"
 -h-1
    [bool] [int] ([string] $query[0]).Trim()
}

function Check-SQLHAGroupReplicaExist($InstanceName, $Name, $PrimaryInstanceName, $sa, $saPassword)
{
    $query = OSQL -S $PrimaryInstanceName -U $sa -P $saPassword -Q "select count(replica_id) from sys.availability_replicas r `
                                        inner join sys.availability_groups g on g.group_id = r.group_id `
                                        where g.name = '$Name' and r.replica_server_name = '$InstanceName' "
 -h-1
    [bool] [int] ([string] $query[0]).Trim()

}

function Get-PureInstanceName ($InstanceName)
{
    $list = $InstanceName.Split("\")
    if ($list.Count -gt 1)
    {
        $list[1]
    }
    else
    {
        "MSSQLSERVER"
    }
}

function Get-SQLInstanceName ($node, $InstanceName)
{
    $pureInstanceName = Get-PureInstanceName -InstanceName $InstanceName

    if ("MSSQLSERVER" -eq $pureInstanceName)
    {
        $node
    }
    else
    {
        $node + "\" + $pureInstanceName
    }
}


function Get-ImpersonatetLib
{
    if ($script:ImpersonateLib)
    {
        return $script:ImpersonateLib
    }

    $sig = @'
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
 
[DllImport("kernel32.dll")]
public static extern Boolean CloseHandle(IntPtr hObject);
'@
 
   $script:ImpersonateLib = Add-Type -PassThru -Namespace 'Lib.Impersonation' -Name ImpersonationLib -MemberDefinition $sig 

   return $script:ImpersonateLib
    
}

function ImpersonateAs([PSCredential] $cred)
{
    [IntPtr] $userToken = [Security.Principal.WindowsIdentity]::GetCurrent().Token
    $userToken
    $ImpersonateLib = Get-ImpersonatetLib

    $bLogin = $ImpersonateLib::LogonUser($cred.GetNetworkCredential().UserName, $cred.GetNetworkCredential().Domain, $cred.GetNetworkCredential().Password, 
    9, 0, [ref]$userToken)
    
    if ($bLogin)
    {
        $Identity = New-Object Security.Principal.WindowsIdentity $userToken
        $context = $Identity.Impersonate()
    }
    else
    {
        throw "Can't Logon as User $cred.GetNetworkCredential().UserName."
    }
    $context, $userToken
}

function CloseUserToken([IntPtr] $token)
{
    $ImpersonateLib = Get-ImpersonatetLib

    $bLogin = $ImpersonateLib::CloseHandle($token)
    if (!$bLogin)
    {
        throw "Can't close token"
    }
}


Export-ModuleMember -Function *-TargetResource