diff --git a/ActiveDirectoryDsc.psd1 b/ActiveDirectoryDsc.psd1 index a063f957b..e0f9294d5 100644 --- a/ActiveDirectoryDsc.psd1 +++ b/ActiveDirectoryDsc.psd1 @@ -12,7 +12,7 @@ Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' # Copyright statement for this module -Copyright = '(c) 2014 Microsoft Corporation. All rights reserved.' +Copyright = '(c) 2019 Microsoft Corporation. All rights reserved.' # Description of the functionality provided by this module Description = 'The ActiveDirectoryDsc module contains DSC resources for deployment and configuration of Active Directory. @@ -25,11 +25,20 @@ PowerShellVersion = '4.0' # Minimum version of the common language runtime (CLR) required by this module CLRVersion = '4.0' +# Nested modules to load when this module is imported. +NestedModules = 'Modules\ActiveDirectoryDsc.Common\ActiveDirectoryDsc.Common.psm1' + # Functions to export from this module -FunctionsToExport = '*' +FunctionsToExport = @( + # Exported so that WaitForADDomain can use this function in a separate scope. + 'Find-DomainController' +) # Cmdlets to export from this module -CmdletsToExport = '*' +CmdletsToExport = @() + +# Aliases to export from this module +AliasesToExport = @() # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ diff --git a/CHANGELOG.md b/CHANGELOG.md index e377310cd..d0e6491d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ - Changes to ActiveDirectoryDsc - BREAKING CHANGE: Renamed the xActiveDirectory to ActiveDirectoryDsc and removed the 'x' from all resource names ([issue #312](https://github.com/PowerShell/ActiveDirectoryDsc/issues/312)). + - The helper function `Find-DomainController` is exported in the module + manifest. When running `Import-Module -Name ActiveDirectoryDsc` the + module will also import the nested module ActiveDirectoryDsc.Common. + It is exported so that the resource WaitForADDomain can reuse code + when running a background job to search for a domain controller. - Added a Requirements section to every DSC resource README with the bullet point stating "Target machine must be running Windows Server 2008 R2 or later" ([issue #399](https://github.com/PowerShell/ActiveDirectoryDsc/issues/399)). @@ -45,6 +50,8 @@ `Credential` in the function `Restore-ADCommonObject` - Removed the alias `DomainAdministratorCredential` from the parameter `Credential` in the function `Get-ADCommonParameters` + - Added function `Find-DomainController`. + - Added function `Get-CurrentUser` (moved from the resource ADKDSKey). - Updated all the examples files to be prefixed with the resource name so they are more easily discovered in PowerShell Gallery and Azure Automation ([issue #416](https://github.com/PowerShell/ActiveDirectoryDsc/issues/416)). @@ -105,7 +112,15 @@ - Added comment-based help ([issue #337](https://github.com/PowerShell/ActiveDirectoryDsc/issues/337)). - Added integration tests ([issue #348](https://github.com/PowerShell/ActiveDirectoryDsc/issues/348)). - Changes to WaitForADDomain - - Added comment-based help ([issue #341](https://github.com/PowerShell/ActiveDirectoryDsc/issues/341)) + - BREAKING CHANGE: Refactored the resource to handle timeout better and + more correctly wait for a specific amount of time, and at the same time + make the resource more intuitive to use. This change has replaced + parameters in the resource ([issue #343](https://github.com/PowerShell/ActiveDirectoryDsc/issues/343)). + - Now the resource can use built-in `PsDscRunAsCredential` instead of + specifying the `Credential` parameter ([issue #367](https://github.com/PowerShell/ActiveDirectoryDsc/issues/367)). + - New parameter `SiteName` can be used to wait for a domain controller + in a specific site in the domain. + - Added comment-based help ([issue #341](https://github.com/PowerShell/ActiveDirectoryDsc/issues/341)). - Changes to ADDomainController - BREAKING CHANGE: Renamed the parameter `DomainAdministratorCredential` to `Credential` to better indicate that it is possible to impersonate diff --git a/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt b/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt index 1a84944ed..5f1b7cf7e 100644 --- a/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt +++ b/DSCResources/MSFT_ADDomainController/en-US/about_ADDomainController.help.txt @@ -111,12 +111,10 @@ Configuration ADDomainController_AddDomainControllerToDomainMinimal_Config WaitForADDomain 'WaitForestAvailability' { - DomainName = 'contoso.com' - DomainUserCredential = $Credential - RetryCount = 10 - RetryIntervalSec = 120 + DomainName = 'contoso.com' + Credential = $Credential - DependsOn = '[WindowsFeature]RSATADPowerShell' + DependsOn = '[WindowsFeature]RSATADPowerShell' } ADDomainController 'DomainControllerMinimal' @@ -171,12 +169,10 @@ Configuration ADDomainController_AddDomainControllerToDomainAllProperties_Config WaitForADDomain 'WaitForestAvailability' { - DomainName = 'contoso.com' - DomainUserCredential = $Credential - RetryCount = 10 - RetryIntervalSec = 120 + DomainName = 'contoso.com' + Credential = $Credential - DependsOn = '[WindowsFeature]RSATADPowerShell' + DependsOn = '[WindowsFeature]RSATADPowerShell' } ADDomainController 'DomainControllerAllProperties' @@ -236,12 +232,10 @@ Configuration ADDomainController_AddDomainControllerToDomainUsingIFM_Config WaitForADDomain 'WaitForestAvailability' { - DomainName = 'contoso.com' - DomainUserCredential = $Credential - RetryCount = 10 - RetryIntervalSec = 120 + DomainName = 'contoso.com' + Credential = $Credential - DependsOn = '[WindowsFeature]RSATADPowerShell' + DependsOn = '[WindowsFeature]RSATADPowerShell' } ADDomainController 'DomainControllerWithIFM' @@ -297,12 +291,10 @@ Configuration ADDomainController_AddReadOnlyDomainController_Config WaitForADDomain 'WaitForestAvailability' { - DomainName = 'contoso.com' - DomainUserCredential = $Credential - RetryCount = 10 - RetryIntervalSec = 120 + DomainName = 'contoso.com' + Credential = $Credential - DependsOn = '[WindowsFeature]RSATADPowerShell' + DependsOn = '[WindowsFeature]RSATADPowerShell' } ADDomainController 'Read-OnlyDomainController(RODC)' diff --git a/DSCResources/MSFT_ADForestProperties/en-US/about_ADForestProperties.help.txt b/DSCResources/MSFT_ADForestProperties/en-US/about_ADForestProperties.help.txt index 6aaae6968..41ce33583 100644 --- a/DSCResources/MSFT_ADForestProperties/en-US/about_ADForestProperties.help.txt +++ b/DSCResources/MSFT_ADForestProperties/en-US/about_ADForestProperties.help.txt @@ -52,7 +52,7 @@ Configuration ADForestProperties_ReplaceForestProperties_Config node 'localhost' { - ADForestProperties $Node.ForestName + ADForestProperties 'contoso.com' { ForestName = 'contoso.com' UserPrincipalNameSuffix = 'fabrikam.com', 'industry.com' diff --git a/DSCResources/MSFT_ADKDSKey/MSFT_ADKDSKey.psm1 b/DSCResources/MSFT_ADKDSKey/MSFT_ADKDSKey.psm1 index 34880dd47..73a337e0b 100644 --- a/DSCResources/MSFT_ADKDSKey/MSFT_ADKDSKey.psm1 +++ b/DSCResources/MSFT_ADKDSKey/MSFT_ADKDSKey.psm1 @@ -513,14 +513,4 @@ function Get-ADRootDomainDN return $rootDomainDN } -<# - .SYNOPSIS - This is used to get the current user context when the resource script runs. - We are putting this in a function so we can mock it with pester -#> -function Get-CurrentUser -{ - return [System.Security.Principal.WindowsIdentity]::GetCurrent() -} - Export-ModuleMember *-TargetResource diff --git a/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.psm1 b/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.psm1 index 730bb3122..3de3391bf 100644 --- a/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.psm1 +++ b/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.psm1 @@ -6,99 +6,215 @@ Import-Module -Name (Join-Path -Path $script:localizationModulePath -ChildPath ' $script:localizedData = Get-LocalizedData -ResourceName 'MSFT_WaitForADDomain' +# This file is used to remember the number of times the node has been rebooted. +$script:restartLogFile = Join-Path $env:temp -ChildPath 'WaitForADDomain_Reboot.tmp' + +# This scriptblock is ran inside the background job. +$script:waitForDomainControllerScriptBlock = { + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $SiteName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + # Only used for unit tests, and debug purpose. + [Parameter()] + $RunOnce = $false + ) + + $domainFound = $false + + do + { + Import-Module ActiveDirectoryDsc + + $findDomainControllerParameters = @{ + DomainName = $DomainName + } + + if ($SiteName) + { + $findDomainControllerParameters['SiteName'] = $SiteName + + } + + if ($null -ne $Credential) + { + $findDomainControllerParameters['Credential'] = $Credential + } + + # Using verbose so that Receive-Job can output whats happened. + $currentDomainController = Find-DomainController @findDomainControllerParameters -Verbose + + if ($currentDomainController) + { + $domainFound = $true + } + else + { + $domainFound = $false + + # Using verbose so that Receive-Job can output whats happened. + Clear-DnsClientCache -Verbose + + Start-Sleep -Seconds 10 + } + } until ($domainFound -or $RunOnce) +} + <# .SYNOPSIS - Gets the current state of the specified Active Directory to see if it - is available. + Returns the current state of the specified Active Directory domain. .PARAMETER DomainName - The name of the Active Directory domain to wait for. + Specifies the fully qualified domain name to wait for. - .PARAMETER DomainUserCredential - The user account credentials to use to perform this task. + .PARAMETER SiteName + Specifies the site in the domain where to look for a domain controller. - .PARAMETER RetryIntervalSec - The interval in seconds between retry attempts. Default value is 60. + .PARAMETER Credential + Specifies the credentials that are used when accessing the domain, + unless the built-in PsDscRunAsCredential is used. - .PARAMETER RetryCount - The number of retries before failing. Default value is 10. + .PARAMETER WaitTimeout + Specifies the timeout in seconds that the resource will wait for the + domain to be accessible. Default value is 300 seconds. - .PARAMETER RebootRetryCount - The number of times to reboot after failing and then restart retrying. - Default value is 0 (zero). + .PARAMETER RestartCount + Specifies the number of times the node will be reboot in an effort to + connect to the domain. #> function Get-TargetResource { [OutputType([System.Collections.Hashtable])] param ( - [Parameter(Mandatory = $true)] [System.String] $DomainName, + [Parameter()] + [System.String] + $SiteName, + [Parameter()] [System.Management.Automation.PSCredential] - $DomainUserCredential, + $Credential, [Parameter()] [System.UInt64] - $RetryIntervalSec = 60, + $WaitTimeout = 300, [Parameter()] [System.UInt32] - $RetryCount = 10, + $RestartCount + ) - [Parameter()] - [System.UInt32] - $RebootRetryCount = 0 + $findDomainControllerParameters = @{ + DomainName = $DomainName + } + + Write-Verbose -Message ( + $script:localizedData.SearchDomainController -f $DomainName ) - if ($DomainUserCredential) + if ($PSBoundParameters.ContainsKey('SiteName')) { - $convertToCimCredential = New-CimInstance -ClassName MSFT_Credential -Namespace 'root/microsoft/windows/desiredstateconfiguration' -ClientOnly -Property @{ - Username = [System.String] $DomainUserCredential.UserName - Password = [System.String] $null - } + $findDomainControllerParameters['SiteName'] = $SiteName + + Write-Verbose -Message ( + $script:localizedData.SearchInSiteOnly -f $SiteName + ) + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $cimCredentialInstance = New-CimCredentialInstance -Credential $Credential + + $findDomainControllerParameters['Credential'] = $Credential + + Write-Verbose -Message ( + $script:localizedData.ImpersonatingCredentials -f $Credential.UserName + ) } else { - $convertToCimCredential = $null + if ($null -ne $PsDscContext.RunAsUser) + { + # Running using PsDscRunAsCredential + Write-Verbose -Message ( + $script:localizedData.ImpersonatingCredentials -f $PsDscContext.RunAsUser + ) + } + else + { + # Running as SYSTEM or current user. + Write-Verbose -Message ( + $script:localizedData.ImpersonatingCredentials -f (Get-CurrentUser).Name + ) + } + + $cimCredentialInstance = $null } - Write-Verbose -Message ($script:localizedData.GetDomain -f $DomainName) + $currentDomainController = Find-DomainController @findDomainControllerParameters - $domain = Get-Domain -DomainName $DomainName -DomainUserCredential $DomainUserCredential + if ($currentDomainController) + { + $domainFound = $true + $domainControllerSiteName = $currentDomainController.SiteName + + Write-Verbose -Message $script:localizedData.FoundDomainController + + } + else + { + $domainFound = $false + $domainControllerSiteName = $null + + Write-Verbose -Message $script:localizedData.NoDomainController + } return @{ - DomainName = $domain.Name - DomainUserCredential = $convertToCimCredential - RetryIntervalSec = $RetryIntervalSec - RetryCount = $RetryCount - RebootRetryCount = $RebootRetryCount + DomainName = $DomainName + SiteName = $domainControllerSiteName + Credential = $cimCredentialInstance + WaitTimeout = $WaitTimeout + RestartCount = $RestartCount + IsAvailable = $domainFound } } <# .SYNOPSIS - Sets the current state of the specified Active Directory to see if a - reboot is required. + Waits for the specified Active Directory domain to have a domain + controller that can serve connections. .PARAMETER DomainName - The name of the Active Directory domain to wait for. + Specifies the fully qualified domain name to wait for. - .PARAMETER DomainUserCredential - The user account credentials to use to perform this task. + .PARAMETER SiteName + Specifies the site in the domain where to look for a domain controller. - .PARAMETER RetryIntervalSec - The interval in seconds between retry attempts. Default value is 60. + .PARAMETER Credential + Specifies the credentials that are used when accessing the domain, + unless the built-in PsDscRunAsCredential is used. - .PARAMETER RetryCount - The number of retries before failing. Default value is 10. + .PARAMETER WaitTimeout + Specifies the timeout in seconds that the resource will wait for the + domain to be accessible. Default value is 300 seconds. - .PARAMETER RebootRetryCount - The number of times to reboot after failing and then restart retrying. - Default value is 0 (zero). + .PARAMETER RestartCount + Specifies the number of times the node will be reboot in an effort to + connect to the domain. #> function Set-TargetResource { @@ -120,96 +236,175 @@ function Set-TargetResource $DomainName, [Parameter()] - [System.Management.Automation.PSCredential] - $DomainUserCredential, + [System.String] + $SiteName, [Parameter()] - [System.UInt64] - $RetryIntervalSec = 60, + [System.Management.Automation.PSCredential] + $Credential, [Parameter()] - [System.UInt32] - $RetryCount = 10, + [System.UInt64] + $WaitTimeout = 300, [Parameter()] [System.UInt32] - $RebootRetryCount = 0 + $RestartCount + ) + Write-Verbose -Message ( + $script:localizedData.WaitingForDomain -f $DomainName, $WaitTimeout ) - $rebootLogFile = "$env:temp\WaitForADDomain_Reboot.tmp" + # Only pass properties that could be used when fetching the domain controller. + $compareTargetResourceStateParameters = @{ + DomainName = $DomainName + SiteName = $SiteName + Credential = $Credential + } + + <# + Removes any keys not bound to $PSBoundParameters. + Need the @() around this to get a new array to enumerate. + #> + @($compareTargetResourceStateParameters.Keys) | ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) + { + $compareTargetResourceStateParameters.Remove($_) + } + } + + <# + This returns array of hashtables which contain the properties ParameterName, + Expected, Actual, and InDesiredState. In this case only the property + 'IsAvailable' will be returned. + #> + $compareTargetResourceStateResult = Compare-TargetResourceState @compareTargetResourceStateParameters + + $isInDesiredState = $compareTargetResourceStateResult.Where({ $_.ParameterName -eq 'IsAvailable' }).InDesiredState - for ($count = 0; $count -lt $RetryCount; $count++) + if (-not $isInDesiredState) { - $domain = Get-Domain -DomainName $DomainName -DomainUserCredential $DomainUserCredential + $startJobParameters = @{ + ScriptBlock = $script:waitForDomainControllerScriptBlock + ArgumentList = @( + $DomainName + $SiteName + $Credential + ) + } + + Write-Verbose -Message $script:localizedData.StartBackgroundJob + + $jobSearchDomainController = Start-Job @startJobParameters - if ($domain) + Write-Verbose -Message $script:localizedData.WaitBackgroundJob + + $waitJobResult = Wait-Job -Job $jobSearchDomainController -Timeout $WaitTimeout + + # Wait-Job returns an object if the job completed or failed within the timeout. + if ($waitJobResult) { - if ($RebootRetryCount -gt 0) + Write-Verbose -Message $script:localizedData.BackgroundJobFinished + switch ($waitJobResult.State) { - Remove-Item $rebootLogFile -ErrorAction SilentlyContinue + 'Failed' + { + Write-Warning -Message $script:localizedData.BackgroundJobFailed + } + + 'Completed' + { + Write-Verbose -Message $script:localizedData.BackgroundJobSuccessful + + if ($PSBoundParameters.ContainsKey('RestartCount')) + { + Remove-RestartLogFile + } + + $foundDomainController = $true + } } - - break } else { - Write-Verbose -Message ($script:localizedData.DomainNotFoundRetrying -f $DomainName, $RetryIntervalSec) + Write-Warning -Message $script:localizedData.TimeoutReached - Start-Sleep -Seconds $RetryIntervalSec - - Clear-DnsClientCache - } - } - - if (-not $domain) - { - if ($RebootRetryCount -gt 0) - { - [System.UInt32] $rebootCount = Get-Content $RebootLogFile -ErrorAction SilentlyContinue - - if ($rebootCount -lt $RebootRetryCount) + if ($PSBoundParameters.ContainsKey('RestartCount')) { - $rebootCount = $rebootCount + 1 + # if the file does not exist this will set $currentRestartCount to 0. + [System.UInt32] $currentRestartCount = Get-Content $restartLogFile -ErrorAction SilentlyContinue - Write-Verbose -Message ($script:localizedData.DomainNotFoundRebooting -f $DomainName, $count, $RetryIntervalSec, $rebootCount, $RebootRetryCount) + if ($currentRestartCount -lt $RestartCount) + { + $currentRestartCount += 1 - Set-Content -Path $RebootLogFile -Value $rebootCount + Set-Content -Path $restartLogFile -Value $currentRestartCount - $global:DSCMachineStatus = 1 - } - else - { - throw ($script:localizedData.DomainNotFoundAfterReboot -f $DomainName, $RebootRetryCount) + Write-Verbose -Message ( + $script:localizedData.RestartWasRequested -f $currentRestartCount, $RestartCount + ) + + $global:DSCMachineStatus = 1 + } } + + # The timeout was reached and no restarts was requested. + $foundDomainController = $false } - else + + # Only output the result from the running job if Verbose was chosen. + if ($PSBoundParameters.ContainsKey('Verbose') -or $waitJobResult.State -eq 'Failed') { - throw ($script:localizedData.DomainNotFoundAfterRetry -f $DomainName, $RetryCount) + Write-Verbose -Message $script:localizedData.StartOutputBackgroundJob + + Receive-Job -Job $jobSearchDomainController + + Write-Verbose -Message $script:localizedData.EndOutputBackgroundJob } + + Write-Verbose -Message $script:localizedData.RemoveBackgroundJob + + # Forcedly remove the job even if it was not completed. + Remove-Job -Job $jobSearchDomainController -Force + } + else + { + $foundDomainController = $true + } + + if ($foundDomainController) + { + Write-Verbose -Message ($script:localizedData.DomainInDesiredState -f $DomainName) + } + else + { + throw $script:localizedData.NoDomainController } } <# .SYNOPSIS - Tests the current state of the specified Active Directory to see if it - is available. + Determines if the specified Active Directory domain have a domain controller + that can serve connections. .PARAMETER DomainName - The name of the Active Directory domain to wait for. + Specifies the fully qualified domain name to wait for. - .PARAMETER DomainUserCredential - The user account credentials to use to perform this task. + .PARAMETER SiteName + Specifies the site in the domain where to look for a domain controller. - .PARAMETER RetryIntervalSec - The interval in seconds between retry attempts. Default value is 60. + .PARAMETER Credential + Specifies the credentials that are used when accessing the domain, + unless the built-in PsDscRunAsCredential is used. - .PARAMETER RetryCount - The number of retries before failing. Default value is 10. + .PARAMETER WaitTimeout + Specifies the timeout in seconds that the resource will wait for the + domain to be accessible. Default value is 300 seconds. - .PARAMETER RebootRetryCount - The number of times to reboot after failing and then restart retrying. - Default value is 0 (zero). + .PARAMETER RestartCount + Specifies the number of times the node will be reboot in an effort to + connect to the domain. #> function Test-TargetResource { @@ -221,92 +416,150 @@ function Test-TargetResource $DomainName, [Parameter()] - [System.Management.Automation.PSCredential] - $DomainUserCredential, + [System.String] + $SiteName, [Parameter()] - [System.UInt64] - $RetryIntervalSec = 60, + [System.Management.Automation.PSCredential] + $Credential, [Parameter()] - [System.UInt32] - $RetryCount = 10, + [System.UInt64] + $WaitTimeout = 300, [Parameter()] [System.UInt32] - $RebootRetryCount = 0 - + $RestartCount ) - $rebootLogFile = "$env:temp\WaitForADDomain_Reboot.tmp" + Write-Verbose -Message ( + $script:localizedData.TestConfiguration -f $DomainName + ) - $domain = Get-Domain -DomainName $DomainName -DomainUserCredential $DomainUserCredential + # Only pass properties that could be used when fetching the domain controller. + $compareTargetResourceStateParameters = @{ + DomainName = $DomainName + SiteName = $SiteName + Credential = $Credential + } - if ($domain) - { - if ($RebootRetryCount -gt 0) + <# + Removes any keys not bound to $PSBoundParameters. + Need the @() around this to get a new array to enumerate. + #> + @($compareTargetResourceStateParameters.Keys) | ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) { - Remove-Item $rebootLogFile -ErrorAction SilentlyContinue + $compareTargetResourceStateParameters.Remove($_) } + } - Write-Verbose -Message ($script:localizedData.DomainInDesiredState -f $DomainName) + <# + This returns array of hashtables which contain the properties ParameterName, + Expected, Actual, and InDesiredState. In this case only the property + 'IsAvailable' will be returned. + #> + $compareTargetResourceStateResult = Compare-TargetResourceState @compareTargetResourceStateParameters - return $true + if ($false -in $compareTargetResourceStateResult.InDesiredState) + { + $testTargetResourceReturnValue = $false + + Write-Verbose -Message ( + $script:localizedData.DomainNotInDesiredState -f $DomainName + ) } else { - Write-Verbose -Message ($script:localizedData.DomainNotInDesiredState -f $DomainName) - return $false + $testTargetResourceReturnValue = $true + + if ($PSBoundParameters.ContainsKey('RestartCount') -and $RestartCount -gt 0 ) + { + Remove-RestartLogFile + } + + Write-Verbose -Message ( + $script:localizedData.DomainInDesiredState -f $DomainName + ) } + + return $testTargetResourceReturnValue } <# .SYNOPSIS - Gets the specified Active Directory domain + Compares the properties in the current state with the properties of the + desired state and returns a hashtable with the comparison result. .PARAMETER DomainName - The name of the Active Directory domain to wait for. + Specifies the fully qualified domain name to wait for. + + .PARAMETER SiteName + Specifies the site in the domain where to look for a domain controller. - .PARAMETER DomainUserCredential - The user account credentials to use to perform this task. + .PARAMETER Credential + Specifies the credentials that are used when accessing the domain, + unless the built-in PsDscRunAsCredential is used. #> -function Get-Domain +function Compare-TargetResourceState { - [OutputType([PSObject])] + [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $DomainName, + [Parameter()] + [System.String] + $SiteName, + [Parameter()] [System.Management.Automation.PSCredential] - $DomainUserCredential + $Credential ) - Write-Verbose -Message ($script:localizedData.CheckDomain -f $DomainName) - - if ($DomainUserCredential) - { - $context = New-Object -TypeName 'System.DirectoryServices.ActiveDirectory.DirectoryContext' -ArgumentList @('Domain', $DomainName, $DomainUserCredential.UserName, $DomainUserCredential.GetNetworkCredential().Password) - } - else - { - $context = New-Object -TypeName 'System.DirectoryServices.ActiveDirectory.DirectoryContext' -ArgumentList @('Domain', $DomainName) + $getTargetResourceParameters = @{ + DomainName = $DomainName + SiteName = $SiteName + Credential = $Credential } - try - { - $domain = ([System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($context)).domain.ToString() + <# + Removes any keys not bound to $PSBoundParameters. + Need the @() around this to get a new array to enumerate. + #> + @($getTargetResourceParameters.Keys) | ForEach-Object { + if (-not $PSBoundParameters.ContainsKey($_)) + { + $getTargetResourceParameters.Remove($_) + } + } - Write-Verbose -Message ($script:localizedData.FoundDomain -f $DomainName) + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters - return @{ - Name = $domain + <# + Only interested in the read-only property IsAvailable, which + should always be compared to the value $true. + #> + $compareResourcePropertyStateParameters = @{ + CurrentValues = $getTargetResourceResult + DesiredValues = @{ + IsAvailable = $true } + Properties = 'IsAvailable' } - catch + + return Compare-ResourcePropertyState @compareResourcePropertyStateParameters +} + +function Remove-RestartLogFile +{ + [CmdletBinding()] + param () + + if (Test-Path -Path $script:restartLogFile) { - Write-Verbose -Message ($script:localizedData.DomainNotFound -f $DomainName) + Remove-Item $script:restartLogFile -Force -ErrorAction SilentlyContinue } } diff --git a/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.schema.mof b/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.schema.mof index 03674d3b8..09e847e51 100644 --- a/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.schema.mof +++ b/DSCResources/MSFT_WaitForADDomain/MSFT_WaitForADDomain.schema.mof @@ -1,9 +1,10 @@ [ClassVersion("1.0.1.0"), FriendlyName("WaitForADDomain")] class MSFT_WaitForADDomain : OMI_BaseResource { - [Key, Description("The name of the Active Directory domain to wait for.")] String DomainName; - [Write, Description("The user account credentials to use to perform this task."), EmbeddedInstance("MSFT_Credential")] String DomainUserCredential; - [Write, Description("The interval in seconds between retry attempts. Default value is 60.")] UInt64 RetryIntervalSec; - [Write, Description("The number of retries before failing. Default value is 10.")] UInt32 RetryCount; - [Write, Description("The number of times to reboot after failing and then restart retrying. Default value is 0 (zero).")] UInt32 RebootRetryCount; + [Key, Description("Specifies the fully qualified domain name to wait for.")] String DomainName; + [Write, Description("Specifies the site in the domain where to look for a domain controller.")] String SiteName; + [Write, Description("Specifies the credentials that are used when accessing the domain, unless the built-in PsDscRunAsCredential is used."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Write, Description("Specifies the timeout in seconds that the resource will wait for the domain to be accessible. Default value is 300 seconds.")] UInt64 WaitTimeout; + [Write, Description("Specifies the number of times the node will be reboot in an effort to connect to the domain.")] UInt32 RestartCount; + [Read, Description("Returns a value indicating if a domain controller was found.")] Boolean IsAvailable; }; diff --git a/DSCResources/MSFT_WaitForADDomain/README.md b/DSCResources/MSFT_WaitForADDomain/README.md index c0eafac10..09d7a8cb9 100644 --- a/DSCResources/MSFT_WaitForADDomain/README.md +++ b/DSCResources/MSFT_WaitForADDomain/README.md @@ -1,6 +1,15 @@ # Description -The WaitForADDomain resource is used to wait for Active Directory to become available. +The WaitForADDomain resource is used to wait for Active Directory domain +controller to become available in the domain, or available in +a specific site in the domain. + +>Running the resource as *NT AUTHORITY\SYSTEM*, only work when +>evaluating the domain on the current node, for example on a +>node that should be a domain controller (which might require a +>restart of the node once the node becomes a domain controller). +>In all other scenarios use either the built-in parameter +>`PsDscRunAsCredential`, or the parameter `Credential`. ## Requirements diff --git a/DSCResources/MSFT_WaitForADDomain/en-US/MSFT_WaitForADDomain.strings.psd1 b/DSCResources/MSFT_WaitForADDomain/en-US/MSFT_WaitForADDomain.strings.psd1 index 30c594028..1bfbae470 100644 --- a/DSCResources/MSFT_WaitForADDomain/en-US/MSFT_WaitForADDomain.strings.psd1 +++ b/DSCResources/MSFT_WaitForADDomain/en-US/MSFT_WaitForADDomain.strings.psd1 @@ -1,13 +1,22 @@ # culture='en-US' ConvertFrom-StringData @' - GetDomain = Getting Domain '{0}'. (WFADD0001) - DomainNotFoundRetrying = Domain '{0}' not found. Will retry again after {1} seconds. (WFADD0002) - DomainNotFoundRebooting = Domain '{0}' not found after {1} attempts with {2} sec interval. Rebooting. Reboot attempt number {3} of {4}. (WFADD0003) - DomainNotFoundAfterReboot = Domain '{0}' NOT found after {1} Reboot attempts. (WFADD0004) - DomainNotFoundAfterRetry = Domain '{0}' NOT found after {1} attempts. (WFADD0005) + SearchDomainController = Searching for a domain controller in the domain '{0}'. (WFADD0001) + RestartWasRequested = A restart was requested when no domain controller was found. Restart number {0} of a total of {1}. (WFADD0003) DomainInDesiredState = Domain '{0}' is in the desired state. (WFADD0006) DomainNotInDesiredState = Domain '{0}' is not in the desired state. (WFADD0007) - CheckDomain = Checking for domain '{0}'. (WFADD0008) - FoundDomain = Found domain '{0}'. (WFADD0009) - DomainNotFound = Domain '{0}' not found. (WFADD0010) + FoundDomainController = Found domain controller. (WFADD0009) + NoDomainController = No domain controller was found. (WFADD0010) + ImpersonatingCredentials = Impersonating the credentials '{0}' when looking for a domain controller. (WFADD0011) + SearchInSiteOnly = Limiting the search scope for a domain controller to the site '{0}'. (WFADD0012) + TestConfiguration = Determining the current state of the Active Directory domain '{0}'. (WFADD0013) + BackgroundJobFinished = The background job finished running. (WFADD0014) + BackgroundJobFailed = The background job failed while searching for the domain controller. Returning the result of the background job. (WFADD0015) + TimeoutReached = The background job did not completed before the timeout period. (WFADD0016) + WaitingForDomain = Waiting for a domain '{0}' is available or until the timeout of {1} seconds has been reached. (WFADD0017) + StartBackgroundJob = Starting background job that will be searching for the domain controller. (WFADD0018) + WaitBackgroundJob = Waiting for the background job to finish, or timeout. (WFADD0019) + BackgroundJobSuccessful = The background job completed successfully. (WFADD0020) + StartOutputBackgroundJob = --- Start of result from background job. (WFADD0021) + EndOutputBackgroundJob = --- End of result from background job. (WFADD0022) + RemoveBackgroundJob = Removing the background job. (WFADD0023) '@ diff --git a/DSCResources/MSFT_WaitForADDomain/en-US/about_WaitForADDomain.help.txt b/DSCResources/MSFT_WaitForADDomain/en-US/about_WaitForADDomain.help.txt index 84b091723..0884a2e88 100644 --- a/DSCResources/MSFT_WaitForADDomain/en-US/about_WaitForADDomain.help.txt +++ b/DSCResources/MSFT_WaitForADDomain/en-US/about_WaitForADDomain.help.txt @@ -10,40 +10,211 @@ .PARAMETER DomainName Key - String - The name of the Active Directory domain to wait for. + Specifies the fully qualified domain name to wait for. -.PARAMETER DomainUserCredential +.PARAMETER SiteName Write - String - The user account credentials to use to perform this task. + Specifies the site in the domain where to look for a domain controller. -.PARAMETER RetryIntervalSec +.PARAMETER Credential + Write - String + Specifies the credentials that are used when accessing the domain, unless the built-in PsDscRunAsCredential is used. + +.PARAMETER WaitTimeout Write - UInt64 - The interval in seconds between retry attempts. Default value is 60. + Specifies the timeout in seconds that the resource will wait for the domain to be accessible. Default value is 300 seconds. -.PARAMETER RetryCount +.PARAMETER RestartCount Write - UInt32 - The number of retries before failing. Default value is 10. + Specifies the number of times the node will be reboot in an effort to connect to the domain. -.PARAMETER RebootRetryCount - Write - UInt32 - The number of times to reboot after failing and then restart retrying. Default value is 0 (zero). +.PARAMETER IsAvailable + Read - Boolean + Returns a value indicating if a domain controller was found. .EXAMPLE 1 -This configuration will wait for an AD Domain to respond before returning. +This configuration will wait for an Active Directory domain controller +to respond within 300 seconds (default) in the domain 'contoso.com' +before returning and allowing the configuration to continue run. +If the timeout is reached an error will be thrown. +This will use the current user when determining if the domain is available, +if run though LCM this will use SYSTEM (which might not have access). + +Configuration WaitForADDomain_WaitForDomainController_Config +{ + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + } + } +} + +.EXAMPLE 2 -Configuration WaitForADDomain_Config +This configuration will wait for an Active Directory domain controller +to respond within 300 seconds (default) in the domain 'contoso.com' +before returning and allowing the configuration to continue run. +If the timeout is reached an error will be thrown. +This will use the user credential passes to the built-in PsDscRunAsCredential +parameter when determining if the domain is available. + +Configuration WaitForADDomain_WaitForDomainControllerUsingBuiltInCredential_Config { + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + Import-DscResource -Module ActiveDirectoryDsc Node localhost { WaitForADDomain 'contoso.com' { - DomainName = 'contoso.com' - RetryIntervalSec = 60 - RetryCount = 10 - RebootRetryCount = 1 + DomainName = 'contoso.com' + + PsDscRunAsCredential = $Credential + } + } +} + +.EXAMPLE 3 + +This configuration will wait for an Active Directory domain controller +to respond within 300 seconds (default) in the domain 'contoso.com' +before returning and allowing the configuration to continue run. +If the timeout is reached an error will be thrown. +This will use the user credential passes to the parameter Credential +when determining if the domain is available. + +Configuration WaitForADDomain_WaitForDomainControllerUsingCredential_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + Credential = $Credential + } + } +} + +.EXAMPLE 4 + +This configuration will wait for an Active Directory domain controller +in the site 'Europe' to respond within 300 seconds (default) in the +domain 'contoso.com' before returning and allowing the configuration to +continue run. +If the timeout is reached an error will be thrown. +This will use the user credential passes to the built-in PsDscRunAsCredential +parameter when determining if the domain is available. + +Configuration WaitForADDomain_WaitForDomainControllerInSite_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + SiteName = 'Europe' + + PsDscRunAsCredential = $Credential + } + } +} + +.EXAMPLE 5 + +This configuration will wait for an Active Directory domain controller +to respond within 300 seconds (default) in the domain 'contoso.com' +before returning and allowing the configuration to continue run. +If the timeout is reached the node will be restarted up to two times +and again wait after each restart. If the no domain controller is found +after the second restart an error will be thrown. +This will use the user credential passes to the built-in PsDscRunAsCredential +parameter when determining if the domain is available. + +Configuration WaitForADDomain_WaitForDomainControllerWithReboot_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + RestartCount = 2 + + PsDscRunAsCredential = $Credential + } + } +} + +.EXAMPLE 6 + +This configuration will wait for an Active Directory domain controller +to respond within 600 seconds in the domain 'contoso.com' before +returning and allowing the configuration to continue run. If the timeout +is reached an error will be thrown. +This will use the user credential passes to the built-in PsDscRunAsCredential +parameter when determining if the domain is available. + +Configuration WaitForADDomain_WaitForDomainControllerWithLongerDelay_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + WaitTimeout = 600 + + PsDscRunAsCredential = $Credential } } } diff --git a/Examples/Resources/ADDomainController/1-ADDomainController_AddDomainControllerToDomainMinimal_Config.ps1 b/Examples/Resources/ADDomainController/1-ADDomainController_AddDomainControllerToDomainMinimal_Config.ps1 index eddf198c8..cbbec002c 100644 --- a/Examples/Resources/ADDomainController/1-ADDomainController_AddDomainControllerToDomainMinimal_Config.ps1 +++ b/Examples/Resources/ADDomainController/1-ADDomainController_AddDomainControllerToDomainMinimal_Config.ps1 @@ -58,12 +58,10 @@ Configuration ADDomainController_AddDomainControllerToDomainMinimal_Config WaitForADDomain 'WaitForestAvailability' { - DomainName = 'contoso.com' - DomainUserCredential = $Credential - RetryCount = 10 - RetryIntervalSec = 120 + DomainName = 'contoso.com' + Credential = $Credential - DependsOn = '[WindowsFeature]RSATADPowerShell' + DependsOn = '[WindowsFeature]RSATADPowerShell' } ADDomainController 'DomainControllerMinimal' diff --git a/Examples/Resources/ADDomainController/2-ADDomainController_AddDomainControllerToDomainAllProperties_Config.ps1 b/Examples/Resources/ADDomainController/2-ADDomainController_AddDomainControllerToDomainAllProperties_Config.ps1 index da7f2649f..a99049d57 100644 --- a/Examples/Resources/ADDomainController/2-ADDomainController_AddDomainControllerToDomainAllProperties_Config.ps1 +++ b/Examples/Resources/ADDomainController/2-ADDomainController_AddDomainControllerToDomainAllProperties_Config.ps1 @@ -58,12 +58,10 @@ Configuration ADDomainController_AddDomainControllerToDomainAllProperties_Config WaitForADDomain 'WaitForestAvailability' { - DomainName = 'contoso.com' - DomainUserCredential = $Credential - RetryCount = 10 - RetryIntervalSec = 120 + DomainName = 'contoso.com' + Credential = $Credential - DependsOn = '[WindowsFeature]RSATADPowerShell' + DependsOn = '[WindowsFeature]RSATADPowerShell' } ADDomainController 'DomainControllerAllProperties' diff --git a/Examples/Resources/ADDomainController/3-ADDomainController_AddDomainControllerToDomainUsingIFM_Config.ps1 b/Examples/Resources/ADDomainController/3-ADDomainController_AddDomainControllerToDomainUsingIFM_Config.ps1 index 6709511e9..741c04280 100644 --- a/Examples/Resources/ADDomainController/3-ADDomainController_AddDomainControllerToDomainUsingIFM_Config.ps1 +++ b/Examples/Resources/ADDomainController/3-ADDomainController_AddDomainControllerToDomainUsingIFM_Config.ps1 @@ -58,12 +58,10 @@ Configuration ADDomainController_AddDomainControllerToDomainUsingIFM_Config WaitForADDomain 'WaitForestAvailability' { - DomainName = 'contoso.com' - DomainUserCredential = $Credential - RetryCount = 10 - RetryIntervalSec = 120 + DomainName = 'contoso.com' + Credential = $Credential - DependsOn = '[WindowsFeature]RSATADPowerShell' + DependsOn = '[WindowsFeature]RSATADPowerShell' } ADDomainController 'DomainControllerWithIFM' diff --git a/Examples/Resources/ADDomainController/4-ADDomainController_AddReadOnlyDomainController_Config.ps1 b/Examples/Resources/ADDomainController/4-ADDomainController_AddReadOnlyDomainController_Config.ps1 index 8bacbd6aa..abf31e806 100644 --- a/Examples/Resources/ADDomainController/4-ADDomainController_AddReadOnlyDomainController_Config.ps1 +++ b/Examples/Resources/ADDomainController/4-ADDomainController_AddReadOnlyDomainController_Config.ps1 @@ -58,12 +58,10 @@ Configuration ADDomainController_AddReadOnlyDomainController_Config WaitForADDomain 'WaitForestAvailability' { - DomainName = 'contoso.com' - DomainUserCredential = $Credential - RetryCount = 10 - RetryIntervalSec = 120 + DomainName = 'contoso.com' + Credential = $Credential - DependsOn = '[WindowsFeature]RSATADPowerShell' + DependsOn = '[WindowsFeature]RSATADPowerShell' } ADDomainController 'Read-OnlyDomainController(RODC)' diff --git a/Examples/Resources/WaitForADDomain/1-WaitForADDomain_Config.ps1 b/Examples/Resources/WaitForADDomain/1-WaitForADDomain_WaitForDomainController_Config.ps1 similarity index 55% rename from Examples/Resources/WaitForADDomain/1-WaitForADDomain_Config.ps1 rename to Examples/Resources/WaitForADDomain/1-WaitForADDomain_WaitForDomainController_Config.ps1 index fc832b6bd..2abb736e3 100644 --- a/Examples/Resources/WaitForADDomain/1-WaitForADDomain_Config.ps1 +++ b/Examples/Resources/WaitForADDomain/1-WaitForADDomain_WaitForDomainController_Config.ps1 @@ -19,9 +19,14 @@ <# .DESCRIPTION - This configuration will wait for an AD Domain to respond before returning. + This configuration will wait for an Active Directory domain controller + to respond within 300 seconds (default) in the domain 'contoso.com' + before returning and allowing the configuration to continue run. + If the timeout is reached an error will be thrown. + This will use the current user when determining if the domain is available, + if run though LCM this will use SYSTEM (which might not have access). #> -Configuration WaitForADDomain_Config +Configuration WaitForADDomain_WaitForDomainController_Config { Import-DscResource -Module ActiveDirectoryDsc @@ -29,10 +34,7 @@ Configuration WaitForADDomain_Config { WaitForADDomain 'contoso.com' { - DomainName = 'contoso.com' - RetryIntervalSec = 60 - RetryCount = 10 - RebootRetryCount = 1 + DomainName = 'contoso.com' } } } diff --git a/Examples/Resources/WaitForADDomain/2-WaitForADDomain_WaitForDomainControllerUsingBuiltInCredential_Config.ps1 b/Examples/Resources/WaitForADDomain/2-WaitForADDomain_WaitForDomainControllerUsingBuiltInCredential_Config.ps1 new file mode 100644 index 000000000..1ef977615 --- /dev/null +++ b/Examples/Resources/WaitForADDomain/2-WaitForADDomain_WaitForDomainControllerUsingBuiltInCredential_Config.ps1 @@ -0,0 +1,50 @@ +<#PSScriptInfo +.VERSION 1.0 +.GUID 5f105122-a318-46f4-a7e9-7dc745c57878 +.AUTHOR Microsoft Corporation +.COMPANYNAME Microsoft Corporation +.COPYRIGHT (c) Microsoft Corporation. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/PowerShell/ActiveDirectoryDsc/blob/master/LICENSE +.PROJECTURI https://github.com/PowerShell/ActiveDirectoryDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES +.PRIVATEDATA +#> + +#Requires -module ActiveDirectoryDsc + +<# + .DESCRIPTION + This configuration will wait for an Active Directory domain controller + to respond within 300 seconds (default) in the domain 'contoso.com' + before returning and allowing the configuration to continue run. + If the timeout is reached an error will be thrown. + This will use the user credential passes to the built-in PsDscRunAsCredential + parameter when determining if the domain is available. +#> +Configuration WaitForADDomain_WaitForDomainControllerUsingBuiltInCredential_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + + PsDscRunAsCredential = $Credential + } + } +} diff --git a/Examples/Resources/WaitForADDomain/3-WaitForADDomain_WaitForDomainControllerUsingCredential_Config.ps1 b/Examples/Resources/WaitForADDomain/3-WaitForADDomain_WaitForDomainControllerUsingCredential_Config.ps1 new file mode 100644 index 000000000..7833077a5 --- /dev/null +++ b/Examples/Resources/WaitForADDomain/3-WaitForADDomain_WaitForDomainControllerUsingCredential_Config.ps1 @@ -0,0 +1,49 @@ +<#PSScriptInfo +.VERSION 1.0 +.GUID 5f105122-a318-46f4-a7e9-7dc745c57878 +.AUTHOR Microsoft Corporation +.COMPANYNAME Microsoft Corporation +.COPYRIGHT (c) Microsoft Corporation. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/PowerShell/ActiveDirectoryDsc/blob/master/LICENSE +.PROJECTURI https://github.com/PowerShell/ActiveDirectoryDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES +.PRIVATEDATA +#> + +#Requires -module ActiveDirectoryDsc + +<# + .DESCRIPTION + This configuration will wait for an Active Directory domain controller + to respond within 300 seconds (default) in the domain 'contoso.com' + before returning and allowing the configuration to continue run. + If the timeout is reached an error will be thrown. + This will use the user credential passes to the parameter Credential + when determining if the domain is available. +#> +Configuration WaitForADDomain_WaitForDomainControllerUsingCredential_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + Credential = $Credential + } + } +} diff --git a/Examples/Resources/WaitForADDomain/4-WaitForADDomain_WaitForDomainControllerInSite_Config.ps1 b/Examples/Resources/WaitForADDomain/4-WaitForADDomain_WaitForDomainControllerInSite_Config.ps1 new file mode 100644 index 000000000..a5e6ca722 --- /dev/null +++ b/Examples/Resources/WaitForADDomain/4-WaitForADDomain_WaitForDomainControllerInSite_Config.ps1 @@ -0,0 +1,52 @@ +<#PSScriptInfo +.VERSION 1.0 +.GUID 5f105122-a318-46f4-a7e9-7dc745c57878 +.AUTHOR Microsoft Corporation +.COMPANYNAME Microsoft Corporation +.COPYRIGHT (c) Microsoft Corporation. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/PowerShell/ActiveDirectoryDsc/blob/master/LICENSE +.PROJECTURI https://github.com/PowerShell/ActiveDirectoryDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES +.PRIVATEDATA +#> + +#Requires -module ActiveDirectoryDsc + +<# + .DESCRIPTION + This configuration will wait for an Active Directory domain controller + in the site 'Europe' to respond within 300 seconds (default) in the + domain 'contoso.com' before returning and allowing the configuration to + continue run. + If the timeout is reached an error will be thrown. + This will use the user credential passes to the built-in PsDscRunAsCredential + parameter when determining if the domain is available. +#> +Configuration WaitForADDomain_WaitForDomainControllerInSite_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + SiteName = 'Europe' + + PsDscRunAsCredential = $Credential + } + } +} diff --git a/Examples/Resources/WaitForADDomain/5-WaitForADDomain_WaitForDomainControllerWithReboot_Config.ps1 b/Examples/Resources/WaitForADDomain/5-WaitForADDomain_WaitForDomainControllerWithReboot_Config.ps1 new file mode 100644 index 000000000..d2f7c7d4e --- /dev/null +++ b/Examples/Resources/WaitForADDomain/5-WaitForADDomain_WaitForDomainControllerWithReboot_Config.ps1 @@ -0,0 +1,53 @@ +<#PSScriptInfo +.VERSION 1.0 +.GUID 5f105122-a318-46f4-a7e9-7dc745c57878 +.AUTHOR Microsoft Corporation +.COMPANYNAME Microsoft Corporation +.COPYRIGHT (c) Microsoft Corporation. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/PowerShell/ActiveDirectoryDsc/blob/master/LICENSE +.PROJECTURI https://github.com/PowerShell/ActiveDirectoryDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES +.PRIVATEDATA +#> + +#Requires -module ActiveDirectoryDsc + +<# + .DESCRIPTION + This configuration will wait for an Active Directory domain controller + to respond within 300 seconds (default) in the domain 'contoso.com' + before returning and allowing the configuration to continue run. + If the timeout is reached the node will be restarted up to two times + and again wait after each restart. If the no domain controller is found + after the second restart an error will be thrown. + This will use the user credential passes to the built-in PsDscRunAsCredential + parameter when determining if the domain is available. +#> +Configuration WaitForADDomain_WaitForDomainControllerWithReboot_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + RestartCount = 2 + + PsDscRunAsCredential = $Credential + } + } +} diff --git a/Examples/Resources/WaitForADDomain/6-WaitForADDomain_WaitForDomainControllerWithLongerDelay_Config.ps1 b/Examples/Resources/WaitForADDomain/6-WaitForADDomain_WaitForDomainControllerWithLongerDelay_Config.ps1 new file mode 100644 index 000000000..700f6896a --- /dev/null +++ b/Examples/Resources/WaitForADDomain/6-WaitForADDomain_WaitForDomainControllerWithLongerDelay_Config.ps1 @@ -0,0 +1,51 @@ +<#PSScriptInfo +.VERSION 1.0 +.GUID 5f105122-a318-46f4-a7e9-7dc745c57878 +.AUTHOR Microsoft Corporation +.COMPANYNAME Microsoft Corporation +.COPYRIGHT (c) Microsoft Corporation. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/PowerShell/ActiveDirectoryDsc/blob/master/LICENSE +.PROJECTURI https://github.com/PowerShell/ActiveDirectoryDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES +.PRIVATEDATA +#> + +#Requires -module ActiveDirectoryDsc + +<# + .DESCRIPTION + This configuration will wait for an Active Directory domain controller + to respond within 600 seconds in the domain 'contoso.com' before + returning and allowing the configuration to continue run. If the timeout + is reached an error will be thrown. + This will use the user credential passes to the built-in PsDscRunAsCredential + parameter when determining if the domain is available. +#> +Configuration WaitForADDomain_WaitForDomainControllerWithLongerDelay_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $Credential + ) + + Import-DscResource -Module ActiveDirectoryDsc + + Node localhost + { + WaitForADDomain 'contoso.com' + { + DomainName = 'contoso.com' + WaitTimeout = 600 + + PsDscRunAsCredential = $Credential + } + } +} diff --git a/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 b/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 index 4f9cc7cb9..6168dce9a 100644 --- a/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 +++ b/Modules/ActiveDirectoryDsc.Common/ActiveDirectoryDsc.Common.psm1 @@ -2100,6 +2100,10 @@ function Get-ADDirectoryContext $Credential.GetNetworkCredential().Password ) } + else + { + Write-Verbose -Message ($script:localizedData.NewDirectoryContextCredential -f (Get-CurrentUser).Name) -Verbose + } $newObjectParameters = @{ TypeName = $typeName @@ -2109,6 +2113,175 @@ function Get-ADDirectoryContext return New-Object @newObjectParameters } +<# + .SYNOPSIS + Gets the specified Active Directory domain + + .PARAMETER DomainName + Specifies the fully qualified domain name to wait for.. + + .PARAMETER DomainName + Specifies the site in the domain where to look for a domain controller. + + .PARAMETER Credential + Specifies the credentials that are used when accessing the domain, + or uses the current user if not specified. + + .NOTES + This function is designed so that it can run on any computer without + having the ActiveDirectory module installed. +#> +function Find-DomainController +{ + [OutputType([System.DirectoryServices.ActiveDirectory.DomainController])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [System.String] + $SiteName, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential + ) + + if ($PSBoundParameters.ContainsKey('SiteName')) + { + Write-Verbose -Message ($script:localizedData.SearchingForDomainControllerInSite -f $SiteName, $DomainName) -Verbose + } + else + { + Write-Verbose -Message ($script:localizedData.SearchingForDomainController -f $DomainName) -Verbose + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $adDirectoryContext = Get-ADDirectoryContext -DirectoryContextType 'Domain' -Name $DomainName -Credential $Credential + } + else + { + $adDirectoryContext = Get-ADDirectoryContext -DirectoryContextType 'Domain' -Name $DomainName + } + + $domainControllerObject = $null + + try + { + if ($PSBoundParameters.ContainsKey('SiteName')) + { + $domainControllerObject = Find-DomainControllerFindOneInSiteWrapper -DirectoryContext $adDirectoryContext -SiteName $SiteName + + Write-Verbose -Message ($script:localizedData.FoundDomainControllerInSite -f $SiteName, $DomainName) -Verbose + } + else + { + $domainControllerObject = Find-DomainControllerFindOneWrapper -DirectoryContext $adDirectoryContext + + Write-Verbose -Message ($script:localizedData.FoundDomainController -f $DomainName) -Verbose + } + } + catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] + { + Write-Verbose -Message ($script:localizedData.FailedToFindDomainController -f $DomainName) -Verbose + } + catch + { + throw $_ + } + + return $domainControllerObject +} + +<# + .SYNOPSIS + This returns a new object of the type System.DirectoryServices.ActiveDirectory.Forest + which is a class that represents an Active Directory Domain Services forest. + + .PARAMETER DirectoryContext + The Active Directory context from which the forest object is returned. + Calling the Get-ADDirectoryContext gets a value that can be provided in + this parameter. + + .NOTES + This is a wrapper to enable unit testing of the function Find-DomainController. + It is not possible to make a stub class to mock these, since these classes + are loaded into the PowerShell session when it starts. + + This function is not exported. +#> +function Find-DomainControllerFindOneWrapper +{ + [CmdletBinding()] + [OutputType([System.DirectoryServices.ActiveDirectory.DomainController])] + param + ( + [Parameter(Mandatory = $true)] + [System.DirectoryServices.ActiveDirectory.DirectoryContext] + $DirectoryContext + ) + + return [System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($DirectoryContext) +} + +<# + .SYNOPSIS + This returns a new object of the type System.DirectoryServices.ActiveDirectory.Forest + which is a class that represents an Active Directory Domain Services forest. + + .PARAMETER DirectoryContext + The Active Directory context from which the forest object is returned. + Calling the Get-ADDirectoryContext gets a value that can be provided in + this parameter. + + .PARAMETER SiteName + Specifies the site in the domain where to look for a domain controller. + + .NOTES + This is a wrapper to enable unit testing of the function Find-DomainController. + It is not possible to make a stub class to mock these, since these classes + are loaded into the PowerShell session when it starts. + + This function is not exported. +#> +function Find-DomainControllerFindOneInSiteWrapper +{ + [CmdletBinding()] + [OutputType([System.DirectoryServices.ActiveDirectory.DomainController])] + param + ( + [Parameter(Mandatory = $true)] + [System.DirectoryServices.ActiveDirectory.DirectoryContext] + $DirectoryContext, + + [Parameter(Mandatory = $true)] + [System.String] + $SiteName + ) + + return [System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($DirectoryContext, $SiteName) +} + +<# + .SYNOPSIS + This is used to get the current user context when the resource + script runs. + + .NOTES + We are putting this in a function so we can mock it with pester +#> +function Get-CurrentUser +{ + [CmdletBinding()] + [OutputType([System.String])] + param () + + return [System.Security.Principal.WindowsIdentity]::GetCurrent() +} + $script:localizedData = Get-LocalizedData -ResourceName 'ActiveDirectoryDsc.Common' -ScriptRoot $PSScriptRoot Export-ModuleMember -Function @( @@ -2146,4 +2319,6 @@ Export-ModuleMember -Function @( 'New-CimCredentialInstance' 'Add-TypeAssembly' 'Get-ADDirectoryContext' + 'Find-DomainController' + 'Get-CurrentUser' ) diff --git a/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 b/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 index 5541b8eaf..00b0ccc56 100644 --- a/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 +++ b/Modules/ActiveDirectoryDsc.Common/en-US/ActiveDirectoryDsc.Common.strings.psd1 @@ -46,4 +46,9 @@ ConvertFrom-StringData @' NewDirectoryContext = Get a new Active Directory context of the type '{0}'. (ADCOMMON0046) NewDirectoryContextTarget = The Active Directory context will target '{0}'. (ADCOMMON0047) NewDirectoryContextCredential = The Active Directory context will be accessed using the '{0}' credentials. (ADCOMMON0048) + FoundDomainController = Found a domain controller in the domain '{0}'. (ADCOMMON0049) + FoundDomainControllerInSite = Found a domain controller in the site '{0}' in the domain '{1}'. (ADCOMMON0050) + FailedToFindDomainController = No domain controller was found in the domain '{0}'. (ADCOMMON0051) + SearchingForDomainController = Searching for a domain controller in the domain '{0}'. (ADCOMMON0052) + SearchingForDomainControllerInSite = Searching for a domain controller in the site '{0}' in the domain '{1}'. (ADCOMMON0053) '@ diff --git a/Tests/Integration/MSFT_ADComputer.config.ps1 b/Tests/Integration/MSFT_ADComputer.config.ps1 index ccfc8571b..163debdcb 100644 --- a/Tests/Integration/MSFT_ADComputer.config.ps1 +++ b/Tests/Integration/MSFT_ADComputer.config.ps1 @@ -13,11 +13,9 @@ if (Test-Path -Path $configFile) } else { - $computersContainerDistinguishedName = (Get-ADDomain).ComputersContainer - if ($computersContainerDistinguishedName -match 'DC=.+') - { - $domainDistinguishedName = $matches[0] - } + $domainDistinguishedName = (Get-ADDomain).DistinguishedName + $currentDomainController = Get-ADDomainController + $domainName = $currentDomainController.Domain $ConfigurationData = @{ AllNodes = @( @@ -34,9 +32,9 @@ else OrganizationalUnitName = 'Global' Location = 'New location' - DnsHostName = 'DSCINTEGTEST01@contoso.com' + DnsHostName = 'DSCINTEGTEST01@{0}' -f $domainName ServicePrincipalNames = @('spn/a', 'spn/b') - UserPrincipalName = 'DSCINTEGTEST01@contoso.com' + UserPrincipalName = 'DSCINTEGTEST01@{0}' -f $domainName DisplayName = 'DSCINTEGTEST01' Description = 'New description' } diff --git a/Tests/Integration/MSFT_ADDomainTrust.config.ps1 b/Tests/Integration/MSFT_ADDomainTrust.config.ps1 index b530e2fa8..d672f6b26 100644 --- a/Tests/Integration/MSFT_ADDomainTrust.config.ps1 +++ b/Tests/Integration/MSFT_ADDomainTrust.config.ps1 @@ -7,8 +7,8 @@ To run this integration test there are prerequisites that need to be setup. - 1. One Domain Controller with forest contoso.com. - 2. One Domain Controller with forest lab.local. + 1. One Domain Controller as source (e.g. forest contoso.com). + 2. One Domain Controller to target with forest lab.local. 3. DNS working between the forests (conditional forwarder). 4. Credentials with permission in the target domain (lab.local). 5. If no certificate path is set to the environment variable @@ -28,16 +28,21 @@ if (Test-Path -Path $configFile) } else { + + $currentDomainController = Get-ADDomainController + $domainName = $currentDomainController.Domain + $forestName = $currentDomainController.Forest + $ConfigurationData = @{ AllNodes = @( @{ NodeName = 'localhost' CertificateFile = $env:DscPublicCertificatePath - SourceDomain = 'contoso.com' + SourceDomain = $domainName TargetDomain = 'lab.local' - SourceForest = 'contoso.com' + SourceForest = $forestName TargetForest = 'lab.local' TargetUserName = 'LAB\Administrator' diff --git a/Tests/Integration/MSFT_ADObjectEnabledState.config.ps1 b/Tests/Integration/MSFT_ADObjectEnabledState.config.ps1 index 776cef3b8..088e3a7b2 100644 --- a/Tests/Integration/MSFT_ADObjectEnabledState.config.ps1 +++ b/Tests/Integration/MSFT_ADObjectEnabledState.config.ps1 @@ -16,10 +16,10 @@ else $ConfigurationData = @{ AllNodes = @( @{ - NodeName = 'localhost' - CertificateFile = $env:DscPublicCertificatePath + NodeName = 'localhost' + CertificateFile = $env:DscPublicCertificatePath - ComputerName = 'DSCINTEGTEST01' + ComputerName = 'DSCINTEGTEST01' } ) } diff --git a/Tests/Integration/MSFT_ADUser.config.ps1 b/Tests/Integration/MSFT_ADUser.config.ps1 index 94f056442..5c15048be 100644 --- a/Tests/Integration/MSFT_ADUser.config.ps1 +++ b/Tests/Integration/MSFT_ADUser.config.ps1 @@ -15,10 +15,7 @@ else { $currentDomain = Get-ADDomain $netBiosDomainName = $currentDomain.NetBIOSName - if ($currentDomain.ComputersContainer -match 'DC=.+') - { - $domainDistinguishedName = $matches[0] - } + $domainDistinguishedName = $currentDomain.DistinguishedName $ConfigurationData = @{ AllNodes = @( diff --git a/Tests/Integration/MSFT_WaitForADDomain.Integration.Tests.ps1 b/Tests/Integration/MSFT_WaitForADDomain.Integration.Tests.ps1 new file mode 100644 index 000000000..74f12460c --- /dev/null +++ b/Tests/Integration/MSFT_WaitForADDomain.Integration.Tests.ps1 @@ -0,0 +1,349 @@ +if ($env:APPVEYOR -eq $true) +{ + Write-Warning -Message 'Integration test is not supported in AppVeyor.' + return +} + +$script:dscModuleName = 'ActiveDirectoryDsc' +$script:dscResourceFriendlyName = 'WaitForADDomain' +$script:dscResourceName = "MSFT_$($script:dscResourceFriendlyName)" + +#region HEADER +# Integration Test Template Version: 1.3.3 +[System.String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $script:moduleRoot -ChildPath 'DscResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -TestType Integration +#endregion + +try +{ + $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:dscResourceName).config.ps1" + . $configFile + + Describe "$($script:dscResourceName)_Integration" { + BeforeAll { + $resourceId = "[$($script:dscResourceFriendlyName)]Integration_Test" + } + + $configurationName = "$($script:dscResourceName)_WaitDomainControllerCurrentUser_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' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.DomainName | Should -Be $ConfigurationData.AllNodes.DomainName + $resourceCurrentState.SiteName | Should -Be $ConfigurationData.AllNodes.SiteName + $resourceCurrentState.Credential | Should -BeNullOrEmpty + $resourceCurrentState.WaitTimeout | Should -Be 300 + $resourceCurrentState.RestartCount | Should -Be 0 + $resourceCurrentState.IsAvailable | Should -BeTrue + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_WaitDomainControllerUsingCredential_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' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.DomainName | Should -Be $ConfigurationData.AllNodes.DomainName + $resourceCurrentState.SiteName | Should -Be $ConfigurationData.AllNodes.SiteName + $resourceCurrentState.Credential.UserName | Should -Be $ConfigurationData.AllNodes.AdministratorUserName + $resourceCurrentState.WaitTimeout | Should -Be 300 + $resourceCurrentState.RestartCount | Should -Be 0 + $resourceCurrentState.IsAvailable | Should -BeTrue + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_WaitDomainControllerUsingPsDscRunAsCredential_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' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.DomainName | Should -Be $ConfigurationData.AllNodes.DomainName + $resourceCurrentState.SiteName | Should -Be $ConfigurationData.AllNodes.SiteName + $resourceCurrentState.Credential | Should -BeNullOrEmpty + $resourceCurrentState.WaitTimeout | Should -Be 300 + $resourceCurrentState.RestartCount | Should -Be 0 + $resourceCurrentState.IsAvailable | Should -BeTrue + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_WaitDomainControllerInSite_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' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.DomainName | Should -Be $ConfigurationData.AllNodes.DomainName + $resourceCurrentState.SiteName | Should -Be $ConfigurationData.AllNodes.SiteName + $resourceCurrentState.Credential | Should -BeNullOrEmpty + $resourceCurrentState.WaitTimeout | Should -Be 300 + $resourceCurrentState.RestartCount | Should -Be 0 + $resourceCurrentState.IsAvailable | Should -BeTrue + } + + It 'Should return $true when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'True' + } + } + + $configurationName = "$($script:dscResourceName)_FailedWaitDomainController_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' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Throw 'No domain controller was found. (WFADD0010)' + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.DomainName | Should -Be 'unknown.local' + $resourceCurrentState.SiteName | Should -BeNullOrEmpty + $resourceCurrentState.Credential | Should -BeNullOrEmpty + $resourceCurrentState.WaitTimeout | Should -Be 3 + $resourceCurrentState.RestartCount | Should -Be 0 + $resourceCurrentState.IsAvailable | Should -BeFalse + } + + It 'Should return $false when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'False' + } + } + + $configurationName = "$($script:dscResourceName)_FailedWaitDomainControllerInSite_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' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Throw 'No domain controller was found. (WFADD0010)' + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName ` + -and $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.DomainName | Should -Be 'unknown.local' + $resourceCurrentState.SiteName | Should -BeNullOrEmpty + $resourceCurrentState.Credential | Should -BeNullOrEmpty + $resourceCurrentState.WaitTimeout | Should -Be 3 + $resourceCurrentState.RestartCount | Should -Be 0 + $resourceCurrentState.IsAvailable | Should -BeFalse + } + + It 'Should return $false when Test-DscConfiguration is run' { + Test-DscConfiguration -Verbose | Should -Be 'False' + } + } + } +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/Tests/Integration/MSFT_WaitForADDomain.config.ps1 b/Tests/Integration/MSFT_WaitForADDomain.config.ps1 new file mode 100644 index 000000000..e5037ac00 --- /dev/null +++ b/Tests/Integration/MSFT_WaitForADDomain.config.ps1 @@ -0,0 +1,202 @@ +#region HEADER +# Integration Test Config Template Version: 1.2.0 +#endregion + +<# + .NOTES + To run this integration test there are prerequisites that need to + be setup. + + Integration tests is assumed to be ran on a existing domain controller. + + 1. One Domain Controller as source (e.g. forest contoso.com). + 2. Credentials that have access to the domain controller in the domain. + 3. If no certificate path is set to the environment variable + `$env:DscPublicCertificatePath` then `PSDscAllowPlainTextPassword = $true` + must be added to the ConfigurationData-block. +#> + +$configFile = [System.IO.Path]::ChangeExtension($MyInvocation.MyCommand.Path, 'json') +if (Test-Path -Path $configFile) +{ + <# + Allows reading the configuration data from a JSON file, for real testing + scenarios outside of the CI. + #> + $ConfigurationData = Get-Content -Path $configFile | ConvertFrom-Json +} +else +{ + $currentDomain = Get-ADDomain + $netBiosDomainName = $currentDomain.NetBIOSName + + $currentDomainController = Get-ADDomainController + $domainName = $currentDomainController.Domain + $siteName = $currentDomainController.Site + + $ConfigurationData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + CertificateFile = $env:DscPublicCertificatePath + + DomainName = $domainName + SiteName = $siteName + + AdministratorUserName = ('{0}\Administrator' -f $netBiosDomainName) + AdministratorPassword = 'P@ssw0rd1' + } + ) + } +} + +<# + .SYNOPSIS + Waits for an domain controller and uses the current credentials + (NT AUTHORITY\SYSTEM when run in the integration test). + + .NOTES + Using NT AUTHORITY\SYSTEM does only work when evaluating the domain on + the current node, for example on a node that should be a domain controller. +#> +Configuration MSFT_WaitForADDomain_WaitDomainControllerCurrentUser_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + WaitForADDomain 'Integration_Test' + { + DomainName = $Node.DomainName + } + } +} + +<# + .SYNOPSIS + Waits for an domain controller and uses the parameter Credential to pass + the credentials to impersonate. +#> +Configuration MSFT_WaitForADDomain_WaitDomainControllerUsingCredential_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + WaitForADDomain 'Integration_Test' + { + DomainName = $Node.DomainName + + Credential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @( + $Node.AdministratorUserName, + (ConvertTo-SecureString -String $Node.AdministratorPassword -AsPlainText -Force) + ) + } + } +} + +<# + .SYNOPSIS + Waits for an domain controller and uses the parameter PsDscRunAsCredential + to pass the credentials to impersonate. +#> +Configuration MSFT_WaitForADDomain_WaitDomainControllerUsingPsDscRunAsCredential_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + WaitForADDomain 'Integration_Test' + { + DomainName = $Node.DomainName + + PsDscRunAsCredential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @( + $Node.AdministratorUserName, + (ConvertTo-SecureString -String $Node.AdministratorPassword -AsPlainText -Force) + ) + } + } +} + +<# + .SYNOPSIS + Waits for an domain controller in a specific site, and uses the parameter + PsDscRunAsCredential to pass the credentials to impersonate. +#> +Configuration MSFT_WaitForADDomain_WaitDomainControllerInSite_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + WaitForADDomain 'Integration_Test' + { + DomainName = $Node.DomainName + SiteName = $Node.SiteName + + PsDscRunAsCredential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @( + $Node.AdministratorUserName, + (ConvertTo-SecureString -String $Node.AdministratorPassword -AsPlainText -Force) + ) + } + } +} + +<# + .SYNOPSIS + A domain controller in the domain fails to respond within the timeout + period. +#> +Configuration MSFT_WaitForADDomain_FailedWaitDomainController_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + WaitForADDomain 'Integration_Test' + { + DomainName = 'unknown.local' + WaitTimeout = 3 + + PsDscRunAsCredential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @( + $Node.AdministratorUserName, + (ConvertTo-SecureString -String $Node.AdministratorPassword -AsPlainText -Force) + ) + } + } +} + +<# + .SYNOPSIS + A domain controller in the specified site in the domain fails to respond + within the timeout period. +#> +Configuration MSFT_WaitForADDomain_FailedWaitDomainControllerInSite_Config +{ + Import-DscResource -ModuleName 'ActiveDirectoryDsc' + + node $AllNodes.NodeName + { + WaitForADDomain 'Integration_Test' + { + DomainName = 'unknown.local' + SiteName = 'Europe' + WaitTimeout = 3 + + PsDscRunAsCredential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @( + $Node.AdministratorUserName, + (ConvertTo-SecureString -String $Node.AdministratorPassword -AsPlainText -Force) + ) + } + } +} diff --git a/Tests/Unit/ActiveDirectory.Common.Tests.ps1 b/Tests/Unit/ActiveDirectory.Common.Tests.ps1 index 8300cc423..a6d42e202 100644 --- a/Tests/Unit/ActiveDirectory.Common.Tests.ps1 +++ b/Tests/Unit/ActiveDirectory.Common.Tests.ps1 @@ -1,3 +1,13 @@ +<# + This module is loaded as a nested module when the ActiveDirectoryDsc module is imported, + remove the module from the session to avoid the error message: + + Multiple Script modules named 'ActiveDirectoryDsc.Common' + are currently loaded. Make sure to remove any extra copies + of the module from your session before testing. +#> +Get-Module -Name 'ActiveDirectoryDsc.Common' -All | Remove-Module -Force + # Import the ActiveDirectoryDsc.Common module to test $script:resourceModulePath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent $script:modulesFolderPath = Join-Path -Path $script:resourceModulePath -ChildPath 'Modules\ActiveDirectoryDsc.Common' @@ -2255,4 +2265,124 @@ InModuleScope 'ActiveDirectoryDsc.Common' { Assert-VerifiableMock } } + + Describe 'ActiveDirectoryDsc.Common\Find-DomainController' -Tag 'FindDomainController' { + Context 'When a domain controller is found in a domain' { + BeforeAll { + $mockAdministratorUser = 'admin@contoso.com' + $mockAdministratorPassword = 'P@ssw0rd-12P@ssw0rd-12' + $mockAdministratorCredential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @( + $mockAdministratorUser, + ($mockAdministratorPassword | ConvertTo-SecureString -AsPlainText -Force) + ) + + $mockDomainName = 'contoso.com' + + Mock -CommandName Find-DomainControllerFindOneInSiteWrapper + Mock -CommandName Find-DomainControllerFindOneWrapper + Mock -CommandName Get-ADDirectoryContext -MockWith { + return New-Object ` + -TypeName 'System.DirectoryServices.ActiveDirectory.DirectoryContext' ` + -ArgumentList @('Domain', $mockDomainName) + } + } + + Context 'When the calling with only the parameter DomainName' { + It 'Should not throw and call the correct mocks' { + { Find-DomainController -DomainName $mockDomainName -Verbose } | Should -Not -Throw + + Assert-MockCalled -CommandName Get-ADDirectoryContext -ParameterFilter { + $Name -eq $mockDomainName ` + -and -not $PSBoundParameters.ContainsKey('Credential') + } -Exactly -Times 1 -Scope It + + Assert-MockCalled -Command Find-DomainControllerFindOneWrapper -Exactly -Times 1 -Scope It + Assert-MockCalled -Command Find-DomainControllerFindOneInSiteWrapper -Exactly -Times 0 -Scope It + } + } + + Context 'When the calling with the parameter SiteName' { + It 'Should not throw and call the correct mocks' { + { Find-DomainController -DomainName $mockDomainName -SiteName 'Europe' -Verbose } | Should -Not -Throw + + Assert-MockCalled -CommandName Get-ADDirectoryContext -ParameterFilter { + $Name -eq $mockDomainName ` + -and -not $PSBoundParameters.ContainsKey('Credential') + } -Exactly -Times 1 -Scope It + + Assert-MockCalled -Command Find-DomainControllerFindOneWrapper -Exactly -Times 0 -Scope It + Assert-MockCalled -Command Find-DomainControllerFindOneInSiteWrapper -Exactly -Times 1 -Scope It + } + } + + Context 'When the calling with the parameter Credential' { + It 'Should not throw and call the correct mocks' { + { Find-DomainController -DomainName $mockDomainName -Credential $mockAdministratorCredential -Verbose } | Should -Not -Throw + + Assert-MockCalled -CommandName Get-ADDirectoryContext -ParameterFilter { + $Name -eq $mockDomainName ` + -and $PSBoundParameters.ContainsKey('Credential') + } -Exactly -Times 1 -Scope It + + Assert-MockCalled -Command Find-DomainControllerFindOneWrapper -Exactly -Times 1 -Scope It + Assert-MockCalled -Command Find-DomainControllerFindOneInSiteWrapper -Exactly -Times 0 -Scope It + } + } + + Assert-VerifiableMock + } + + Context 'When no domain controller is found' { + BeforeAll { + Mock -CommandName Get-ADDirectoryContext -MockWith { + return New-Object ` + -TypeName 'System.DirectoryServices.ActiveDirectory.DirectoryContext' ` + -ArgumentList @('Domain', $mockDomainName) + } + + Mock -CommandName Find-DomainControllerFindOneWrapper -MockWith { + throw New-object -TypeName 'System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException' + } + + Mock -CommandName Write-Verbose -ParameterFilter { + $Message -eq ($script:localizedData.FailedToFindDomainController -f $mockDomainName) + } -MockWith { + Write-Verbose -Message ('VERBOSE OUTPUT FROM MOCK: {0}' -f $Message) -Verbose + } + } + + It 'Should not throw and call the correct mocks' { + { Find-DomainController -DomainName $mockDomainName -Verbose } | Should -Not -Throw + + Assert-MockCalled -Command Find-DomainControllerFindOneWrapper -Exactly -Times 1 -Scope It + Assert-MockCalled -Command Write-Verbose -Exactly -Times 1 -Scope It + } + + Assert-VerifiableMock + } + + Context 'When the lookup for a domain controller fails' { + BeforeAll { + Mock -CommandName Get-ADDirectoryContext -MockWith { + return New-Object ` + -TypeName 'System.DirectoryServices.ActiveDirectory.DirectoryContext' ` + -ArgumentList @('Domain', $mockDomainName) + } + + $mockErrorMessage = 'Mocked error' + + Mock -CommandName Find-DomainControllerFindOneWrapper -MockWith { + throw $mockErrorMessage + } + } + + It 'Should throw the correct error' { + { Find-DomainController -DomainName $mockDomainName -Verbose } | Should -Throw $mockErrorMessage + + Assert-MockCalled -Command Find-DomainControllerFindOneWrapper -Exactly -Times 1 -Scope It + } + + Assert-VerifiableMock + } + } } diff --git a/Tests/Unit/MSFT_WaitForADDomain.Tests.ps1 b/Tests/Unit/MSFT_WaitForADDomain.Tests.ps1 index acd031149..60594b465 100644 --- a/Tests/Unit/MSFT_WaitForADDomain.Tests.ps1 +++ b/Tests/Unit/MSFT_WaitForADDomain.Tests.ps1 @@ -36,141 +36,595 @@ try Invoke-TestSetup InModuleScope $script:dscResourceName { - $domainUserCredential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @( - 'Username', - $(ConvertTo-SecureString -String 'Password' -AsPlainText -Force) + $mockUserName = 'User1' + $mockDomainUserCredential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @( + $mockUserName, + (ConvertTo-SecureString -String 'Password' -AsPlainText -Force) ) - $domainName = 'example.com' - $testParams = @{ - DomainName = $domainName - DomainUserCredential = $domainUserCredential - RetryIntervalSec = 10 - RetryCount = 5 - } + $mockDomainName = 'example.com' + $mockSiteName = 'Europe' - $rebootTestParams = @{ - DomainName = $domainName - DomainUserCredential = $domainUserCredential - RetryIntervalSec = 10 - RetryCount = 5 - RebootRetryCount = 3 + $mockDefaultParameters = @{ + DomainName = $mockDomainName + Verbose = $true } - $fakeDomainObject = @{Name = $domainName} - #region Function Get-TargetResource - Describe 'WaitForADDomain\Get-TargetResource' { - It 'Returns a "System.Collections.Hashtable" object type' { - Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} - $targetResource = Get-TargetResource @testParams - $targetResource -is [System.Collections.Hashtable] | Should -Be $true - } + Describe 'WaitForADDomain\Get-TargetResource' -Tag 'Get' { + Context 'When the system is in the desired state' { + Context 'When no domain controller is found in the domain' { + BeforeAll { + Mock -CommandName Find-DomainController -MockWith { + return $null + } - It "Returns DomainName = $($testParams.DomainName) when domain is found" { - Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} - $targetResource = Get-TargetResource @testParams - $targetResource.DomainName | Should -Be $testParams.DomainName - } + $getTargetResourceParameters = $mockDefaultParameters.Clone() + } + + It 'Should return the same values as passed as parameters' { + $result = Get-TargetResource @getTargetResourceParameters + $result.DomainName | Should -Be $mockDomainName + } + + It 'Should return default value for property WaitTimeout' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + $getTargetResourceResult.WaitTimeout | Should -Be 300 + } + + It 'Should return $null for the rest of the properties' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + $getTargetResourceResult.SiteName | Should -BeNullOrEmpty + $getTargetResourceResult.Credential | Should -BeNullOrEmpty + $getTargetResourceResult.RestartCount | Should -Be 0 + } + } + + Context 'When a domain controller is found in the domain' { + BeforeAll { + Mock -CommandName Find-DomainController -MockWith { + return New-Object -TypeName PSObject | + Add-Member -MemberType ScriptProperty -Name 'Domain' -Value { + New-Object -TypeName PSObject | + Add-Member -MemberType ScriptMethod -Name 'ToString' -Value { + return $mockDomainName + } -PassThru -Force + } -PassThru | + Add-Member -MemberType NoteProperty -Name 'SiteName' -Value $mockSiteName -PassThru -Force + } + + $getTargetResourceParameters = $mockDefaultParameters.Clone() + } + + Context 'When using the default parameters' { + It 'Should return the same values as passed as parameters' { + $result = Get-TargetResource @getTargetResourceParameters + $result.DomainName | Should -Be $mockDomainName + } + + It 'Should return default value for property WaitTimeout' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + $getTargetResourceResult.WaitTimeout | Should -Be 300 + } + + It 'Should return $null for the rest of the properties' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + $getTargetResourceResult.SiteName | Should -Be 'Europe' + $getTargetResourceResult.Credential | Should -BeNullOrEmpty + $getTargetResourceResult.RestartCount | Should -Be 0 + } + } - It "Returns an empty DomainName when domain is not found" { - Mock -CommandName Get-Domain - $targetResource = Get-TargetResource @testParams - $targetResource.DomainName | Should -Be $null + Context 'When using all available parameters' { + BeforeAll { + $getTargetResourceParameters['Credential'] = $mockDomainUserCredential + $getTargetResourceParameters['SiteName'] = 'Europe' + $getTargetResourceParameters['WaitTimeout'] = 600 + $getTargetResourceParameters['RestartCount'] = 2 + } + + It 'Should return the same values as passed as parameters' { + $result = Get-TargetResource @getTargetResourceParameters + $result.DomainName | Should -Be $mockDomainName + $result.SiteName | Should -Be 'Europe' + $result.WaitTimeout | Should -Be 600 + $result.RestartCount | Should -Be 2 + $result.Credential.UserName | Should -Be $mockUserName + } + } + + Context 'When using all available parameters' { + BeforeAll { + $mockBuiltInCredentialName = 'BuiltInCredential' + + # Mock PsDscRunAsCredential context. + $PsDscContext = @{ + RunAsUser = $mockBuiltInCredentialName + } + + Mock -CommandName Write-Verbose -ParameterFilter { + $Message -eq ($script:localizedData.ImpersonatingCredentials -f $mockBuiltInCredentialName) + } -MockWith { + Write-Verbose -Message ('VERBOSE OUTPUT FROM MOCK: {0}' -f $Message) -Verbose + } + + $getTargetResourceParameters = $mockDefaultParameters.Clone() + } + + It 'Should return the same values as passed as parameters' { + $result = Get-TargetResource @getTargetResourceParameters + $result.DomainName | Should -Be $mockDomainName + $result.Credential | Should -BeNullOrEmpty + + Assert-MockCalled -CommandName Write-Verbose -Exactly -Times 1 -Scope It + } + } + } } } #endregion #region Function Test-TargetResource - Describe 'WaitForADDomain\Test-TargetResource' { - It 'Returns a "System.Boolean" object type' { - Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} - $targetResource = Test-TargetResource @testParams - $targetResource -is [System.Boolean] | Should -Be $true - } + Describe 'WaitForADDomain\Test-TargetResource' -tag 'Test' { + Context 'When the system is in the desired state' { + Context 'When a domain controller is found' { + BeforeAll { + Mock -CommandName Compare-TargetResourceState -MockWith { + return @( + @{ + ParameterName = 'IsAvailable' + InDesiredState = $true + } + ) + } + } + + It 'Should return $true' { + $testTargetResourceResult = Test-TargetResource @mockDefaultParameters + $testTargetResourceResult | Should -BeTrue + + Assert-MockCalled -CommandName Compare-TargetResourceState -Exactly -Times 1 -Scope It + } + } + + Context 'When a domain controller is found, and RestartCount was used' { + BeforeAll { + Mock -CommandName Compare-TargetResourceState -MockWith { + return @( + @{ + ParameterName = 'IsAvailable' + InDesiredState = $true + } + ) + } + + Mock -CommandName Remove-Item + Mock -CommandName Test-Path -MockWith { + return $true + } - It 'Passes when domain found' { - Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} - Test-TargetResource @testParams | Should -Be $true + $testTargetResourceParameters = $mockDefaultParameters.Clone() + $testTargetResourceParameters['RestartCount'] = 2 + } + + It 'Should return $true' { + $testTargetResourceResult = Test-TargetResource @testTargetResourceParameters + $testTargetResourceResult | Should -BeTrue + + Assert-MockCalled -CommandName Compare-TargetResourceState -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Remove-Item -Exactly -Times 1 -Scope It + } + } } - It 'Fails when domain not found' { - Mock -CommandName Get-Domain - Test-TargetResource @testParams | Should -Be $false + Context 'When the system is not in the desired state' { + Context 'When a domain controller cannot be reached' { + BeforeAll { + Mock -CommandName Compare-TargetResourceState -MockWith { + return @( + @{ + ParameterName = 'IsAvailable' + InDesiredState = $false + } + ) + } + } + + It 'Should return $false' { + $testTargetResourceResult = Test-TargetResource @mockDefaultParameters + $testTargetResourceResult | Should -BeFalse + + Assert-MockCalled -CommandName Compare-TargetResourceState -Exactly -Times 1 -Scope It + } + } } } #endregion + Describe 'MSFT_ADDomainTrust\Compare-TargetResourceState' -Tag 'Compare' { + BeforeAll { + $mockGetTargetResource_Absent = { + return @{ + DomainName = $mockDomainName + SiteName = $null + Credential = $null + WaitTimeout = 300 + RestartCount = 0 + IsAvailable = $false + } + } - #region Function Set-TargetResource - Describe 'WaitForADDomain\Set-TargetResource' { - BeforeEach{ - $global:DSCMachineStatus = $null + $mockGetTargetResource_Present = { + return @{ + DomainName = $mockDomainName + SiteName = $mockSiteName + Credential = $null + WaitTimeout = 300 + RestartCount = 0 + IsAvailable = $true + } + } } - It "Doesn't throw exception and doesn't call Start-Sleep, Clear-DnsClientCache or set `$global:DSCMachineStatus when domain found" { - Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} - Mock -CommandName Start-Sleep - Mock -CommandName Clear-DnsClientCache - {Set-TargetResource @testParams} | Should -Not -Throw - $global:DSCMachineStatus | Should -Not -Be 1 - Assert-MockCalled -CommandName Start-Sleep -Times 0 -Scope It - Assert-MockCalled -CommandName Clear-DnsClientCache -Times 0 -Scope It + Context 'When the system is in the desired state' { + Context 'When a domain controller is found' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith $mockGetTargetResource_Present + + $testTargetResourceParameters = $mockDefaultParameters.Clone() + $testTargetResourceParameters['DomainName'] = $mockDomainName + } + + It 'Should return the correct values' { + $compareTargetResourceStateResult = Compare-TargetResourceState @testTargetResourceParameters + $compareTargetResourceStateResult | Should -HaveCount 1 + + $comparedReturnValue = $compareTargetResourceStateResult.Where( { $_.ParameterName -eq 'IsAvailable' }) + $comparedReturnValue | Should -Not -BeNullOrEmpty + $comparedReturnValue.Expected | Should -Be $true + $comparedReturnValue.Actual | Should -Be $true + $comparedReturnValue.InDesiredState | Should -BeTrue + + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 -Scope It + } + } + } - It "Throws exception and does not set `$global:DSCMachineStatus when domain not found after $($testParams.RetryCount) retries when RebootRetryCount is not set" { - Mock -CommandName Get-Domain - {Set-TargetResource @testParams} | Should -Throw - $global:DSCMachineStatus | Should -Not -Be 1 + Context 'When the system is not in the desired state' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith $mockGetTargetResource_Absent + + $testTargetResourceParameters = $mockDefaultParameters.Clone() + $testTargetResourceParameters['DomainName'] = $mockDomainName + } + + It 'Should return the correct values' { + $compareTargetResourceStateResult = Compare-TargetResourceState @testTargetResourceParameters + $compareTargetResourceStateResult | Should -HaveCount 1 + + $comparedReturnValue = $compareTargetResourceStateResult.Where( { $_.ParameterName -eq 'IsAvailable' }) + $comparedReturnValue | Should -Not -BeNullOrEmpty + $comparedReturnValue.Expected | Should -Be $true + $comparedReturnValue.Actual | Should -Be $false + $comparedReturnValue.InDesiredState | Should -BeFalse + + Assert-MockCalled -CommandName Get-TargetResource -Exactly -Times 1 -Scope It + } } + } - It "Throws exception when domain not found after $($rebootTestParams.RebootRetryCount) reboot retries when RebootRetryCount is exceeded" { - Mock -CommandName Get-Domain - Mock -CommandName Get-Content -MockWith {return $rebootTestParams.RebootRetryCount} - {Set-TargetResource @rebootTestParams} | Should -Throw + #region Function Set-TargetResource + Describe 'WaitForADDomain\Set-TargetResource' -Tag 'Set' { + BeforeEach { + $global:DSCMachineStatus = 0 } - It "Calls Set-Content if reboot count is less than RebootRetryCount when domain not found" { - Mock -CommandName Get-Domain - Mock -CommandName Get-Content -MockWith {return 0} - Mock -CommandName Set-Content - {Set-TargetResource @rebootTestParams} | Should -Not -Throw - Assert-MockCalled -CommandName Set-Content -Times 1 -Exactly -Scope It + Context 'When the system is in the desired state' { + BeforeAll { + Mock -CommandName Remove-RestartLogFile + Mock -CommandName Receive-Job + Mock -CommandName Start-Job + Mock -CommandName Wait-Job + Mock -CommandName Remove-Job + + Mock -CommandName Compare-TargetResourceState -MockWith { + return @( + @{ + ParameterName = 'IsAvailable' + InDesiredState = $true + } + ) + } + } + + Context 'When a domain controller is found' { + It 'Should not throw and call the correct mocks' { + { Set-TargetResource @mockDefaultParameters } | Should -Not -Throw + + $global:DSCMachineStatus | Should -Be 0 + + Assert-MockCalled -CommandName Compare-TargetResourceState -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Receive-Job -Exactly -Times 0 -Scope It + Assert-MockCalled -CommandName Start-Job -Exactly -Times 0 -Scope It + Assert-MockCalled -CommandName Wait-Job -Exactly -Times 0 -Scope It + Assert-MockCalled -CommandName Remove-Job -Exactly -Times 0 -Scope It + Assert-MockCalled -CommandName Remove-RestartLogFile -Exactly -Times 0 -Scope It + } + } } - It "Sets `$global:DSCMachineStatus = 1 and does not throw an exception if the domain is not found and RebootRetryCount is not exceeded" { - Mock -CommandName Get-Domain - Mock -CommandName Get-Content -MockWith {return 0} - {Set-TargetResource @rebootTestParams} | Should -Not -Throw - $global:DSCMachineStatus | Should -Be 1 + Context 'When the system is not in the desired state' { + BeforeAll { + Mock -CommandName Remove-RestartLogFile + Mock -CommandName Receive-Job + + <# + The code being tested is using parameter Job, so here + that parameter must be avoided so that we don't mock + in an endless loop. + #> + Mock -CommandName Start-Job -ParameterFilter { + $PSBoundParameters.ContainsKey('ArgumentList') + } -MockWith { + <# + Need to mock an object by actually creating a job + that completes successfully. + #> + $mockJobObject = Start-Job -ScriptBlock { + Start-Sleep -Milliseconds 1 + } + + Remove-Job -Id $mockJobObject.Id -Force + + return $mockJobObject + } + + <# + The code being tested is using parameter Job, so here + that parameter must be avoided so that we don't mock + in an endless loop. + #> + Mock -CommandName Remove-Job -ParameterFilter { + $null -ne $Job + } + + Mock -CommandName Compare-TargetResourceState -MockWith { + return @( + @{ + ParameterName = 'IsAvailable' + InDesiredState = $false + } + ) + } + } + + Context 'When a domain controller is reached before the timeout period' { + BeforeAll { + <# + The code being tested is using parameter Job, so here + that parameter must be avoided so that we don't mock + in an endless loop. + #> + Mock -CommandName Wait-Job -ParameterFilter { + $null -ne $Job + } -MockWith { + <# + Need to mock an object by actually creating a job + that completes successfully. + #> + $mockJobObject = Start-Job -ScriptBlock { + Start-Sleep -Milliseconds 1 + } + + <# + The variable name must not be the same as the one + used in the call to Wait-Job. + #> + $mockWaitJobObject = Wait-Job -Id $mockJobObject.Id + + Remove-Job -Id $mockJobObject.Id -Force + + return $mockJobObject + } + } + + It 'Should not throw and call the correct mocks' { + { Set-TargetResource @mockDefaultParameters } | Should -Not -Throw + + $global:DSCMachineStatus | Should -Be 0 + + Assert-MockCalled -CommandName Compare-TargetResourceState -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Receive-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Wait-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Remove-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Remove-RestartLogFile -Exactly -Times 0 -Scope It + } + + Context 'When a restart was requested' { + BeforeAll { + $setTagetResourceParameters = $mockDefaultParameters.Clone() + $setTagetResourceParameters['RestartCount'] = 1 + } + + It 'Should not throw and call the correct mocks' { + { Set-TargetResource @setTagetResourceParameters } | Should -Not -Throw + + $global:DSCMachineStatus | Should -Be 0 + + Assert-MockCalled -CommandName Remove-RestartLogFile -Exactly -Times 1 -Scope It + } + } + } + + Context 'When the script that searches for a domain controller fails' { + BeforeAll { + <# + The code being tested is using parameter Job, so here + that parameter must be avoided so that we don't mock + in an endless loop. + #> + Mock -CommandName Wait-Job -ParameterFilter { + $null -ne $Job + } -MockWith { + <# + Need to mock an object by actually creating a job + that completes successfully. + #> + $mockJobObject = Start-Job -ScriptBlock { + throw 'Mocked error in mocked script' + } + + <# + The variable name must not be the same as the one + used in the call to Wait-Job. + #> + $mockWaitJobObject = Wait-Job -Id $mockJobObject.Id + + Remove-Job -Id $mockJobObject.Id -Force + + return $mockJobObject + } + + $setTagetResourceParameters = $mockDefaultParameters.Clone() + + <# + To test that the background job output is written when + the job fails even if `Verbose` is not set. + #> + $setTagetResourceParameters.Remove('Verbose') + } + + It 'Should not throw and call the correct mocks' { + { Set-TargetResource @setTagetResourceParameters } | Should -Throw $script:localizedData.NoDomainController + + $global:DSCMachineStatus | Should -Be 0 + + Assert-MockCalled -CommandName Compare-TargetResourceState -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Receive-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Wait-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Remove-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Remove-RestartLogFile -Exactly -Times 0 -Scope It + } + } + + Context 'When a domain controller cannot be reached before the timeout period' { + BeforeAll { + <# + The code being tested is using parameter Job, so here + that parameter must be avoided so that we don't mock + in an endless loop. + #> + Mock -CommandName Wait-Job -ParameterFilter { + $null -ne $Job + } -MockWith { + return $null + } + } + + It 'Should throw the correct error message and call the correct mocks' { + { Set-TargetResource @mockDefaultParameters } | Should -Throw $script:localizedData.NoDomainController + + $global:DSCMachineStatus | Should -Be 0 + + Assert-MockCalled -CommandName Compare-TargetResourceState -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Receive-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Wait-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Remove-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Remove-RestartLogFile -Exactly -Times 0 -Scope It + } + + Context 'When a restart is requested when a domain controller cannot be found' { + BeforeAll { + Mock -CommandName Get-Content + Mock -CommandName Set-Content + + <# + The code being tested is using parameter Job, so here + that parameter must be avoided so that we don't mock + in an endless loop. + #> + Mock -CommandName Wait-Job -ParameterFilter { + $null -ne $Job + } -MockWith { + return $null + } + + $setTagetResourceParameters = $mockDefaultParameters.Clone() + $setTagetResourceParameters['RestartCount'] = 1 + } + + It 'Should throw the correct error message and call the correct mocks' { + { Set-TargetResource @setTagetResourceParameters } | Should -Throw $script:localizedData.NoDomainController + + $global:DSCMachineStatus | Should -Be 1 + + Assert-MockCalled -CommandName Compare-TargetResourceState -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Receive-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Wait-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Remove-Job -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Get-Content -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Set-Content -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Remove-RestartLogFile -Exactly -Times 0 -Scope It + } + } + } } + } + #endregion - It "Calls Get-Domain exactly $($testParams.RetryCount) times when domain not found" { - Mock -CommandName Get-Domain - Mock -CommandName Start-Sleep + Describe 'MSFT_ADDomainTrust\WaitForDomainControllerScriptBlock' -Tag 'Helper' { + BeforeAll { Mock -CommandName Clear-DnsClientCache - {Set-TargetResource @testParams} | Should -Throw - Assert-MockCalled -CommandName Get-Domain -Times $testParams.RetryCount -Exactly -Scope It + Mock -CommandName Start-Sleep } - It "Calls Start-Sleep exactly $($testParams.RetryCount) times when domain not found" { - Mock -CommandName Get-Domain - Mock -CommandName Start-Sleep - Mock -CommandName Clear-DnsClientCache - {Set-TargetResource @testParams} | Should -Throw - Assert-MockCalled -CommandName Start-Sleep -Times $testParams.RetryCount -Exactly -Scope It + Context 'When a domain controller cannot be found' { + BeforeAll { + Mock -CommandName Find-DomainController + } + + It 'Should not throw and call the correct mocks' { + Invoke-Command -ScriptBlock $script:waitForDomainControllerScriptBlock -ArgumentList @( + 'contoso.com' # DomainName + 'Europe', # SiteName + $mockDomainUserCredential, # Credential + $true # RunOnce + ) + + Assert-MockCalled -CommandName Find-DomainController -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Clear-DnsClientCache -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-Sleep -Exactly -Times 1 -Scope It + } } - It "Calls Clear-DnsClientCache exactly $($testParams.RetryCount) times when domain not found" { - Mock -CommandName Get-Domain - Mock -CommandName Start-Sleep - Mock -CommandName Clear-DnsClientCache - {Set-TargetResource @testParams} | Should -Throw - Assert-MockCalled -CommandName Clear-DnsClientCache -Times $testParams.RetryCount -Exactly -Scope It + Context 'When a domain controller is found' { + BeforeAll { + Mock -CommandName Find-DomainController -MockWith { + return New-Object -TypeName 'PSObject' + } + } + + It 'Should not throw and call the correct mocks' { + Invoke-Command -ScriptBlock $script:waitForDomainControllerScriptBlock -ArgumentList @( + 'contoso.com' # DomainName + 'Europe', # SiteName + $null, # Credential + $true # RunOnce + ) + + Assert-MockCalled -CommandName Find-DomainController -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Clear-DnsClientCache -Exactly -Times 0 -Scope It + Assert-MockCalled -CommandName Start-Sleep -Exactly -Times 0 -Scope It + } } } - #endregion } #endregion }