Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
774ac48
Invoke-DbaDbLogShipping - Add Azure blob storage support
github-actions[bot] Nov 7, 2025
754748c
Invoke-DbaDbLogShipping - Update parameter validation test for Azure …
github-actions[bot] Nov 8, 2025
5b97e10
Invoke-DbaDbLogShipping - Convert backticks to splats and add Azure c…
github-actions[bot] Nov 8, 2025
69bf79a
Refactor log shipping parameter validation logic
potatoqualitee Nov 9, 2025
fcbd804
Add Azure blob storage integration tests for log shipping
potatoqualitee Nov 9, 2025
f6823f4
Refactor Azure log shipping integration tests
potatoqualitee Nov 9, 2025
4e379f5
Invoke-DbaDbLogShipping - Add Azure blob storage integration tests
potatoqualitee Nov 9, 2025
bc54180
Restore default SqlCredential after Azure tests
potatoqualitee Nov 9, 2025
96fa1d8
Invoke-DbaDbLogShipping - Fix Azure log shipping tests by restoring a…
potatoqualitee Nov 9, 2025
13d86da
Invoke-DbaDbLogShipping - Remove SharedPath default in Azure tests to…
potatoqualitee Nov 9, 2025
a4c5197
Invoke-DbaDbLogShipping - Move Azure log shipping tests before Get-Db…
potatoqualitee Nov 9, 2025
66dc23d
Invoke-DbaDbLogShipping - Pass SqlCredential explicitly instead of re…
potatoqualitee Nov 9, 2025
51b18b5
Improve Azure SQL compatibility in log shipping
potatoqualitee Nov 9, 2025
2788809
Invoke-DbaDbLogShipping - Use SqlCredential instead of Credential for…
potatoqualitee Nov 9, 2025
0aa9a53
Invoke-DbaDbLogShipping - Add default SAS token credential for Azure …
potatoqualitee Nov 9, 2025
4c3ff6f
Delete settings.local.json
potatoqualitee Nov 9, 2025
ebb9a86
Update .gitignore
potatoqualitee Nov 9, 2025
a453a7e
gh-actions - Fix Get-DbaCredential authentication in Azure log shippi…
potatoqualitee Nov 9, 2025
19ed326
Invoke-DbaDbLogShipping - Fix Azure backup URL to use base URL instea…
potatoqualitee Nov 9, 2025
ee798c4
Invoke-DbaDbLogShipping - Pass database-specific Azure URL with expli…
potatoqualitee Nov 9, 2025
80ee4a1
Invoke-DbaDbLogShipping - Fix SAS token authentication by not passing…
potatoqualitee Nov 9, 2025
365e12d
Invoke-DbaDbLogShipping - Fix Azure backup to use base URL instead of…
potatoqualitee Nov 9, 2025
88b892b
Invoke-DbaDbLogShipping - Fix BackupFileName to use simple filename i…
potatoqualitee Nov 9, 2025
f114d59
Invoke-DbaDbLogShipping - Fix Azure backup to use container base URL …
potatoqualitee Nov 9, 2025
2e34f8f
Invoke-DbaDbLogShipping - Strip leading ? from SAS token when creatin…
potatoqualitee Nov 9, 2025
1741d94
Backup-DbaDatabase - Fix credentialName initialization for Azure cont…
potatoqualitee Nov 9, 2025
22a8353
Invoke-DbaDbLogShipping - Fix Azure container URL from /sql to /dbatools
potatoqualitee Nov 9, 2025
6b18809
New-DbaLogShippingPrimaryDatabase, New-DbaLogShippingSecondaryPrimary…
potatoqualitee Nov 9, 2025
b450bca
Invoke-DbaDbLogShipping - Pass SourceSqlCredential to secondary setup…
potatoqualitee Nov 9, 2025
00fabe8
Invoke-DbaDbLogShipping - Add AzureCredential support for storage acc…
potatoqualitee Nov 9, 2025
7cbf102
gh-actions - Remove deprecated storage account key log shipping test
potatoqualitee Nov 9, 2025
4744e29
gh-actions - Add detailed error logging for Azure log shipping test f…
potatoqualitee Nov 9, 2025
55b83ae
gh-actions - Fix Azure log shipping test to check correct property name
potatoqualitee Nov 9, 2025
a372946
Invoke-DbaDbLogShipping - Skip copy job creation for Azure blob storage
potatoqualitee Nov 9, 2025
0107e91
Add debug output for copy job existence in script
potatoqualitee Nov 9, 2025
c76c89f
Invoke-DbaDbLogShipping - Remove copy job created by sp_add_log_shipp…
potatoqualitee Nov 9, 2025
33ad494
Invoke-DbaDbLogShipping - Add documentation for Azure copy job creati…
potatoqualitee Nov 9, 2025
1415b40
Update Remove-DbaDbLogShipping parameter names
potatoqualitee Nov 9, 2025
420dc28
Refactor log shipping removal to use splatting
potatoqualitee Nov 9, 2025
194e20f
gh-actions - Fix Remove-DbaDbLogShipping parameter names in Azure log…
potatoqualitee Nov 9, 2025
89cfd9d
gh-actions - Add Azure blob cleanup to log shipping test
potatoqualitee Nov 9, 2025
aa51744
gh-actions - Fix Azure blob cleanup to use SAS token authentication
potatoqualitee Nov 9, 2025
397b266
Invoke-DbaDbLogShipping - Clarify Azure authentication methods in doc…
potatoqualitee Nov 9, 2025
cf7b374
Invoke-DbaDbLogShipping - Simplify Azure documentation and remove non…
potatoqualitee Nov 9, 2025
1f5423e
Invoke-DbaDbLogShipping - Add complete SAS credential creation to Azu…
potatoqualitee Nov 9, 2025
df6f24c
Invoke-DbaDbLogShipping - Use New-DbaCredential in Azure example
potatoqualitee Nov 9, 2025
5b3052d
Invoke-DbaDbLogShipping - Use Get-Credential for SAS token in Azure e…
potatoqualitee Nov 9, 2025
861754a
Merge branch 'claude/issue-5278-20251107-1711' of https://github.com/…
potatoqualitee Nov 9, 2025
7a26eb1
(do Backup-DbaDatabase, Restore-DbaDatabase, Invoke-DbaDbLogShipping)
potatoqualitee Nov 9, 2025
832d4de
Invoke-DbaDbLogShipping - Improve Azure URL validation and error mess…
github-actions[bot] Nov 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions .github/scripts/gh-actions.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,146 @@ exec sp_addrolemember 'userrole','bob';
(Get-DbaDatabase -SqlInstance $server -Database test).Name | Should -Be "test"
}

It -Skip:(-not $env:azurepasswd) "sets up log shipping to Azure blob storage using SAS token" {
# Restore credentials after Azure tests cleared PSDefaultParameterValues
$password = ConvertTo-SecureString "dbatools.IO" -AsPlainText -Force
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "sqladmin", $password

$azureUrl = "https://dbatools.blob.core.windows.net/dbatools"
$dbName = "dbatoolsci_logship_azure"

# Create SAS token credential on both instances
$primaryServer = Connect-DbaInstance -SqlInstance localhost -SqlCredential $cred
if (Get-DbaCredential -SqlInstance localhost -SqlCredential $cred -Name "[$azureUrl]") {
$primaryServer.Query("DROP CREDENTIAL [$azureUrl]")
}
# Strip leading ? from SAS token if present
$sasToken = $env:azurepasswd.TrimStart("?")
$sql = "CREATE CREDENTIAL [$azureUrl] WITH IDENTITY = N'SHARED ACCESS SIGNATURE', SECRET = N'$sasToken'"
$primaryServer.Query($sql)

$secondaryServer = Connect-DbaInstance -SqlInstance localhost:14333 -SqlCredential $cred
if (Get-DbaCredential -SqlInstance localhost:14333 -SqlCredential $cred -Name "[$azureUrl]") {
$secondaryServer.Query("DROP CREDENTIAL [$azureUrl]")
}
$secondaryServer.Query($sql)

# Create test database
$null = New-DbaDatabase -SqlInstance localhost -SqlCredential $cred -Name $dbName

# Set up log shipping
$splatLogShipping = @{
SourceSqlInstance = "localhost"
SourceSqlCredential = $cred
DestinationSqlInstance = "localhost:14333"
DestinationSqlCredential = $cred
Database = $dbName
AzureBaseUrl = $azureUrl
GenerateFullBackup = $true
Force = $true
}
$Error.Clear()
$results = Invoke-DbaDbLogShipping @splatLogShipping

# If failed, output detailed error information for debugging
if ($results.Result -ne "Success") {
Write-Host "=== Log Shipping Failed ==="
Write-Host "Results object:"
$results | Format-List * | Out-String | Write-Host
Write-Host "`nError details:"
$Error | Select-Object -First 5 | ForEach-Object {
Write-Host "---"
Write-Host "Exception: $($_.Exception.Message)"
Write-Host "Category: $($_.CategoryInfo.Category)"
Write-Host "TargetObject: $($_.TargetObject)"
if ($_.Exception.InnerException) {
Write-Host "InnerException: $($_.Exception.InnerException.Message)"
}
}
Write-Host "==========================="
}

$results.Result | Should -Be "Success"

# Verify backup job created
$jobs = Get-DbaAgentJob -SqlInstance localhost -SqlCredential $cred
$backupJob = $jobs | Where-Object Name -like "*LSBackup*$dbName*"
$backupJob | Should -Not -BeNullOrEmpty

# Verify restore job created
$jobs = Get-DbaAgentJob -SqlInstance localhost:14333 -SqlCredential $cred
$restoreJob = $jobs | Where-Object Name -like "*LSRestore*$dbName*"
$restoreJob | Should -Not -BeNullOrEmpty

# Verify NO copy job created (Azure optimization)
$copyJob = $jobs | Where-Object Name -like "*LSCopy*$dbName*"

# Debug: Show all jobs if copy job exists
if ($copyJob) {
Write-Host "=== COPY JOB STILL EXISTS ==="
Write-Host "All jobs on secondary:"
$jobs | Where-Object Name -like "*$dbName*" | ForEach-Object {
Write-Host " - $($_.Name) (Enabled: $($_.IsEnabled))"
}
Write-Host "Copy job details:"
$copyJob | Format-List Name, IsEnabled, OwnerLoginName, DateCreated | Out-String | Write-Host
Write-Host "============================"
}

$copyJob | Should -BeNullOrEmpty

# Cleanup
$splatRemoveLogShipping = @{
PrimarySqlInstance = "localhost"
PrimarySqlCredential = $cred
SecondarySqlInstance = "localhost:14333"
SecondarySqlCredential = $cred
Database = $dbName
WarningAction = "SilentlyContinue"
}
$null = Remove-DbaDbLogShipping @splatRemoveLogShipping
$null = Remove-DbaDatabase -SqlInstance localhost -SqlCredential $cred -Database $dbName -Confirm:$false
$null = Remove-DbaDatabase -SqlInstance localhost:14333 -SqlCredential $cred -Database $dbName -Confirm:$false
$primaryServer.Query("DROP CREDENTIAL [$azureUrl]")
$secondaryServer.Query("DROP CREDENTIAL [$azureUrl]")

# Clean up Azure blob storage test files
if ($env:azurepasswd) {
try {
$splatAzList = @(
"storage", "blob", "list"
"--account-name", "dbatools"
"--container-name", "dbatools"
"--prefix", $dbName
"--sas-token", $sasToken
"--query", "[].name"
"--output", "tsv"
)
$blobs = & az @splatAzList 2>$null
if ($blobs) {
$blobs -split "`n" | Where-Object { $_ } | ForEach-Object {
$splatAzDelete = @(
"storage", "blob", "delete"
"--account-name", "dbatools"
"--container-name", "dbatools"
"--name", $_
"--sas-token", $sasToken
"--output", "none"
)
$null = & az @splatAzDelete 2>$null
}
}
} catch {
# Ignore Azure cleanup errors - test may run in environments without Azure CLI
}
}
}

# Storage account key test removed - deprecated authentication method
# - Storage account keys create page blobs (limited to 1 TB, more expensive)
# - Microsoft recommends SAS tokens for SQL Server 2016+ (creates block blobs, up to 12.8 TB striped)
# - Use the SAS token test above for modern Azure blob storage log shipping

It "tests Get-DbaLastGoodCheckDb against Azure" {
$PSDefaultParameterValues.Clear()
$securestring = ConvertTo-SecureString $env:CLIENTSECRET -AsPlainText -Force
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ jobs:
CLIENTSECRET: ${{secrets.CLIENTSECRET}}
CLIENT_GUID: ${{secrets.CLIENT_GUID}}
CLIENT_GUID_SECRET: ${{secrets.CLIENT_GUID_SECRET}}
azurepasswd: ${{secrets.AZUREPASSWD}}
azurelegacypasswd: ${{secrets.AZURELEGACYPASSWD}}
run: |
Import-Module ./dbatools.psd1 -Force
Get-DbatoolsConfigValue -FullName sql.connection.trustcert | Write-Warning
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ allcommands.ps1
/.aitools
.aider*
/.shadowgit.git
/.claude
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ param(
- Use `[Parameter(Mandatory)]` not `[Parameter(Mandatory = $true)]`
- Use `[switch]` for boolean flags, not `[bool]` parameters
- Keep non-boolean attributes with values: `[Parameter(ValueFromPipelineByPropertyName = "Name")]`
- Avoid ParameterSets - their error messages are terrible and hard to use. Use Test-Bound instead and provide users with useful, concrete error messages.
- No extra line breaks between parameter declarations - keep parameter blocks compact without blank lines separating individual parameters.

### POWERSHELL v3 COMPATIBILITY

Expand Down
64 changes: 57 additions & 7 deletions private/functions/New-DbaLogShippingPrimaryDatabase.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function New-DbaLogShippingPrimaryDatabase {
It will also remove the any present schedules with the same name for the specific job.

.NOTES
Author: Sander Stad (@sqlstad, sqlstad.nl)
Author: Sander Stad (@sqlstad, sqlstad.nl), Azure blob storage support added by Claude
Website: https://dbatools.io
Copyright: (c) 2018 by dbatools, licensed under MIT
License: MIT https://opensource.org/licenses/MIT
Expand Down Expand Up @@ -117,6 +117,7 @@ function New-DbaLogShippingPrimaryDatabase {
[object]$MonitorServerSecurityMode = 1,
[System.Management.Automation.PSCredential]$MonitorCredential,
[switch]$ThresholdAlertEnabled,
[string]$AzureCredential,
[switch]$EnableException,
[switch]$Force
)
Expand All @@ -129,14 +130,63 @@ function New-DbaLogShippingPrimaryDatabase {
return
}

# Check if the backup UNC path is correct and reachable
if ([bool]([uri]$BackupShare).IsUnc -and $BackupShare -notmatch '^\\(?:\\[^<>:`"/\\|?*]+)+$') {
Stop-Function -Message "The backup share path $BackupShare should be formatted in the form \\server\share." -Target $SqlInstance
return
# Check if using Azure blob storage or traditional UNC path
$IsAzureUrl = $BackupShare -match '^https?://'

if ($IsAzureUrl) {
# Azure blob storage URL - validate format
Write-Message -Message "Using Azure blob storage for log shipping backups: $BackupShare" -Level Verbose

if ($BackupShare -notmatch '^https?://[a-z0-9]{3,24}\.blob\.core\.windows\.net/[a-z0-9]([a-z0-9\-]*[a-z0-9])?') {
Stop-Function -Message "The Azure backup URL $BackupShare should be in the format https://storageaccount.blob.core.windows.net/container (example: https://mystorageaccount.blob.core.windows.net/logshipping)" -Target $SqlInstance
return
}

# Check SQL Server version (Azure backup requires SQL Server 2012+)
if ($server.Version.Major -lt 11) {
Stop-Function -Message "Azure blob storage backup requires SQL Server 2012 or later. Instance is version $($server.Version.Major)" -Target $SqlInstance
return
}

# Validate Azure credential exists on SQL Server instance
# For storage account key authentication, use explicit credential name if provided
# For SAS token authentication, credential name must match the container URL
if ($AzureCredential) {
# Explicit credential name provided (storage account key authentication)
$credentialName = $AzureCredential
Write-Message -Message "Using explicit Azure credential name: $credentialName" -Level Verbose
} else {
# No explicit credential - assume SAS token (credential name must match URL)
# Extract base container URL from database-specific path if needed
$base = $BackupShare -split "/"
if ($base.Count -gt 4) {
# URL has subfolders (database-specific path), extract base container URL
$credentialName = $base[0] + "//" + $base[2] + "/" + $base[3]
Write-Message -Message "Extracted base credential name from database path: $credentialName" -Level Verbose
} else {
# URL is just the container
$credentialName = $BackupShare
}
}

$credential = $server.Credentials | Where-Object Name -eq $credentialName

if (-not $credential) {
Stop-Function -Message "Azure blob storage requires a SQL Server credential named '$credentialName' to exist on instance $SqlInstance. Create the credential using New-DbaCredential with either a Shared Access Signature (SAS) token or storage account key before setting up log shipping." -Target $SqlInstance
return
}

Write-Message -Message "Found Azure credential: $credentialName" -Level Verbose
} else {
if (-not ((Test-DbaPath -Path $BackupShare -SqlInstance $server) -and ((Get-Item $BackupShare).PSProvider.Name -eq 'FileSystem'))) {
Stop-Function -Message "The backup share path $BackupShare is not valid or can't be reached." -Target $SqlInstance
# Traditional UNC path - validate format and accessibility
if ([bool]([uri]$BackupShare).IsUnc -and $BackupShare -notmatch '^\\(?:\\[^<>:`"/\\|?*]+)+$') {
Stop-Function -Message "The backup share path $BackupShare should be formatted in the form \\server\share." -Target $SqlInstance
return
} else {
if (-not ((Test-DbaPath -Path $BackupShare -SqlInstance $server) -and ((Get-Item $BackupShare).PSProvider.Name -eq 'FileSystem'))) {
Stop-Function -Message "The backup share path $BackupShare is not valid or can't be reached." -Target $SqlInstance
return
}
}
}

Expand Down
Loading