diff --git a/CHANGELOG.md b/CHANGELOG.md index cb2e784cd..e8d212900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ - Changes to SqlAlias - Fixed issue where exception was thrown if reg keys did not exist ([issue #949](https://github.com/PowerShell/SqlServerDsc/issues/949)). +- Changes to SqlServiceAccount + - Default services are now properly detected + ([issue #930](https://github.com/PowerShell/SqlServerDsc/issues/930)). ## 10.0.0.0 diff --git a/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.psm1 b/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.psm1 index b3f0a0af6..16ad7dcba 100644 --- a/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.psm1 +++ b/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.psm1 @@ -276,22 +276,12 @@ function Get-ServiceObject # Connect to SQL WMI $managedComputer = New-Object Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer $ServerName - # Change the regex pattern for a default instance - if ($InstanceName -ieq 'MSSQLServer') - { - $serviceNamePattern = '^MSSQLServer$' - } - else - { - $serviceNamePattern = ('\${0}$' -f $InstanceName) - } - - # Get the proper enum value - $serviceTypeFilter = ConvertTo-ManagedServiceType -ServiceType $ServiceType + # Get the service name for the specified instance and type + $serviceNameFilter = Get-SqlServiceName -InstanceName $InstanceName -ServiceType $ServiceType # Get the Service object for the specified instance/type $serviceObject = $managedComputer.Services | Where-Object -FilterScript { - ($_.Type -eq $serviceTypeFilter) -and ($_.Name -imatch $serviceNamePattern) + $_.Name -eq $serviceNameFilter } return $serviceObject @@ -365,3 +355,86 @@ function ConvertTo-ManagedServiceType return $serviceTypeValue -as [Microsoft.SqlServer.Management.Smo.Wmi.ManagedServiceType] } + +<# + .SYNOPSIS + Gets the name of a service based on the instance name and type. + + .PARAMETER InstanceName + Name of the SQL instance. + + .PARAMETER ServiceType + Type of service to be named. Must be one of the following: + DatabaseEngine, SQLServerAgent, Search, IntegrationServices, AnalysisServices, ReportingServices, SQLServerBrowser, NotificationServices. + + .EXAMPLE + Get-SqlServiceName -InstanceName 'MSSQLSERVER' -ServiceType ReportingServices +#> +function Get-SqlServiceName +{ + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $InstanceName = 'MSSQLSERVER', + + [Parameter(Mandatory = $true)] + [ValidateSet('DatabaseEngine', 'SQLServerAgent', 'Search', 'IntegrationServices', 'AnalysisServices', 'ReportingServices', 'SQLServerBrowser', 'NotificationServices')] + [System.String] + $ServiceType + ) + + # Base path in the registry for service name definitions + $serviceRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Services' + + # The value grabbed varies for a named vs default instance + if ($InstanceName -eq 'MSSQLSERVER') + { + $propertyName = 'Name' + $returnValue = '{0}' + } + else + { + $propertyName = 'LName' + $returnValue = '{0}{1}' + } + + # Map the specified type to a ManagedServiceType + $managedServiceType = ConvertTo-ManagedServiceType -ServiceType $ServiceType + + # Get the required naming property + $serviceTypeDefinition = Get-ChildItem -Path $serviceRegistryKey | Where-Object -FilterScript { + $_.GetValue('Type') -eq ($managedServiceType -as [int]) + } + + # Ensure we got a service definition + if ($serviceTypeDefinition) + { + # Multiple definitions found (thank you SSRS!) + if ($serviceTypeDefinition.Count -gt 0) + { + $serviceNamingScheme = $serviceTypeDefinition | ForEach-Object -Process { + $_.GetValue($propertyName) + } | Select-Object -Unique + } + else + { + $serviceNamingScheme = $serviceTypeDefinition.GetValue($propertyName) + } + } + else + { + $errorMessage = $script:localizedData.UnknownServiceType -f $ServiceType + New-InvalidArgumentException -Message $errorMessage -ArgumentName 'ServiceType' + } + + if ([String]::IsNullOrEmpty($serviceNamingScheme)) + { + $errorMessage = $script:localizedData.NotInstanceAware -f $ServiceType + New-InvalidResultException -Message $errorMessage + } + + # Build the name of the service and return it + return ($returnValue -f $serviceNamingScheme, $InstanceName) +} diff --git a/DSCResources/MSFT_SqlServiceAccount/en-US/MSFT_SqlServiceAccount.strings.psd1 b/DSCResources/MSFT_SqlServiceAccount/en-US/MSFT_SqlServiceAccount.strings.psd1 index f7929ba27..f65eed259 100644 --- a/DSCResources/MSFT_SqlServiceAccount/en-US/MSFT_SqlServiceAccount.strings.psd1 +++ b/DSCResources/MSFT_SqlServiceAccount/en-US/MSFT_SqlServiceAccount.strings.psd1 @@ -8,4 +8,6 @@ ConvertFrom-StringData @' RestartingService = Restarting '{0}' and any dependent services. ServiceNotFound = The {0} service on {1}\\{2} could not be found. SetServiceAccountFailed = Unable to set the service account for {0} on {1}. Message {2} + UnknownServiceType = Unknown or unsupported service type '{0}' specified! + NotInstanceAware = Service type '{0}' is not instance aware. '@ diff --git a/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 b/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 index c7f613014..61c735852 100644 --- a/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 @@ -141,7 +141,7 @@ try } <# - Creates a new ManagedComputer object for a default instance that thows an exception + Creates a new ManagedComputer object for a default instance that throws an exception when attempting to set the service account #> $mockNewObject_ManagedComputer_DefaultInstance_SetServiceAccountException = { @@ -224,6 +224,128 @@ try Verifiable = $true } + # Registry key used to index service type mappings + $testServicesRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Services' + + # Hashtable mirroring HKLM:\Software\Microsoft\Microsoft SQL Server\Services + $testServicesRegistryTable = @{ + 'Analysis Server' = @{ + LName = 'MSOLAP$' + Name = 'MSSQLServerOLAPService' + Type = 5 + } + + 'Full Text' = @{ + LName = 'msftesql$' + Name = 'msftesql' + Type = 3 + } + + 'Full-text Filter Daemon Launcher' = @{ + LName = 'MSSQLFDLauncher$' + Name = 'MSSQLFDLauncher' + Type = 9 + } + + 'Launchpad Service' = @{ + LName = 'MSSQLLaunchpad$' + Name = 'MSSQLLaunchpad' + Type = 12 + } + + 'Notification Services' = @{ + LName = 'NS$' + Name = 'NsService' + Type = 8 + } + + 'Report Server' = @{ + LName = 'ReportServer$' + Name = 'ReportServer' + Type = 6 + } + + 'ReportServer' = @{ + LName = 'ReportServer$' + Name = 'ReportServer' + Type = 6 + } + + 'SQL Agent' = @{ + LName = 'SQLAGENT$' + Name = 'SQLSERVERAGENT' + Type = 2 + } + + 'SQL Browser' = @{ + LName = '' + Name = 'SQLBrowser' + Type = 7 + } + + 'SQL Server' = @{ + LName = 'MSSQL$' + Name = 'MSSQLSERVER' + Type = 1 + } + + 'SQL Server Polybase Data Movement Service' = @{ + LName = 'SQLPBDMS$' + Name = 'SQLPBDMS' + Type = 11 + } + + 'SQL Server Polybase Engine' = @{ + LName = 'SQLPBENGINE$' + Name = 'SQLPBENGINE' + Type = 10 + } + + 'SSIS Server' = @{ + LName = '' + Name = 'MsDtsServer' + Type = 4 + } + } + + # Used by Get-SqlServiceName for service name resolution + $mockGetChildItem = { + return @( + foreach($serviceType in $testServicesRegistryTable.Keys) + { + New-Object -TypeName PSObject -Property @{ + MockKeyName = $serviceType + MockName = $testServicesRegistryTable.$serviceType.Name + MockLName = $testServicesRegistryTable.$serviceType.LName + MockType = $testServicesRegistryTable.$serviceType.Type + } | Add-Member -MemberType ScriptMethod -Name 'GetValue' -Value { + param + ( + [Parameter()] + [System.String] + $Property + ) + + $propertyToReturn = "Mock$($Property)" + return $this.$propertyToReturn + } -PassThru + } + ) + } + + # Parameter filter for Get-ChildItem mock + $mockGetChildItem_ParameterFilter = { + $Path -eq $testServicesRegistryKey + } + + # Splat to simplify creation of Mock for Get-ChildItem + $mockGetChildItemParameters = @{ + CommandName = 'Get-ChildItem' + MockWith = $mockGetChildItem + ParameterFilter = $mockGetChildItem_ParameterFilter + Verifiable = $true + } + Describe 'MSFT_SqlServerServiceAccount\ConvertTo-ManagedServiceType' -Tag 'Helper' { Context 'Translating service types' { $testCases = @( @@ -287,6 +409,163 @@ try } } + Describe 'MSFT_SqlServerServiceAccount\Get-SqlServiceName' -Tag 'Helper' { + BeforeAll { + Mock @mockGetChildItemParameters + } + + Context 'When getting the service name for a default instance' { + # Define cases for the various parameters to test + $testCases = @( + @{ + ServiceType = 'DatabaseEngine' + ExpectedServiceName = 'MSSQLSERVER' + }, + @{ + ServiceType = 'SQLServerAgent' + ExpectedServiceName = 'SQLSERVERAGENT' + }, + @{ + ServiceType = 'Search' + ExpectedServiceName = 'msftesql' + }, + @{ + ServiceType = 'IntegrationServices' + ExpectedServiceName = 'MsDtsServer' + }, + @{ + ServiceType = 'AnalysisServices' + ExpectedServiceName = 'MSSQLServerOLAPService' + }, + @{ + ServiceType = 'ReportingServices' + ExpectedServiceName = 'ReportServer' + }, + @{ + ServiceType = 'SQLServerBrowser' + ExpectedServiceName = 'SQLBrowser' + }, + @{ + ServiceType = 'NotificationServices' + ExpectedServiceName = 'NsService' + } + ) + + It 'Should return the correct service name for ' -TestCases $testCases { + param + ( + [Parameter()] + [System.String] + $ServiceType, + + [Parameter()] + [System.String] + $ExpectedServiceName + ) + + # Get the service name + Get-SqlServiceName -InstanceName $mockDefaultInstanceName -ServiceType $ServiceType | Should -Be $ExpectedServiceName + + # Ensure the mock is utilized + Assert-MockCalled -CommandName Get-ChildItem -ParameterFilter $mockGetChildItem_ParameterFilter -Scope It -Exactly -Times 1 + } + } + + Context 'When getting the service name for a named instance' { + BeforeAll { + # Define cases for the various parameters to test + $instanceAwareTestCases = @( + @{ + ServiceType = 'DatabaseEngine' + ExpectedServiceName = ('MSSQL${0}' -f $mockNamedInstance) + }, + @{ + ServiceType = 'SQLServerAgent' + ExpectedServiceName = ('SQLAGENT${0}' -f $mockNamedInstance) + }, + @{ + ServiceType = 'Search' + ExpectedServiceName = ('MSFTESQL${0}' -f $mockNamedInstance) + }, + @{ + ServiceType = 'AnalysisServices' + ExpectedServiceName = ('MSOLAP${0}' -f $mockNamedInstance) + }, + @{ + ServiceType = 'ReportingServices' + ExpectedServiceName = ('ReportServer${0}' -f $mockNamedInstance) + }, + @{ + ServiceType = 'NotificationServices' + ExpectedServiceName = ('NS${0}' -f $mockNamedInstance) + } + ) + + $notInstanceAwareTestCases = @( + @{ + ServiceType = 'IntegrationServices' + }, + @{ + ServiceType = 'SQLServerBrowser' + } + ) + } + + It 'Should return the correct service name for ' -TestCases $instanceAwareTestCases { + param + ( + [Parameter()] + [System.String] + $ServiceType, + + [Parameter()] + [System.String] + $ExpectedServiceName + ) + + # Get the service name + Get-SqlServiceName -InstanceName $mockNamedInstance -ServiceType $ServiceType | Should -Be $ExpectedServiceName + + # Ensure the mock is utilized + Assert-MockCalled -CommandName Get-ChildItem -ParameterFilter $mockGetChildItem_ParameterFilter -Scope It -Exactly -Times 1 + } + + It 'Should throw an error for which is not instance-aware' -TestCases $notInstanceAwareTestCases { + param + ( + [Parameter()] + [System.String] + $ServiceType + ) + + # Get the localized error message + $testErrorMessage = $script:localizedData.NotInstanceAware -f $ServiceType + + # An exception should be raised + { Get-SqlServiceName -InstanceName $mockNamedInstance -ServiceType $ServiceType } | Should -Throw $testErrorMessage + } + } + + Context 'When getting the service name for a type that is not defined' { + BeforeAll { + $mockGetChildItemParameters_NoServices = $mockGetChildItemParameters.Clone() + $mockGetChildItemParameters_NoServices.MockWith = { return @() } + + # Mock the Get-ChildItem command + Mock @mockGetChildItemParameters_NoServices + } + + It 'Should throw an exception if the service name cannot be derived' { + $testErrorMessage = $script:localizedData.UnknownServiceType -f 'DatabaseEngine' + + { Get-SqlServiceName -InstanceName $mockNamedInstance -ServiceType DatabaseEngine } | Should -Throw $testErrorMessage + + # Ensure the mock was called + Assert-MockCalled -CommandName Get-ChildItem -Times 1 -Exactly -Scope It + } + } + } + Describe 'MSFT_SqlServerServiceAccount\Get-ServiceObject' -Tag 'Helper' { Mock -CommandName Import-SQLPSModule -Verifiable