<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Archives des PowerShell - dbi Blog</title>
	<atom:link href="https://www.dbi-services.com/blog/tag/powershell/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.dbi-services.com/blog/tag/powershell/</link>
	<description></description>
	<lastBuildDate>Mon, 03 Aug 2026 21:50:39 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/05/cropped-favicon_512x512px-min-32x32.png</url>
	<title>Archives des PowerShell - dbi Blog</title>
	<link>https://www.dbi-services.com/blog/tag/powershell/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>SQL Server Snapshot Backup and Restore with Proxmox ZFS &#8211; Powershell implementation (2/4)</title>
		<link>https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/</link>
					<comments>https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/#respond</comments>
		
		<dc:creator><![CDATA[Amine Haloui]]></dc:creator>
		<pubDate>Thu, 14 May 2026 21:35:41 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[Operating systems]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[proxmox]]></category>
		<category><![CDATA[ZFS]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=44497</guid>

					<description><![CDATA[<p>In the previous section, we discussed the drawbacks of running the commands manually. Indeed, the manual process was taking too much time and could directly impact the database state while the freeze was occurring. To address this issue, it is possible to automate the solution with PowerShell. The idea is to automate the different operations [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/">SQL Server Snapshot Backup and Restore with Proxmox ZFS &#8211; Powershell implementation (2/4)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In the previous section, we discussed the drawbacks of running the commands manually. Indeed, the manual process was taking too much time and could directly impact the database state while the freeze was occurring.</p>



<p class="wp-block-paragraph">To address this issue, it is possible to automate the solution with PowerShell. The idea is to automate the different operations involved in the snapshot backup and restore process.</p>



<p class="wp-block-paragraph">We will use two scripts:</p>



<ul class="wp-block-list">
<li>One script to perform the backups and create the snapshots.</li>



<li>One script to perform the restores.</li>
</ul>



<h2 class="wp-block-heading" id="h-backup-process">Backup process</h2>



<p class="wp-block-paragraph">Here is how the backup process works:</p>



<ul class="wp-block-list">
<li>We connect to the corresponding SQL Server instance.</li>



<li>We change the state of the database using ALTER DATABASE &#8230; SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON. At this point, the I/Os are frozen.</li>



<li>We connect to the hypervisor through SSH.</li>



<li>We create the snapshot.</li>



<li>We back up the database using BACKUP DATABASE &#8230; WITH METADATA_ONLY.</li>



<li>We change the state of the database using ALTER DATABASE &#8230; SET SUSPEND_FOR_SNAPSHOT_BACKUP = OFF. At this point, the I/Os are unfrozen.</li>
</ul>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="627" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-1024x627.png" alt="" class="wp-image-44499" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-1024x627.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-300x184.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-768x470.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-1536x941.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-50-2048x1254.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Powershell implementation (backup)</h2>



<p class="wp-block-paragraph">Here is the code used to perform the backup:</p>



<pre class="wp-block-code"><code>param(
    &#091;string]$SqlInstance = "VM-WS25-SQL2",
    &#091;string]$Database    = "StackOverflow",
    &#091;string]$BackupDir   = "D:\Backups",
    &#091;string]$PveHost     = "192.168.1.110",
    &#091;string]$PveUser     = "MyUser",
    &#091;string&#091;]]$Zvols     = @("sqlpool/pve/vm-302-disk-0")
)

$Timestamp = Get-Date -Format "yyyyMMddTHHmmss"
$SnapName  = "sql_${Database}_${Timestamp}"

$DbSafe = $Database.Replace("]", "]]")
$BackupFile = Join-Path $BackupDir "${Database}_${Timestamp}.bkm"

$ZfsSnapshots = $Zvols | ForEach-Object { "$_@$SnapName" }
$ZfsSnapshotArgs = $ZfsSnapshots -join " "

$MediaDescription = "zfs|$PveHost|$ZfsSnapshotArgs"

$BackupFileSql = $BackupFile.Replace("'", "''")
$MediaSql = $MediaDescription.Replace("'", "''")

$connString = "Server=$SqlInstance;Database=master;Integrated Security=True;TrustServerCertificate=True;Application Name=ZFS-TSQL-Snapshot;"
$conn = New-Object System.Data.SqlClient.SqlConnection $connString

function Invoke-SqlNonQuery {
    param(&#091;string]$Sql)

    $cmd = $conn.CreateCommand()
    $cmd.CommandTimeout = 0
    $cmd.CommandText = $Sql
    &#091;void]$cmd.ExecuteNonQuery()
}

try {
    $conn.Open()

    Write-Host "Freezing SQL database writes..."
    Invoke-SqlNonQuery "ALTER DATABASE &#091;$DbSafe] SET SUSPEND_FOR_SNAPSHOT_BACKUP = ON;"

    Write-Host "Taking ZFS snapshot on Proxmox..."
    ssh "$PveUser@$PveHost" "zfs snapshot $ZfsSnapshotArgs &amp;&amp; zfs hold sqlsnap $ZfsSnapshotArgs"

    if ($LASTEXITCODE -ne 0) {
        throw "ZFS snapshot failed on $PveHost"
    }

    Write-Host "Writing SQL metadata backup..."

    Invoke-SqlNonQuery @"
BACKUP DATABASE &#091;$DbSafe]
TO DISK = N'$BackupFileSql'
WITH METADATA_ONLY,
     MEDIADESCRIPTION = N'$MediaSql',
     NAME = N'$SnapName';
"@

    Write-Host "Snapshot backup completed:"
    Write-Host "  Snapshot: $ZfsSnapshotArgs"
    Write-Host "  Metadata: $BackupFile"
}
catch {
    Write-Warning $_

    try {
        Write-Warning "Attempting to unfreeze SQL database..."
        Invoke-SqlNonQuery "ALTER DATABASE &#091;$DbSafe] SET SUSPEND_FOR_SNAPSHOT_BACKUP = OFF;"
    }
    catch {
        Write-Warning "Could not unfreeze cleanly. Check SQL Server error log."
    }

    throw
}
finally {
    $conn.Close()
}</code></pre>



<h2 class="wp-block-heading">Restore process</h2>



<p class="wp-block-paragraph">Here is how the restore process works:</p>



<ul class="wp-block-list">
<li>We connect to the corresponding SQL Server instance.</li>



<li>We take the database offline.</li>



<li>The volume dedicated to the StackOverflow database is taken offline.</li>



<li>We connect to the hypervisor through SSH.</li>



<li>We roll back the corresponding snapshot.</li>



<li>We restore the database using the corresponding backup, which was created at the same time as the snapshot.</li>
</ul>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="627" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-1024x627.png" alt="" class="wp-image-44501" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-1024x627.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-300x184.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-768x470.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-1536x941.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-51-2048x1254.png 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Powershell implementation (restore)</h2>



<p class="wp-block-paragraph">Here is the code used to perform the restore:</p>



<pre class="wp-block-code"><code>param(
    &#091;string]$SqlInstance = "VM-WS25-SQL2",
    &#091;string]$Database    = "StackOverflow",
    &#091;string]$BackupFile  = "D:\Backups\StackOverflow_20260514T122642.bkm",
    &#091;string]$SnapName    = "sql_StackOverflow_20260514T122642",
    &#091;string]$PveHost     = "192.168.1.110",
    &#091;string]$PveUser     = "MyUser",
    &#091;string&#091;]]$Zvols     = @("sqlpool/pve/vm-302-disk-0"),
    &#091;string&#091;]]$DatabaseDriveLetters = @("T"),
    &#091;switch]$NoRecovery
)

$ErrorActionPreference = "Stop"

function Assert-SafeName {
    param(
        &#091;string]$Value,
        &#091;string]$Name,
        &#091;string]$Pattern
    )

    if ($Value -notmatch $Pattern) {
        throw "$Name contained not allowed characters : $Value"
    }
}

function Normalize-DriveLetter {
    param(&#091;string]$DriveLetter)

    $letter = $DriveLetter.Trim().TrimEnd(":").ToUpperInvariant()

    if ($letter -notmatch '^&#091;A-Z]$') {
        throw "Drive letter invalid : $DriveLetter"
    }

    return $letter
}

function Get-DiskForDriveLetter {
    param(&#091;string]$DriveLetter)

    $letter = Normalize-DriveLetter $DriveLetter

    $partition = Get-Partition -DriveLetter $letter -ErrorAction Stop
    $disk = $partition | Get-Disk -ErrorAction Stop

    return &#091;pscustomobject]@{
        DriveLetter = $letter
        DiskNumber  = &#091;int]$disk.Number
        IsOffline   = &#091;bool]$disk.IsOffline
        FriendlyName = $disk.FriendlyName
        Size        = $disk.Size
    }
}

function Invoke-SshChecked {
    param(&#091;string]$Command)

    Write-Host "SSH $PveUser@$PveHost :: $Command"

    &amp; ssh "$PveUser@$PveHost" "$Command"

    if ($LASTEXITCODE -ne 0) {
        throw "SSH command failed with code $LASTEXITCODE : $Command"
    }
}

function New-SqlConnection {
    $connString = "Server=$SqlInstance;Database=master;Integrated Security=True;TrustServerCertificate=True;Application Name=ZFS-TSQL-Restore-NoVmRestart;"
    return New-Object System.Data.SqlClient.SqlConnection $connString
}

function Invoke-SqlNonQuery {
    param(&#091;string]$Sql)

    $conn = New-SqlConnection

    try {
        $conn.Open()
        $cmd = $conn.CreateCommand()
        $cmd.CommandTimeout = 0
        $cmd.CommandText = $Sql
        &#091;void]$cmd.ExecuteNonQuery()
    }
    finally {
        $conn.Close()
    }
}

function Invoke-SqlScalar {
    param(&#091;string]$Sql)

    $conn = New-SqlConnection

    try {
        $conn.Open()
        $cmd = $conn.CreateCommand()
        $cmd.CommandTimeout = 0
        $cmd.CommandText = $Sql
        return $cmd.ExecuteScalar()
    }
    finally {
        $conn.Close()
    }
}

function Set-DatabaseDisksOffline {
    param(&#091;object&#091;]]$DiskInfos)

    $offlinedByScript = @()

    foreach ($diskInfo in ($DiskInfos | Sort-Object DiskNumber -Unique)) {
        if ($diskInfo.IsOffline) {
            Write-Host "Disque $($diskInfo.DiskNumber) déjà offline. Lecteur $($diskInfo.DriveLetter):"
            continue
        }

        Write-Host "Taking the Windows disk offline $($diskInfo.DiskNumber), drive $($diskInfo.DriveLetter):"
        Set-Disk -Number $diskInfo.DiskNumber -IsOffline $true

        $offlinedByScript += $diskInfo
    }

    return $offlinedByScript
}

function Set-DatabaseDisksOnline {
    param(&#091;object&#091;]]$DiskInfos)

    foreach ($diskInfo in ($DiskInfos | Sort-Object DiskNumber -Unique)) {
        Write-Host "Bringing the Windows disk back online. $($diskInfo.DiskNumber), drive $($diskInfo.DriveLetter):"
        Set-Disk -Number $diskInfo.DiskNumber -IsOffline $false
    }

    Write-Host "Update-HostStorageCache..."
    Update-HostStorageCache
}

Assert-SafeName -Value $SnapName -Name "SnapName" -Pattern '^&#091;A-Za-z0-9_.:-]{1,160}$'

foreach ($zvol in $Zvols) {
    Assert-SafeName -Value $zvol -Name "Zvol" -Pattern '^&#091;A-Za-z0-9_.:/-]{1,240}$'
}

$DbQuoted = "&#091;" + $Database.Replace("]", "]]") + "]"
$DbLiteral = $Database.Replace("'", "''")
$BackupFileSql = $BackupFile.Replace("'", "''")

$ZfsSnapshots = $Zvols | ForEach-Object { "$_@$SnapName" }
$ZfsSnapshotArgs = ($ZfsSnapshots | ForEach-Object { "'$_'" }) -join " "

$RecoveryOption = if ($NoRecovery) { "NORECOVERY" } else { "RECOVERY" }

$DatabaseDiskInfos = @()
$DisksOfflinedByScript = @()

Write-Host ""
Write-Host "Restore SQL Server from a ZFS snapshot, without restarting the VM"
Write-Host "SQL Instance : $SqlInstance"
Write-Host "Database     : $Database"
Write-Host "BackupFile   : $BackupFile"
Write-Host "DB volumes   : $($DatabaseDriveLetters -join ', ')"
Write-Host "Snapshots    :"
$ZfsSnapshots | ForEach-Object { Write-Host "  $_" }
Write-Host ""

try {
    Write-Host "Checking ZFS snapshots..."
    Invoke-SshChecked "zfs list -H -t snapshot -o name $ZfsSnapshotArgs &gt;/dev/null"

    Write-Host "Identifying Windows disks containing SQL Server files..."
    foreach ($driveLetter in $DatabaseDriveLetters) {
        $diskInfo = Get-DiskForDriveLetter $driveLetter
        $DatabaseDiskInfos += $diskInfo

        Write-Host "Drive $($diskInfo.DriveLetter): -&gt; Windows disk $($diskInfo.DiskNumber) &#091;$($diskInfo.FriendlyName)]"
    }

    $backupDrive = $null
    if ($BackupFile -match '^(&#091;A-Za-z]):\\') {
        $backupDrive = Normalize-DriveLetter $Matches&#091;1]

        try {
            $backupDiskInfo = Get-DiskForDriveLetter $backupDrive
            $targetDiskNumbers = @($DatabaseDiskInfos | ForEach-Object { $_.DiskNumber } | Select-Object -Unique)

            if ($targetDiskNumbers -contains $backupDiskInfo.DiskNumber) {
                throw @"
The backup file $BackupFile is located on drive $backupDrive, which is on the same Windows disk as the SQL Server data volume.
Taking the data disk offline would make the .bkm file inaccessible, and a rollback could also make the .bkm file disappear.
Move the .bkm file to C:, a network share, or another disk that is not rolled back.
"@
            }
        }
        catch {
            throw
        }
    }

    Write-Host "Checking whether the SQL Server database exists..."
    $DbExists = Invoke-SqlScalar "SELECT CASE WHEN DB_ID(N'$DbLiteral') IS NULL THEN 0 ELSE 1 END;"

    if ($DbExists -eq 1) {
        Write-Host "Taking database $Database OFFLINE..."
        Invoke-SqlNonQuery @"
ALTER DATABASE $DbQuoted SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
ALTER DATABASE $DbQuoted SET OFFLINE WITH ROLLBACK IMMEDIATE;
"@
    }
    else {
        Write-Host "Database $Database does not exist in SQL Server. Continuing with disk offline and ZFS rollback."
    }

    Write-Host "Taking Windows disks containing MDF/LDF files offline..."
    $DisksOfflinedByScript = Set-DatabaseDisksOffline -DiskInfos $DatabaseDiskInfos

    Write-Host "Rolling back ZFS snapshot..."
    $RollbackCommands = ($ZfsSnapshots | ForEach-Object { "zfs rollback -r '$_'" }) -join "; "
    Invoke-SshChecked "set -e; $RollbackCommands"

    Write-Host "Bringing Windows disks back online..."
    Set-DatabaseDisksOnline -DiskInfos $DisksOfflinedByScript
    $DisksOfflinedByScript = @()

    Write-Host "Short pause to let Windows and SQL Server detect the restored disk state..."
    Start-Sleep -Seconds 5

    Write-Host "Restoring SQL Server metadata-only backup..."

    $RestoreSql = @"
RESTORE DATABASE $DbQuoted
FROM DISK = N'$BackupFileSql'
WITH METADATA_ONLY,
     REPLACE,
     $RecoveryOption;
"@

    Invoke-SqlNonQuery $RestoreSql

    if (-not $NoRecovery) {
        Write-Host "Setting database back to MULTI_USER..."
        Invoke-SqlNonQuery @"
ALTER DATABASE $DbQuoted SET MULTI_USER;
"@
    }

    Write-Host ""
    Write-Host "Restore completed."
    Write-Host "Database : $Database"
    Write-Host "Snapshot : $SnapName"
    Write-Host "Backup   : $BackupFile"
}
catch {
    Write-Warning "Restore failed: $_"

    if ($DisksOfflinedByScript.Count -gt 0) {
        try {
            Write-Warning "Attempting to bring disks offlined by the script back online..."
            Set-DatabaseDisksOnline -DiskInfos $DisksOfflinedByScript
            $DisksOfflinedByScript = @()
        }
        catch {
            Write-Warning "Unable to automatically bring the disks back online. Check with Get-Disk."
        }
    }

    try {
        $DbExistsAfterError = Invoke-SqlScalar "SELECT CASE WHEN DB_ID(N'$DbLiteral') IS NULL THEN 0 ELSE 1 END;"

        if ($DbExistsAfterError -eq 1 -and -not $NoRecovery) {
            Write-Warning "Attempting to set the database back ONLINE/MULTI_USER..."
            Invoke-SqlNonQuery @"
ALTER DATABASE $DbQuoted SET ONLINE;
ALTER DATABASE $DbQuoted SET MULTI_USER;
"@
        }
    }
    catch {
        Write-Warning "Unable to automatically set the database back ONLINE/MULTI_USER."
    }

    throw
}</code></pre>



<h2 class="wp-block-heading">What does it look like?</h2>



<p class="wp-block-paragraph">We start the backup process:</p>



<figure class="wp-block-image size-full"><img decoding="async" width="530" height="82" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-52.png" alt="" class="wp-image-44503" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-52.png 530w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-52-300x46.png 300w" sizes="(max-width: 530px) 100vw, 530px" /></figure>



<p class="wp-block-paragraph">We verify that the snapshot is present:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="750" height="131" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-53.png" alt="" class="wp-image-44504" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-53.png 750w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-53-300x52.png 300w" sizes="auto, (max-width: 750px) 100vw, 750px" /></figure>



<p class="wp-block-paragraph">We verify that the backup is present:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="601" height="36" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-54.png" alt="" class="wp-image-44505" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-54.png 601w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-54-300x18.png 300w" sizes="auto, (max-width: 601px) 100vw, 601px" /></figure>



<p class="wp-block-paragraph">We drop the StackOverflow database:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="314" height="301" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-55.png" alt="" class="wp-image-44506" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-55.png 314w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-55-300x288.png 300w" sizes="auto, (max-width: 314px) 100vw, 314px" /></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="310" height="231" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-56.png" alt="" class="wp-image-44507" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-56.png 310w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-56-300x224.png 300w" sizes="auto, (max-width: 310px) 100vw, 310px" /></figure>



<p class="wp-block-paragraph">We start the restore process:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="951" height="384" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-57.png" alt="" class="wp-image-44508" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-57.png 951w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-57-300x121.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/05/image-57-768x310.png 768w" sizes="auto, (max-width: 951px) 100vw, 951px" /></figure>



<p class="wp-block-paragraph">The database is available again. The restore took only a few seconds for a database of approximately 200 GB.</p>



<h2 class="wp-block-heading">Major drawbacks</h2>



<p class="wp-block-paragraph">In my case, the solution is executed from the SQL Server itself. Ideally, it should rather be hosted on another server or client machine. We could also imagine running these scripts from a scheduler such as RedDeck, for example.</p>



<p class="wp-block-paragraph">During the database restore, the database is switched to SINGLE_USER mode. This could be an issue if the applications using the database reconnect very frequently. A better approach would probably be to explicitly terminate the active sessions using the KILL command.</p>



<p class="wp-block-paragraph">We have also not yet covered the use of a REST API.</p>



<p class="wp-block-paragraph">Thank you. <a href="https://www.linkedin.com/in/amine-haloui-76968056/">Amine Haloui</a></p>
<p>L’article <a href="https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/">SQL Server Snapshot Backup and Restore with Proxmox ZFS &#8211; Powershell implementation (2/4)</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/sql-server-snapshot-backup-and-restore-with-proxmox-zfs-2-3/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Scaling SSRS Migrations: Multi-Threaded Automation for PBIRS 2025</title>
		<link>https://www.dbi-services.com/blog/scaling-ssrs-migrations-multi-threaded-automation-for-pbirs-2025/</link>
					<comments>https://www.dbi-services.com/blog/scaling-ssrs-migrations-multi-threaded-automation-for-pbirs-2025/#respond</comments>
		
		<dc:creator><![CDATA[Louis Tochon]]></dc:creator>
		<pubDate>Tue, 07 Apr 2026 12:58:14 +0000</pubDate>
				<category><![CDATA[Business Intelligence]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[PBIRS]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[SQLServer]]></category>
		<category><![CDATA[SSRS]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=43802</guid>

					<description><![CDATA[<p>Migrate SSRS to PBIRS 2025: a PowerShell ETL to automate extraction, XML patching, and parallelized deployment.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/scaling-ssrs-migrations-multi-threaded-automation-for-pbirs-2025/">Scaling SSRS Migrations: Multi-Threaded Automation for PBIRS 2025</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Modernizing a reporting platform is a pivotal milestone for any BI infrastructure. Whether it’s a standard upgrade or a forced transition to <strong><a href="https://learn.microsoft.com/en-us/power-bi/report-server/download-powerbi-report-server">Power BI Report Server (PBIRS)</a></strong> following the decommissioning of SSRS in SQL Server 2025, the operation is critical. For the purposes of our lab, we will use an SSRS 2017 source, but the logic remains universal: regardless of the original version, the goal is to ensure the continuity of your decision-making services without sacrificing your mental health in the process.</p>



<p class="wp-block-paragraph">As my colleague Amine Haloui explained in <a href="https://www.dbi-services.com/blog/sql-server-2025-retirement-of-sql-server-reporting-services-ssrs/" id="https://www.dbi-services.com/blog/sql-server-2025-retirement-of-sql-server-reporting-services-ssrs/">a recent blog post</a>, several strategies exist for migrating an instance. The &#8220;Lift and Shift&#8221; method (restoring the <code>ReportServer</code> database onto a new instance) is often the most attractive on paper. However, the reality on the ground can be more temperamental.</p>



<p class="wp-block-paragraph">In some production environments, the target PBIRS instance already exists, hosts its own content, or follows specific configurations that prohibit simply overwriting its underlying <code>ReportServer</code> database. Therefore, we are proceeding here on the premise of a selective and granular migration: we must inject the SSRS catalog into an active PBIRS environment without burning everything to the ground in the process.</p>



<p class="wp-block-paragraph">When faced with inventories exceeding hundreds or even thousands of reports (RDL), folders, and datasources, a manual approach via the web interface is not an option and automation becomes a necessity.</p>



<p class="wp-block-paragraph">This article analyzes a systematic approach based on the <code>ReportingServicesTools</code> PowerShell module. The objective is to provide a robust methodology to extract your catalog and redeploy it intelligently, while managing the necessary reconfigurations along the way.</p>



<h2 class="wp-block-heading" id="h-phase-1-smart-dumping-building-the-local-staging-area">Phase 1: Smart Dumping – Building the Local Staging Area</h2>



<p class="wp-block-paragraph">To migrate cleanly, objects must first be isolated. The idea is not to blindly vacuum everything, but to target the critical folders of your SSRS instance and transform them into flat files (.rdl and .rds) within a local staging area. If your SSRS instance contains specific object types, the scripts can easily be adapted to include them as well.</p>



<p class="wp-block-paragraph">This is where the power of the <strong><a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/new-webserviceproxy?view=powershell-5.1">SOAP Proxy</a></strong> comes into play. Rather than multiplying slow HTTP calls, we use the native service interface to list and extract our components:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$sourceUrl  = &quot;http://your-ssrs-server/ReportServer&quot;
$exportRoot = &quot;H:\Migration_Dump&quot;

$proxySource = New-RsWebServiceProxy -ReportServerUri $sourceUrl
</pre></div>


<p class="wp-block-paragraph">In a production environment, SSRS folders are often a messy mix of reports, data sources, images, and sometimes obsolete semantic models. To maintain total control over what we export, we isolate the filtering logic.</p>



<p class="wp-block-paragraph">This <code>Get-AllItemsByType</code> function allows us to retrieve only what truly matters to us, based on the <strong>TypeName</strong> and <strong>file extension</strong> returned by the API.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
function Get-AllItemsByType {
    param(
        &#x5B;string]$CurrentPath,
        $Proxy,
        &#x5B;string]$TypeName 
    )
    try {
        return $Proxy.ListChildren($CurrentPath, $true) | Where-Object { $_.TypeName -eq $TypeName }
    } catch {
        Write-Host &quot;  &#x5B;!] Error on $CurrentPath : $($_.Exception.Message)&quot; -ForegroundColor Red
        return $null
    }
}
</pre></div>


<p class="wp-block-paragraph">This mapping between the file type and its extension must be defined upfront in a dictionary:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$extensionMap = @{
    &quot;Report&quot; = &quot;.rdl&quot;
    &quot;DataSource&quot; = &quot;.rds&quot;
}
</pre></div>


<p class="wp-block-paragraph">A crucial point in extracting SSRS objects is preserving their context. To ensure a seamless import into PBIRS 2025, we must recreate the exact folder hierarchy of the source server locally.</p>



<p class="wp-block-paragraph">The trick lies in transforming the SSRS path (formatted as <code>/Folder/SubFolder/Report</code>) into a valid Windows path, while simultaneously handling the extension mapping (<code>.rdl</code> for reports, <code>.rds</code> for DataSources).</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
function Export-SsrsItems {
    param(
        &#x5B;string]$RootPath,
        $Proxy,
        &#x5B;string]$TypeName,
        &#x5B;string]$ExportRoot
    )

    $items = Get-AllItemsByType -CurrentPath $RootPath -Proxy $Proxy -TypeName $TypeName

    foreach ($item in $items) {
        $relativeItemPath = $item.Path.TrimStart(&#039;/&#039;).Replace(&quot;/&quot;, &quot;\&quot;)
        $localFilePath    = Join-Path $ExportRoot $relativeItemPath
        $localDirectory   = Split-Path -Path $localFilePath -Parent

        if (-not (Test-Path $localDirectory)) {
            New-Item -ItemType Directory -Path $localDirectory -Force | Out-Null
        }

        Out-RsCatalogItem -Path $item.Path -Destination $localDirectory -Proxy $Proxy
    }
}
</pre></div>


<p class="wp-block-paragraph">By doing this, your <code>H:\Migration_Dump</code> becomes the exact mirror of your SSRS portal. This structural rigor is what will allow us, in the next step, to remap our data sources without having to hunt down which report belongs to which department.</p>



<div class="wp-block-columns no-bottom-margin is-layout-flex wp-container-core-columns-is-layout-8f761849 wp-block-columns-is-layout-flex">
<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="284" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-7-1024x284.png" alt="" class="wp-image-43818" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-7-1024x284.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-7-300x83.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-7-768x213.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-7.png 1145w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>
</div>



<div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow">
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="887" height="255" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-6.png" alt="" class="wp-image-43817" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-6.png 887w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-6-300x86.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-6-768x221.png 768w" sizes="auto, (max-width: 887px) 100vw, 887px" /></figure>
</div>
</div>



<p class="wp-block-paragraph">Finally, we define the folders we wish to export along with the document types they contain (since a migration is often the perfect time for a bit of spring cleaning):</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$exportTasks = @(
    @{ Path = &quot;/Migration_Source_2&quot;; Types = @(&quot;Report&quot;) },
    @{ Path = &quot;/Data Sources&quot;;  Types = @(&quot;DataSource&quot;) }
)

Write-Host &quot;--- Selective Export Started ---&quot; -ForegroundColor Cyan

foreach ($task in $exportTasks) {
    foreach ($typeName in $task.Types) {
        $ext = $extensionMap&#x5B;$typeName]
        Export-SsrsItems `
            -RootPath   $task.Path `
            -Proxy      $proxySource `
            -TypeName   $typeName `
            -Extension  $ext `
            -ExportRoot $exportRoot
    }
}
</pre></div>

<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="682" height="385" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-1.png" alt="" class="wp-image-43807" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-1.png 682w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-1-300x169.png 300w" sizes="auto, (max-width: 682px) 100vw, 682px" /></figure>
</div>


<h2 class="wp-block-heading" id="h-phase-2-data-source-patching-mass-xml-transformation">Phase 2: Data Source Patching – Mass XML Transformation</h2>



<p class="wp-block-paragraph">Once the extraction is complete, you have a local mirror of your source instance, but the data sources still point to the legacy infrastructure.</p>



<p class="wp-block-paragraph">Instead of manually fixing each connection after the import (the best way to miss half of them), we will apply an automated transformation directly to our local XML files. This allows us to update connection strings in bulk before a single report even hits the target server.</p>



<p class="wp-block-paragraph">The idea is simple: use PowerShell to inject the new SQL instance wherever necessary, ensuring a functional deployment from the very first second:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$allDataSources = Get-ChildItem -Path $exportRoot -Filter &quot;*.rds&quot; -Recurse

Write-Host &quot;&#x5B;&gt;] Datasources updated in : $exportRoot&quot; -ForegroundColor Yellow

foreach ($dsFile in $allDataSources) {
    &#x5B;xml]$xmlContent = Get-Content $dsFile.FullName
 
    $node = $xmlContent.SelectSingleNode(&quot;//ConnectString&quot;)
    
    if ($null -ne $node) {
        $oldValue = $node.&quot;#text&quot; 
        if ($null -eq $oldValue) { $oldValue = $node.InnerText }

        $newValue = $oldValue -replace &quot;OLD_REPORTING_INSTANCE&quot;, &quot;NEW_REPORTING_INSTANCE&quot;
        
        if ($oldValue -ne $newValue) {
            $node.InnerText = $newValue
            $xmlContent.Save($dsFile.FullName)
            Write-Host &quot;  &#x5B;v] ConnectString updated in : $($dsFile.Name)&quot; -ForegroundColor Green
        }
    } else {
        Write-Host &quot;  &#x5B;!] ConnectString not found in file $($dsFile.Name)&quot; -ForegroundColor Red
    }
}
</pre></div>


<p class="wp-block-paragraph">Moreover, since we are interacting directly with the file’s XML structure, this logic isn&#8217;t limited to connection strings: you can apply the same principle to automate changes for any XML property, from timeouts to provider names.</p>



<h2 class="wp-block-heading" id="h-phase-3-mass-deployment-rebuilding-the-reporting-portal">Phase 3: Mass Deployment – Rebuilding the Reporting Portal</h2>



<p class="wp-block-paragraph">At this stage, the operation is purely mechanical. We once again leverage the <strong>ReportingServicesTools</strong> module to recreate the folder structure and upload the <code>.rds</code> and <code>.rdl</code> files. By following this specific order, PBIRS will automatically restore the links between your reports and their newly patched data sources.</p>



<p class="wp-block-paragraph">It is worth noting that the script allows for importing into a specific root folder (defined by the <code>$destroot</code> variable). This is particularly useful if you want to isolate the migrated assets into a dedicated directory, such as <code>SSRS_Folder</code> to keep them distinct from the existing hierarchy. Furthermore, this script is designed with safety in mind: it cannot overwrite or delete anything. If a report with the same name already exists in the same location, the <code>-Overwrite:$false</code> argument prevents replacement, ensuring that the import process never destroys existing content.</p>



<p class="wp-block-paragraph">Here is the final block to complete your migration:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$destUrl   = &quot;http://your-pbirs-server/ReportServer&quot; 
$localDump = &quot;H:\Migration_Dump&quot;
$destRoot  = &quot;/&quot; #Start import in the root folder
$proxyDest = New-RsWebServiceProxy -ReportServerUri $destUrl

$extensionMap = @{
    &quot;Report&quot;     = &quot;.rdl&quot;
    &quot;DataSource&quot; = &quot;.rds&quot;
}

function Ensure-RsFolderBruteForce {
    param($fullFolderPath, $Proxy)
    $parts = $fullFolderPath.Split(&#039;/&#039;) | Where-Object { $_ -ne &#039;&#039; }
    $currentPath = &#039;&#039;
    
    foreach ($part in $parts) {
        $parent = if ($currentPath -eq &#039;&#039;) { &quot;/&quot; } else { $currentPath }
        $target = if ($currentPath -eq &#039;&#039;) { &quot;/$part&quot; } else { &quot;$currentPath/$part&quot; }
        
        try {
            $Proxy.CreateFolder($part, $parent, $null) | Out-Null
            Write-Host &quot;  &#x5B;DIR] Created : $target&quot; -ForegroundColor Cyan
        } catch {
            if ($_.Exception.Message -match &quot;AlreadyExists&quot;) {
                # Folder already exists but we continue
            } else {
                Write-Host &quot;  &#x5B;!] Error for folder $target : $($_.Exception.Message)&quot; -ForegroundColor Red
            }
        }
        $currentPath = $target
    }
}

function Import-SsrsItem {
    param(
        &#x5B;System.IO.FileInfo]$File,
        &#x5B;string]$LocalDump,
        &#x5B;string]$DestRoot,
        $Proxy
    )

    $relativeDir       = $File.DirectoryName.Replace($LocalDump, &#039;&#039;).Replace(&quot;\&quot;, &quot;/&quot;)
    $targetFolderPath  = ($DestRoot + $relativeDir).Replace(&quot;//&quot;, &quot;/&quot;)
    $fullItemPath      = ($targetFolderPath + &quot;/&quot; + $File.BaseName).Replace(&quot;//&quot;, &quot;/&quot;)

    Ensure-RsFolderBruteForce -fullFolderPath $targetFolderPath -Proxy $Proxy

    try {
        Write-RsCatalogItem -Path $File.FullName -Destination $targetFolderPath -Proxy $Proxy -Overwrite:$false
        Write-Host &quot;  &#x5B;DONE] Imported: $fullItemPath&quot; -ForegroundColor Green
    }
    catch {
        if ($_.Exception.Message -match &quot;already exists&quot;) {
            Write-Host &quot;  &#x5B;SKIP] Already created : $fullItemPath&quot; -ForegroundColor Gray
        } else {
            Write-Host &quot;  &#x5B;FAIL] Error $fullItemPath : $($_.Exception.Message)&quot; -ForegroundColor Red
        }
    }
}

$importOrder = @(&quot;DataSource&quot;, &quot;Report&quot;)

foreach ($typeName in $importOrder) {
    $extension = $extensionMap&#x5B;$typeName]
    Write-Host &quot;`n&#x5B;PASS] Import of object with type : $typeName ($extension)&quot; -ForegroundColor Magenta
    
    $filesToImport = Get-ChildItem -Path $localDump -Filter &quot;*$extension&quot; -Recurse

    if ($filesToImport.Count -eq 0) {
        Write-Host &quot;  &#x5B;i] No file with $extension found.&quot; -ForegroundColor Gray
        continue
    }

    foreach ($file in $filesToImport) {
        Import-SsrsItem -File $file -LocalDump $localDump -DestRoot $destRoot -Proxy $proxyDest
    }
}

Write-Host &quot;`nImport done!&quot; -ForegroundColor Green
</pre></div>


<p class="wp-block-paragraph">Importing via SOAP is more resource-intensive than extraction, as the server must validate every piece of metadata and physically recreate the path for each report. On large volumes, this stage can become a bottleneck (averaging ~1 second per report).</p>



<p class="wp-block-paragraph">To overcome this, we can parallelize the import by folder, creating multiple background jobs running on separate threads. Here is the general skeleton to implement this multi-threaded approach:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: powershell; title: ; notranslate">
$maxJobs = 5 

foreach ($file in $filesToImport) {
    while ((Get-Job -State Running).Count -ge $maxJobs) {
        Start-Sleep -Milliseconds 500
    }

    Start-Job -Name &quot;Import_$($file.Name)&quot; -ScriptBlock {
        param($f, $url, $targetPath)

        $Proxy = New-RsWebServiceProxy -ReportServerUri $url
        
        try {
            Write-RsCatalogItem -Path $f.FullName -Destination $targetPath -Proxy $Proxy -Overwrite:$false
            return &quot;SUCCESS: $($f.Name)&quot;
        } catch {
            return &quot;ERROR: $($f.Name) -&gt; $($_.Exception.Message)&quot;
        }
    } -ArgumentList $file, $destUrl, $targetFolderPath
}
</pre></div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="520" height="330" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image.png" alt="" class="wp-image-43806" style="width:522px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image.png 520w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2026/04/image-300x190.png 300w" sizes="auto, (max-width: 520px) 100vw, 520px" /></figure>
</div>


<p class="wp-block-paragraph"><strong>Note :</strong> The <code><a href="https://www.powershelladmin.com/wiki/PowerShell_foreach_loops_and_ForEach-Object.php" id="https://www.powershelladmin.com/wiki/PowerShell_foreach_loops_and_ForEach-Object.php">-Parallel</a></code> parameter is a feature of the <code>ForEach-Object</code> cmdlet introduced in PowerShell 7 to enable native multi-threading. While it allows for processing multiple objects simultaneously, it is not reliably supported by the <code>ReportingServicesTools</code> library as the underlying API is not thread-safe. To ensure stability and avoid session collisions, it is recommended to use the <code>Start-Job</code> method instead, as it provides better process isolation for each task.</p>



<h2 class="wp-block-heading" id="h-key-takeaways-for-a-seamless-cutover">Key Takeaways for a Seamless Cutover</h2>



<p class="wp-block-paragraph">Migrating to Power BI Report Server shouldn&#8217;t be a manual challenge. By adopting this <strong>PowerShell-driven ETL approach</strong>, you replace the uncertainty of manual intervention with industrial-grade rigor.</p>



<p class="wp-block-paragraph">The primary advantage lies in consistency: regardless of the report volume or folder complexity, the script guarantees an identical and predictable result every single time. By isolating extraction, XML transformation, and ordered importation, you maintain total control over your data integrity.</p>



<p class="wp-block-paragraph">Ultimately, automation is about securing your delivery and freeing up time for what truly matters: leveraging your data on your brand-new PBIRS 2025 platform.</p>



<p class="wp-block-paragraph">Happy migrating!</p>
<p>L’article <a href="https://www.dbi-services.com/blog/scaling-ssrs-migrations-multi-threaded-automation-for-pbirs-2025/">Scaling SSRS Migrations: Multi-Threaded Automation for PBIRS 2025</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/scaling-ssrs-migrations-multi-threaded-automation-for-pbirs-2025/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to patch SQL Server instances with Qualys Patch Management</title>
		<link>https://www.dbi-services.com/blog/how-to-patch-sql-server-instances-with-qualys-patch-management/</link>
					<comments>https://www.dbi-services.com/blog/how-to-patch-sql-server-instances-with-qualys-patch-management/#respond</comments>
		
		<dc:creator><![CDATA[Stéphane Savorgnano]]></dc:creator>
		<pubDate>Tue, 30 Sep 2025 14:09:37 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[MS Teams]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[AlwaysOn]]></category>
		<category><![CDATA[patching]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Qualy Patch Management]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=40478</guid>

					<description><![CDATA[<p>By one of our Customers, I have to use Qualys to patch their production SQL Server instances. And to be more precise the Qualys Patch Management application which provides a solution to manage vulnerabilities and deploy patches to secure and keep assets up-to-date.I will use the job functionality of Qualys Patch Management to automate the [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/how-to-patch-sql-server-instances-with-qualys-patch-management/">How to patch SQL Server instances with Qualys Patch Management</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">By one of our Customers, I have to use <a href="https://www.qualys.com/">Qualys</a> to patch their production SQL Server instances. And to be more precise the Qualys Patch Management application which provides a solution to manage vulnerabilities and deploy patches to secure and keep assets up-to-date.<br>I will use the job functionality of Qualys Patch Management to automate the patching based on a specific schedule.</p>



<p class="wp-block-paragraph">My customer has the following context, a two nodes Always On active-active cluster with 4 instances where each instance owns between five and seven Availability Groups spread on the two cluster nodes.</p>



<p class="wp-block-paragraph">The first step is to connect to the Qualys subscription of my customer:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="922" height="469" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/image-18.png" alt="" class="wp-image-40483" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/image-18.png 922w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/image-18-300x153.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/image-18-768x391.png 768w" sizes="auto, (max-width: 922px) 100vw, 922px" /></figure>



<p class="wp-block-paragraph">And to navigate to the Patch Management application:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="922" height="537" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/image-19.png" alt="" class="wp-image-40484" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/image-19.png 922w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/image-19-300x175.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/image-19-768x447.png 768w" sizes="auto, (max-width: 922px) 100vw, 922px" /></figure>



<p class="wp-block-paragraph">Once in the Patch Management, I go in the job menu to create the first job which will patch the first node of my Always On cluster.<br>As this job will be executed first it will have to execute the following tasks:</p>



<ul class="wp-block-list">
<li>Save the Availability group configuration for all instances in order to be able to re-dispatch the Availability Groups on both nodes once the patching will be finished</li>



<li>Fail-over all Availability Groups from the node server1 to the node server2</li>



<li>Patch the instances on node server1 with the available patches</li>



<li>Reboot the server</li>



<li>Fail-over all Availability Groups from the node server2 to the node server1</li>
</ul>



<p class="wp-block-paragraph">In terms of Qualys job I have to create a new job with the following information:</p>



<ul class="wp-block-list">
<li>A name to identify the job, here SQLServer_server1_M_3rd_Saturday_2200 as this job will be executed the third Saturday of each month at 10PM</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="633" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info1_noname-1024x633.jpg" alt="" class="wp-image-40488" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info1_noname-1024x633.jpg 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info1_noname-300x185.jpg 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info1_noname-768x475.jpg 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info1_noname.jpg 1429w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<ul class="wp-block-list">
<li>Select the asset (here the server) where the patches will be executed: server1</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="636" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info2_noname-1024x636.jpg" alt="" class="wp-image-40489" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info2_noname-1024x636.jpg 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info2_noname-300x186.jpg 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info2_noname-768x477.jpg 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info2_noname.jpg 1413w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<ul class="wp-block-list">
<li>Pre-actions: this step provides some actions which can be executed before to patch the assets. It can be: to run a script, install a software, change a registry key, uninstall a software or a system reboot.<br>I will use it to execute my PowerShell script to:<br><strong>1.</strong> Save the Availability Groups configuration in a JSON file (you can find here an example of this file below)<br><strong>2.</strong> fail-over the Availability Groups which are primary on node server1 to server2. The PowerShell script is also logging in a file</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="527" height="882" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/JSON_File.jpg" alt="" class="wp-image-40490" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/JSON_File.jpg 527w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/JSON_File-179x300.jpg 179w" sizes="auto, (max-width: 527px) 100vw, 527px" /></figure>
</div>


<pre class="wp-block-code"><code>###Logging functions
Function Out-Log() {
param(
&#091;ValidateSet('INFO','WARNING','ERROR')]
&#091;String] $Type = 'INFO',
&#091;String] $Message
)

    '&#091;'+(Get-Date -f 'yyyy-MM-dd HH:mm:ss') +'] ' + ' - &#091;' + $Type + '] - ' + $Message | Out-File -FilePath $LogFile -Append;
}

Function Add-Warning() {
param(
&#091;String] $Warning
)
    If ($Warning) {
    Out-Log -Type WARNING -Message $Warning;
    }
}

Function Add-Error() {
param(
&#091;String] $Message
)
    Out-Log -Type ERROR -Message $Message;
}

###AGs functions
#Function to save Availability Group configuration in a JSON file
Function AGsSaveConfiguration(){
param (
&#091;Array] $instances,
&#091;String] $ConfigFile
)
    Out-Log -Type INFO "The following configurations have been saved:"
    $initialState = @()
    foreach ($instance in $instances) {
        $ags = Get-DbaAvailabilityGroup -SqlInstance $instance
        foreach ($ag in $ags) {
            $initialState += &#091;PSCustomObject]@{
                Instance = $instance
                AGName   = $ag.Name
                Primary  = $ag.PrimaryReplica
            }
            $output = "Instance = $instance, AGName = $($ag.Name), Primary  = $($ag.PrimaryReplica)"
            Out-Log -Type INFO "$output" 
        }
    }
    #Add configuration to a JSON file
    $initialState | ConvertTo-Json | Out-File $ConfigFile
    $output = $initialState | Format-Table
}

#Function to save Availability Group configuration in a JSON file
Function AGsRestoreConfiguration(){
param (
&#091;Array] $instances,
&#091;String] $ConfigFile
)
    if (!(Test-Path -Path $ConfigFile)) {
        Add-Error "Configuration file is missing"
        Add-Error "Exit without having restore the AG configurations"
        Return
    }

    $initialState = Get-Content $ConfigFile | ConvertFrom-Json

    foreach ($entry in $initialState) {
        write-host $entry
        $ag = Get-DbaAvailabilityGroup -SqlInstance $entry.Instance -AvailabilityGroup $entry.AGName
        if (($ag.PrimaryReplica -ne $entry.Primary) -and ($ag.LocalReplicaRole -eq "Secondary")) {
            Write-Host "Failover of Availability Group $($entry.AGName) to $($entry.Primary)"
            Out-Log -Type INFO "Failover of Availability Group $($entry.AGName) to $($entry.Primary)"
            Try {
                Invoke-DbaAgFailover -SqlInstance $entry.Instance -AvailabilityGroup $entry.AGName -Confirm:$false
            }
            Catch {
                Write-Host "Error during failover of Availability Group $($entry.AGName) to $($entry.Primary)"
                Add-Error "Error during failover of Availability Group $($entry.AGName) to $($entry.Primary)"                
            }
        }
    }
}

#Function to failover a list of instances to a specific host
Function AGsFailoverTo(){
param (
&#091;Array] $instances,
&#091;String] $TargetNode
)
    foreach ($instance in $instances) {
        $ags = Get-DbaAvailabilityGroup -SqlInstance $instance
        foreach ($ag in $ags) {
            if (($ag.ComputerName -eq $TargetNode) -and ($ag.LocalReplicaRole -eq "Secondary")) {
                Write-Host "Failover of $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                Out-Log -Type INFO "Failover of $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                Try {
                    Invoke-DbaAgFailover -SqlInstance $instance -AvailabilityGroup $ag.Name -Confirm:$false
                }
                Catch {
                    Write-Host "Error during failover of Availability Group $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                    Add-Error "Error during failover of Availability Group $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                }
            }
        }
    }
    Out-Log -Type INFO "All Availability Groups have been failover to node $TargetNode"

}

#LOG file path and name
$LogFileName = "AGsFailoverForPatching.txt"
$LogFilePath = "\\ShareFolder\LOG"
$LogFile = "$LogFilePath\$LogFileName"

#Configuration file path and name
$AGsConfigFileName = "initial_state.json"
$AGsConfigFilePath = "\\ShareFolder\LOG"
$AGsConfigFile = "$AGsConfigFilePath\$AGsConfigFileName"

###################################################################
#Save Availability Group configuration for all instances
Out-Log -Type INFO "******************** START PROCESS ********************"
Out-Log -Type INFO "START TO SAVE AVAILABILITY GROUPS CONFIGURATION"
Out-Log -Type INFO "Configuraton will be saved in file $AGsConfigFile"

#Find instances by list
$instances = @('server1\Instance1','server1\Instance2','server1\Instance3','server1\Instance4','server2\Instance1','server2\Instance2','server2\Instance3','server2\Instance4')
Out-Log -Type INFO "Instances available: $instances"

#Call the function AGsSaveConfiguration
AGsSaveConfiguration -instances $instances -ConfigFile $AGsConfigFile

Out-Log -Type INFO "Configurations saved successfully"

###################################################################
#Failover all Availability group to a specify node
Out-Log -Type INFO "START TO FAILOVER ALL AVAILABILITY GROUPS"
#Target node
$targetNode = "server2"
Out-Log -Type INFO "Target node: $targetNode"

#Call the function AGsFailoverTo
AGsFailoverTo -instances $instances -TargetNode $targetNode</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="632" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info3_noname-1024x632.jpg" alt="" class="wp-image-40495" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info3_noname-1024x632.jpg 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info3_noname-300x185.jpg 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info3_noname-768x474.jpg 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info3_noname.jpg 1417w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<ul class="wp-block-list">
<li>Select the patches to apply to the assets, here we will select then automatically based on the filter patch.appFamily: SQL Server</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="484" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info6_noname-1024x484.jpg" alt="" class="wp-image-40496" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info6_noname-1024x484.jpg 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info6_noname-300x142.jpg 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info6_noname-768x363.jpg 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info6_noname-1536x726.jpg 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info6_noname.jpg 1836w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Qualys automatically detects that SQL Server 2019 instances are installed on this server as well as SQL Server Management Studio and provides the available patches.<br>Once patched have been applied, the server will automatically reboot.</p>



<ul class="wp-block-list">
<li>Post-actions: the possible post actions are: run a script, install a software, change a registry key or uninstall a software. As the instances have been patched and the server rebooted, we need to fail-over all Availability Groups which are primary on node server2 to server1, as the node server2 will be patched by the second job. A PowerShell script is then executed to do that:</li>
</ul>



<pre class="wp-block-code"><code>###Logging functions
Function Out-Log() {
param(
&#091;ValidateSet('INFO','WARNING','ERROR')]
&#091;String] $Type = 'INFO',
&#091;String] $Message
)
    '&#091;'+(Get-Date -f 'yyyy-MM-dd HH:mm:ss') +'] ' + ' - &#091;' + $Type + '] - ' + $Message | Out-File -FilePath $LogFile -Append;
}

Function Add-Warning() {
param(
&#091;String] $Warning
)
    If ($Warning) {
    Out-Log -Type WARNING -Message $Warning;
    }
}

Function Add-Error() {
param(
&#091;String] $Message
)
    Out-Log -Type ERROR -Message $Message;
}

###AGs functions
#Function to save Availability Group configuration in a JSON file
Function AGsSaveConfiguration(){
param (
&#091;Array] $instances,
&#091;String] $ConfigFile
)
    Out-Log -Type INFO "The following configurations have been saved:"
    $initialState = @()
    foreach ($instance in $instances) {
        $ags = Get-DbaAvailabilityGroup -SqlInstance $instance
        foreach ($ag in $ags) {
            $initialState += &#091;PSCustomObject]@{
                Instance = $instance
                AGName   = $ag.Name
                Primary  = $ag.PrimaryReplica
            }
            $output = "Instance = $instance, AGName = $($ag.Name), Primary  = $($ag.PrimaryReplica)"
            Out-Log -Type INFO "$output" 
        }
    }
    #Add configuration to a JSON file
    $initialState | ConvertTo-Json | Out-File $ConfigFile
    $output = $initialState | Format-Table
}

#Function to save Availability Group configuration in a JSON file
Function AGsRestoreConfiguration(){
param (
&#091;Array] $instances,
&#091;String] $ConfigFile
)
    if (!(Test-Path -Path $ConfigFile)) {
        Add-Error "Configuration file is missing"
        Add-Error "Exit without having restore the AG configurations"
        Return
    }

    $initialState = Get-Content $ConfigFile | ConvertFrom-Json

    foreach ($entry in $initialState) {
        write-host $entry
        $ag = Get-DbaAvailabilityGroup -SqlInstance $entry.Instance -AvailabilityGroup $entry.AGName
        if (($ag.PrimaryReplica -ne $entry.Primary) -and ($ag.LocalReplicaRole -eq "Secondary")) {
            Write-Host "Failover of Availability Group $($entry.AGName) to $($entry.Primary)"
            Out-Log -Type INFO "Failover of Availability Group $($entry.AGName) to $($entry.Primary)"
            Try {
                Invoke-DbaAgFailover -SqlInstance $entry.Instance -AvailabilityGroup $entry.AGName -Confirm:$false
            }
            Catch {
                Write-Host "Error during failover of Availability Group $($entry.AGName) to $($entry.Primary)"
                Add-Error "Error during failover of Availability Group $($entry.AGName) to $($entry.Primary)"                
            }
        }
    }
}

#Function to failover a list of instances to a specific host
Function AGsFailoverTo(){
param (
&#091;Array] $instances,
&#091;String] $TargetNode
)
    foreach ($instance in $instances) {
        $ags = Get-DbaAvailabilityGroup -SqlInstance $instance
        foreach ($ag in $ags) {
            if (($ag.ComputerName -eq $TargetNode) -and ($ag.LocalReplicaRole -eq "Secondary")) {
                Write-Host "Failover of $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                Out-Log -Type INFO "Failover of $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                Try {
                    Invoke-DbaAgFailover -SqlInstance $instance -AvailabilityGroup $ag.Name -Confirm:$false
                }
                Catch {
                    Write-Host "Error during failover of Availability Group $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                    Add-Error "Error during failover of Availability Group $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                }
            }
        }
    }
    Out-Log -Type INFO "All Availability Groups have been failover to node $TargetNode"

}

#LOG file path and name
$LogFileName = "AGsFailoverForPatching.txt"
$LogFilePath = "\\ShareFolder\LOG"
$LogFile = "$LogFilePath\$LogFileName"

#Configuration file path and name
$AGsConfigFileName = "initial_state.json"
$AGsConfigFilePath = "\\ShareFolder\LOG"
$AGsConfigFile = "$AGsConfigFilePath\$AGsConfigFileName"

###################################################################
#Failover all Availability group to a specify node
Out-Log -Type INFO "START TO FAILOVER ALL AVAILABILITY GROUPS"
#Target node
$targetNode = "server1"
Out-Log -Type INFO "Target node: $targetNode"

#Find all instances
$instances = @('server1\Instance1','server1\Instance2','server1\Instance3','server1\Instance4','server2\Instance1','server2\Instance2','server2\Instance3','server2\Instance4')
Out-Log -Type INFO "Instances available: $instances"

#Call the function AGsFailoverTo
AGsFailoverTo -instances $instances -TargetNode $targetNode</code></pre>



<ul class="wp-block-list">
<li>Schedule is the third Saturday of each month at 10PM with a duration of 2 hours</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="640" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info9_noname-1024x640.jpg" alt="" class="wp-image-40500" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info9_noname-1024x640.jpg 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info9_noname-300x188.jpg 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info9_noname-768x480.jpg 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info9_noname.jpg 1399w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<ul class="wp-block-list">
<li>There are also some possibles options that can be enabled or disabled for the job.<br>I select the Reboot Countdown to send a message to the users of the server before the reboot, a message is also sent to a distribution list at the execution start and after completion and the patches will try to be downloaded by the Qualys agent before the job schedule.</li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="795" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info10_noname-1024x795.jpg" alt="" class="wp-image-40501" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info10_noname-1024x795.jpg 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info10_noname-300x233.jpg 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info10_noname-768x597.jpg 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info10_noname.jpg 1039w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="881" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info11_noname-1024x881.jpg" alt="" class="wp-image-40502" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info11_noname-1024x881.jpg 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info11_noname-300x258.jpg 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info11_noname-768x660.jpg 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/09/Qualys_Basis_Info11_noname.jpg 1064w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">It&#8217;s finish for my first job, I need to create now the second one.<br>Qualys gives the possibility to clone job, so I&#8217;m using this feacture and change the needed information for my second job.<br>This job will patch my second server and will have to:</p>



<ul class="wp-block-list">
<li>Failover all Availability Groups from the node server2 to the node server1 (already done in the first job as post-action, but I do it again)</li>



<li>Patch the instances on node server2 with the available patches</li>



<li>Reboot the server</li>



<li>Redistribute the Availability Groups based on the JSON file created in the pre-action of the first job</li>
</ul>



<p class="wp-block-paragraph">In terms of Qualys, this job has the following information:</p>



<ul class="wp-block-list">
<li>A name to identify the job, here SQLServer_server2_M_3rd_Sunday_0001 as this job will be executed the third Sunday of each month at 00:01AM, after the completion of the first one</li>



<li>Select the asset (here the server) where the patches will be executed: server2</li>



<li>Pre-actions: it will be a PowerShell script which will:<br>o Failover the Availability Groups from node server2 to server1 with the following PowerShell script (the script is also logging in a file)</li>
</ul>



<pre class="wp-block-code"><code>###Logging functions
Function Out-Log() {
param(
&#091;ValidateSet('INFO','WARNING','ERROR')]
&#091;String] $Type = 'INFO',
&#091;String] $Message
)
    '&#091;'+(Get-Date -f 'yyyy-MM-dd HH:mm:ss') +'] ' + ' - &#091;' + $Type + '] - ' + $Message | Out-File -FilePath $LogFile -Append;
}

Function Add-Warning() {
param(
&#091;String] $Warning
)
    If ($Warning) {
    Out-Log -Type WARNING -Message $Warning;
    }
}

Function Add-Error() {
param(
&#091;String] $Message
)
    Out-Log -Type ERROR -Message $Message;
}

###AGs functions
#Function to save Availability Group configuration in a JSON file
Function AGsSaveConfiguration(){
param (
&#091;Array] $instances,
&#091;String] $ConfigFile
)
    Out-Log -Type INFO "The following configurations have been saved:"
    $initialState = @()
    foreach ($instance in $instances) {
        $ags = Get-DbaAvailabilityGroup -SqlInstance $instance
        foreach ($ag in $ags) {
            $initialState += &#091;PSCustomObject]@{
                Instance = $instance
                AGName   = $ag.Name
                Primary  = $ag.PrimaryReplica
            }
            $output = "Instance = $instance, AGName = $($ag.Name), Primary  = $($ag.PrimaryReplica)"
            Out-Log -Type INFO "$output" 
        }
    }
    #Add configuration to a JSON file
    $initialState | ConvertTo-Json | Out-File $ConfigFile
    $output = $initialState | Format-Table
}

#Function to save Availability Group configuration in a JSON file
Function AGsRestoreConfiguration(){
param (
&#091;Array] $instances,
&#091;String] $ConfigFile
)
    if (!(Test-Path -Path $ConfigFile)) {
        Add-Error "Configuration file is missing"
        Add-Error "Exit without having restore the AG configurations"
        Return
    }

    $initialState = Get-Content $ConfigFile | ConvertFrom-Json

    foreach ($entry in $initialState) {
        write-host $entry
        $ag = Get-DbaAvailabilityGroup -SqlInstance $entry.Instance -AvailabilityGroup $entry.AGName
        if (($ag.PrimaryReplica -ne $entry.Primary) -and ($ag.LocalReplicaRole -eq "Secondary")) {
            Write-Host "Failover of Availability Group $($entry.AGName) to $($entry.Primary)"
            Out-Log -Type INFO "Failover of Availability Group $($entry.AGName) to $($entry.Primary)"
            Try {
                Invoke-DbaAgFailover -SqlInstance $entry.Instance -AvailabilityGroup $entry.AGName -Confirm:$false
            }
            Catch {
                Write-Host "Error during failover of Availability Group $($entry.AGName) to $($entry.Primary)"
                Add-Error "Error during failover of Availability Group $($entry.AGName) to $($entry.Primary)"                
            }
        }
    }
}

#Function to failover a list of instances to a specific host
Function AGsFailoverTo(){
param (
&#091;Array] $instances,
&#091;String] $TargetNode
)
    foreach ($instance in $instances) {
        $ags = Get-DbaAvailabilityGroup -SqlInstance $instance
        foreach ($ag in $ags) {
            if (($ag.ComputerName -eq $TargetNode) -and ($ag.LocalReplicaRole -eq "Secondary")) {
                Write-Host "Failover of $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                Out-Log -Type INFO "Failover of $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                Try {
                    Invoke-DbaAgFailover -SqlInstance $instance -AvailabilityGroup $ag.Name -Confirm:$false
                }
                Catch {
                    Write-Host "Error during failover of Availability Group $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                    Add-Error "Error during failover of Availability Group $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                }
            }
        }
    }
    Out-Log -Type INFO "All Availability Groups have been failover to node $TargetNode"
}

#LOG file path and name
$LogFileName = "AGsFailoverForPatching.txt"
$LogFilePath = "\\ShareFolder\LOG"
$LogFile = "$LogFilePath\$LogFileName"

#Configuration file path and name
$AGsConfigFileName = "initial_state.json"
$AGsConfigFilePath = "\\ShareFolder\LOG"
$AGsConfigFile = "$AGsConfigFilePath\$AGsConfigFileName"

###################################################################
#Failover all Availability group to a specify node
Out-Log -Type INFO "START TO FAILOVER ALL AVAILABILITY GROUPS"
#Target node
$targetNode = "server1"
Out-Log -Type INFO "Target node: $targetNode"

#Find all instances
$instances = @('server1\Instance1','server1\Instance2','server1\Instance3','server1\Instance4','server2\Instance1','server2\Instance2','server2\Instance3','server2\Instance4')
Out-Log -Type INFO "Instances available: $instances"

#Call the function AGsFailoverTo
AGsFailoverTo -instances $instances -TargetNode $targetNode
</code></pre>



<ul class="wp-block-list">
<li>Select the patches to apply to the assets, here we will select then automatically based on the filter patch.appFamily: SQL Server (like for the first job)</li>



<li>Post-actions: once the instances have been patched and the server rebooted, we need to redistribute the Availability Groups over the 2 nodes based on the JSON file created at the beginning of this patch process. For that we execute the following PowerShell script:</li>
</ul>



<pre class="wp-block-code"><code>###Logging functions
Function Out-Log() {
param(
&#091;ValidateSet('INFO','WARNING','ERROR')]
&#091;String] $Type = 'INFO',
&#091;String] $Message
)
    '&#091;'+(Get-Date -f 'yyyy-MM-dd HH:mm:ss') +'] ' + ' - &#091;' + $Type + '] - ' + $Message | Out-File -FilePath $LogFile -Append;
}

Function Add-Warning() {
param(
&#091;String] $Warning
)
    If ($Warning) {
    Out-Log -Type WARNING -Message $Warning;
    }
}

Function Add-Error() {
param(
&#091;String] $Message
)
    Out-Log -Type ERROR -Message $Message;
}

###AGs functions
#Function to save Availability Group configuration in a JSON file
Function AGsSaveConfiguration(){
param (
&#091;Array] $instances,
&#091;String] $ConfigFile
)
    Out-Log -Type INFO "The following configurations have been saved:"
    $initialState = @()
    foreach ($instance in $instances) {
        $ags = Get-DbaAvailabilityGroup -SqlInstance $instance
        foreach ($ag in $ags) {
            $initialState += &#091;PSCustomObject]@{
                Instance = $instance
                AGName   = $ag.Name
                Primary  = $ag.PrimaryReplica
            }
            $output = "Instance = $instance, AGName = $($ag.Name), Primary  = $($ag.PrimaryReplica)"
            Out-Log -Type INFO "$output" 
        }
    }
    #Add configuration to a JSON file
    $initialState | ConvertTo-Json | Out-File $ConfigFile
    $output = $initialState | Format-Table
}

#Function to save Availability Group configuration in a JSON file
Function AGsRestoreConfiguration(){
param (
&#091;Array] $instances,
&#091;String] $ConfigFile
)
    if (!(Test-Path -Path $ConfigFile)) {
        Add-Error "Configuration file is missing"
        Add-Error "Exit without having restore the AG configurations"
        Return
    }

    $initialState = Get-Content $ConfigFile | ConvertFrom-Json

    foreach ($entry in $initialState) {
        write-host $entry
        $ag = Get-DbaAvailabilityGroup -SqlInstance $entry.Instance -AvailabilityGroup $entry.AGName
        if (($ag.PrimaryReplica -ne $entry.Primary) -and ($ag.LocalReplicaRole -eq "Secondary")) {
            Write-Host "Failover of Availability Group $($entry.AGName) to $($entry.Primary)"
            Out-Log -Type INFO "Failover of Availability Group $($entry.AGName) to $($entry.Primary)"
            Try {
                Invoke-DbaAgFailover -SqlInstance $entry.Instance -AvailabilityGroup $entry.AGName -Confirm:$false
            }
            Catch {
                Write-Host "Error during failover of Availability Group $($entry.AGName) to $($entry.Primary)"
                Add-Error "Error during failover of Availability Group $($entry.AGName) to $($entry.Primary)"                
            }
        }
    }
}

#Function to failover a list of instances to a specific host
Function AGsFailoverTo(){
param (
&#091;Array] $instances,
&#091;String] $TargetNode
)
    foreach ($instance in $instances) {
        $ags = Get-DbaAvailabilityGroup -SqlInstance $instance
        foreach ($ag in $ags) {
            if (($ag.ComputerName -eq $TargetNode) -and ($ag.LocalReplicaRole -eq "Secondary")) {
                Write-Host "Failover of $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                Out-Log -Type INFO "Failover of $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                Try {
                    Invoke-DbaAgFailover -SqlInstance $instance -AvailabilityGroup $ag.Name -Confirm:$false
                }
                Catch {
                    Write-Host "Error during failover of Availability Group $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                    Add-Error "Error during failover of Availability Group $($ag.Name) to $TargetNode from $($ag.PrimaryReplica)"
                }
            }
        }
    }
    Out-Log -Type INFO "All Availability Groups have been failover to node $TargetNode"

}

#LOG file path and name
$LogFileName = "AGsFailoverForPatching.txt"
$LogFilePath = "\\ShareFolderLOG"
$LogFile = "$LogFilePath\$LogFileName"

#Configuration file path and name
$AGsConfigFileName = "initial_state.json"
$AGsConfigFilePath = "\\ShareFolder\LOG"
$AGsConfigFile = "$AGsConfigFilePath\$AGsConfigFileName"

###################################################################
#Restore Availability Group configuration for all instances
Out-Log -Type INFO "START TO RESTORE AVAILABILITY GROUPS CONFIGURATION"

#Call the function AGsRestoreConfiguration
$instances = @('server1\Instance1','server1\Instance2','server1\Instance3','server1\Instance4','server2\Instance1','server2\Instance2','server2\Instance3','server2\Instance4')
AGsRestoreConfiguration -instances $instances -ConfigFile $AGsConfigFile

Out-Log -Type INFO "Availability Group configuratons restored successfully"
Out-Log -Type INFO "******************** END PROCESS ********************"
</code></pre>



<ul class="wp-block-list">
<li>Schedule is the third Sunday of each month at 00:01AM with a duration of 2 hours. With this schedule this job will be executed after the end of the first one. In Qualys there is for the moment no possibility to start a job when another one is completed…</li>



<li>For options, I select the Reboot Countdown to send a message to the users of the server before the reboot, a message is also sent to a distribution list at the execution start and after completion to admin and the patches will try to be downloaded by the Qualys agent before the job schedule.</li>
</ul>



<p class="wp-block-paragraph">Both jobs are now created. After their execution the two Always On cluster nodes will have been patched and the Availability Groups redistributed as it was before to start.</p>



<p class="wp-block-paragraph">Qualys Patch Management is relatively easy to use. All patches based on your selection are availables and can be applied easily.<br>The pre and post actions give the possibility to prepare the patching: here fail-over, save the inital configuration, redistribute the AGs at the end.<br>It was a good experience and I hope it can help <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br> </p>
<p>L’article <a href="https://www.dbi-services.com/blog/how-to-patch-sql-server-instances-with-qualys-patch-management/">How to patch SQL Server instances with Qualys Patch Management</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/how-to-patch-sql-server-instances-with-qualys-patch-management/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Set SQL Server Trace Flags Automatically and Smoothly</title>
		<link>https://www.dbi-services.com/blog/set-sql-server-trace-flags-automatically-and-smoothly/</link>
					<comments>https://www.dbi-services.com/blog/set-sql-server-trace-flags-automatically-and-smoothly/#respond</comments>
		
		<dc:creator><![CDATA[Hocine Mechara]]></dc:creator>
		<pubDate>Tue, 29 Apr 2025 15:56:09 +0000</pubDate>
				<category><![CDATA[Database management]]></category>
		<category><![CDATA[Development & Performance]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Automation]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[databases]]></category>
		<category><![CDATA[PowerShell]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=38241</guid>

					<description><![CDATA[<p>Managing SQL Server trace flags effectively is a common task for DBAs and system engineers, especially when tuning the SQL Server behavior for specific use cases or performance optimizations. While trace flags can be enabled programmatically using T-SQL commands such as DBCC TRACEON, this approach has an inherent limitation: the flags are session-based or global [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/set-sql-server-trace-flags-automatically-and-smoothly/">Set SQL Server Trace Flags Automatically and Smoothly</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Managing SQL Server trace flags effectively is a common task for DBAs and system engineers, especially when tuning the SQL Server behavior for specific use cases or performance optimizations. While trace flags can be enabled programmatically using T-SQL commands such as DBCC TRACEON, this approach has an inherent limitation: the flags are session-based or global only until the SQL Server instance is restarted. After a service restart, these trace flags are no longer active unless they are configured as startup parameters.</p>



<p class="wp-block-paragraph">To ensure persistence across restarts, trace flags need to be set as startup parameters. Traditionally, this can be done manually through two main avenues: using the SQL Server Configuration Manager or editing the Windows Registry directly. However, both of these approaches require manual interaction and are not ideal for automated deployments, large-scale environments or automated deployments.</p>



<p class="wp-block-paragraph">Unfortunately, SQL Server doesn’t provide a built-in command-line tool or T-SQL syntax to set trace flags as startup parameters programmatically. This gap leaves many DBAs and system engineers either scripting complex registry edits themselves or relying on manual configurations.</p>



<p class="wp-block-paragraph">That’s exactly the problem I set out to solve. I developed a PowerShell function that allows you to set SQL Server trace flags as startup parameters in a programmatical way and I’m using this function already at one of my customers to set Trace flags within an automated deployment process. This function not only configures the registry settings required for each SQL Server instance on a server, but it also offers optional functionality to restart the SQL Server service—ensuring that your changes take effect immediately. Even better, it includes logic to detect which trace flags are already in place and avoids redundant updates.</p>



<p class="wp-block-paragraph">In this blog post, I’ll walk you through how the function works, how to use it in your own environment, and how it can fit into your broader infrastructure automation strategy. Whether you manage a single SQL Server or dozens across a large environment, this solution aims to simplify your workflow and reduce the risk of human error.</p>



<h2 class="wp-block-heading" id="h-let-s-take-a-look-at-how-to-set-the-trace-flag-programmatically-with-the-powershell-function"><strong>Let’s take a look at how to set the Trace flag programmatically with the PowerShell Function:</strong></h2>



<p class="wp-block-paragraph">I have the function stored in a ps1 file within my visual studio project directory.</p>



<figure class="wp-block-image size-full is-resized is-style-default wp-duotone-unset-1"><img loading="lazy" decoding="async" width="226" height="221" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-39.png" alt="" class="wp-image-38244" style="width:421px;height:auto" /></figure>



<p class="wp-block-paragraph">To execute the function and pass the appropriate input parameters to the function, I create a second .ps1 file in the same directory.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="229" height="220" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-38.png" alt="" class="wp-image-38243" style="width:424px;height:auto" /></figure>



<p class="wp-block-paragraph">In this file I add the following values in variables:</p>



<p class="wp-block-paragraph"><strong>$Trace flags</strong> &#8211; The trace flags I would like to set for the particular instances</p>



<p class="wp-block-paragraph"><strong>$Restart</strong> &#8211; This defines if the SQL-Server Service should be restarted after setting the trace flags (Y = restart, N = no restart)</p>



<p class="wp-block-paragraph"><strong>$ServerName</strong> &#8211; This defines the Server on which the trace flags should be set as a startup parameter for every instance running on it</p>



<p class="wp-block-paragraph"><strong>$Cred &#8211;</strong>This is the credential which is used to access the remote Server</p>



<p class="wp-block-paragraph">Then I import the function as a module and execute the function with the values stored in the variables as input parameters.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="602" height="205" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-40.png" alt="" class="wp-image-38245" style="width:840px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-40.png 602w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-40-300x102.png 300w" sizes="auto, (max-width: 602px) 100vw, 602px" /></figure>



<p class="wp-block-paragraph">After saving the file I execute it from the terminal. You can see that the Trace flags have been successfully set.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="602" height="206" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-41.png" alt="" class="wp-image-38246" style="width:840px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-41.png 602w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-41-300x103.png 300w" sizes="auto, (max-width: 602px) 100vw, 602px" /></figure>



<p class="wp-block-paragraph">When we take a look in the registry on the particular server we can see, that the Trace flags have been added.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="602" height="177" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-42.png" alt="" class="wp-image-38247" style="width:840px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-42.png 602w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-42-300x88.png 300w" sizes="auto, (max-width: 602px) 100vw, 602px" /></figure>



<p class="wp-block-paragraph">We can see the same result when taking a look on the startup parameters from the SQL Server Configuration Manager.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="328" height="401" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-43.png" alt="" class="wp-image-38248" style="width:388px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-43.png 328w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-43-245x300.png 245w" sizes="auto, (max-width: 328px) 100vw, 328px" /></figure>



<p class="wp-block-paragraph">Let’s take a look from the SQL Server Management Studio. With the DBCC TRACESTATUS() function. You can see, that no trace flags are currently active.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="602" height="218" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-44.png" alt="" class="wp-image-38249" style="width:840px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-44.png 602w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-44-300x109.png 300w" sizes="auto, (max-width: 602px) 100vw, 602px" /></figure>



<p class="wp-block-paragraph">This is because the trace flags are set as startup parameters and will become active on the next service start. </p>



<p class="wp-block-paragraph">After restarting the SQL Server service, we can see that the trace flags are now active. </p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="496" height="307" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-45.png" alt="" class="wp-image-38250" style="width:484px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-45.png 496w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-45-300x186.png 300w" sizes="auto, (max-width: 496px) 100vw, 496px" /></figure>



<p class="wp-block-paragraph">In a running production environment, you can also enable the trace flags using the <code>DBCC TRACEON()</code> command with a global scope (<code>-1</code>) to avoid service interruption. This allows the trace flags to take effect immediately without restarting the SQL Server instance. Since the script also adds them as startup parameters in the registry, the trace flags will persist after the next restart.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
DBCC TRACEON (2371, -1);
GO
DBCC TRACEON (3226, -1);
GO
</pre></div>


<p class="wp-block-paragraph">The function is as well “intelligent” enough to see which trace flags are already in place and only sets the trace flags which are missing.</p>



<p class="wp-block-paragraph">When I execute the function again with the same trace flags, you can see, that the functions tells you that the trace flags are already in place.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="602" height="230" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-46.png" alt="" class="wp-image-38251" style="width:840px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-46.png 602w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-46-300x115.png 300w" sizes="auto, (max-width: 602px) 100vw, 602px" /></figure>



<p class="wp-block-paragraph">The function can also trigger a restart of the SQL-Server service to ensure that the trace flags become active immediately. Let consider therefore, that we want to set additionally the trace flag 1211. I change as well the $Restart variable to ‘Y’ to trigger a service restart.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="602" height="212" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-47.png" alt="" class="wp-image-38252" style="width:840px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-47.png 602w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-47-300x106.png 300w" sizes="auto, (max-width: 602px) 100vw, 602px" /></figure>



<p class="wp-block-paragraph">After saving the file and executing it again from the terminal you can see that the trace flag 1211 has been set and that the instance has been restarted.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="602" height="158" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-48.png" alt="" class="wp-image-38253" style="width:840px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-48.png 602w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-48-300x79.png 300w" sizes="auto, (max-width: 602px) 100vw, 602px" /></figure>



<p class="wp-block-paragraph">As the function triggered a restart of the instance, you can now see from the SQL Server Management Studio that the trace flag has become already active.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="452" height="321" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-49.png" alt="" class="wp-image-38254" style="width:523px;height:auto" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-49.png 452w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/04/image-49-300x213.png 300w" sizes="auto, (max-width: 452px) 100vw, 452px" /></figure>



<p class="wp-block-paragraph">I hope this post was interesting for you <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Let me know your thoughts in the comment section below.</p>



<p class="wp-block-paragraph">I’ve uploaded the PowerShell function to GitHub. You can access it under this link: <a href="https://github.com/HocineMechara/SetSQLServerTraceFlags.git">https://github.com/HocineMechara/SetSQLServerTrace flags.git</a></p>
<p>L’article <a href="https://www.dbi-services.com/blog/set-sql-server-trace-flags-automatically-and-smoothly/">Set SQL Server Trace Flags Automatically and Smoothly</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/set-sql-server-trace-flags-automatically-and-smoothly/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Starting with PowerShell 7 and parallelization</title>
		<link>https://www.dbi-services.com/blog/starting-with-powershell-7-and-parallelization/</link>
					<comments>https://www.dbi-services.com/blog/starting-with-powershell-7-and-parallelization/#respond</comments>
		
		<dc:creator><![CDATA[Stéphane Savorgnano]]></dc:creator>
		<pubDate>Wed, 26 Feb 2025 08:49:14 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Database management]]></category>
		<category><![CDATA[Development & Performance]]></category>
		<category><![CDATA[MS Teams]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[Parallelization]]></category>
		<category><![CDATA[PowerShell]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=36946</guid>

					<description><![CDATA[<p>For the time being Windows PowerShell 5.1 is installed with Windows Server. It means that if you want to use or even test PowerShell 7 you need to install it by your own.To be honest, even if I&#8217;m using PowerShell as a DBA more or less every day, I did not take too much care [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/starting-with-powershell-7-and-parallelization/">Starting with PowerShell 7 and parallelization</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">For the time being Windows PowerShell 5.1 is installed with Windows Server. It means that if you want to use or even test PowerShell 7 you need to install it by your own.<br>To be honest, even if I&#8217;m using PowerShell as a DBA more or less every day, I did not take too much care of PowerShell 7 until I used it at a customer place with a new parallelization functionality that we will discuss later on.</p>



<h2 class="wp-block-heading" id="h-introduction">Introduction</h2>



<p class="wp-block-paragraph">For reminder, PowerShell 7 is an open-source and cross-platform edition of PowerShell. It means that it can be used on Windows platforms but also on MacOS or Linux.<br>The good point is that you can install it without removing Windows PowerShell 5.1. Both version can cohabit because of:</p>



<ul class="wp-block-list">
<li>Separate installation path and executable name
<ul class="wp-block-list">
<li>path with 5.1 like $env:WINDIR\System32\WindowsPowerShell\v1.0</li>



<li>path with 7 like $env:ProgramFiles\PowerShell\7</li>



<li>executable with PowerShell 5.1 is powershell.exe and with PowerShell 7 is pwsh.exe</li>
</ul>
</li>



<li>Separate PSModulePath</li>



<li>Separate profiles for each version
<ul class="wp-block-list">
<li>path with 5.1 is $HOME\Documents\WindowsPowerShell</li>



<li>path with 7 is $HOME\Documents\PowerShell</li>
</ul>
</li>



<li>Improved module compatibility</li>



<li>New remoting endpoints</li>



<li>Group policy support</li>



<li>Separate Event logs</li>
</ul>



<h2 class="wp-block-heading" id="h-installation">Installation</h2>



<p class="wp-block-paragraph">To install PowerShell 7 on Windows Servers the easiest way is to use a MSI package. The last version to download is the<a href="https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/PowerShell-7.5.0-win-x64.msi"> 7.5.0</a>. Once downloaded, double click the msi file and follow the installation:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="491" height="382" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install1.jpg" alt="" class="wp-image-36954" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install1.jpg 491w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install1-300x233.jpg 300w" sizes="auto, (max-width: 491px) 100vw, 491px" /></figure>



<p class="wp-block-paragraph">By default and as mentioned previously, PowerShell 7 will be installed on C:\Program Files\PowerShell\ :</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="489" height="385" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install2.jpg" alt="" class="wp-image-36957" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install2.jpg 489w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install2-300x236.jpg 300w" sizes="auto, (max-width: 489px) 100vw, 489px" /></figure>



<p class="wp-block-paragraph">Some customization are possible, we will keep the default selections:<br><br></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="492" height="383" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install3.jpg" alt="" class="wp-image-36959" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install3.jpg 492w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install3-300x234.jpg 300w" sizes="auto, (max-width: 492px) 100vw, 492px" /></figure>



<p class="wp-block-paragraph">Starting with PowerShell 7.2, it is possible to update PowerShell 7 with traditional Microsoft Update:<br></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="490" height="386" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install4.jpg" alt="" class="wp-image-36961" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install4.jpg 490w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install4-300x236.jpg 300w" sizes="auto, (max-width: 490px) 100vw, 490px" /></figure>



<p class="wp-block-paragraph">After some seconds installation is done:<br></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="489" height="385" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install7.jpg" alt="" class="wp-image-36965" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install7.jpg 489w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install7-300x236.jpg 300w" sizes="auto, (max-width: 489px) 100vw, 489px" /></figure>



<p class="wp-block-paragraph">We can start the PowerShell 7 with the cmd pwsh.exe. As we can see below both versions coexist on my Windows Server:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="686" height="619" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install8.jpg" alt="" class="wp-image-36966" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install8.jpg 686w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2025/01/PS7_install8-300x271.jpg 300w" sizes="auto, (max-width: 686px) 100vw, 686px" /></figure>



<h2 class="wp-block-heading" id="h-new-features">New features</h2>



<p class="wp-block-paragraph">PowerShell 7 introduces some interesting new features compare to PowerShell 5.</p>



<ul class="wp-block-list">
<li><a href="https://learn.microsoft.com/en-us/previous-versions/powershell/scripting/whats-new/what-s-new-in-powershell-70?view=powershell-7.3#parallel-execution-added-to-foreach-object">ForEach-Object with parallel execution</a><br>Execute the script block in parallel for each object. A parameter ThrottleLimit limits the number of script blocks running at the same time, default value is 5.<br>Here we search the instance properties by server and limit the parallelization to 2 server at the same time.<br>$computers = &#8216;thor90&#8242;,&#8217;thor91&#8242;,&#8217;thor10&#8242;,&#8217;thor11&#8217;<br>$InstanceProperties = $computers | ForEach-Object -Parallel {<br>$instances = (Find-DbaInstance -ComputerName $_).SqlInstance;<br>Get-DbaInstanceProperty -SqlInstance $instances<br>} -ThrottleLimit 2<br></li>



<li><a href="https://learn.microsoft.com/en-us/previous-versions/powershell/scripting/whats-new/what-s-new-in-powershell-70?view=powershell-7.3#ternary-operator">Ternary operator</a><br>A simplified if-else statement with &lt;condition&gt; ? &lt;condition true&gt; : &lt;condition false&gt;<br></li>



<li><a href="https://learn.microsoft.com/en-us/previous-versions/powershell/scripting/whats-new/what-s-new-in-powershell-70?view=powershell-7.3#pipeline-chain-operators">Pipeline chain operators</a><br>The &amp;&amp; operator executes the right-hand pipeline, if the left-hand pipeline succeeded. Reverse, the || operator executes the right-hand pipeline if the left-hand pipeline failed.<br></li>



<li><a href="https://learn.microsoft.com/en-us/previous-versions/powershell/scripting/whats-new/what-s-new-in-powershell-70?view=powershell-7.3#null-coalescing-assignment-and-conditional-operators">coalescence, assignment and conditional operators</a><br>PowerShell 7 includes Null coalescing operator ??, Null conditional assignment ??=, and Null conditional member access operators ?. and ?[]<br></li>



<li><a href="https://learn.microsoft.com/en-us/previous-versions/powershell/scripting/whats-new/what-s-new-in-powershell-70?view=powershell-7.3#new-view-conciseview-and-cmdlet-get-error">New management of error message and new cmdlet Get-Error</a><br>This new cmdlet Get-Error displays the full detailed of the last error with inner exceptions.<br>A parameter Newest allows to select the number of error you would like to display</li>
</ul>



<p class="wp-block-paragraph">On this blog, post I would to concentrate to the parallelization with the ForEach-Object -Parallel</p>



<h2 class="wp-block-heading" id="h-powershell-7-parallelization">PowerShell 7 parallelization</h2>



<p class="wp-block-paragraph">This new feature comes with the know ForEach-Object cmdlet which performs an operation on each item in a collection of input objects.<br>Starting with PowerShell 7.0 a new parameter set, called &#8220;Parallel&#8221;, gives the possibility to run each script block in parallel instead of sequentially. The &#8220;ThrottleLimit&#8221; parameter, if used, limits the number of script blocks which will run at the same time, if it is not specified the default value is 5.</p>



<p class="wp-block-paragraph"> ForEach-Object -Parallel &lt;scriptblock&gt; -ThrottleLimit</p>



<p class="wp-block-paragraph">We can test this new feature with a small example.<br>If we execute the following script as before, the script block is executed sequentially:</p>



<pre class="wp-block-code"><code>PS C:\Users\administrator.ADSTS&gt; 1..16 | ForEach-Object { Get-Date; sleep 10 }

Friday, February 14, 2025 9:00:02 AM
Friday, February 14, 2025 9:00:12 AM
Friday, February 14, 2025 9:00:22 AM
Friday, February 14, 2025 9:00:32 AM
Friday, February 14, 2025 9:00:42 AM
Friday, February 14, 2025 9:00:52 AM
Friday, February 14, 2025 9:01:02 AM
Friday, February 14, 2025 9:01:12 AM
Friday, February 14, 2025 9:01:22 AM
Friday, February 14, 2025 9:01:32 AM
Friday, February 14, 2025 9:01:42 AM
Friday, February 14, 2025 9:01:52 AM
Friday, February 14, 2025 9:02:02 AM
Friday, February 14, 2025 9:02:12 AM
Friday, February 14, 2025 9:02:22 AM
Friday, February 14, 2025 9:02:32 AM</code></pre>



<p class="wp-block-paragraph">Each line as 10 seconds more than the previous one.<br>But, if we execute this script with the new parameter Parallel and use a throttle limit of 4 we have:</p>



<pre class="wp-block-code"><code>PS C:\Users\administrator.ADSTS&gt; 1..16 | ForEach-Object -Parallel { Get-Date; sleep 10 } -ThrottleLimit 4

Friday, February 14, 2025 8:59:01 AM
Friday, February 14, 2025 8:59:01 AM
Friday, February 14, 2025 8:59:01 AM
Friday, February 14, 2025 8:59:01 AM
Friday, February 14, 2025 8:59:11 AM
Friday, February 14, 2025 8:59:11 AM
Friday, February 14, 2025 8:59:11 AM
Friday, February 14, 2025 8:59:11 AM
Friday, February 14, 2025 8:59:21 AM
Friday, February 14, 2025 8:59:21 AM
Friday, February 14, 2025 8:59:21 AM
Friday, February 14, 2025 8:59:21 AM
Friday, February 14, 2025 8:59:31 AM
Friday, February 14, 2025 8:59:31 AM
Friday, February 14, 2025 8:59:31 AM
Friday, February 14, 2025 8:59:31 AM</code></pre>



<p class="wp-block-paragraph">Here we have 4 groups of 4 lines with the same time as we executed the script block in parallel with limitation of the parallelization to 4.<br>Of course, the different commands included in the script block are executed sequentially.</p>



<p class="wp-block-paragraph">This feature uses the PowerShell runspaces to execute script blocks in parallel.<br>Variables can be passed into the script block with the $using: keyword, the only variable automatically passed is the pipe object.<br>Each runspace will execute a script block in a thread, so the ThrottleLimit parameter needs to be set according to the number of core of the server where you are running. If you VM has 2 cores, it makes no sense to put the limit to 4&#8230;</p>



<p class="wp-block-paragraph">This new script will execute a maintenance job on different instances of the same server, passing the job name in the block script with the $using: keyword:</p>



<pre class="wp-block-code"><code>PS C:\Users\administrator.ADSTS&gt; $ThrottleLimit = 2
PS C:\Users\administrator.ADSTS&gt; $JobName = 'DBI_MAINTENANCE_MAINTENANCE_USER_DATABASES'
PS C:\Users\administrator.ADSTS&gt; $computers = 'thor90'
PS C:\Users\administrator.ADSTS&gt; $SqlInstances = Find-DbaInstance -ComputerName $computers -EnableException
PS C:\Users\administrator.ADSTS&gt; $SqlInstances

ComputerName InstanceName SqlInstance    Port  Availability Confidence ScanTypes
------------ ------------ -----------    ----  ------------ ---------- ---------
thor90       CMS          thor90\CMS     50074 Available    High       Default
thor90       SQL16_1      thor90\SQL16_1 62919 Available    High       Default
thor90       SQL19_1      thor90\SQL19_1 1433  Available    High       Default
thor90       SQL22_1      thor90\SQL22_1 50210 Available    High       Default
thor90       MSSQLSERVER  thor90         1434  Available    High       Default

PS C:\Users\administrator.ADSTS&gt; $SqlInstances | ForEach-Object -Parallel {
&gt;&gt;     $Out = "Starting Job on $using:JobName &#091;" + $_.SqlInstance + "]"
&gt;&gt;     Write-Host $Out
&gt;&gt;     $res = Start-DbaAgentJob -SqlInstance $_.SqlInstance -Job $using:JobName -Wait
&gt;&gt; } -ThrottleLimit $ThrottleLimit
Starting Job on DBI_MAINTENANCE_MAINTENANCE_USER_DATABASES &#091;thor90\CMS]
Starting Job on DBI_MAINTENANCE_MAINTENANCE_USER_DATABASES &#091;thor90\SQL16_1]
Starting Job on DBI_MAINTENANCE_MAINTENANCE_USER_DATABASES &#091;thor90\SQL19_1]
Starting Job on DBI_MAINTENANCE_MAINTENANCE_USER_DATABASES &#091;thor90\SQL22_1]
Starting Job on DBI_MAINTENANCE_MAINTENANCE_USER_DATABASES &#091;thor90]
PS C:\Users\administrator.ADSTS&gt;</code></pre>



<p class="wp-block-paragraph">We use this kind of script at a customer place to execute SQL Server Agent Jobs in parallel on instances of a big physical servers with more than 10 instances.</p>



<h2 class="wp-block-heading" id="h-conclusion">Conclusion</h2>



<p class="wp-block-paragraph">This PowerShell 7 parallelization feature can improve performance in lots of different scenarios. But test it and don&#8217;t think that because of parallelization all your scripts will be executed quickly as running a script in parallel adds some overhead which will decrease execution of trivial script.</p>



<p class="wp-block-paragraph"></p>
<p>L’article <a href="https://www.dbi-services.com/blog/starting-with-powershell-7-and-parallelization/">Starting with PowerShell 7 and parallelization</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/starting-with-powershell-7-and-parallelization/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Make SQL Server services more secure with Managed Service Accounts</title>
		<link>https://www.dbi-services.com/blog/make-sql-server-services-more-secure-with-managed-service-accounts/</link>
					<comments>https://www.dbi-services.com/blog/make-sql-server-services-more-secure-with-managed-service-accounts/#respond</comments>
		
		<dc:creator><![CDATA[Microsoft Team]]></dc:creator>
		<pubDate>Wed, 05 Jun 2024 18:04:09 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Service Accounts]]></category>
		<category><![CDATA[SQL Security]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=32013</guid>

					<description><![CDATA[<p>In the past years, I actively have been involved in securing MSSQL Instances (and other services).This lead me to use the Managed Service Accounts (MSA) and the grouped Managed Service Accounts (gMSA)The MSA have been introduced in Windows Server 2008 R2 and the gMSA in Windows Server 2012. I. What exactly are MSA or gMSA [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/make-sql-server-services-more-secure-with-managed-service-accounts/">Make SQL Server services more secure with Managed Service Accounts</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In the past years, I actively have been involved in securing MSSQL Instances (and other services).<br>This lead me to use the Managed Service Accounts (MSA) and the grouped Managed Service Accounts (gMSA)<br>The MSA have been introduced in Windows Server 2008 R2 and the gMSA in Windows Server 2012.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h2 class="wp-block-heading" id="h-i-what-exactly-are-msa-or-gmsa">I. <strong><u>What exactly are MSA or gMSA</u></strong></h2>



<p class="wp-block-paragraph">MSA (Managed Service Accounts) or gMSA (group Managed Service Accounts) are Active Directory Managed Accounts used to start services (Service Accounts).</p>



<p class="wp-block-paragraph">They have several advantages though they are still not permanently used (at least from what I saw at most of my customers).</p>



<ul class="wp-block-list">
<li>They are linked to a single server (MSA) or to a group of Server (gMSA) and cannot be used on other server than the ones they are dedicated to.</li>



<li>They cannot be used to gain access to the server as they can&#8217;t be used to login (no privilege escalation is possible).</li>
</ul>



<p class="wp-block-paragraph">They simplify the management of Service Principle Names (SPN) as the SPN for the related service is automatically generated when the service starts the first time.<br>This is obviously a huge advantage in terms of security as Kerberos Authentication is factually running on the go.</p>
</div></div>



<h2 class="wp-block-heading" id="h-ii-implementation-configuration-of-msa-gmsa">II. Implementation / Configuration of MSA/gMSA</h2>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>II.1 Prerequisites:</strong></p>



<p class="wp-block-paragraph">In order to create a MSA or a gMSA you will need to go the Powershell way.</p>



<p class="wp-block-paragraph">Normally the setup will be performed on a Server which is not an AD Server.</p>



<p class="wp-block-paragraph">Therefore the first step will be to install the RSAT Tools:</p>



<p class="wp-block-paragraph">On Windows Server:</p>



<p class="wp-block-paragraph">Install-WindowsFeature RSAT-AD-PowerShell</p>



<p class="wp-block-paragraph">On Windows 10/11: Add-WindowsCapability -Name Rsat.ActiveDirectory.DS-LDS.Tools -Online</p>



<p class="wp-block-paragraph">The creation of both types can easily be performed with simple scripts though this requires AD Admin permissions (or at least a delegation for creating Accounts)</p>
</div></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>II.2 Create a MSA:</strong></p>



<p class="wp-block-paragraph"><strong>New-ADServiceAccount -Name NAME -Enabled $true -Description &#8220;Managed Service Account for xxxx&#8221; -DisplayName &#8220;MSA 1 – xxxx&#8221; -RestrictToSingleComputer</strong></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="72" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-5.png" alt="" class="wp-image-33397" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-5.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-5-300x23.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-5-768x59.png 768w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>



<p class="wp-block-paragraph">Once the account got created, it will be added in Active Directory under Managed Service Accounts:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="305" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-6.png" alt="" class="wp-image-33398" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-6.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-6-300x97.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-6-768x249.png 768w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>
</div></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>II.3 Create a gMSA:</strong></p>



<p class="wp-block-paragraph">gMSA creation is slightful different as there’s a need to create the Account and grant the defined computer Objects to use it. Fortunately, this can als easily be done with a single PS Query:</p>



<p class="wp-block-paragraph"><strong>New-ADServiceAccount -Name msa_exhib2 -DNSHostName DNS Server -PrincipalsAllowedToRetrieveManagedPassword Server1$, Server2$</strong></p>



<p class="wp-block-paragraph">Here it is important to add the $ sign at the end of the device name as the query gives the permissions to the Computer Object.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="56" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-7.png" alt="" class="wp-image-33399" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-7.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-7-300x18.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-7-768x46.png 768w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="90" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-8.png" alt="" class="wp-image-33400" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-8.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-8-300x29.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-8-768x74.png 768w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>
</div></div>



<h2 class="wp-block-heading" id="h-iii-installation-and-usage-of-the-msa-gmsa">III. Installation and Usage of the MSA / gMSA:</h2>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>III.1 Installation of the created Account</strong></p>



<p class="wp-block-paragraph">Once the Account got created it will need to be installed on the target server (Requires the RSAT AD Tools to be installed as mentioned previously):</p>



<p class="wp-block-paragraph">Install-ADServiceAccount -Identity ServiceName</p>



<p class="wp-block-paragraph">Be Aware that the MSA is restricted only to one server. Therefore, it can only be installed once. Trying to install it on another server will end with the error:</p>



<p class="wp-block-paragraph">Once the Account got created it will need to be installed on the target server (Requires the RSAT AD Tools to be installed as mentioned previously):</p>



<p class="wp-block-paragraph"><strong>Install-ADServiceAccount -Identity ServiceName</strong></p>



<p class="wp-block-paragraph">Be Aware that the MSA is restricted only to one server. Therefore, it can only be installed once. Trying to install it on another server will end with the error:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="940" height="191" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-9.png" alt="" class="wp-image-33401" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-9.png 940w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-9-300x61.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2024/06/image-9-768x156.png 768w" sizes="auto, (max-width: 940px) 100vw, 940px" /></figure>



<p class="wp-block-paragraph"><mark class="has-inline-color has-luminous-vivid-orange-color">If the action gets confirmed, the MSA will be removed from it’s original server and the services relying on it will be stopped.</mark></p>



<p class="wp-block-paragraph">The issue will not exist with gMSA as it can be installed on all Server / Computer which have been granted the access to it.</p>
</div></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>III.2 Grant the permissions to the MSA / gMSA</strong></p>



<p class="wp-block-paragraph">The created accounts need to be granted segregated permissions. As my Colleague <a href="https://www.dbi-services.com/blog/author/stephane-haby/">Stéphane Haby</a> already mentioned in <a href="https://www.dbi-services.com/blog/sql-server-security-ensure-that-sql-server-service-accounts-are-not-a-member-of-the-windows-local-administrator-group/">this Article</a>, for security reasons a SQL Server Service Account should never be granted the Local Administrator Permissions.</p>



<p class="wp-block-paragraph">This can be performed on several ways:</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<ul class="wp-block-list">
<li>GPO: This requires the Domain Architect to define the GPOs properly</li>



<li>By managing the Local Security Policies:</li>
</ul>



<p class="wp-block-paragraph">Under Local Policies\User Rights Assignment add the created account to all relevant permissions (Basically, for a SQL Server: Logon as a Batch Job, Logon as a service, Perform Volume Maintenance Tasks (eventually Lock Pages in Memory if this is required by the software using MSSQL)</p>



<ul class="wp-block-list">
<li>By using a powershell script:</li>
</ul>
</div></div>
</div></div>



<pre class="wp-block-code"><code># Variables used - change as required
$TempLocation = "C:\Service"
$SQLServiceAccount = $ServiceName #Account used for the SQL Service

## This lines are required to change in the cfg file
$ChangeFrom = "SeManageVolumePrivilege = "
$ChangeFrom2 = "SeLockMemoryPrivilege = "
$ChangeFrom3 = "SeBatchLogonRight = "
$ChangeFrom4 = "SeServiceLogonRight = "
$ChangeFrom5 = "SeAuditPrivilege = "

## Build the new lines 
$ChangeTo = "SeManageVolumePrivilege = $SQLServiceAccount,"
$ChangeTo2 = "SeLockMemoryPrivilege = $SQLServiceAccount,"
$ChangeTo3 = "SeBatchLogonRight = $SQLServiceAccount,"
$ChangeTo4 = "SeServiceLogonRight = $SQLServiceAccount,"
$ChangeTo5 = "SeAuditPrivilege = $SQLServiceAccount,"


## Set a name for the Security Policy cfg file.
$fileName = "$TempLocation\SecPolExport.cfg"

## export currect Security Policy config
Write-Host "Exporting Security Policy to file: $filename"
secedit /export /cfg $filename
Copy-Item $filename -Destination "$filename.save_before"
Write-Host "make a copy of initial Security policy export: $filename.save_before"
Write-Host "start to modify Security Policy Export file"

## delete the last 4 lines in the export file; this is needed if some attrubutes are not yet set and they are added at the end of the file; after this end-section (last 4 lines) the secpol is not importing it, so delete these values and add them at the end of the script
$content = get-content $filename
$content&#091;0..($content.length-4)] | out-file $filename


# Use Get-Content to change the text in the cfg file and then save it

# As the line for the option only exists if there is something already in the group
# this will check for it and add your $SQLServiceAccount or use Add-Contect to append option and your $SQLServiceAccount

#Option SeManageVolumePrivilege (Perform maintenance volumne tasks)
IF ((Get-Content $fileName) | where { $_.Contains("SeManageVolumePrivilege") })
{
Write-Host "Appending line containing SeManageVolumePrivilege with $SQLServiceAccount"
(Get-Content $fileName) -replace $ChangeFrom, $ChangeTo | Set-Content $fileName
}
else
{
Write-Host "Adding new line containing SeManageVolumePrivilege"
Add-Content $filename "`nSeManageVolumePrivilege = $SQLServiceAccount"
}

## Option SeLockMemoryPrivilege (Lock Pages in Memory)
## This is optinal depending on the requirements
## IF ((Get-Content $fileName) | where { $_.Contains("SeLockMemoryPrivilege") })
## {
## Write-Host "Appending line containing SeLockMemoryPrivilege with $SQLServiceAccount"
## (Get-Content $fileName) -replace $ChangeFrom2, $ChangeTo2 | Set-Content $fileName
## }
## else
## {
## Write-Host "Adding new line containing SeLockMemoryPrivilege"
## Add-Content $filename "`nSeLockMemoryPrivilege = $SQLServiceAccount"
## }

#Option SeBatchLogonRight (Log on as Batch job)
IF ((Get-Content $fileName) | where { $_.Contains("SeBatchLogonRight") })
{
Write-Host "Appending line containing SeBatchLogonRight with $SQLServiceAccount"
(Get-Content $fileName) -replace $ChangeFrom3, $ChangeTo3 | Set-Content $fileName
}
else
{
Write-Host "Adding new line containing SeBatchLogonRight"
Add-Content $filename "`nSeBatchLogonRight = $SQLServiceAccount"
}


#Option SeServiceLogonRight (log on as a service)
IF ((Get-Content $fileName) | where { $_.Contains("SeServiceLogonRight") })
{
Write-Host "Appending line containing SeServiceLogonRight with $SQLServiceAccount"
(Get-Content $fileName) -replace $ChangeFrom4, $ChangeTo4 | Set-Content $fileName
}
else
{
Write-Host "Adding new line containing SeServiceLogonRight"
Add-Content $filename "`nSeServiceLogonRight = $SQLServiceAccount"
}</code></pre>
</div></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>III.3 Configure the services to use the MSA / gMSA</strong></p>



<p class="wp-block-paragraph">Once the Accounts have been granted the required permissions, the services need to be configured to use them.</p>



<p class="wp-block-paragraph">This can be performed by using a command of the <a href="https://dbatools.io/">DBATools Module</a> :</p>



<pre class="wp-block-code"><code>Update-DbaServiceAccount -ServiceName 'MSSQLSERVER','SQLSERVERAGENT' -UserName 'Domain\MSAName$'</code></pre>
</div></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>IV. An easy way to configure everything in one shot</strong></p>



<p class="wp-block-paragraph">One of my teacher once told me that a good administrator is a lazy administrator. With these words he meant everything what can be scripted or automated should be.</p>



<p class="wp-block-paragraph">That&#8217;s why you will find a complete script to perform the above mentioned tasks below:</p>



<pre class="wp-block-code"><code>## Install DbaTools (Requires the Server to have an Internet Connection

Write-Host "Now let's install DBATools"
Install-Module DBATools -Scope AllUsers
read-host "If no error occured, press any key to continue"
## Create Service Folder

Write-Host "Creation of the Service Folder"
if (Test-Path "C:\Service") {
    # Folder exists - Do something here
    Write-host "Folder Exists!" -f Green
}
else {
    # Folder does not exist - Do something else here
    New-Item -ItemType Directory -Path C:\Service
}

## Install AD Powershell Tools

Write-Host "First we install the AD Powershell Tools"
Install-WindowsFeature RSAT-AD-PowerShell
read-host "RSAT Tools installed - press enter to continue"

## Define Variables

$ServerName = hostname
$ServiceName = 'sv'+ $ServerName ## Here you can define your own naming convention for the MSA
#Create MSA for SQL Server

Write-Host "Creation of the Managed Service Account for this Server"
if (!(Get-ADServiceAccount -Filter "Name -like '$ServiceName'")) {
New-ADServiceAccount -Name $ServiceName -Enabled $true -Description "Managed Service Account for SQL Server $ServerName" -DisplayName "MSA - $ServerName" -RestrictToSingleComputer
Write-Host "Service Account has been created"
}
else {
Write-host "Service Account already exists"
}

read-host "Service Account created - please press enter to continue"

#Install MSA on Local Server

Write-Host "Installation of the MSA on this Server"
Install-ADServiceAccount -Identity $ServiceName
read-host "Service Account installed - Press Enter to continue"

## Adding sql service account to local policy
write-host "Local Security Policies will be applied according to the default SQL settings"
#read-host "this is only for debug purporsed - please press enter to proceed"

# Variables used - change as required
$TempLocation = "C:\Service"

## This lines are required to change in the cfg file
$ChangeFrom = "SeManageVolumePrivilege = "
$ChangeFrom2 = "SeLockMemoryPrivilege = "
$ChangeFrom3 = "SeBatchLogonRight = "
$ChangeFrom4 = "SeServiceLogonRight = "
$ChangeFrom5 = "SeAuditPrivilege = "

## Build the new lines 
$ChangeTo = "SeManageVolumePrivilege = $ServiceName,"
$ChangeTo2 = "SeLockMemoryPrivilege = $ServiceName,"
$ChangeTo3 = "SeBatchLogonRight = $ServiceName,"
$ChangeTo4 = "SeServiceLogonRight = $ServiceName,"
$ChangeTo5 = "SeAuditPrivilege = $ServiceName,"


## Set a name for the Security Policy cfg file.
$fileName = "$TempLocation\SecPolExport.cfg"

## export currect Security Policy config
Write-Host "Exporting Security Policy to file: $filename"
secedit /export /cfg $filename
Copy-Item $filename -Destination "$filename.save_before"
Write-Host "make a copy of initial Security policy export: $filename.save_before"
Write-Host "start to modify Security Policy Export file"

## delete the last 4 lines in the export file; this is needed if some attrubutes are not yet set and they are added at the end of the file; after this end-section (last 4 lines) the secpol is not importing it, so delete these values and add them at the end of the script
$content = get-content $filename
$content&#091;0..($content.length-4)] | out-file $filename


# Use Get-Content to change the text in the cfg file and then save it

# As the line for the option only exists if there is something already in the group
# this will check for it and add your $ServiceName or use Add-Contect to append option and your $ServiceName

#Option SeManageVolumePrivilege (Perform maintenance volumne tasks)
IF ((Get-Content $fileName) | where { $_.Contains("SeManageVolumePrivilege") })
{
Write-Host "Appending line containing SeManageVolumePrivilege with $ServiceName"
(Get-Content $fileName) -replace $ChangeFrom, $ChangeTo | Set-Content $fileName
}
else
{
Write-Host "Adding new line containing SeManageVolumePrivilege"
Add-Content $filename "`nSeManageVolumePrivilege = $ServiceName"
}

## Option SeLockMemoryPrivilege (Lock Pages in Memory)
## This is optinal depending on the requirements
## IF ((Get-Content $fileName) | where { $_.Contains("SeLockMemoryPrivilege") })
## {
## Write-Host "Appending line containing SeLockMemoryPrivilege with $ServiceName"
## (Get-Content $fileName) -replace $ChangeFrom2, $ChangeTo2 | Set-Content $fileName
## }
## else
## {
## Write-Host "Adding new line containing SeLockMemoryPrivilege"
## Add-Content $filename "`nSeLockMemoryPrivilege = $ServiceName"
## }

#Option SeBatchLogonRight (Log on as Batch job)
IF ((Get-Content $fileName) | where { $_.Contains("SeBatchLogonRight") })
{
Write-Host "Appending line containing SeBatchLogonRight with $ServiceName"
(Get-Content $fileName) -replace $ChangeFrom3, $ChangeTo3 | Set-Content $fileName
}
else
{
Write-Host "Adding new line containing SeBatchLogonRight"
Add-Content $filename "`nSeBatchLogonRight = $ServiceName"
}


#Option SeServiceLogonRight (log on as a service)
IF ((Get-Content $fileName) | where { $_.Contains("SeServiceLogonRight") })
{
Write-Host "Appending line containing SeServiceLogonRight with $ServiceName"
(Get-Content $fileName) -replace $ChangeFrom4, $ChangeTo4 | Set-Content $fileName
}
else
{
Write-Host "Adding new line containing SeServiceLogonRight"
Add-Content $filename "`nSeServiceLogonRight = $ServiceName"
}


#Option SeAuditPrivilege (Generate security audits)
## Optional: Only required if a Log Management Server is configured and you want to generate Audit Files 
## IF ((Get-Content $fileName) | where { $_.Contains("SeAuditPrivilege") })
## {
## Write-Host "Appending line containing SeAuditPrivilege with $ServiceName"
## (Get-Content $fileName) -replace $ChangeFrom5, $ChangeTo5 | Set-Content $fileName
## }
## else
## {
## Write-Host "Adding new line containing SeAuditPrivilege"
## Add-Content $filename "`nSeAuditPrivilege = $ServiceName"
## }


# Import new Security Policy cfg (using '1&gt; $null' to keep the output quiet)
Write-Host "Importing Security Policy..."
secedit /configure /db secedit.sdb /cfg $fileName 1&gt; $null
Write-Host "done: local security policies changed according to SQL Standards"

## Set the SQL Services to run with the new Managed Service account
Update-DbaServiceAccount -ServiceName 'MSSQLSERVER','SQLSERVERAGENT' -UserName $ServiceName</code></pre>
</div></div>


<p>L’article <a href="https://www.dbi-services.com/blog/make-sql-server-services-more-secure-with-managed-service-accounts/">Make SQL Server services more secure with Managed Service Accounts</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/make-sql-server-services-more-secure-with-managed-service-accounts/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Move SQL Server database files to different folders with PowerShell</title>
		<link>https://www.dbi-services.com/blog/move-sql-server-database-files-to-different-folders-with-powershell/</link>
					<comments>https://www.dbi-services.com/blog/move-sql-server-database-files-to-different-folders-with-powershell/#respond</comments>
		
		<dc:creator><![CDATA[Nathan Courtine]]></dc:creator>
		<pubDate>Fri, 03 Mar 2023 20:52:32 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[AlwaysOn]]></category>
		<category><![CDATA[PowerShell]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=23218</guid>

					<description><![CDATA[<p>In my previous post, I explained how to move SQL Server databases files programmatically by leveraging the high availability of an Availability Group (AG).But when you can afford to have a database unavailable for a short time, and if it is of a reasonable size, then an offline approach may be desirable. This will have [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/move-sql-server-database-files-to-different-folders-with-powershell/">Move SQL Server database files to different folders with PowerShell</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In my previous <a href="https://www.dbi-services.com/blog/move-secondary-database-files-in-sql-server-ag-with-powershell/">post</a>, I explained how to move SQL Server databases files programmatically by leveraging the high availability of an Availability Group (AG).<br>But when you can afford to have a database unavailable for a short time, and if it is of a reasonable size, then an offline approach may be desirable. This will have the advantage of also working for standalone databases, but also of not needing to reapply the manipulation on the primary after a fail-over to a secondary.</p>



<p class="wp-block-paragraph"><br>In this blog, I will again use <a href="https://dbatools.io/">dbatools </a>module in my PowerShell script. For automation in SQL Server, this is now a must-have language.</p>



<p class="wp-block-paragraph">In the following script, it needs to be run on the Primary server because I am using local paths to move database files.<br>For a remote approach, you can either use PowerShell remote sessions or remote paths to achieve this goal.</p>



<p class="wp-block-paragraph">The script will also remove the database from AG during the detach/attach operation, and will add back the database in the AG with the synchronization.</p>



<pre class="wp-block-code"><code># IMPORTANT: this script has to be run on the Primary only
# dbatools module is required

$Primary = '&lt;MyPrimary&gt;';
$Secondary = '&lt;MySecondary&gt;';

$Database = '&lt;MyDatabase&gt;';
$AvailabilityGroup = '&lt;MyAG&gt;';

$NewDataFolder = '&lt;MyNewDataFolder&gt;';
$NewLogsFolder = '&lt;MyNewLogFolder&gt;';


Try
{
	Import-Module -Name dbatools;
	
	$dbFiles = Get-DbaDbFile -SqlInstance $Primary -Database $Database;
	$dbFiles | Format-Table ComputerName, InstanceName, Database, PhysicalName, LogicalName, TypeDescription, Size;
	
	$title   = 'Moving database to another folder (Offline)'
	$msg     = "Do you want to Move data file(s) to $NewDataFolder and logs file to $NewLogsFolder ?"
	$options = '&amp;Yes', '&amp;No'
	$default = 1  # 0=Yes, 1=No

	$Continue = $False;
	
	do {
		$response = $Host.UI.PromptForChoice($title, $msg, $options, $default)
		if ($response -eq 0) {
			$Continue = $True;
			$response = 1;
		}
	} until ($response -eq 1)
	
	If (-not $Continue){
		Write-Warning -Message "Aborted by user...";
		Return;
	}
	
	# Test path New data folder
	Write-Output -Message "Testing New Data folder $NewDataFolder";
	If (-not (Test-Path -Path $NewDataFolder)){
		Write-Warning -Message "Target data folder $NewDataFolder does not exist. Aborting...";
		Return;
	};
    Write-Output '...ok';
	
	# Test path New logs folder
	Write-Output -Message "Testing New Logs folder $NewLogsFolder";
	If (-not (Test-Path -Path $NewLogsFolder)){
		Write-Warning -Message "Target logs folder $NewLogsFolder does not exist. Aborting...";
		Return;
	};
    Write-Output '...ok';
	
	# Check if AG is configured
	If ($AvailabilityGroup){
		# Remove database from AG
		Write-Output "Start removing database $Database from AG $AvailabilityGroup on instance $Primary";
		Remove-DbaAgDatabase -SqlInstance $Primary -Database $Database -AvailabilityGroup $AvailabilityGroup -EnableException;
        Write-Output '...ok';
	}

	# Detach database on Primary
	Write-Output "Detaching database $Database on instance $Primary";
	Dismount-DbaDatabase -SqlInstance $Primary -Database $Database -EnableException;
    Write-Output '...ok';

	
	# Move data files
	Write-Output "Moving data file(s) of $Database on instance $Primary";
	($dbFiles | Where-Object Type -eq 0).PhysicalName | ForEach-Object -Process {Move-Item -Path $_ -Destination $NewDataFolder;}
    Write-Output '...ok';
	
	# Move logs files
    Write-Output "Moving logs file of database $Database on instance $Primary";
	($dbFiles | Where-Object Type -eq 1).PhysicalName | ForEach-Object -Process {Move-Item -Path $_ -Destination $NewLogsFolder;}
    Write-Output '...ok';
	
	# Create new file structure for database to attach
	$newdbFiles = @(Get-ChildItem $NewDataFolder) + @(Get-ChildItem $NewLogsFolder);
	$fs = New-Object System.Collections.Specialized.StringCollection; 
	@($newdbFiles) | ForEach-Object -Process { `
		$Null = $fs.Add($_.FullName); `	
	}; 
	
	# Attach database on Primary
    Write-Output "Attaching database $database on instance $Primary"
	Mount-DbaDatabase -SqlInstance $Primary -Database $Database -FileStructure $fs;
    Write-Output "...ok";
	

	# Add database to AG
	If ($Secondary -and $AvailabilityGroup){
		
        Write-Output "Removing database $database on secondary $Secondary"; 
		Remove-dbaDatabase -SqlInstance $Secondary -Database $Database;
        Write-Output "...ok";
		
        Write-Output "Start adding database $Database from AG $AvailabilityGroup on instance $Primary to secondary $Secondary";
		Add-DbaAgDatabase -SqlInstance $Primary -Database $Database -AvailabilityGroup $AvailabilityGroup -Secondary $Secondary -SeedingMode Automatic;
        Write-Output "...ok";
	}

}
Catch
{
	Write-Error $_.Exception.toString();
}</code></pre>



<p class="wp-block-paragraph">Running this script on a production environment, I have seen a few seconds of downtime in the worst case with &lt;500GB databases.<br>But this will depend mainly on your infrastructure and whether the database files are transferred to another physical drive. Another thing to consider is the database cache will be reset with this approach.<br><br>Enjoy automation!</p>
<p>L’article <a href="https://www.dbi-services.com/blog/move-sql-server-database-files-to-different-folders-with-powershell/">Move SQL Server database files to different folders with PowerShell</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/move-sql-server-database-files-to-different-folders-with-powershell/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Move secondary database files in SQL Server AG with PowerShell</title>
		<link>https://www.dbi-services.com/blog/move-secondary-database-files-in-sql-server-ag-with-powershell/</link>
					<comments>https://www.dbi-services.com/blog/move-secondary-database-files-in-sql-server-ag-with-powershell/#respond</comments>
		
		<dc:creator><![CDATA[Nathan Courtine]]></dc:creator>
		<pubDate>Wed, 01 Mar 2023 21:39:38 +0000</pubDate>
				<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[AlwaysOn]]></category>
		<category><![CDATA[PowerShell]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=23120</guid>

					<description><![CDATA[<p>For some circumstances, you may want to move data and log files for a database to a different location.Usually, this operation has to be made offline. But with an Availability Group (AG) environment, it is possible to choose an approach which is slightly transparent. If you have a lot of databases to process, you may [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/move-secondary-database-files-in-sql-server-ag-with-powershell/">Move secondary database files in SQL Server AG with PowerShell</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">For some circumstances, you may want to move data and log files for a database to a different location.<br>Usually, this operation has to be made offline. But with an Availability Group (AG) environment, it is possible to choose an approach which is slightly transparent.</p>



<p class="wp-block-paragraph">If you have a lot of databases to process, you may want to automate this operation: PowerShell is quite convenient for that.<br>In this blog, I will present you how I did using <a href="https://dbatools.io/">dbatools </a>module.<br></p>



<p class="wp-block-paragraph">In the following script, I took the primary as reference for my restore: this is just as an example.<br>Of course, you can customize the target folders based on your needs.</p>



<pre class="wp-block-code"><code># IMPORTANT: dbatools needs to be installed on the server where running this script

# Configure the following parameters
$Primary = '&lt;MyPrimary&gt;';
$Secondary = '&lt;MySecondary&gt;';
$Database = '&lt;MyDatabase&gt;';
$AvailabilityGroup = '&lt;MyAG&gt;';

$SharedBackup = '&lt;MyShare&gt;';


Try
{
    Write-Output "Importing module dbatools";
    Import-Module -Name dbatools;
    Write-Output '...ok';
    
    $DatabaseFilesQuery =  "
    SELECT SERVERPROPERTY('ServerName') as Server_Instance,
    '$($Database)' as Database_Name,
    name as Logical_Name,
    physical_name as Physical_Name, 
    type_desc as Type_Description,
    size/128 as Size_MB
    FROM sys.master_files 
    WHERE database_id = DB_ID('$($Database)')";

    Write-Output "Retrieving information about current database files";
    $dbFiles = Invoke-DbaQuery -SqlInstance $Secondary -Database 'master' -Query $DatabaseFilesQuery -EnableException;

    $dbFiles | Format-Table Server_Instance, Database_Name, Physical_Name, Type_Description, Size_MB;
    Write-Output '...ok';

    # Retrieve locations from primary
    Write-Output "Retrieving information about new database folders";
    
    $Files = Get-DbaDbFile -SqlInstance $Primary -Database $Database;

    # Locate all data files based on primary data file
    $NewDataFolder = Split-Path (($Files | Where-Object { ($_.Type -eq 0) -and ($_.ID -eq 1) -and ($_.FileGroupName -eq 'PRIMARY')}).PhysicalName);
	
	# Assume there is only one log file
    $NewLogFolder = Split-Path (($Files | Where-Object { ($_.Type -eq 1) -and ($_.ID -eq 2)}).PhysicalName)
    Write-Output '...ok';
    

	$title   = 'Removing secondary database from old folers'
	$msg     = "Do you want to Move database file(s) to  $($NewDataFolder) and to $($NewLogFolder) on Secondary $($Secondary)?"
	$options = '&amp;Yes', '&amp;No'
	$default = 1  # 0=Yes, 1=No

	$Continue = $False;
	
	do {
		$response = $Host.UI.PromptForChoice($title, $msg, $options, $default)
		if ($response -eq 0) {
			$Continue = $True;
			$response = 1;
		}
	} until ($response -eq 1)
	
	If (-not $Continue){
		Write-Warning "Aborted by user...";
		Return;
    }


    # Create subfolder to database
    Write-Output "Creating new database folders";
    $CreateSubFoldersQuery = "
    EXECUTE &#091;master].dbo.xp_create_subdir '$($NewDataFolder)';
    EXECUTE &#091;master].dbo.xp_create_subdir '$($NewLogFolder)';
    "
    $Null = Invoke-DbaQuery -SqlInstance $Secondary -Database 'master' -Query $CreateSubFoldersQuery -EnableException;
    Write-Output '...ok';

    # Remove database from AG on secondary
    Write-Output "Removing database $($Database) from Secondary $($Secondary)";
    $RemoveDBSecondaryQuery = "
    ALTER DATABASE &#091;$($Database)] SET HADR OFF;
    ";
    $Null = Invoke-DbaQuery -SqlInstance $Secondary -Database 'master' -Query $RemoveDBSecondaryQuery -EnableException;
    Write-Output '...ok';

    # Backup Source database - Copy Only option is configurable with this command
    Write-Output "Performing full backup of database $($Database) to Shared Backup $($SharedBackup)";
    $FullBackups = Backup-DbaDatabase -SqlInstance $Primary -Database $Database -Path $SharedBackup -Type Full -FileCount 4;
    Write-Output '...ok';

    Write-Output "Performing log backup of database $($Database) to Shared Backup $($SharedBackup)";
    $LogBackup = Backup-DbaDatabase -SqlInstance $Primary -Database $Database -Path $SharedBackup -Type Log;
    Write-Output '...ok';

    # Restore to secondary with moving files
    Write-Output "Restoring FULL backup for database $($Database) on Secondary $($Secondary)";
    Restore-DbaDatabase -Path $FullBackups -EnableException -SqlInstance $Secondary -WithReplace -NoRecovery -DestinationDataDirectory "$($NewDataFolder)" -DestinationLogDirectory "$($NewLogFolder)";
    Write-Output '...ok';

    Write-Output "Restoring LOG backup for database $($Database) on Secondary $($Secondary)";
    Restore-DbaDatabase -Path $LogBackup -EnableException -SqlInstance $Secondary -NoRecovery -Continue;
    Write-Output '...ok';

    # Add database AG to secondary
    Write-Output "Adding database $($Database) on Secondary $($Secondary)";
    $AddDBSecondaryQuery = "
    ALTER DATABASE &#091;$($Database)] SET HADR AVAILABILITY GROUP = $($AvailabilityGroup);
    ";
    $Null = Invoke-DbaQuery -SqlInstance $Secondary -Database 'master' -Query $AddDBSecondaryQuery -EnableException;
    Write-Output '...ok';

    # Clean up backup files
    Write-Output "Cleaning full backup files";
    Remove-Item -Path $FullBackups.FullName;
    Write-Output '...ok';

}
Catch
{
	Write-Error $_.Exception.toString();
}</code></pre>



<p class="wp-block-paragraph">The script includes a TRY/CATCH block to stop at any unattended error: it offers a safe way to avoid any side effects.<br><br>Enjoy automation!<br></p>
<p>L’article <a href="https://www.dbi-services.com/blog/move-secondary-database-files-in-sql-server-ag-with-powershell/">Move secondary database files in SQL Server AG with PowerShell</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/move-secondary-database-files-in-sql-server-ag-with-powershell/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Installing Azure AZ Module on Windows</title>
		<link>https://www.dbi-services.com/blog/installing-azure-az-module-on-windows/</link>
					<comments>https://www.dbi-services.com/blog/installing-azure-az-module-on-windows/#respond</comments>
		
		<dc:creator><![CDATA[Nathan Courtine]]></dc:creator>
		<pubDate>Wed, 30 Nov 2022 19:52:33 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[PowerShell]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=20812</guid>

					<description><![CDATA[<p>Since Azure (or AzureRM) module is marked as deprecated some years ago, AZ module is the official replacement to manage Azure resources with PowerShell. This module is running with at least PowerShell 7.0.6 LTS (Long Term Support) or PowerShell 7.1.3, but higher versions are recommended. When PowerShell from 1.0 to 5.1 are component of Windows [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/installing-azure-az-module-on-windows/">Installing Azure AZ Module on Windows</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Since <a href="https://azure.microsoft.com/en-us/blog/azure-powershell-cross-platform-az-module-replacing-azurerm/">Azure (or AzureRM) module</a> is marked as deprecated some years ago, <a href="https://learn.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-9.1.0">AZ module</a> is the official replacement to manage Azure resources with PowerShell.</p>



<p class="wp-block-paragraph">This module is running with at least PowerShell 7.0.6 LTS (Long Term Support) or PowerShell 7.1.3, but higher versions are recommended.</p>



<p class="wp-block-paragraph">When PowerShell from 1.0 to 5.1 are component of Windows operating systems, PowerShell 7 is cross-platform and is at top of .NET Core.<br>As a result, <a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?WT.mc_id=THOMASMAURER-blog-thmaure&amp;view=powershell-7">PowerShell Core</a> has to be deployed on the environment to use this module.</p>



<p class="wp-block-paragraph">If you try to use AZ module on Windows PowerShell, you will encounter the following error:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="389" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-16-1024x389.png" alt="" class="wp-image-20816" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-16-1024x389.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-16-300x114.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-16-768x292.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-16-1536x583.png 1536w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-16-2048x778.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Indeed, I have not the recommended versions specified in the documentation:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="954" height="466" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-18.png" alt="" class="wp-image-20818" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-18.png 954w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-18-300x147.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-18-768x375.png 768w" sizes="auto, (max-width: 954px) 100vw, 954px" /></figure>



<p class="wp-block-paragraph">For my example, I needed to install PowerShell core. To do so, I did an installation with an msi installer with PowerShell TLS release.<br>This kind of release only contains security fixes and servicing fixes to minimize the impact on Prod environments. They are of course included in <a href="https://learn.microsoft.com/en-us/powershell/scripting/install/powershell-support-lifecycle?view=powershell-7.2">Microsoft Lifecycle Policy</a>.<br></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="488" height="387" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-17.png" alt="" class="wp-image-20817" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-17.png 488w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-17-300x238.png 300w" sizes="auto, (max-width: 488px) 100vw, 488px" /></figure>



<p class="wp-block-paragraph">NOTE: PowerShell 7.0 is no more supported, and only at least PowerShell 7.1 is now available.</p>



<p class="wp-block-paragraph">After the my previous installation, here is the version I get:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="758" height="345" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-19.png" alt="" class="wp-image-20819" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-19.png 758w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-19-300x137.png 300w" sizes="auto, (max-width: 758px) 100vw, 758px" /></figure>



<p class="wp-block-paragraph">This new module allows you to download only the necessary packages you need, which avoids having too much resources loaded in your PowerShell Console.</p>



<p class="wp-block-paragraph">Keep in mind that executing <em>Install-Module -Name AZ</em> will install ALL packages:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="559" height="1016" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-20.png" alt="" class="wp-image-20820" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-20.png 559w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-20-165x300.png 165w" sizes="auto, (max-width: 559px) 100vw, 559px" /></figure>



<p class="wp-block-paragraph">In most cases, you will prefer only installing packages relative to Azure resources you want to manage.</p>



<p class="wp-block-paragraph">In addition, using <em>Import-Module -Name AZ</em> will load all packages.<br>Here is an extract of the PowerShell Module Script associated to AZ module:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="939" src="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-21-1024x939.png" alt="" class="wp-image-20821" srcset="https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-21-1024x939.png 1024w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-21-300x275.png 300w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-21-768x704.png 768w, https://www.dbi-services.com/blog/wp-content/uploads/sites/2/2022/11/image-21.png 1220w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">If you still have scripts running with AzureRM module, you should update them <a href="https://learn.microsoft.com/en-us/powershell/azure/migrate-from-azurerm-to-az?view=azps-9.1.0">before 29 February 2024</a>.</p>
<p>L’article <a href="https://www.dbi-services.com/blog/installing-azure-az-module-on-windows/">Installing Azure AZ Module on Windows</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/installing-azure-az-module-on-windows/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Patch a SQL Server instance automatically with PowerShell</title>
		<link>https://www.dbi-services.com/blog/patch-a-sql-server-instance-automatically-with-powershell/</link>
					<comments>https://www.dbi-services.com/blog/patch-a-sql-server-instance-automatically-with-powershell/#comments</comments>
		
		<dc:creator><![CDATA[Stéphane Savorgnano]]></dc:creator>
		<pubDate>Fri, 11 Nov 2022 14:00:26 +0000</pubDate>
				<category><![CDATA[Database Administration & Monitoring]]></category>
		<category><![CDATA[Apply CU]]></category>
		<category><![CDATA[apply SP]]></category>
		<category><![CDATA[automatic]]></category>
		<category><![CDATA[dbatools]]></category>
		<category><![CDATA[Patching]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[SQL Server]]></category>
		<guid isPermaLink="false">https://www.dbi-services.com/blog/?p=20420</guid>

					<description><![CDATA[<p>In my last blog post I explained how to automatically download the last Service Pack and Cumulative Update for all versions of SQL Server.Here I will show you how to patch your SQL Server instances automatically with some cmdlets from the dbatools. First we need a SQL Credential to be able to remotely connect to [&#8230;]</p>
<p>L’article <a href="https://www.dbi-services.com/blog/patch-a-sql-server-instance-automatically-with-powershell/">Patch a SQL Server instance automatically with PowerShell</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In my <a href="https://www.dbi-services.com/blog/how-to-automatically-download-last-sql-server-sp-and-cu-with-powershell/">last blog post</a> I explained how to automatically download the last Service Pack and Cumulative Update for all versions of SQL Server.<br>Here I will show you how to patch your SQL Server instances automatically with some cmdlets from the <a href="https://dbatools.io/commands/">dbatools</a>.</p>



<p class="wp-block-paragraph">First we need a SQL Credential to be able to remotely connect to the server where we will patch our instances:</p>



<pre class="wp-block-code"><code>$OScred = Get-Credential;</code></pre>



<p class="wp-block-paragraph">Once done we will have to find the instances located on the server. Here multiple possibilities: </p>



<ul class="wp-block-list">
<li>you can create just a list with your instances $InstanceList = @(&#8216;Thor90\SQL19_1&#8242;,&#8217;Thor91\SQL19_1&#8217;);</li>



<li>you can search the instance on your server with $InstanceList = Get-DbaService | where-object { $_.ServiceType -eq &#8216;Engine&#8217; -and $_.State -eq &#8216;Running&#8217; };</li>



<li>you can also select your instance in your CMS with $InstanceList = Get-DbaRegServer -SqlInstance &#8216;Thor90\CMS&#8217; -Group &#8220;Prod\SQL2019&#8221;; (my case)</li>
</ul>



<p class="wp-block-paragraph">Now that we have our list of instances, we will loop on those instances, run a quick connection test and if it succeeds patch our instance.<br>To patch the instance, I will use 2 dbatools cmdlets which are:</p>



<ul class="wp-block-list">
<li><a href="https://docs.dbatools.io/Get-DbaBuildReference#Get-DbaBuild">Get-DbaBuildReference</a>: will return information about the instance like the SP, CU and used with the -Update switch will update the local reference with the most up to date one </li>



<li><a href="https://docs.dbatools.io/Update-DbaInstance">Update_DbaInstance</a>: will start a process which will update a SQL Server instance to a specified version<br>This cmdlet is really powerful with lots of parameter, possible options and also risks, I let you go through its definition.<br>I will use it  in my example with the -version parameter which is the target version I want to reach, with the switch -Restart to automatically restart my server after the installation, the parameter -Path to specif the location where my patches have been downloaded, the -Credential to have permission to log remotely to my server and -Confirm to false to run without confirmation.</li>
</ul>



<p class="wp-block-paragraph">I&#8217;ll use also some logging cmdlets wrote by myself which are Out-Log, Add-Warning and Add-Error.</p>



<p class="wp-block-paragraph">The loop on all my instance list will look like:</p>



<pre class="wp-block-code"><code>ForEach ($Instance in $InstanceList){
     Out-Log -Message "Patching instance $($Instance.InstanceName)" -LogFile $LogFile
        
    $connection = Test-DBAConnection -SqlInstance $instance.InstanceName -WarningVariable warningvar;
    Add-Warning -Warning $Warningvar;
    If ($connection -and $connection.ConnectSuccess) {
        Out-Log -Message "Connection to instance $($Instance.InstanceName) succeeded" -LogFile $LogFile

        #Update the instance build which is used by the cmdlet Update-DbaInstance
        Out-Log -Message "Update the build of the instance $($Instance.InstanceName)" -LogFile $LogFile
        Get-DbaBuildReference -SqlInstance $instance.InstanceName -Update -WarningVariable warningvar;
        Add-Warning -Warning $Warningvar;

        Out-Log -Message "Start patching of the instance $Instance.InstanceName" -LogFile $LogFile
        try {
            $res = Update-DbaInstance -ComputerName $Instance.ComputerName -InstanceName $Instance.InstanceName -credential $OScred -Version CU16 -Path \\Thor90\sources\SQL2019 -Restart -Confirm:$false -WarningVariable warningvar;
            Add-Warning -Warning $Warningvar;
        }
        catch {
            Add-Error -Message "Patching of the instance $instance$Instance.InstanceName not possible, Exception: $($error&#091;0].Exception.Message)";
        }

    }
    Else {
        Add-Error -Message "Impossible to connect to the instance $instance$Instance.InstanceName.";
    }
}</code></pre>



<p class="wp-block-paragraph">This script will patch my production SQL Server 2019 instances to CU16 with the KB located on my share drive \\Thor90\sources\SQL2019 and restart the server once succeeded.<br>Lots of scenario can be covered by using this cmdlet, I let you playing with it, but on your test environment first <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>L’article <a href="https://www.dbi-services.com/blog/patch-a-sql-server-instance-automatically-with-powershell/">Patch a SQL Server instance automatically with PowerShell</a> est apparu en premier sur <a href="https://www.dbi-services.com/blog">dbi Blog</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.dbi-services.com/blog/patch-a-sql-server-instance-automatically-with-powershell/feed/</wfw:commentRss>
			<slash:comments>6</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 
Lazy Loading (feed)

Served from: www.dbi-services.com @ 2026-08-16 07:07:26 by W3 Total Cache
-->