SqlNinjaSyncAG.psm1

#requires -version 3

function Invoke-SqlNinjaCmd
{
    <#
    .SYNOPSIS
        Runs a T-SQL script.
    .DESCRIPTION
        Runs a T-SQL script. Invoke-SqlNinjaCmd runs the whole script and only captures the first selected result set, such as the output of PRINT statements when -verbose parameter is specified.
        Parameterized queries are supported.
    .NOTES
        File Name : SqlNinjaSyncAG.psm1
        Author : Sarah BESSARD (sarah.bessard@concatskills.com)
        Prerequisite : PowerShell V5 over Vista and upper.
        Copyright 2018 - Sarah BESSARD / CONCAT SKILLS
    .LINK
        Script posted over:
        Company website : http://www.concatskills.com
        Github : https://github.com/concatskills
    .EXAMPLE
        Invoke-SqlNinjaCmd -ServerInstance SRVNAME\INSTANCENAME -UserName bidule -Password chouette -Database MyDb -Query "SELECT @@VERSION"
    #>


    [CmdletBinding()] 
    param( 
    [Parameter(Position=0, Mandatory=$true)] [string]$ServerInstance, 
    [Parameter(Position=1, Mandatory=$false)] [string]$Database, 
    [Parameter(Position=2, Mandatory=$false)] [string]$Query, 
    [Parameter(Position=3, Mandatory=$false)] [string]$Username, 
    [Parameter(Position=4, Mandatory=$false)] [string]$Password, 
    [Parameter(Position=5, Mandatory=$false)] [Int32]$QueryTimeout=600, 
    [Parameter(Position=6, Mandatory=$false)] [Int32]$ConnectionTimeout=15, 
    [Parameter(Position=7, Mandatory=$false)] [ValidateScript({test-path $_})] [string]$InputFile, 
    [Parameter(Position=8, Mandatory=$false)] [ValidateSet("Batch", "DataSet", "DataTable", "DataRow")] [string]$As="DataRow" 
    ) 
 
    if ($InputFile) 
    { 
        $filePath = $(resolve-path $InputFile).path 
        $Query =  [System.IO.File]::ReadAllText("$filePath") 
    } 
 
    $conn=new-object System.Data.SqlClient.SQLConnection 
      
    if ($Username) 
    { $ConnectionString = "Server={0};Database={1};User ID={2};Password={3};Trusted_Connection=False;Connect Timeout={4}" -f $ServerInstance,$Database,$Username,$Password,$ConnectionTimeout } 
    else 
    { $ConnectionString = "Server={0};Database={1};Integrated Security=True;Connect Timeout={2}" -f $ServerInstance,$Database,$ConnectionTimeout } 
 
    $conn.ConnectionString=$ConnectionString 
     
    #Following EventHandler is used for PRINT and RAISERROR T-SQL statements. Executed when -Verbose parameter specified by caller
    if ($PSBoundParameters.Verbose) 
    { 
        $conn.FireInfoMessageEventOnUserErrors=$true 
        $handler = [System.Data.SqlClient.SqlInfoMessageEventHandler] {Write-Verbose "$($_)"} 
        $conn.add_InfoMessage($handler) 
    }  
     
    $conn.Open() 

    if ($As -eq "Batch") {

        $batches = $Query -split "GO\r\n"

        foreach($batch in $batches)
        {
            if ($batch.Trim() -ne ""){

                $cmd=new-object system.Data.SqlClient.SqlCommand($batch,$conn) 
                $cmd.CommandTimeout=$QueryTimeout
                $returnVal = $cmd.ExecuteNonQuery()

                if ($returnVal -ne -1)
                {
                    Write-Host ("Error in T-SQL sync : `n" + $batch)
                }


            }
        }

        $conn.Close()

    } 
    else 
    {

        $cmd=new-object system.Data.SqlClient.SqlCommand($Query,$conn) 
        $cmd.CommandTimeout=$QueryTimeout 

        $ds=New-Object system.Data.DataSet 
        $da=New-Object system.Data.SqlClient.SqlDataAdapter($cmd) 
        [void]$da.fill($ds) 
        $conn.Close()

        switch ($As) 
        { 

            'DataSet'   { Write-Output ($ds) } 
            'DataTable' { Write-Output ($ds.Tables) } 
            'DataRow'   { Write-Output ($ds.Tables[0]) } 
        } 
    
    }
 
}

function Export-SqlNinjaEncryptedPwd
{

    <#
    .SYNOPSIS
        Encrypt SQL login password
    .DESCRIPTION
        Encrypt SQL login password
    .NOTES
        File Name : SqlNinjaSyncAG.psm1
        Author : Sarah BESSARD (sarah.bessard@concatskills.com)
        Prerequisite : PowerShell V5 over Vista and upper.
        Copyright 2018 - Sarah BESSARD / CONCAT SKILLS
    .LINK
        Script posted over:
        Company website : http://www.concatskills.com
        Github : https://github.com/concatskills
    .EXAMPLE
        Export-SqlNinjaEncryptedPwd -Username sqlaccounttosync
    .EXAMPLE
        Export-SqlNinjaEncryptedPwd -Username sqlaccounttosync -Password MyPWD
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$False)] [string]$Username,
        [Parameter(Mandatory=$False)] [string]$Password
        )
     begin 
            {

                $ScriptDirectory = Get-Location
            }
    process
            {  

                if ([string]::IsNullOrEmpty($Username)) {
                    $Username = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
                }

                if ([string]::IsNullOrEmpty($Password)) {
                    [System.Security.SecureString]$SecurePassword = Read-Host "Enter Password" -AsSecureString
                    [String]$Password = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword));
                }

                $FileName = "Pwd_" + $($env:USERNAME) + ".txt"

                $Secure = ConvertTo-SecureString $Password -force -asPlainText
                $bytes = ConvertFrom-SecureString $Secure
                $bytes | out-file $FileName

        }

}


function Start-SqlNinjaSyncAG
{

    <#
    .SYNOPSIS
        Synchronize Secondaries replicas in Availability Group or generate synchronization script for each replica
    .DESCRIPTION
        Synchronize Secondaries replicas in Availability Group or generate synchronization script for each replica
    .NOTES
        File Name : SqlNinjaSyncAG.psm1
        Author : Sarah BESSARD (sarah.bessard@concatskills.com)
        Prerequisite : PowerShell V5 over Vista and upper.
        Copyright 2018 - Sarah BESSARD / CONCAT SKILLS
    .LINK
        Script posted over:
        Company website : http://www.concatskills.com
        Github : https://github.com/concatskills
    .EXAMPLE
        Start-SqlNinjaSyncAG -InputFile MyConf.json
        This command generate synchronisation script for each secondary replicas
    .EXAMPLE
        Start-SqlNinjaSyncAG -InputFile MyConf.json -Execute $True
        This command generate and execute synchronisation script for each secondary replicas
    .EXAMPLE
        Start-SqlNinjaSyncAG -InputFile MyConf.json -LogRetentionDays 5
        This command allow to change days retention for execution logs (3 days by default)
    #>


    [CmdletBinding()]
    param(
            [Parameter(Mandatory=$True)] [string]$InputFile,
            [Parameter(Mandatory=$False)] [bool]$Execute=$False,
            [Parameter(Mandatory=$False)] [int]$LogRetentionDays=3
        )
        begin 
        {

            $ScriptDirectory = Get-Location
            $ScriptDirectoryIn = $ScriptDirectory
            $ScriptDirectorySql = "$($ScriptDirectory)\sql"
            $ScriptDirectoryOut = "$($ScriptDirectory)\out"
            $ScriptDirectoryLog = "$($ScriptDirectory)\log\"
        
            [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SmoExtended") | out-null
            [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.ConnectionInfo") | out-null
            [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null

            #clear-host

        }
        process
        {  

            function ConvertTo-PsCustomObjectFromHashtable { 
                 param ( 
                     [Parameter(  
                         Position = 0,   
                         Mandatory = $true,   
                         ValueFromPipeline = $true,  
                         ValueFromPipelineByPropertyName = $true  
                     )] [object[]]$hashtable 
                 ); 
     
                 begin { $i = 0; } 
     
                 process { 
                     foreach ($myHashtable in $hashtable) { 
                         if ($myHashtable.GetType().Name -eq 'hashtable') { 
                             $output = New-Object -TypeName PsObject; 
                             Add-Member -InputObject $output -MemberType ScriptMethod -Name AddNote -Value {  
                                 Add-Member -InputObject $this -MemberType NoteProperty -Name $args[0] -Value $args[1]; 
                             }; 
                             $myHashtable.Keys | Sort-Object | % {  
                                 $output.AddNote($_, $myHashtable.$_);  
                             } 
                             $output; 
                         } else { 
                             Write-Warning "Index $i is not of type [hashtable]"; 
                         } 
                         $i += 1;  
                     } 
                 } 
            }

            function GetServer
            {
                Param(
                    [Parameter(Mandatory=$True)][string]$Instance,
                    [Parameter(Mandatory=$False)][string]$Login,
                    [Parameter(Mandatory=$False)][string]$Password
                )
                begin {}
                process
                {

                    $Conn = New-Object Microsoft.SqlServer.Management.Common.ServerConnection
                    $Conn.ServerInstance=$Instance

                    $Server = New-Object Microsoft.SqlServer.Management.Smo.Server($Conn)
                    if (![string]::IsNullOrEmpty($Login)) {
                        $Server.ConnectionContext.LoginSecure = $false
                        $Server.ConnectionContext.Login=$Login
                        $Server.ConnectionContext.Password=$Password
                    }

                    return $Server 
                }
                end {}
            }

            function AddExclusions
            {
                Param(
                    [Parameter(Mandatory=$True)][string]$ObjectType,
                    [Parameter(Mandatory=$False)][string[]]$UserExclusions,
                    [Parameter(Mandatory=$False)][Object[]]$SystemExclusions
                )
                begin {}
                process
                {

                    # Manage exclusions
                    $Exclusions = @()
                    # Custom exclusions
                    [string[]]$NewUserExclusions = $UserExclusions | sort -Unique
                    # System exclusion
                    [string[]]$NewSystemExclusions = $SystemExclusions | Where-Object { $_.Type -eq $ObjectType } | Foreach { "$($_.Name)" }

                    $Exclusions = $NewUserExclusions + $NewSystemExclusions

                    return $Exclusions 
                }
                end {}
            }

            try
            {          

                $InputFile = "$($ScriptDirectoryIn)\$($InputFile)"

                $LogRetentionDays = [System.Math]::Abs($LogRetentionDays) * (-1)
                $LogFile = $ScriptDirectoryLog + "sync_" + (Get-Date -format 'yyyyMMdd_HHmmss') + "_" + ([System.IO.Path]::GetFileNameWithoutExtension($InputFile)) + ".log"

                ######################
                ### Initialization ###
                ######################

                if (($PSVersionTable.PSVersion.Major) -ge 4){
                    Start-Transcript -path $LogFile
                }

                If(!(test-path $ScriptDirectoryOut)) { New-Item -ItemType Directory -Force -Path $ScriptDirectoryOut } else { Get-ChildItem -Path $ScriptDirectoryOut -Include *.* -Recurse  | remove-Item -Recurse -Force }

                Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - FILESYSTEM -> Clean-up 'out' directory")
    
                If(!(test-path $ScriptDirectoryLog)) { 
                    New-Item -ItemType Directory -Force -Path $ScriptDirectoryOut
                    Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - FILESYSTEM -> Create up 'log' directory")
                } else 
                { 
                    $limit = (Get-Date).AddDays($LogRetentionDays)

                    Get-ChildItem $ScriptDirectoryLog -Recurse | ? {
                      -not $_.PSIsContainer -and $_.CreationTime -lt $limit
                    } | Remove-Item -Force 
                    Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - FILESYSTEM -> Clean-up 'log' directory")
                }

                Import-LocalizedData -BaseDirectory $PSScriptRoot  -BindingVariable Data

                $Queries = $Data.PrivateData.Queries

                $ConfObjToSync = ConvertTo-PsCustomObjectFromHashtable ($Data.PrivateData.ObjectToSync)

                $ScriptCreateLogins = $Queries.CreateLogins
                $ScriptDropLogins = $Queries.DropLogins
                $ScriptCreateCredentials = $Queries.CreateCredentials
                $ScriptDropCredentials = $Queries.DropCredentials
                $ScriptGetSysObjects = $Queries.GetSysObjects
                $ScriptGetAdmGroups = $Queries.GetAdmGroups
                $ScriptDropInstanceObjects = "$($ScriptDirectoryOut)\01 - drop_instance_objects.sql"
                $ScriptCreateInstanceObjects = "$($ScriptDirectoryOut)\02 - create_instance_objects.sql"

                $json = (Get-Content $InputFile -Raw) | ConvertFrom-Json

                Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - FILESYSTEM -> Get content from configuration file : " + $InputFile)

                # AvaibilityGroup Infos
                $AvaibilityGroup = $json.AvaibilityGroup
                # Fix Owner : transfer to sa
                # Replicas Exclusions
                $ReplicasExclusions = $json.Replicas.Exclusions
                # Job : First Step Name for AG
                $FirstStepNameCheck = $json.Jobs.FirstStepNameCheck

                # Objects to synchronize
                $ObjToSync = $json.Objects
                # Logins to keep to be safe

                $out = $null

                if ([bool]::TryParse($json.FixOwner, [ref]$out)) {
                    $FixOwner = $out
                } else {
                    $FixOwner = $False
                }

                if ([bool]::TryParse($json.Jobs.ExcludeIfNoCheck, [ref]$out)) {
                    $ExcludeIfNoCheck = $out
                } else {
                    $ExcludeIfNoCheck = $False
                }
  
                # Add System Properties to ObjToSync
                ForEach($obj in $ObjToSync){

                    $out = $null

                    if ([bool]::TryParse($obj.ToSync, [ref]$out)) {
                        $obj.ToSync = $out
                    } else {
                        $obj.ToSync = $False
                    }

                   $obj | Add-Member -MemberType NoteProperty -Name "Scope" -Value ($ConfObjToSync | Where-Object { $_.Type -eq  $obj.Type }).Scope
                   $obj | Add-Member -MemberType NoteProperty -Name "SortDrop" -Value ($ConfObjToSync | Where-Object { $_.Type -eq  $obj.Type }).SortDrop
                   $obj | Add-Member -MemberType NoteProperty -Name "SortCreate" -Value ($ConfObjToSync | Where-Object { $_.Type -eq  $obj.Type } ).SortCreate

                }

                if (![string]::IsNullOrEmpty($AvaibilityGroup.Login) -And [string]::IsNullOrEmpty($AvaibilityGroup.Password)) {
                    [System.Security.SecureString]$SecurePassword = Read-Host "Enter Password" -AsSecureString
                    [String]$AvaibilityGroup.Password = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword));
                } elseif (![string]::IsNullOrEmpty($AvaibilityGroup.Login) -And ![string]::IsNullOrEmpty($AvaibilityGroup.Password)) {
                    $SecurePwd =  $AvaibilityGroup.Password | ConvertTo-SecureString
                    $Credential = New-Object System.Management.Automation.PSCredential -ArgumentList $AvaibilityGroup.Login, $SecurePwd
                    $AvaibilityGroup.Password = ($credential.GetNetworkCredential()).Password
                }

                $WindowsLogin = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
                if ([string]::IsNullOrEmpty($AvaibilityGroup.Login)) 
                { 
                    $CheckWindowsLogin = $True
                    $SafeLoginsExclusion = @('sa',$WindowsLogin) 
                }
                else 
                { 
                    $CheckWindowsLogin = $False
                    $SafeLoginsExclusion = @('sa',$AvaibilityGroup.Login) 
                }

                # Logins to exclude : NT SERVICE, NT AUTHORITY, local MACHINE, etc
                $LoginRegex = "^({0})\\.*$"
                # Scripts to exclude for clean up
                $ScriptRegex = "^(sync_|02 - create_instance_objects).*$"
                # Batch separator in T-SQL scripts
                $BatchSeperator = "`r`nGO"

                $My = "Microsoft.SqlServer.Management.Smo"

                # Primary Replica
                $ScriptOptionsCreate = new-object ("$My.ScriptingOptions")
                $ScriptOptionsCreate.ContinueScriptingOnError = $true
                $ScriptOptionsCreate.ScriptBatchTerminator = $true
                $ScriptOptionsCreate.IncludeHeaders = $true
                $ScriptOptionsCreate.ToFileOnly = $true
                $ScriptOptionsCreate.IncludeIfNotExists = $false
                $ScriptOptionsCreate.Filename =  $ScriptCreateInstanceObjects
                $ScriptOptionsCreate.AppendToFile =  $true

                # Secondaries Replicas
                $ScriptOptionsDrop = new-object ("$My.ScriptingOptions")
                $ScriptOptionsDrop.ContinueScriptingOnError = $true
                $ScriptOptionsDrop.ScriptBatchTerminator = $true
                $ScriptOptionsDrop.IncludeHeaders = $true
                $ScriptOptionsDrop.ToFileOnly = $true
                $ScriptOptionsDrop.IncludeIfNotExists = $true
                $ScriptOptionsDrop.Filename =  $ScriptDropInstanceObjects
                $ScriptOptionsDrop.AppendToFile =  $true
                $ScriptOptionsDrop.ScriptDrops = $true

                $PrimaryReplica = GetServer -Instance $AvaibilityGroup.Listener -Login $AvaibilityGroup.Login -Password $AvaibilityGroup.Password

                Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - PRIMARY REPLICA " + $PrimaryReplica.Name + " -> Connecting to " + $AvaibilityGroup.Listener)
    
                if ($PrimaryReplica.AvailabilityGroups -eq $null) 
                {
                    Write-Error "!!! Connection fails !!!"
                }
                elseif ([string]::IsNullOrEmpty($PrimaryReplica.AvailabilityGroups)) 
                {         
                    Write-Error "!!! None Availability Group on this instance !!!" 
                } 
                else
                { 

                    $AllReplicas = $PrimaryReplica.AvailabilityGroups | Where-Object { $_.Name -eq $AvaibilityGroup.Name } | Select-Object -ExpandProperty AvailabilityReplicas | Where-Object { $_.ConnectionState -eq "Connected" }

                    Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - PRIMARY REPLICA " + $PrimaryReplica.Name + " -> Get all replicas : " + $AllReplicas.Count + " replica(s) found" )

                    $SrvPrimaryReplica = ($AllReplicas | Where-Object { $_.Role -eq "Primary" } | Select -ExpandProperty Name).Split("\")[0]

                    Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - PRIMARY REPLICA " + $PrimaryReplica.Name + " -> Get primary replica Name")

                    ############################################
                    ### PRIMARY REPLICA : Get system objects ###
                    ############################################

                    $SysObjects = Invoke-SqlNinjaCmd -ServerInstance $AvaibilityGroup.Listener -Username $AvaibilityGroup.Login -Password $AvaibilityGroup.Password -Database master -Query $ScriptGetSysObjects -As DataRow 

                    #################################################################################
                    ### PRIMARY REPLICA : Generate script to CREATE Server Objects & Server Jobs ###
                    #################################################################################

                     # Get SharedSchedules from Jobs exclusions
                    $ExcludedJobs = AddExclusions -ObjectType "Jobs" -UserExclusions ($ObjToSync | Where-Object { $_.ToSync -eq $True -And $_.Type -eq "Jobs" }).Exclusions -SystemExclusions $SysObjects
                    $ExcludedSharedSchedules = @()
                    $SQLAgent = $PrimaryReplica.JobServer.Jobs | Where-Object { $_.Name -in $ExcludedJobs } | % { 
                        $ExcludedSharedSchedules += $_.JobSchedules
                    }
                            
                    # Add SharedSchedules exclusions from Jobs exclusions
                    $ObjToSync | Where-Object { $_.Type -eq "SharedSchedules" } | % { 
                        $_.Exclusions = $_.Exclusions + $ExcludedSharedSchedules
                    }

                    foreach ($Item in $ObjToSync | Sort-Object -Property SortCreate) {

                        if ($Item.ToSync -eq $True) {
           
                            $Exclusions = AddExclusions -ObjectType $Item.Type -UserExclusions $Item.Exclusions -SystemExclusions $SysObjects

                            $Objects = if ($Item.Scope -eq "Server") { $PrimaryReplica.$($Item.Type) } elseif ($Item.Scope -eq "JobServer") { $PrimaryReplica.JobServer.$($Item.Type) } 
                            $Objects = $Objects | Where-Object { $_.name -notin $Exclusions }

                            Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - PRIMARY REPLICA " + $PrimaryReplica.Name + " -> Generate creation script for " + $Item.Type + " (SortCreate : "+ $Item.SortCreate +")")

                            switch ($Item.Type) 
                            { 
                                "Logins" 
                                {                               
                        
                                    ######################################################################################
                                    ### PRIMARY REPLICA : Generate script to CREATE logins with sid & crypted password ###
                                    ######################################################################################

                                    $LoginResult = Invoke-SqlNinjaCmd -ServerInstance $AvaibilityGroup.Listener -Username $AvaibilityGroup.Login -Password $AvaibilityGroup.Password -Database master -Query $ScriptCreateLogins -As DataRow

                                    ##################################################################################################
                                    ### PRIMARY REPLICA : Check exclusions for groups if Windows login is not existing on instance ###
                                    ##################################################################################################

                                    if ($CheckWindowsLogin) 
                                    {
                                        if (($LoginResult | Where-Object {!$_.IsDisabled -And $_.Name -eq $WindowsLogin }).Count -eq 0)
                                        {                                       
                                            $GroupsIn = "'" + ([system.String]::Join("','", $Exclusions)) + "'"
                                            $CustomScriptGetAdmGroups = $ScriptGetAdmGroups -f $WindowsLogin, $GroupsIn

                                            $Group = (Invoke-SqlNinjaCmd -ServerInstance $AvaibilityGroup.Listener -Username $AvaibilityGroup.Login -Password $AvaibilityGroup.Password -Database master -Query $CustomScriptGetAdmGroups -As DataRow).CntGroup

                                            if ($Group -eq 0) 
                                            {
                                                Throw "Your are currently logged as $WindowsLogin that seems member of group. Add its group in logins exclusions and check if group is sysadmin !"
                                            }
                                        }
                                    }

                                    $CustomLoginRegex = $LoginRegex -f $SrvPrimaryReplica

                                    $Objects = $LoginResult | Where-Object {!$_.IsDisabled -And $_.Name -notmatch $CustomLoginRegex -And $_.Name -notin $SafeLoginsExclusion -And $_.Name -notin $Exclusions }
                                    $Objects | Select-Object -ExpandProperty CreateScript | Out-File $ScriptCreateInstanceObjects
                                    $Logins = $Objects | Select -ExpandProperty Name 

                                } 
                                "Credentials"
                                {

                                   $CustomScriptCreateCredentials = $ScriptCreateCredentials
                            
                                   foreach ($Object in $Objects) 
                                   {
                                        $CustomScriptCreateCredentials -f $Object.Name, $Object.Identity | Out-File $ScriptCreateInstanceObjects -Append
                                   }

                                }
                                "MemberRoles"
                                {
                        
                                    ########################################################################
                                    ### PRIMARY REPLICA : Generate script to CREATE Server Roles Members ###
                                    ########################################################################

                                    foreach ($Role in $PrimaryReplica.Roles) 
                                    { 
                                        $Role.EnumServerRoleMembers() | Where-Object { $_ -in $Logins -And $_ -notmatch "^($SrvPrimaryReplica)\\.*$" } |% { 
                                            "EXEC master..sp_addsrvrolemember @loginame = N'{0}', @rolename = N'{1}'{2}" -f ($_,$Role.Name, $BatchSeperator) | Out-File $ScriptCreateInstanceObjects -Append
                                        }
                                    };
                                }
                                "Permissions"
                                {
                        
                                    #############################################################################
                                    ### PRIMARY REPLICA : Generate script to CREATE Server Object Permissions ###
                                    #############################################################################
                        
                                    $PrimaryReplica.EnumObjectPermissions() | Where-Object { $_.Grantee -in $Logins -And @("sa","dbo","information_schema","sys") -notcontains $_.Grantee -And $_.Grantee -notmatch "^(NT SERVICE|NT AUTHORITY|$SrvPrimaryReplica)\\.*$" } |% {
                                        if ($_.PermissionState -eq "GrantWithGrant") { $wg = "WITH GRANT OPTION"} else { $wg = ""};
                                        "{0} {1} ON {2}::[{3}] TO [{4}] {5}{6}" -f ($_.PermissionState.ToString().Replace("WithGrant","").ToUpper(),$_.PermissionType,$_.ObjectClass.ToString().ToUpper(),$_.ObjectName,$_.Grantee,$wg,$BatchSeperator) | Out-File $ScriptCreateInstanceObjects -Append
                                    };

                                }
                                "Jobs"
                                {
                                    foreach ($Object in $Objects) 
                                    {
                                        $JobSteps = $Object.JobSteps | Where-Object { $_.ID -eq 1 -And $_.Name -eq $FirstStepNameCheck } | Select-Object
                                        if ($JobSteps -eq $null) {  Write-Host ((" " * 24) + "Job without AG Check : " + $Object.Name + " - [Enabled = " + $Object.IsEnabled + "]" ) } 
                                        if ($ExcludeIfNoCheck -eq $False -Or $JobSteps -ne $Null) { $Object.Script($ScriptOptionsCreate) }                     
                                    }

                                }
                                default 
                                {
                                    foreach ($Object in $Objects) 
                                    {   
                                        $Object.Script($ScriptOptionsCreate)
                                    }
                                }
                            }

                        }

                    }

                    #################################
                    ### Find secondaries replicas ###
                    #################################

                    $SecondariesReplicas = $AllReplicas | Where-Object { $_.Role -eq "Secondary" -And $_.Name -notin $ReplicasExclusions }

                    Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - PRIMARY REPLICA " + $PrimaryReplica.Name + " -> Get secondaries replicas : " + $SecondariesReplicas.Count + " replica(s) found")

                    ######################################
                    ### SECONDARIES REPLICAS : Parsing ###
                    ######################################

                    foreach ($SecondaryReplica in $SecondariesReplicas) {

                        # Remove generated DropScript for previous secondary replica
                        If((test-path $ScriptDropInstanceObjects)) { Remove-Item -Force $ScriptDropInstanceObjects }

                        $SrvSecondaryReplica = ($SecondaryReplica.Name).Split("\")[0]

                        try
                        {

                            $SecondaryReplica = GetServer -Instance $SecondaryReplica.Name -Login $AvaibilityGroup.Login -Password $AvaibilityGroup.Password

                            Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - SECONDARY REPLICA " + $SecondaryReplica.Name + " -> Connecting to " + $SecondaryReplica.Name)

                            #################################################################################
                            ### SECONDARY REPLICA : Generate script to DROP Server Objects & Server Jobs ###
                            #################################################################################
                        
                            # Availibility Group owner to sa
                            if ($FixOwner) 
                            {
                                "ALTER AUTHORIZATION ON AVAILABILITY GROUP::[{0}] TO sa{1}" -f $AvaibilityGroup.Name, $BatchSeperator | Out-File $ScriptDropInstanceObjects -Append
                            }

                            # Get Operators from Agent FailSafeOperator to exclude
                            $ExcludedOperator = $SecondaryReplica.JobServer.AlertSystem.FailSafeOperator

                            # Add SharedSchedules exclusions from Jobs exclusions
                            $ObjToSync | Where-Object { $_.Type -eq "Operators" } | % { 
                                $_.Exclusions = $_.Exclusions + $ExcludedOperator
                            }
                        
                            foreach ($Item in $ObjToSync | Sort-Object -Property SortDrop) {

                                if ($Item.ToSync -eq $True) {

                                    $Exclusions = AddExclusions -ObjectType $Item.Type -UserExclusions $Item.Exclusions -SystemExclusions $SysObjects

                                    $Objects = if ($Item.Scope -eq "Server") { $SecondaryReplica.$($Item.Type) } elseif ($Item.Scope -eq "JobServer") { $SecondaryReplica.JobServer.$($Item.Type) } 
                                    $Objects = $Objects | Where-Object { $_.name -notin $Exclusions }

                                    switch ($Item.Type) 
                                    {

                                        "Logins" 
                                        {
                                        
                                            #######################################################################################
                                            ### SECONDARY REPLICA : Generate script to kill/disconnect sessions and DROP logins ###
                                            #######################################################################################

                                            $CustomScriptDropLogins = $ScriptDropLogins
                                            $CustomLoginRegex = $LoginRegex -f $SrvSecondaryReplica

                                            $Objects = $Objects | Where-Object {!$_.IsDisabled -And $_.Name -notmatch $CustomLoginRegex -And $_.Name -notin $SafeLoginsExclusion }
                                            $Logins = $Objects | Select -ExpandProperty Name 
                            
                                            foreach ($Object in $Objects) 
                                            {
                                                $CustomScriptDropLogins -f $Object.Name | Out-File $ScriptDropInstanceObjects -Append
                                            }
                                
                                        }
                                        "Permissions"
                                        {
                        
                                            #########################################################################
                                            ### SECONDARY REPLICA : Generate script to CHANGE OWNER Server Object ###
                                            #########################################################################
                                        
                                            if ($FixOwner) 
                                            {
                                                $SecondaryReplica.EnumObjectPermissions() | Where-Object { $_.Grantor -in $Logins -And $_.Grantor -notmatch "^(NT SERVICE|NT AUTHORITY|$SrvSecondaryReplica)\\.*$" } |% {
                                                    "ALTER AUTHORIZATION ON {0}::[{1}] TO [sa]; {2}" -f ($_.ObjectClass.ToString().ToUpper(),$_.ObjectName,$BatchSeperator) | Out-File $ScriptDropInstanceObjects -Append
                                                 };
                                            }

                                        }
                                        "Credentials"
                                        {

                                           $CustomScriptDropCredentials = $ScriptDropCredentials
                            
                                           foreach ($Object in $Objects) 
                                           {
                                                $CustomScriptDropCredentials -f $Object.Name | Out-File $ScriptDropInstanceObjects -Append
                                           }

                                        }
                                        default 
                                        {
                                            foreach ($Object in $Objects) 
                                            { 
                                                $Object.Script($ScriptOptionsDrop) 
                                            } 
                                        }
                                    }
                    
                                }

                                Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - SECONDARY REPLICA " + $SecondaryReplica.Name + " -> Generate drop script for " + $Item.Type + " (SortDrop : "+ $Item.SortDrop +")")

                            }

                            #####################################################
                            ### Merge SQL files to apply on secondary replica ###
                            #####################################################
        
                            # Define name for synchronization script
                            $ScriptToApply = "$($ScriptDirectoryOut)\sync_$($SrvSecondaryReplica).sql"

                            # Merge sql files into synchronization script
                            Get-ChildItem -path $ScriptDirectoryOut -recurse |?{ ! $_.PSIsContainer } |?{($_.name).contains(".sql") -And $_.Name -notlike "sync_*" } | %{ Out-File -filepath $ScriptToApply -inputobject (get-content $_.fullname) -Append }

                            # Clean-up sql files after merge
                            Get-ChildItem -Path $ScriptDirectoryOut -Include *.sql* -Recurse -File | Where-Object { $_.Name -notmatch $ScriptRegex } | remove-Item -Force

                            Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - FILESYSTEM -> Create Synchronization script for secondary repliqua " + $SecondaryReplica.Name + " : " + $ScriptToApply)

                            if ($Execute) {

                                ########################################################
                                ### PRIMARY REPLICA : Change Owner for all databases ###
                                ########################################################

                                if ($FixOwner) 
                                {

                                    $PrimaryReplica = GetServer -Instance $PrimaryReplica.Name -Login $AvaibilityGroup.Login -Password $AvaibilityGroup.Password

                                    foreach ($db in $PrimaryReplica.Databases)
                                    {
                                        if ($db.Owner -ne "sa")
                                        { 
                                            $db.SetOwner("sa", $True)
                                            $db.Alter()
                                            Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - PRIMARY REPLICA " + $PrimaryReplica.Name + " -> Change Owner (" + $db.Owner +") to sa on database " + $db.Name)
                                        }
                                    }

                                }

                                ########################################################
                                ### SECONDARY REPLICA : Apply synchronization script ###
                                ########################################################

                                Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - SECONDARY REPLICA " + $SecondaryReplica.Name + " -> Launching Synchronization...")

                                Invoke-SqlNinjaCmd -ServerInstance "$($SecondaryReplica.Name)" -Username "$($AvaibilityGroup.Login)" -Password "$($AvaibilityGroup.Password)" -Database master -QueryTimeout 0 -InputFile "$($ScriptToApply)" -verbose -As Batch

                                Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - SECONDARY REPLICA " + $SecondaryReplica.Name + " -> Synchronization done")

                                #####################################################
                                ### SECONDARY REPLICA : Change Owner for all jobs ###
                                #####################################################

                                if ($FixOwner) 
                                {

                                    # Post synchronisation : init SMO
                                    $SecondaryReplica = GetServer -Instance $SecondaryReplica.Name -Login $AvaibilityGroup.Login -Password $AvaibilityGroup.Password
                
                                    $SQLAgent = $SecondaryReplica.JobServer;
                                    $SQLAgent.Jobs | Where-Object { $_.OwnerLoginName -ne "sa" -And $_.OwnerLoginName -notlike "##*" } | % { 
                                      $_.set_OwnerLoginName("sa")
                                      $_.Alter()
                                      Write-Host ((Get-Date -format 'yyyy-MM-dd HH:mm:ss') + " - SECONDARY REPLICA " + $SecondaryReplica.Name + " -> Change Owner to sa on job " + $_.Name)
                                      }

                                }
                            }
            
                        }
                        Catch
                        {   
                          $errorMessage = $_.Exception.Message
                          $line = $_.InvocationInfo.ScriptLineNumber
                          Write-Error "Error occurred on line $line : $ErrorMessage" 
                        }

                    }

                }

                If(test-path $ScriptCreateInstanceObjects) { remove-Item $ScriptCreateInstanceObjects -Force }

            }
            Catch
            {   
                $errorMessage = $_.Exception.Message
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Error "Error occurred on line $line : $ErrorMessage" 
            }
            Finally
            {
                if (($PSVersionTable.PSVersion.Major) -ge 4){
                    Stop-Transcript
                }
            }

    }

}