From b2c36022a59c13222d2966b926f8d1d063bc6f66 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Mon, 16 Apr 2018 19:53:10 +0200 Subject: [PATCH] SqlAlwaysOnService: Update integration tests to use loopback adapter (#1110) - Changes to CommonTestHelpers - Added new test helper functions in the CommonTestHelpers module. These are used by the integration tests. - Changes to SqlAlwaysOnService - Updated the integration tests to use a loopback adapter to be less intrusive in the build worker environment. --- CHANGELOG.md | 12 ++ ...T_SqlAlwaysOnService.Integration.Tests.ps1 | 107 +++++++++-- .../MSFT_SqlAlwaysOnService.config.ps1 | 94 ++++++++-- Tests/Integration/README.md | 22 +++ Tests/TestHelpers/CommonTestHelper.psm1 | 168 +++++++++++++++++- 5 files changed, 373 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a28de53fa..d8184ee28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,22 @@ ## Unreleased +- Changes to SqlServerDsc + - Added new test helper functions in the CommonTestHelpers module. These are used + by the integration tests. + - **New-IntegrationLoopbackAdapter:** Installs the PowerShell module + 'LoopbackAdapter' from PowerShell Gallery and creates a new network + loopback adapter. + - **Remove-IntegrationLoopbackAdapter:** Removes a new network loopback adapter. + - **Get-NetIPAddressNetwork:** Returns the IP network address from an IPv4 address + and prefix length. - Changes to Unit Tests - [Michael Fyffe (@TraGicCode)](https://github.com/TraGicCode): Updated the following resources unit test template to version 1.2.1 - SqlWaitForAG ([issue #1088](https://github.com/PowerShell/SqlServerDsc/issues/1088)). +- Changes to SqlAlwaysOnService + - Updated the integration tests to use a loopback adapter to be less intrusive + in the build worker environment. ## 11.1.0.0 diff --git a/Tests/Integration/MSFT_SqlAlwaysOnService.Integration.Tests.ps1 b/Tests/Integration/MSFT_SqlAlwaysOnService.Integration.Tests.ps1 index 27199c948..a2f8be771 100644 --- a/Tests/Integration/MSFT_SqlAlwaysOnService.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_SqlAlwaysOnService.Integration.Tests.ps1 @@ -25,6 +25,11 @@ $TestEnvironment = Initialize-TestEnvironment ` #endregion +$integrationErrorMessagePrefix = 'INTEGRATION ERROR MESSAGE:' + +$testRootFolderPath = Split-Path -Path $PSScriptRoot -Parent +Import-Module -Name (Join-Path -Path $testRootFolderPath -ChildPath (Join-Path -Path 'TestHelpers' -ChildPath 'CommonTestHelper.psm1')) -Force + $mockSqlInstallAccountPassword = ConvertTo-SecureString -String 'P@ssw0rd1' -AsPlainText -Force $mockSqlInstallAccountUserName = "$env:COMPUTERNAME\SqlInstall" $mockSqlInstallCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $mockSqlInstallAccountUserName, $mockSqlInstallAccountPassword @@ -34,6 +39,18 @@ try $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DSCResourceName).config.ps1" . $configFile + # These sets variables used for verification from the dot-sourced $ConfigurationData variable. + $mockLoopbackAdapterName = $ConfigurationData.AllNodes.LoopbackAdapterName + + <# + Create the loopback adapter to be used with the cluster. + The loopback adapter is kept on the build worker after test finishes + so that the cluster is available. + The IP address of the loopback adapter is set in the dependencies + configuration. + #> + New-IntegrationLoopbackAdapter -AdapterName $mockLoopbackAdapterName + Describe "$($script:DSCResourceName)_Integration" { BeforeAll { $resourceId = "[$($script:DSCResourceFriendlyName)]Integration_Test" @@ -61,7 +78,15 @@ try ErrorAction = 'Stop' } - Start-DscConfiguration @startDscConfigurationParameters + try + { + Start-DscConfiguration @startDscConfigurationParameters + } + catch + { + Write-Verbose -Message ('{0} {1}' -f $integrationErrorMessagePrefix, $_) -Verbose + throw $_ + } } | Should -Not -Throw } } @@ -89,7 +114,15 @@ try ErrorAction = 'Stop' } - Start-DscConfiguration @startDscConfigurationParameters + try + { + Start-DscConfiguration @startDscConfigurationParameters + } + catch + { + Write-Verbose -Message ('{0} {1}' -f $integrationErrorMessagePrefix, $_) -Verbose + throw $_ + } } | Should -Not -Throw } @@ -115,14 +148,33 @@ try Context ('When using configuration {0}' -f $configurationName) { It 'Should compile and apply the MOF without throwing' { { - # The variable $ConfigurationData was dot-sourced above. - & $configurationName ` - -SqlInstallCredential $mockSqlInstallCredential ` - -OutputPath $TestDrive ` - -ConfigurationData $ConfigurationData - - Start-DscConfiguration -Path $TestDrive ` - -ComputerName localhost -Wait -Verbose -Force + $configurationParameters = @{ + SqlInstallCredential = $mockSqlInstallCredential + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + try + { + Start-DscConfiguration @startDscConfigurationParameters + } + catch + { + Write-Verbose -Message ('{0} {1}' -f $integrationErrorMessagePrefix, $_) -Verbose + throw $_ + } } | Should -Not -Throw } @@ -142,6 +194,41 @@ try $resourceCurrentState.IsHadrEnabled | Should -Be $false } } + + $configurationName = "$($script:DSCResourceName)_CleanupDependencies_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + try + { + Start-DscConfiguration @startDscConfigurationParameters + } + catch + { + Write-Verbose -Message ('{0} {1}' -f $integrationErrorMessagePrefix, $_) -Verbose + throw $_ + } + } | Should -Not -Throw + } + } } } finally diff --git a/Tests/Integration/MSFT_SqlAlwaysOnService.config.ps1 b/Tests/Integration/MSFT_SqlAlwaysOnService.config.ps1 index 6f1826802..a2f245360 100644 --- a/Tests/Integration/MSFT_SqlAlwaysOnService.config.ps1 +++ b/Tests/Integration/MSFT_SqlAlwaysOnService.config.ps1 @@ -2,6 +2,27 @@ [Microsoft.DscResourceKit.IntegrationTest(OrderNumber = 2)] param() +<# + Get all adapters with static IP addresses, all of which should be ignored + when creating the cluster. +#> +$ignoreAdapterIpAddress = Get-NetAdapter | + Get-NetIPInterface | + Where-Object -FilterScript { + $_.Dhcp -eq 'Disabled' + } | Get-NetIPAddress + +$ignoreIpNetwork = @() +foreach ($adapterIpAddress in $ignoreAdapterIpAddress) +{ + <# + Get-NetIPAddressNetwork is in CommonTestHelper.psm1 which is imported + withing the integration test before this file is dot-sourced. + #> + $ipNetwork = (Get-NetIPAddressNetwork -IPAddress $adapterIpAddress.IPAddress -PrefixLength $adapterIpAddress.PrefixLength).NetworkAddress + $ignoreIpNetwork += ('{0}/{1}' -f $ipNetwork, $adapterIpAddress.PrefixLength) +} + $ConfigurationData = @{ AllNodes = @( @{ @@ -10,6 +31,13 @@ $ConfigurationData = @{ InstanceName = 'DSCSQL2016' RestartTimeout = 120 + LoopbackAdapterName = 'ClusterNetwork' + LoopbackAdapterIpAddress = '192.168.40.10' + LoopbackAdapterGateway = '192.168.40.254' + + ClusterStaticIpAddress = '192.168.40.11' + IgnoreNetwork = $ignoreIpNetwork + PSDscAllowPlainTextPassword = $true } ) @@ -18,18 +46,39 @@ $ConfigurationData = @{ Configuration MSFT_SqlAlwaysOnService_CreateDependencies_Config { Import-DscResource -ModuleName 'PSDscResources' + Import-DscResource -ModuleName 'xNetworking' - node localhost { + node localhost + { WindowsFeature 'AddFeatureFailoverClustering' { - Ensure = "Present" - Name = "Failover-clustering" + Ensure = 'Present' + Name = 'Failover-clustering' } WindowsFeature 'AddFeatureFailoverClusteringPowerShellModule' { - Ensure = "Present" - Name = "RSAT-Clustering-PowerShell" + Ensure = 'Present' + Name = 'RSAT-Clustering-PowerShell' + } + + xIPAddress LoopbackAdapterIPv4Address + { + IPAddress = $Node.LoopbackAdapterIpAddress + InterfaceAlias = $Node.LoopbackAdapterName + AddressFamily = 'IPv4' + } + + <# + Must have a default gateway for the Cluster to be able to use the + loopback adapter as clustered network. + This will be removed directly after the cluster has been created. + #> + xDefaultGatewayAddress LoopbackAdapterIPv4DefaultGateway + { + Address = $Node.LoopbackAdapterGateway + InterfaceAlias = $Node.LoopbackAdapterName + AddressFamily = 'IPv4' } <# @@ -40,24 +89,16 @@ Configuration MSFT_SqlAlwaysOnService_CreateDependencies_Config Script 'CreateActiveDirectoryDetachedCluster' { SetScript = { - <# - This is used to get the correct IP address in AppVeyor. - The logic is to get the IP address of the first NIC not - named something like 'Internal' and then addition 1 to the - last number. For example if the NIC has and IP address - of 10.0.0.10, then cluster IP address will be 10.0.0.11. - #> - $ipAddress = Get-NetIPConfiguration | Where-Object -FilterScript { - $_.InterfaceAlias -notlike '*Internal*' - } | Select-Object -ExpandProperty IPv4Address | Select-Object IPAddress - $ipAddressParts = ($ipAddress[0].IPAddress -split '\.') - [System.UInt32] $ipAddressParts[3] += 1 - $clusterStaticIpAddress = ($ipAddressParts -join '.') + $clusterStaticIpAddress = $Using:Node.ClusterStaticIpAddress + $ignoreNetwork = $Using:Node.IgnoreNetwork + + Write-Verbose -Message ('Ignoring networks: ''{0}''' -f ($ignoreNetwork -join ', ')) -Verbose $newClusterParameters = @{ Name = 'DSCCLU01' Node = $env:COMPUTERNAME StaticAddress = $clusterStaticIpAddress + IgnoreNetwork = $ignoreNetwork NoStorage = $true AdministrativeAccessPoint = 'Dns' @@ -125,6 +166,23 @@ Configuration MSFT_SqlAlwaysOnService_CreateDependencies_Config } } +Configuration MSFT_SqlAlwaysOnService_CleanupDependencies_Config +{ + Import-DscResource -ModuleName 'xNetworking' + + node localhost + { + <# + Removing the default gateway from the loopback adapter. + #> + xDefaultGatewayAddress LoopbackAdapterIPv4DefaultGateway + { + InterfaceAlias = $Node.LoopbackAdapterName + AddressFamily = 'IPv4' + } + } +} + Configuration MSFT_SqlAlwaysOnService_EnableAlwaysOn_Config { param diff --git a/Tests/Integration/README.md b/Tests/Integration/README.md index 475f7fcac..72824cdeb 100644 --- a/Tests/Integration/README.md +++ b/Tests/Integration/README.md @@ -57,6 +57,28 @@ sa | P@ssw0rd1 | Administrator of the Database Engine instances DSCSQL2016. | with this user and that means that this user must have permission to access the properties `IsClustered` and `IsHadrEnable`.* +## SqlAlwaysOnService + +**Run order:** 2 + +**Depends on:** SqlSetup + +The integration test will install a loopback adapter named 'ClusterNetwork' with +an IP address of '192.168.40.10'. To be able to activate the AlwaysOn service the +tests creates an Active Directory Detached Cluster with an IP address of +'192.168.40.11' and the cluster will ignore any other static IP addresses. + +>**Note:** During the tests the gateway of the loopback adatper named 'ClusterNetwork' +>will be set to '192.168.40.254', because it is a requirement to create the cluster, +>but the gateway will be removed in the last clean up test. Gateway is removed so +>that there will be no conflict with the default gateway. + +>*Note:** The Active Directory Detached Cluster is not fully functioning in the +>sense that it cannot start the Name resource in the 'Cluster Group', but it +>starts enough to be able to run integration tests for AlwaysOn service.s + +The tests will leave the AlwaysOn service disabled. + ## SqlRS **Run order:** 2 diff --git a/Tests/TestHelpers/CommonTestHelper.psm1 b/Tests/TestHelpers/CommonTestHelper.psm1 index 964edf35c..fdcce7fa9 100644 --- a/Tests/TestHelpers/CommonTestHelper.psm1 +++ b/Tests/TestHelpers/CommonTestHelper.psm1 @@ -18,14 +18,14 @@ function Import-SQLModuleStub $SQLVersion, [Parameter(ParameterSetName = 'Module')] - [ValidateSet('SQLPS','SqlServer')] + [ValidateSet('SQLPS', 'SqlServer')] [System.String] $ModuleName = 'SqlServer' ) # Translate the module names to their appropriate stub name $modulesAndStubs = @{ - SQLPS = 'SQLPSStub' + SQLPS = 'SQLPSStub' SqlServer = 'SqlServerStub' } @@ -64,3 +64,167 @@ function Import-SQLModuleStub Import-Module -Name $moduleStubPath -Force -Global } } + +<# + .SYNOPSIS + Installs the PowerShell module 'LoopbackAdapter' from PowerShell Gallery + and creates a new network loopback adapter. + + .PARAMETER AdapterName + The name of the loopback adapter to create. +#> +function New-IntegrationLoopbackAdapter +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $AdapterName + ) + + # Ensure the loopback adapter module is downloaded + $LoopbackAdapterModuleName = 'LoopbackAdapter' + $LoopbackAdapterModulePath = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\$LoopbackAdapterModuleName" + + # This is a helper function from DscResource.Tests\TestHelper.psm1. + Install-ModuleFromPowerShellGallery ` + -ModuleName $LoopbackAdapterModuleName ` + -DestinationPath $LoopbackAdapterModulePath + + $LoopbackAdapterModule = Join-Path ` + -Path $LoopbackAdapterModulePath ` + -ChildPath "$($LoopbackAdapterModuleName).psm1" + + # Import the loopback adapter module + Import-Module -Name $LoopbackAdapterModule -Force + + $loopbackAdapterParameters = @{ + Name = $AdapterName + } + + try + { + # Does the loopback adapter already exist? + $null = Get-LoopbackAdapter @loopbackAdapterParameters + } + catch + { + # The loopback Adapter does not exist so create it + $loopbackAdapterParameters['Force'] = $true + $loopbackAdapterParameters['ErrorAction'] = 'Stop' + + $null = New-LoopbackAdapter @loopbackAdapterParameters + } # try +} # function New-IntegrationLoopbackAdapter + +<# + .SYNOPSIS + Removes a new network loopback adapter. + + .PARAMETER AdapterName + The name of the loopback adapter to remove. +#> +function Remove-IntegrationLoopbackAdapter +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $AdapterName + ) + + $loopbackAdapterParameters = @{ + Name = $AdapterName + } + + try + { + # Does the loopback adapter exist? + $null = Get-LoopbackAdapter @loopbackAdapterParameters + } + catch + { + # Loopback Adapter does not exist - do nothing + return + } + + if ($env:APPVEYOR) + { + # Running in AppVeyor so force silent uninstall of LoopbackAdapter + $forceUninstall = $true + } + else + { + $forceUninstall = $false + } + + $loopbackAdapterParameters['Force'] = $forceUninstall + + # Remove Loopback Adapter + Remove-LoopbackAdapter @loopbackAdapterParameters +} # function Remove-IntegrationLoopbackAdapter + +<# + .SYNOPSIS + Returns the IP network address from an IPv4 address and prefix length. + + .PARAMETER IpAddress + The IP address to look up. + + .PARAMETER PrefixLength + The prefix length of the network. +#> +function Get-NetIPAddressNetwork +{ + Param + ( + [Parameter(Mandatory = $true)] + [IPAddress] + $IPAddress, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $PrefixLength + ) + + $cidrNotation = $PrefixLength + + # Create array to hold our output mask. + $subnetMaskOctet = @() + + # For loop to run through each octet. + for ($octetNumber = 0; $octetNumber -lt 4; $octetNumber++) + { + # If there are 8 or more bits left. + if ($cidrNotation -gt 7) + { + # Add 255 to mask array, and subtract 8 bits. + $subnetMaskOctet += [byte] 255 + $cidrNotation -= 8 + } + else + { + <# + Bits are less than 8, calculate octet bits and + zero out the $cidrNotation variable. + #> + $subnetMaskOctet += [byte] 255 -shl (8 - $cidrNotation) + $cidrNotation = 0 + } + } + + # Convert the calculated octets to the subnet representation. + $subnetMask = [IPAddress] ($subnetMaskOctet -join '.') + + $networkAddress = ([IPAddress]($IPAddress.Address -band $subnetMask.Address)).IPAddressToString + + $networkObject = New-Object -TypeName PSCustomObject + Add-Member -InputObject $networkObject -MemberType NoteProperty -Name 'IPAddress' -Value $IPAddress + Add-Member -InputObject $networkObject -MemberType NoteProperty -Name 'PrefixLength' -Value $PrefixLength + Add-Member -InputObject $networkObject -MemberType NoteProperty -Name 'SubnetMask' -Value $subnetMask + Add-Member -InputObject $networkObject -MemberType NoteProperty -Name 'NetworkAddress' -Value $networkAddress + + return $networkObject +}