Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
15 changes: 11 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Services configuration instances with consistent error handling. This function
is used by `Enable-SqlDscRsSecureConnection`, `Disable-SqlDscRsSecureConnection`,
and the `SqlRS` resource.
- Added private function `Get-HResultMessage` to translate common Windows HRESULT
error codes into human-readable messages. Used by `Invoke-RsCimMethod` to
provide actionable error messages when Reporting Services CIM methods fail
without detailed error information.
- Added parameters `RetryCount`, `RetryDelaySeconds`, and `SkipRetry` to provide
configurable retry behavior for transient CIM method failures. By default,
retries up to 3 times with 30-second delays. Handles both HRESULT failures
and exceptions from `Invoke-CimMethod`. Collects unique errors across retry
attempts with attempt number prefixes for comprehensive error reporting.
- `Invoke-ReportServerSetupAction`
- Now uses `Format-Path` with `-ExpandEnvironmentVariable` to expand environment
variables in all path parameters (`MediaPath`, `LogPath`, `InstallFolder`)
Expand Down Expand Up @@ -248,6 +249,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- SqlServerDsc
- Consolidated Reporting Services post-service-account-change integration tests
into three version-specific test files: `Post.ServiceAccountChange.SQL2017.RS`,
`Post.ServiceAccountChange.SQL2019-2022.RS`, and `Post.ServiceAccountChange.PowerBI.RS`.
SQL Server 2017 uses a workaround with `Remove-SqlDscRSEncryptedInformation`
and `Set-SqlDscRSDatabaseConnection` because the encryption key commands
fail with "Keyset does not exist" errors on SQL Server 2017.
- Split the `Test_HQRM` pipeline job into two parallel jobs (`Test_QA` and
`Test_HQRM`) to reduce overall pipeline execution time by approximately
15 minutes.
Expand Down
21 changes: 7 additions & 14 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -589,17 +589,13 @@ stages:
# Group 6 - Service account change
'tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1'
'tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1'
'tests/Integration/Commands/Post.DatabaseRights.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Post.EncryptedInformation.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Post.DatabaseConnection.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1'
'tests/Integration/Commands/New-SqlDscRSEncryptionKey.Integration.Tests.ps1'
'tests/Integration/Commands/Post.UrlReservationRecreate.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Post.ServiceAccountChange.SQL2017.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Post.ServiceAccountChange.SQL2019-2022.RS.Integration.Tests.ps1'
# Group 8
'tests/Integration/Commands/Repair-SqlDscReportingService.Integration.Tests.ps1'
'tests/Integration/Commands/Remove-SqlDscRSUrlReservation.Integration.Tests.ps1'
'tests/Integration/Commands/Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1'
'tests/Integration/Commands/New-SqlDscRSEncryptionKey.Integration.Tests.ps1'
'tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1'
# Group 9
'tests/Integration/Commands/Uninstall-SqlDscReportingService.Integration.Tests.ps1'
Expand Down Expand Up @@ -690,15 +686,12 @@ stages:
# Group 6 - Service account change
'tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1'
'tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1'
'tests/Integration/Commands/Post.DatabaseRights.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1'
'tests/Integration/Commands/New-SqlDscRSEncryptionKey.Integration.Tests.ps1'
'tests/Integration/Commands/Post.UrlReservationRecreate.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1'
'tests/Integration/Commands/Post.ServiceAccountChange.PowerBI.RS.Integration.Tests.ps1'
# Group 8
'tests/Integration/Commands/Repair-SqlDscPowerBIReportServer.Integration.Tests.ps1'
'tests/Integration/Commands/Remove-SqlDscRSUrlReservation.Integration.Tests.ps1'
'tests/Integration/Commands/Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1'
'tests/Integration/Commands/New-SqlDscRSEncryptionKey.Integration.Tests.ps1'
'tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1'
# Group 9
'tests/Integration/Commands/Uninstall-SqlDscPowerBIReportServer.Integration.Tests.ps1'
Expand Down
102 changes: 0 additions & 102 deletions source/Private/Get-HResultMessage.ps1

This file was deleted.

167 changes: 139 additions & 28 deletions source/Private/Invoke-RsCimMethod.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
handles both ExtendedErrors and Error properties that can be returned
by the CIM method.

By default, the function retries failed method calls (HRESULT failures)
up to 2 times with a 30-second delay between attempts. This behavior
can be customized using the RetryCount and RetryDelaySeconds parameters,
or disabled entirely using the SkipRetry switch. Exceptions thrown by
Invoke-CimMethod are not retried and will immediately terminate.

.PARAMETER CimInstance
The CIM instance object that contains the method to call.

Expand All @@ -17,6 +23,22 @@
.PARAMETER Arguments
A hashtable of arguments to pass to the method.

.PARAMETER Timeout
Specifies the timeout in seconds for the CIM operation. If not specified,
the default timeout of the CIM session is used.

.PARAMETER RetryCount
Specifies the number of retry attempts after the initial failure. The
default is 2. Set to 0 to disable retries (equivalent to SkipRetry).

.PARAMETER RetryDelaySeconds
Specifies the number of seconds to wait between retry attempts. The
default is 30.

.PARAMETER SkipRetry
When specified, disables retry behavior entirely. The method will only
be attempted once.

.OUTPUTS
Microsoft.Management.Infrastructure.CimMethodResult

Expand All @@ -33,11 +55,29 @@
Invoke-RsCimMethod -CimInstance $config -MethodName 'SetSecureConnectionLevel' -Arguments @{ Level = 1 }

Invokes the SetSecureConnectionLevel method with the Level argument.

.EXAMPLE
$config = Get-SqlDscRSConfiguration -InstanceName 'SSRS'
Invoke-RsCimMethod -CimInstance $config -MethodName 'GenerateDatabaseCreationScript' -Arguments @{ DatabaseName = 'ReportServer' } -Timeout 240

Invokes the GenerateDatabaseCreationScript method with a 240 second timeout.

.EXAMPLE
$config = Get-SqlDscRSConfiguration -InstanceName 'SSRS'
Invoke-RsCimMethod -CimInstance $config -MethodName 'SetDatabaseConnection' -RetryCount 5 -RetryDelaySeconds 60

Invokes the SetDatabaseConnection method with up to 5 retries and a 60 second
delay between attempts.

.EXAMPLE
$config = Get-SqlDscRSConfiguration -InstanceName 'SSRS'
Invoke-RsCimMethod -CimInstance $config -MethodName 'ListReservedUrls' -SkipRetry

Invokes the ListReservedUrls method without any retry behavior.
#>
function Invoke-RsCimMethod
{
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidThrowOutsideOfTry', '', Justification = 'Because the code throws based on an prior expression')]
[CmdletBinding()]
[CmdletBinding(DefaultParameterSetName = 'Retry')]
[OutputType([Microsoft.Management.Infrastructure.CimMethodResult])]
param
(
Expand All @@ -51,7 +91,23 @@

[Parameter()]
[System.Collections.Hashtable]
$Arguments
$Arguments,

[Parameter()]
[System.UInt32]
$Timeout,

[Parameter(ParameterSetName = 'Retry')]
[System.UInt32]
$RetryCount = 2,

[Parameter(ParameterSetName = 'Retry')]
[System.UInt32]
$RetryDelaySeconds = 30,

[Parameter(ParameterSetName = 'NoRetry')]
[System.Management.Automation.SwitchParameter]
$SkipRetry
)

$invokeCimMethodParameters = @{
Expand All @@ -64,48 +120,103 @@
$invokeCimMethodParameters['Arguments'] = $Arguments
}

$invokeCimMethodResult = $CimInstance | Invoke-CimMethod @invokeCimMethodParameters
if ($PSBoundParameters.ContainsKey('Timeout'))
{
$invokeCimMethodParameters['OperationTimeoutSec'] = $Timeout
}

# Calculate total attempts (1 initial + RetryCount retries, unless SkipRetry is specified).
$maxAttempts = if ($SkipRetry.IsPresent)
{
1
}
else
{
1 + $RetryCount
}

<#
Successfully calling the method returns $invokeCimMethodResult.HRESULT -eq 0.
If a general error occurs in the Invoke-CimMethod, like calling a method
that does not exist, returns $null in $invokeCimMethodResult.
# Track unique errors across attempts to provide comprehensive error information.
$collectedErrors = [System.Collections.Generic.List[System.String]]::new()
$uniqueErrorKeys = [System.Collections.Generic.HashSet[System.String]]::new()

cSpell: ignore HRESULT
#>
if ($invokeCimMethodResult -and $invokeCimMethodResult.HRESULT -ne 0)
# cSpell: ignore HRESULT
for ($attemptNumber = 1; $attemptNumber -le $maxAttempts; $attemptNumber++)
{
$errorDetails = $null
$invokeCimMethodResult = $CimInstance | Invoke-CimMethod @invokeCimMethodParameters

<#
The returned object property ExtendedErrors is an array
so that needs to be concatenated. Check if it has actual
content before using it.
Successfully calling the method returns $invokeCimMethodResult.HRESULT -eq 0.
If a general error occurs in the Invoke-CimMethod, like calling a method
that does not exist, returns $null in $invokeCimMethodResult or throws
an exception (which will terminate immediately without retry).
#>
if (($invokeCimMethodResult | Get-Member -Name 'ExtendedErrors') -and $invokeCimMethodResult.ExtendedErrors)
$isSuccess = $null -ne $invokeCimMethodResult -and $invokeCimMethodResult.HRESULT -eq 0

if ($isSuccess)
{
$errorDetails = $invokeCimMethodResult.ExtendedErrors -join ';'
return $invokeCimMethodResult
}

# Fall back to Error property if ExtendedErrors was empty or not present.
if (-not $errorDetails -and ($invokeCimMethodResult | Get-Member -Name 'Error') -and $invokeCimMethodResult.Error)
# Build error details for this attempt.
$errorDetails = $null
$errorKey = $null

if ($null -ne $invokeCimMethodResult -and $invokeCimMethodResult.HRESULT -ne 0)
{
$errorDetails = $invokeCimMethodResult.Error
# Handle HRESULT failure.
$hResult = $invokeCimMethodResult.HRESULT
$methodErrorDetails = $null

<#
The returned object property ExtendedErrors is an array
so that needs to be concatenated. Check if it has actual
content before using it.
#>
if (($invokeCimMethodResult | Get-Member -Name 'ExtendedErrors') -and $invokeCimMethodResult.ExtendedErrors)
{
$methodErrorDetails = $invokeCimMethodResult.ExtendedErrors -join ';'
}

# Fall back to Error property if ExtendedErrors was empty or not present.
if (-not $methodErrorDetails -and ($invokeCimMethodResult | Get-Member -Name 'Error') -and $invokeCimMethodResult.Error)
{
$methodErrorDetails = $invokeCimMethodResult.Error
}

# Use a fallback message if neither property had content.
if (-not $methodErrorDetails)
{
$methodErrorDetails = $script:localizedData.Invoke_RsCimMethod_NoErrorDetails
}

$errorDetails = $script:localizedData.Invoke_RsCimMethod_HResultError -f $hResult, $methodErrorDetails
$errorKey = "HRESULT:$hResult`:$methodErrorDetails"
}

# Use a fallback message if neither property had content.
if (-not $errorDetails)
# Track unique errors with attempt number prefix.
if ($errorDetails -and -not $uniqueErrorKeys.Contains($errorKey))
{
$errorDetails = $script:localizedData.Invoke_RsCimMethod_NoErrorDetails
$null = $uniqueErrorKeys.Add($errorKey)

$attemptError = $script:localizedData.Invoke_RsCimMethod_AttemptError -f $attemptNumber, $errorDetails
$collectedErrors.Add($attemptError)
}

# Try to translate the HRESULT code into a human-readable message.
$hResultMessage = Get-HResultMessage -HResult $invokeCimMethodResult.HRESULT
Write-Debug -Message ($script:localizedData.Invoke_RsCimMethod_AttemptFailed -f $attemptNumber, $errorDetails)

$errorMessage = $script:localizedData.Invoke_RsCimMethod_FailedToInvokeMethod -f $MethodName, $errorDetails, $hResultMessage, $invokeCimMethodResult.HRESULT
# If there are more attempts, wait before retrying.
if ($attemptNumber -lt $maxAttempts)
{
Write-Debug -Message ($script:localizedData.Invoke_RsCimMethod_WaitingBeforeRetry -f $RetryDelaySeconds, ($attemptNumber + 1))

throw $errorMessage
Start-Sleep -Seconds $RetryDelaySeconds
}
}

return $invokeCimMethodResult
# All attempts failed, throw with collected unique errors.
$allErrors = $collectedErrors -join ' '

$errorMessage = $script:localizedData.Invoke_RsCimMethod_FailedToInvokeMethod -f $MethodName, $allErrors

Write-Error -Message $errorMessage -Category 'InvalidResult' -ErrorId 'IRCM0001' -TargetObject $MethodName -ErrorAction 'Stop'
}
Loading
Loading