DSCResources/MicrosoftAzure_xSqlNewAGDatabase/MicrosoftAzure_xSqlNewAGDatabase.psm1

function Get-TargetResource
{
    param
    (
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [PSCredential] $SqlAdministratorCredential,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String[]]$DatabaseNames,
        
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$PrimaryReplica,
        
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SecondaryReplica,
         
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SqlAlwaysOnAvailabilityGroupName
    )

   $retVal = @{
        DatabaseNames = $DatabaseNames
        PrimaryReplica = $PrimaryReplica
        SecondaryReplica = $SecondaryReplica
        SqlAlwaysOnAvailabilityGroupName = $SqlAlwaysOnAvailabilityGroupName
        SqlAdministratorCredential = $SqlAdministratorCredential.UserName
    }

    $retVal
}

function Set-TargetResource
{
    param
    (
        
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SqlAlwaysOnAvailabilityGroupName,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String[]]$DatabaseNames,
        
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$PrimaryReplica,
        
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SecondaryReplica,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [PSCredential] $SqlAdministratorCredential

      
    )

    Configure-Databases -DatabaseNames $DatabaseNames -SqlAdministratorCredential $SqlAdministratorCredential -PrimaryReplica $PrimaryReplica -SecondaryReplica $SecondaryReplica -SqlAlwaysOnAvailabilityGroupName $SqlAlwaysOnAvailabilityGroupName
 
}

function Test-TargetResource
{
    param
    (
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [PSCredential] $SqlAdministratorCredential,

        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String[]]$DatabaseNames,
        
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$PrimaryReplica,
        
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SecondaryReplica,
         
        [parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$SqlAlwaysOnAvailabilityGroupName
    )
    
    Test-Databases -DatabaseNames $DatabaseNames -SqlAdministratorCredential $SqlAdministratorCredential -PrimaryReplica $PrimaryReplica -SecondaryReplica $SecondaryReplica -SqlAlwaysOnAvailabilityGroupName $SqlAlwaysOnAvailabilityGroupName
}


function Test-Databases
{
    param(
        [String[]]$DatabaseNames,
        [PSCredential]$SqlAdministratorCredential,
        [string]$PrimaryReplica,
        [string]$SecondaryReplica,
        [string]$SqlAlwaysOnAvailabilityGroupName
    )

    # Required SQL managability modules
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SmoExtended") | Out-Null
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.ConnectionInfo") | Out-Null
    
    if ($null -ne $DatabaseNames) {
        # Primamry Replica connection
        $primaryServer = Get-SqlServer $PrimaryReplica -SqlAdministratorCredential $SqlAdministratorCredential
        $replicaServer = Get-SqlServer $SecondaryReplica -SqlAdministratorCredential $SqlAdministratorCredential
        
        $primaryAG = $primaryServer.AvailabilityGroups | where { $_.Name -eq $SqlAlwaysOnAvailabilityGroupName }
        $secondaryAG = $replicaServer.AvailabilityGroups | where { $_.Name -eq $SqlAlwaysOnAvailabilityGroupName }

        foreach ($database in $DatabaseNames)
        {

            if($null -ne $primaryServer.Databases[$database] ) {

                if (($secondaryAG.AvailabilityDatabases | Where-Object { $_.Name -eq $database }).IsJoined) {
                    continue
                }
                else {
                    return $false
                }
                 
            }
            else {
                return $false
            }
        } 
    }

    return $true
}
function Configure-Databases
{
    param(
        [String[]]$DatabaseNames,
        [PSCredential]$SqlAdministratorCredential,
        [string]$PrimaryReplica,
        [string]$SecondaryReplica,
        [string]$SqlAlwaysOnAvailabilityGroupName


    )
    
    # Required SQL managability modules
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SmoExtended") | Out-Null


    #If there are databases specified, then we create them, backup them up and add them to the specified AG replicas
    if ($null -ne $DatabaseNames)
    {
        
        # Primamry Replica connection
        $primaryServer = Get-SqlServer $PrimaryReplica -SqlAdministratorCredential $SqlAdministratorCredential

        #create database on the primary, then add them to all replicas and sync them
        foreach ($database in $DatabaseNames)
        {
            Write-Verbose -Message "Creating sample database '$($database)' ..."
            Create-SqlAlwaysOnDatabase -DatabaseName $database -Server $primaryServer

            #synchronize existing availability group replicas
            Update-SqlAlwaysOnAvailabilityGroupDatabases -SqlAlwaysOnAvailabilityGroupName $SqlAlwaysOnAvailabilityGroupName -PrimaryReplica $PrimaryReplica -SecondaryReplica $SecondaryReplica -SqlAdministratorCredential $SqlAdministratorCredential
        }

        Write-Verbose -Message "Adding databases Availability Group '$SqlAlwaysOnAvailabilityGroupName' completed."
    }
    else
    {
        Write-Verbose -Message "No databases were specified to add to Availability Group '$SqlAlwaysOnAvailabilityGroupName'."
    }
}

function Update-SqlAlwaysOnAvailabilityGroupDatabases([String]$SqlAlwaysOnAvailabilityGroupName, [String]$PrimaryReplica, [String]$SecondaryReplica, [PSCredential]$SqlAdministratorCredential)
{
    # Required SQL managability modules
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SmoExtended") | Out-Null

    # Primamry Replica connection
    $primaryServer = Get-SqlServer $PrimaryReplica -SqlAdministratorCredential $SqlAdministratorCredential

    # Secondary Replica connection
    $replicaServer = Get-SqlServer $SecondaryReplica -SqlAdministratorCredential $SqlAdministratorCredential

    # AG on primary
    $primaryAG = $primaryServer.AvailabilityGroups | where { $_.Name -eq $SqlAlwaysOnAvailabilityGroupName }

    # AG on secondary
    $secondaryAG = $replicaServer.AvailabilityGroups | where { $_.Name -eq $SqlAlwaysOnAvailabilityGroupName }

    # Prepare the backup share
    $backupFolder = [guid]::NewGuid().ToString()
    $backupPath = "$env:TEMP\$backupFolder"
    $backupShare = "\\$env:COMPUTERNAME\$backupFolder"
    Create-SqlAlwaysOnBackupShare -BackupShare $backupFolder -BackupPath $backupPath -ServiceAccount $primaryServer.ServiceAccount

    # Sync existing primary and secondary databases
    $databases = $primaryServer.Databases | where { $_.IsSystemObject -eq $false }

    foreach ($database in $databases)
    {
        # Skip any databases joined to the availability group on the secondary replica.
        if (($secondaryAG.AvailabilityDatabases | Where-Object { $_.Name -eq $Database.Name }).IsJoined)
        {
            Write-Verbose -Message "Database '$($database.Name)' already joined to availability group '$($secondaryAG.Name)', skipping ..."
            continue
        }

        # Backup the database and log from the primary replica.
        $device = "$backupShare\$($database.Name).bak"
        Write-Verbose -Message "Backing up database '$($database.Name)' from '$($PrimaryServer.Name)' to '$($device)' ..."
        $backup = New-Object Microsoft.SqlServer.Management.Smo.Backup
        $backup.Database = $database.Name
        $backup.Action = [Microsoft.SqlServer.Management.Smo.BackupActionType]::Database
        $backup.Initialize = $true
        $backup.Devices.AddDevice($device, [Microsoft.SqlServer.Management.Smo.DeviceType]::File)
        $backup.SqlBackup($PrimaryServer)
        Write-Verbose -Message "Successfully backed up database '$($database.Name)'."

        $device = "$backupShare\$($database.Name).log"
        Write-Verbose -Message "Backing up log for database '$($database.Name)' from '$($PrimaryServer.Name)' to '$($device)' ..."
        $backup = New-Object Microsoft.SqlServer.Management.Smo.Backup
        $backup.Database = $database.Name
        $backup.Action = [Microsoft.SqlServer.Management.Smo.BackupActionType]::Log
        $backup.Initialize = $true
        $backup.Devices.AddDevice($device, [Microsoft.SqlServer.Management.Smo.DeviceType]::File)
        $backup.SqlBackup($PrimaryServer)
        Write-Verbose -Message "Successfully backed up log for database '$($database.Name)'."

        # Restore the database and log to the secondary replica.
        $device = "$backupShare\$($database.Name).bak"
        Write-Verbose -Message "Restoring database '$($database.Name)' from '$($device)' to '$($PrimaryServer.Name)' ..."
        $restore = New-Object Microsoft.SqlServer.Management.Smo.Restore
        $restore.Database = $database.Name
        $restore.Action = [Microsoft.SqlServer.Management.Smo.RestoreActionType]::Database
        $restore.Devices.AddDevice($device, [Microsoft.SqlServer.Management.Smo.DeviceType]::File)
        $restore.NoRecovery = $true
        $restore.SqlRestore($replicaServer)
        Write-Verbose -Message "Successfully restored database '$($database.Name)'."

        $device = "$backupShare\$($database.Name).log"
        Write-Verbose -Message "Restoring log for database '$($database.Name)' from '$($device)' to '$($PrimaryServer.Name)' ..."
        $restore = New-Object Microsoft.SqlServer.Management.Smo.Restore
        $restore.Database = $database.Name
        $restore.Action = [Microsoft.SqlServer.Management.Smo.RestoreActionType]::Log
        $restore.Devices.AddDevice($device, [Microsoft.SqlServer.Management.Smo.DeviceType]::File)
        $restore.NoRecovery = $true
        $restore.SqlRestore($replicaServer)
        Write-Verbose -Message "Successfully restored database '$($database.Name)'."

        # Add the database to the availability group.
        if (-not ($primaryAG.AvailabilityDatabases | Where-Object { $_.Name -eq $Database.Name }))
        {
            Write-Verbose -Message "Adding database '$($database.Name)' to availability group '$($primaryAG.Name)' ..."
            $adb = New-Object Microsoft.SqlServer.Management.Smo.AvailabilityDatabase $primaryAG,$database.Name
            $primaryAG.AvailabilityDatabases.Add($adb)
            $adb.Create()
            $primaryAG.Alter()
            Write-Verbose -Message "Successfully added database '$($database.Name)' to availability group."
        }

        # It can take some time before the database shows up in the availability group on the secondary replica.
        while ($true)
        {
            $secondaryAG.AvailabilityDatabases.Refresh()
            $databaseOnSecondary = $secondaryAG.AvailabilityDatabases | Where-Object { $_.Name -eq $Database.Name }
            if ($databaseOnSecondary)
            {
                break
            }

            Write-Verbose -Message "Waiting for database '$($database.Name)' to be available ..."
            Start-Sleep -Seconds 20
        }

        # Join the database to availabiliy group on secondary replica.
        if (-not $databaseOnSecondary.IsJoined)
        {
            Write-Verbose -Message "Joining database '$($databaseOnSecondary.Name)' to availability group '$($secondaryAG.Name)' ..."
            $databaseOnSecondary.JoinAvailablityGroup()
            Write-Verbose -Message "Successfully joined database '$($databaseOnSecondary.Name)' to availability group."
        }
    }

    # AG Replica
    $primaryAG.AvailabilityReplicas.Refresh()
    $secondaryAGReplica = $primaryAG.AvailabilityReplicas | where { $_.Name -eq $SecondaryReplica }

    # Verify the replica is in read only mode. If not, then set it
    if($secondaryAGReplica.ConnectionModeInSecondaryRole -ne [Microsoft.SqlServer.Management.Smo.AvailabilityReplicaConnectionModeInSecondaryRole]::AllowReadIntentConnectionsOnly)
    {
        Write-Verbose -Message "Setting replica $SecondaryReplica read only mode"

        $secondaryAGReplica.ConnectionModeInSecondaryRole = [Microsoft.SqlServer.Management.Smo.AvailabilityReplicaConnectionModeInSecondaryRole]::AllowReadIntentConnectionsOnly
        $secondaryAGReplica.Alter()

        Write-Verbose -Message "Set replica $SecondaryReplica read only mode completed!"
    }
    else
    {
        Write-Verbose -Message "Replica $SecondaryReplica is already in read only mode"
    }

    Write-Verbose -Message "Cleaning up backups ..."
    Remove-SmbShare -Name $backupFolder -Force | Out-Null

    Write-Verbose -Message "Removed share '$($backupShare)'."
    Remove-Item -Path $backupPath -Recurse -Force | Out-Null
    Write-Verbose -Message "Removed directory '$($backupPath)'."
}


# Create a database on a SQL Server instance if the database does not already exists. The user can provide a custom location of the database
# Data and Log files or accept the default by not setting the corresponding variables.
function Create-SqlAlwaysOnDatabase([string]$DatabaseName, [string]$DataPath, [string]$LogPath, [Microsoft.SqlServer.Management.Smo.Server]$Server)
{
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.ConnectionInfo") | Out-Null
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SMO") | Out-Null

    if($null -eq $Server.Databases[$DatabaseName] )
    {
        #create the database
        $db = new-object ('Microsoft.SqlServer.Management.Smo.Database') ($Server, $DatabaseName)


        # if a location for data and log is provided then use the user provided. Otherwise, we create it using the default ones
        if($DataPath -and $LogPath)
        {
            if (!(Test-Path -path $DataPath))
            {
                #create the flder
                New-Item $DataPath -Type Directory
            }

            if (!(Test-Path -path $LogPath))
            {
                #create the flder
                New-Item $LogPath -Type Directory
            }

            $sysfg = new-object ('Microsoft.SqlServer.Management.Smo.FileGroup') ($db, 'PRIMARY')

            $db.FileGroups.Add($sysfg)

            $appfg = new-object ('Microsoft.SqlServer.Management.Smo.FileGroup') ($db, 'AppFileGroup')

            $db.FileGroups.Add($appfg)

            $syslogname = $DatabaseName + '_SysData'

            $dbdsysfile = new-object ('Microsoft.SqlServer.Management.Smo.DataFile') ($sysfg, $syslogname)

            # Create the file for the raw data
            $sysfg.Files.Add($dbdsysfile)
            $dbdsysfile.FileName = $DataPath + '\' + $syslogname + '.mdf'
            $dbdsysfile.Size = [double](5.0 * 1024.0)
            $dbdsysfile.GrowthType = 'None'
            $dbdsysfile.IsPrimaryFile = 'True'

            # Create the file for the Application tables
            $applogname = $DatabaseName + '_AppData'
            $dbdappfile = new-object ('Microsoft.SqlServer.Management.Smo.DataFile') ($appfg, $applogname)
            $appfg.Files.Add($dbdappfile)
            $dbdappfile.FileName = $DataPath + '\' + $applogname + '.ndf'
            $dbdappfile.Size = [double](25.0 * 1024.0)
            $dbdappfile.GrowthType = 'Percent'
            $dbdappfile.Growth = 25.0
            $dbdappfile.MaxSize = [double](100.0 * 1024.0)

            # Create the file for the log
            $loglogname = $DatabaseName + '_Log'
            $dblfile = new-object ('Microsoft.SqlServer.Management.Smo.LogFile') ($db, $loglogname)
            $db.LogFiles.Add($dblfile)
            $dblfile.FileName = $LogPath + '\' + $loglogname + '.ldf'
            $dblfile.Size = [double](10.0 * 1024.0)
            $dblfile.GrowthType = 'Percent'
            $dblfile.Growth = 25.0
        }

        # Create the database
        $db.Create()

        $createDate = $db.CreateDate
        Write-Verbose -Message "Created database '$DatabaseName' on '$createDate'"

        #refresh the server connection
        $Server.Refresh()
    }
    else
    {
        Write-Verbose -Message "Database '$DatabaseName' already exists."
    }
}

# create a folder and share it for AlwaysOn backup.
# If the folder already exists, then we don't create it again
# If the folder already shared, then we don't modify sharing settings
# Otherwise, the function creates and share the folder
function Create-SqlAlwaysOnBackupShare([string]$BackupShare, [string]$BackupPath, [string]$ServiceAccount)
{
    Write-Verbose -Message "Creating directory '$($BackupFolder)' ..."

    # create folder if it does not exist
    if (!(Test-Path -path $BackupPath))
    {
        #create the flder
        New-Item $BackupPath -Type Directory
    }

    # always ACL it for service account
    icacls.exe "$BackupPath" /grant:r ($ServiceAccount + ":(OI)(CI)F") | Out-Null

    # escape '\'
    $WMIFolderPath = $BackupPath -replace '\\','\\'

    # if the directory is not shared, then share it
    if(Get-CimInstance -Query "SELECT * FROM Win32_Share WHERE Path='$WMIFolderPath'")
    {
          Write-Verbose  -Message "Folder $WMIFolderPath already shared"
    }
    else
    {
        New-SmbShare -Name $BackupShare -Path $BackupPath -FullAccess $ServiceAccount -Temporary | Out-Null

        Write-Verbose  -Message "Shared folder $BackupPath as $BackupShare"
    }
}

# Create SQL Server SMO object using provided isntance name and credentials
function Get-SqlServer([string]$InstanceName, [PSCredential]$SqlAdministratorCredential)
{
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.ConnectionInfo") | Out-Null
    $sc = New-Object Microsoft.SqlServer.Management.Common.ServerConnection

    $list = $InstanceName.Split("\")
    if ($list.Count -gt 1 -and $list[1] -eq "MSSQLSERVER")
    {
        $sc.ServerInstance = $list[0]
    }
    else
    {
        $sc.ServerInstance = $InstanceName
    }

    $sc.ConnectAsUser = $true

    Write-Verbose "name is $($SqlAdministratorCredential.UserName)"

    if ($SqlAdministratorCredential.GetNetworkCredential().Domain -and $SqlAdministratorCredential.GetNetworkCredential().Domain -ne $env:COMPUTERNAME)
    {
        $sc.ConnectAsUserName = "$($SqlAdministratorCredential.GetNetworkCredential().UserName)@$($SqlAdministratorCredential.GetNetworkCredential().Domain)"
    }
    else
    {
        $sc.ConnectAsUserName = $SqlAdministratorCredential.GetNetworkCredential().UserName
    }
    $sc.ConnectAsUserPassword = $SqlAdministratorCredential.GetNetworkCredential().Password

    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null

    $s = New-Object Microsoft.SqlServer.Management.Smo.Server $sc

    $s
}

Export-ModuleMember -Function *-TargetResource