diff --git a/DSCResources/MSFT_xADDomainController/MSFT_xADDomainController.psm1 b/DSCResources/MSFT_xADDomainController/MSFT_xADDomainController.psm1 index 1c59555c3..1df141eb9 100644 --- a/DSCResources/MSFT_xADDomainController/MSFT_xADDomainController.psm1 +++ b/DSCResources/MSFT_xADDomainController/MSFT_xADDomainController.psm1 @@ -70,8 +70,9 @@ function Get-TargetResource ) $returnValue = @{ - DomainName = $DomainName - Ensure = $false + DomainName = $DomainName + Ensure = $false + IsGlobalCatalog = $false } try @@ -89,26 +90,33 @@ function Get-TargetResource { Write-Verbose -Message "Current node '$($dc.Name)' is already a domain controller for domain '$($dc.Domain)'." - $serviceNTDS = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' + $serviceNTDS = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' $serviceNETLOGON = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' - $returnValue.Ensure = $true + $returnValue.Ensure = $true $returnValue.DatabasePath = $serviceNTDS.'DSA Working Directory' - $returnValue.LogPath = $serviceNTDS.'Database log files path' - $returnValue.SysvolPath = $serviceNETLOGON.SysVol -replace '\\sysvol$', '' - $returnValue.SiteName = $dc.Site + $returnValue.LogPath = $serviceNTDS.'Database log files path' + $returnValue.SysvolPath = $serviceNETLOGON.SysVol -replace '\\sysvol$', '' + $returnValue.SiteName = $dc.Site + $returnValue.IsGlobalCatalog = $dc.IsGlobalCatalog } } catch { - if ($error[0]) {Write-Verbose $error[0].Exception} + if ($error[0]) + { + Write-Verbose $error[0].Exception + } Write-Verbose -Message "Current node does not host a domain controller." } } } catch [System.Management.Automation.CommandNotFoundException] { - if ($error[0]) {Write-Verbose $error[0].Exception} + if ($error[0]) + { + Write-Verbose $error[0].Exception + } Write-Verbose -Message "Current node is not running AD WS, and hence is not a domain controller." } $returnValue @@ -142,6 +150,9 @@ function Get-TargetResource .PARAMETER InstallationMediaPath Provide the path for the IFM folder that was created with ntdsutil. This should not be on a share but locally to the Domain Controller being promoted. + + .PARAMETER IsGlobalCatalog + Specifies if the domain controller will be a Global Catalog (GC). #> function Set-TargetResource { @@ -178,53 +189,75 @@ function Set-TargetResource [Parameter()] [System.String] - $InstallationMediaPath + $InstallationMediaPath, + + [Parameter()] + [System.Boolean] + $IsGlobalCatalog ) # Debug can pause Install-ADDSDomainController, so we remove it. - $parameters = $PSBoundParameters.Remove("Debug") - $parameters = $PSBoundParameters.Remove('InstallationMediaPath') - $targetResource = Get-TargetResource @PSBoundParameters + $getTargetResourceParameters = @{} + $PSBoundParameters + $getTargetResourceParameters.Remove('Debug') + $getTargetResourceParameters.Remove('InstallationMediaPath') + $getTargetResourceParameters.Remove('IsGlobalCatalog') + $targetResource = Get-TargetResource @getTargetResourceParameters if ($targetResource.Ensure -eq $false) { - ## Node is not a domain controllr so we promote it + ## Node is not a domain controller so we promote it Write-Verbose -Message "Checking if domain '$($DomainName)' is present ..." + $domain = $null; + try { $domain = Get-ADDomain -Identity $DomainName -Credential $DomainAdministratorCredential } catch { - if ($error[0]) {Write-Verbose $error[0].Exception} + if ($error[0]) + { + Write-Verbose $error[0].Exception + } + throw (New-Object -TypeName System.InvalidOperationException -ArgumentList "Domain '$($DomainName)' could not be found.") } Write-Verbose -Message "Verified that domain '$($DomainName)' is present, continuing ..." $params = @{ - DomainName = $DomainName + DomainName = $DomainName SafeModeAdministratorPassword = $SafemodeAdministratorPassword.Password - Credential = $DomainAdministratorCredential - NoRebootOnCompletion = $true - Force = $true + Credential = $DomainAdministratorCredential + NoRebootOnCompletion = $true + Force = $true } + if ($DatabasePath -ne $null) { $params.Add("DatabasePath", $DatabasePath) } + if ($LogPath -ne $null) { $params.Add("LogPath", $LogPath) } + if ($SysvolPath -ne $null) { $params.Add("SysvolPath", $SysvolPath) } + if ($SiteName -ne $null -and $SiteName -ne "") { $params.Add("SiteName", $SiteName) } + + if ($PSBoundParameters.ContainsKey('IsGlobalCatalog') -and $IsGlobalCatalog -eq $false) + { + $params.Add("NoGlobalCatalog", $true) + } + if (-not [string]::IsNullOrWhiteSpace($InstallationMediaPath)) { $params.Add("InstallationMediaPath", $InstallationMediaPath) @@ -239,6 +272,33 @@ function Set-TargetResource } elseif ($targetResource.Ensure) { + ## Check if Node Global Catalog state is correct + if ($PSBoundParameters.ContainsKey('IsGlobalCatalog') -and $targetResource.IsGlobalCatalog -ne $IsGlobalCatalog) + { + ## DC is not in the expected Global Catalog state + Write-Verbose "Setting the Global Catalog state to '$IsGlobalCatalog'" + if ($IsGlobalCatalog) + { + $value = 1 + } + else + { + $value = 0 + } + + $dc = Get-ADDomainController -Identity $env:COMPUTERNAME -Credential $DomainAdministratorCredential -ErrorAction 'Stop' + if ($dc) + { + Set-ADObject -Identity $dc.NTDSSettingsObjectDN -replace @{ + options = $value + } + } + else + { + throw 'Could not get the distinguished name of the NTDSSettingsObject directory object that represents this domain controller.' + } + } + ## Node is a domain controller. We check if other properties are in desired state if ($PSBoundParameters["SiteName"] -and $targetResource.SiteName -ne $SiteName) { @@ -277,6 +337,9 @@ function Set-TargetResource .PARAMETER InstallationMediaPath Provide the path for the IFM folder that was created with ntdsutil. This should not be on a share but locally to the Domain Controller being promoted. + + .PARAMETER IsGlobalCatalog + Specifies if the domain controller will be a Global Catalog (GC). #> function Test-TargetResource { @@ -314,7 +377,11 @@ function Test-TargetResource [Parameter()] [System.String] - $InstallationMediaPath + $InstallationMediaPath, + + [Parameter()] + [System.Boolean] + $IsGlobalCatalog ) if ($PSBoundParameters.SiteName) @@ -329,30 +396,41 @@ function Test-TargetResource try { - $parameters = $PSBoundParameters.Remove("Debug") - $parameters = $PSBoundParameters.Remove('InstallationMediaPath') - $existingResource = Get-TargetResource @PSBoundParameters + $getTargetResourceParameters = @{} + $PSBoundParameters + $getTargetResourceParameters.Remove('Debug') + $getTargetResourceParameters.Remove('InstallationMediaPath') + $getTargetResourceParameters.Remove('IsGlobalCatalog') + $existingResource = Get-TargetResource @getTargetResourceParameters $isCompliant = $existingResource.Ensure if ([System.String]::IsNullOrEmpty($SiteName)) { - #If SiteName is not specified confgiuration is compliant + #If SiteName is not specified configuration is compliant } elseif ($existingResource.SiteName -ne $SiteName) { Write-Verbose "Domain Controller Site is not in a desired state. Expected '$SiteName', actual '$($existingResource.SiteName)'" $isCompliant = $false } + + ## Check Global Catalog Config + if ($PSBoundParameters.ContainsKey('IsGlobalCatalog') -and $existingResource.IsGlobalCatalog -ne $IsGlobalCatalog) + { + $isCompliant = $false + } } catch { - if ($error[0]) {Write-Verbose $error[0].Exception} + if ($error[0]) + { + Write-Verbose $error[0].Exception + } + Write-Verbose -Message "Domain '$($DomainName)' is NOT present on the current node." $isCompliant = $false } $isCompliant - } Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/MSFT_xADDomainController/MSFT_xADDomainController.schema.mof b/DSCResources/MSFT_xADDomainController/MSFT_xADDomainController.schema.mof index cdf183d9d..f9d4003fb 100644 --- a/DSCResources/MSFT_xADDomainController/MSFT_xADDomainController.schema.mof +++ b/DSCResources/MSFT_xADDomainController/MSFT_xADDomainController.schema.mof @@ -9,5 +9,6 @@ class MSFT_xADDomainController : OMI_BaseResource [Write, Description("The path where the Sysvol will be stored.")] String SysvolPath; [Write, Description("The name of the site this Domain Controller will be added to.")] String SiteName; [Write, Description("The path of the media you want to use install the Domain Controller.")] String InstallationMediaPath; + [Write, Description("Specifies if the domain controller will be a Global Catalog (GC).")] Boolean IsGlobalCatalog; [Read, Description("The state of the Domain Controller.")] String Ensure; }; diff --git a/README.md b/README.md index 6ccdeab43..3b33193e1 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ The xADDomainController DSC resource will install and configure domain controlle * **`[String]` SysvolPath** _(Write)_: Specifies the fully qualified, non-UNC path to a directory on a fixed disk of the local computer where the Sysvol file will be written. * **`[String]` SiteName** _(Write)_: Specify the name of an existing site where new domain controller will be placed. * **`[String]` InstallationMediaPath** _(Write)_: Specify the path of the folder containg the Installation Media created in NTDSutil. +* **`[String]` IsGlobalCatalog** _(Write)_: Specifies if the domain controller will be a Global Catalog (GC). * **`[String]` Ensure** _(Read)_: The state of the Domain Controller, returned with Get. ### **xADDomainDefaultPasswordPolicy** @@ -402,6 +403,12 @@ The xADForestProperties DSC resource will manage User Principal Name (UPN) suffi ### Unreleased * Added xADManagedServiceAccount resource to manage Managed Service Accounts (MSAs). [@awickham10](https://github.com/awickham10) and [@kungfu71186](https://github.com/kungfu71186) +* Changes to xAdDomainController + * Added new parameter to disable or enable the Global Catalog (GC) + ([issue #75](https://github.com/PowerShell/xActiveDirectory/issues/75)). [Eric Foskett @Merto410](https://github.com/Merto410) + * Fixed a bug with the parameter `InstallationMediaPath` that it would + not be added if it was specified in a configuration. Now the parameter + `InstallationMediaPath` is correctly passed to `Install-ADDSDomainController`. ### 2.25.0.0 diff --git a/Tests/Unit/MSFT_xADDomainController.Tests.ps1 b/Tests/Unit/MSFT_xADDomainController.Tests.ps1 index 8550d6b5d..e2800fae8 100644 --- a/Tests/Unit/MSFT_xADDomainController.Tests.ps1 +++ b/Tests/Unit/MSFT_xADDomainController.Tests.ps1 @@ -8,9 +8,9 @@ $moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) # Download DSCResource.Tests if not found, import the tests helper, and init the test environment if ( (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` - (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) + (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) { - & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\')) + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\')) } Import-Module (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force $TestEnvironment = Initialize-TestEnvironment ` @@ -38,14 +38,15 @@ try } #region Pester Test Variable Initialization - $correctDomainName = 'present.com' - $testAdminCredential = [System.Management.Automation.PSCredential]::Empty - $correctDatabasePath = 'C:\Windows\NTDS' - $correctLogPath = 'C:\Windows\NTDS' - $correctSysvolPath = 'C:\Windows\SYSVOL' - $correctSiteName = 'PresentSite' - $incorrectSiteName = 'IncorrectSite' - $correctInstallationMediaPath = 'Testdrive:\IFM' + $correctDomainName = 'present.com' + $testAdminCredential = [System.Management.Automation.PSCredential]::Empty + $correctDatabasePath = 'C:\Windows\NTDS' + $correctLogPath = 'C:\Windows\NTDS' + $correctSysvolPath = 'C:\Windows\SYSVOL' + $correctSiteName = 'PresentSite' + $incorrectSiteName = 'IncorrectSite' + $correctInstallationMediaPath = 'Testdrive:\IFM' + $mockNtdsSettingsObjectDn = 'CN=NTDS Settings,CN=ServerName,CN=Servers,CN=PresentSite,CN=Sites,CN=Configuration,DC=present,DC=com' $testDefaultParams = @{ DomainAdministratorCredential = $testAdminCredential @@ -54,8 +55,8 @@ try $commonAssertParams = @{ ModuleName = $dscResourceName - Scope = 'It' - Exactly = $true + Scope = 'It' + Exactly = $true } #Fake function because it is only available on Windows Server @@ -109,8 +110,9 @@ try Mock -CommandName Get-ADDomain -MockWith { return $true } Mock -CommandName Get-ADDomainController { return @{ - Site = $correctSiteName - Domain = $correctDomainName + Site = $correctSiteName + Domain = $correctDomainName + IsGlobalCatalog = $true } } Mock -CommandName Get-ItemProperty -ParameterFilter { $Path -eq 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters' } -MockWith { @@ -125,17 +127,18 @@ try } } - New-Item -Path Testdrive:\ -ItemType Directory -Name IFM + New-Item -Path 'TestDrive:\' -ItemType Directory -Name IFM $result = Get-TargetResource @testDefaultParams -DomainName $correctDomainName It 'Returns current Domain Controller properties' { - $result.DomainName | Should -Be $correctDomainName + $result.DomainName | Should -Be $correctDomainName $result.DatabasePath | Should -Be $correctDatabasePath - $result.LogPath | Should -Be $correctLogPath - $result.SysvolPath | Should -Be $correctSysvolPath - $result.SiteName | Should -Be $correctSiteName - $result.Ensure | Should -Be $true + $result.LogPath | Should -Be $correctLogPath + $result.SysvolPath | Should -Be $correctSysvolPath + $result.SiteName | Should -Be $correctSiteName + $result.Ensure | Should -Be $true + $result.IsGlobalCatalog | Should -Be $true } } @@ -147,12 +150,14 @@ try $result = Get-TargetResource @testDefaultParams -DomainName $correctDomainName It 'Returns Ensure = False' { - $result.DomainName | Should -Be $correctDomainName + $result.DomainName | Should -Be $correctDomainName $result.DatabasePath | Should -BeNullOrEmpty - $result.LogPath | Should -BeNullOrEmpty - $result.SysvolPath | Should -BeNullOrEmpty - $result.SiteName | Should -BeNullOrEmpty + $result.LogPath | Should -BeNullOrEmpty + $result.SysvolPath | Should -BeNullOrEmpty + $result.SiteName | Should -BeNullOrEmpty $result.Ensure | Should -Be $false + $result.IsGlobalCatalog | Should -Be $false + $result.NtdsSettingsObjectDn | Should -BeNullOrEmpty } } } @@ -172,41 +177,41 @@ try } $stubDomainController = @{ - Site = $incorrectSiteName - Domain = $correctDomainName + Site = $incorrectSiteName + Domain = $correctDomainName } Mock -CommandName Get-ADDomain -MockWith { return $true } Mock -CommandName Get-ADDomainController -MockWith { return $stubDomainController } Mock -CommandName Test-ADReplicationSite -MockWith { return $true } - Mock -CommandName Get-ItemProperty -MockWith { return @{} } + Mock -CommandName Get-ItemProperty -MockWith { return @{ } } $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName - $result | Should Be $false + $result | Should -Be $false } It 'Returns "True" when "SiteName" matches' { $stubDomainController = @{ - Site = $correctSiteName + Site = $correctSiteName Domain = $correctDomainName } Mock -CommandName Get-ADDomain -MockWith { return $true } Mock -CommandName Get-ADDomainController -MockWith { return $stubDomainController } Mock -CommandName Test-ADReplicationSite -MockWith { return $true } - Mock -CommandName Get-ItemProperty -MockWith { return @{} } + Mock -CommandName Get-ItemProperty -MockWith { return @{ } } $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName - $result | Should Be $true + $result | Should -Be $true } It 'Throws if "SiteName" is wrong' { $stubDomainController = @{ - Site = $correctSiteName + Site = $correctSiteName Domain = $correctDomainName } @@ -214,7 +219,50 @@ try Mock -CommandName Get-ADDomainController -MockWith { return $stubDomainController } Mock -CommandName Test-ADReplicationSite -MockWith { return $false } { Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $incorrectSiteName } | - Should Throw "Site '$($incorrectSiteName)' could not be found." + Should Throw "Site '$($incorrectSiteName)' could not be found." + } + + It 'Returns "False" when "IsGlobalCatalog" does not match' { + $stubDomain = @{ + DNSRoot = $correctDomainName + } + + $stubDomainController = @{ + Site = $correctSiteName + Domain = $correctDomainName + IsGlobalCatalog = $false + } + + Mock -CommandName Get-ADDomain -MockWith { return $true } + Mock -CommandName Get-ADDomainController -MockWith { return $stubDomainController } + Mock -CommandName Test-ADReplicationSite -MockWith { return $true } + Mock -CommandName Get-ItemProperty -MockWith { return @{ } } + + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName -IsGlobalCatalog $true + + $result | Should -Be $false + + } + + It 'Returns "True" when "IsGlobalCatalog" matches' { + $stubDomain = @{ + DNSRoot = $correctDomainName + } + + $stubDomainController = @{ + Site = $correctSiteName + Domain = $correctDomainName + IsGlobalCatalog = $true + } + + Mock -CommandName Get-ADDomain -MockWith { return $true } + Mock -CommandName Get-ADDomainController -MockWith { return $stubDomainController } + Mock -CommandName Test-ADReplicationSite -MockWith { return $true } + Mock -CommandName Get-ItemProperty -MockWith { return @{ } } + + $result = Test-TargetResource @testDefaultParams -DomainName $correctDomainName -SiteName $correctSiteName -IsGlobalCatalog $true + + $result | Should -Be $true } } #endregion @@ -239,7 +287,8 @@ try Assert-MockCalled -CommandName Install-ADDSDomainController -Times 1 -ParameterFilter { $SiteName -eq $correctSiteName } } - New-Item -Path Testdrive:\ -ItemType Directory -Name IFM + New-Item -Path 'TestDrive:\' -ItemType Directory -Name IFM + It 'Calls "Install-ADDSDomainController" with InstallationMediaPath specified' { Mock -CommandName Get-ADDomain -MockWith { return $true @@ -250,18 +299,18 @@ try Ensure = $false } } - Mock -CommandName Install-ADDSDomainController -ParameterFilter {$InstallationMediaPath -eq $correctInstallationMediaPath} + Mock -CommandName Install-ADDSDomainController -ParameterFilter { $InstallationMediaPath -eq $correctInstallationMediaPath } Set-TargetResource @testDefaultParams -DomainName $correctDomainName -InstallationMediaPath $correctInstallationMediaPath Assert-MockCalled -CommandName Install-ADDSDomainController -Times 1 ` - -ParameterFilter {$InstallationMediaPath -eq $correctInstallationMediaPath} @commonAssertParams + -ParameterFilter { $InstallationMediaPath -eq $correctInstallationMediaPath } @commonAssertParams } It 'Calls "Move-ADDirectoryServer" when "SiteName" does not match' { Mock -CommandName Get-TargetResource -MockWith { return $stubTargetResource = @{ - Ensure = $true + Ensure = $true SiteName = 'IncorrectSite' } } @@ -278,7 +327,7 @@ try It 'Does not call "Move-ADDirectoryServer" when "SiteName" matches' { Mock -CommandName Get-TargetResource -MockWith { return $stubTargetResource = @{ - Ensure = $true + Ensure = $true SiteName = 'PresentSite' } } @@ -293,7 +342,7 @@ try It 'Does not call "Move-ADDirectoryServer" when "SiteName" is not specified' { Mock -CommandName Get-TargetResource -MockWith { return $stubTargetResource = @{ - Ensure = $true + Ensure = $true SiteName = 'PresentSite' } } @@ -304,6 +353,80 @@ try Assert-MockCalled -CommandName Move-ADDirectoryServer -Times 0 @commonAssertParams } + + Context 'When specifying the parameter IsGlobalCatalog' { + BeforeAll { + Mock -CommandName Set-ADObject + Mock -CommandName Get-ADDomainController { + return @{ + Site = $correctSiteName + Domain = $correctDomainName + IsGlobalCatalog = $true + NTDSSettingsObjectDN = $mockNtdsSettingsObjectDn + } + } + } + + It 'Calls "Set-ADObject" when "IsGlobalCatalog" Should -Be "True" and does not match' { + Mock -CommandName Get-TargetResource -MockWith { + return $stubTargetResource = @{ + Ensure = $true + SiteName = 'PresentSite' + IsGlobalCatalog = $false + } + } + + Set-TargetResource @testDefaultParams -DomainName $correctDomainName -IsGlobalCatalog $true + + Assert-MockCalled Set-ADObject -Times 1 -ParameterFilter { + $Replace['options'] -eq 1 + } @commonAssertParams + } + + It 'Calls "Set-ADObject" when "IsGlobalCatalog" Should -Be "False" and does not match' { + Mock -CommandName Get-TargetResource -MockWith { + return $stubTargetResource = @{ + Ensure = $true + SiteName = 'PresentSite' + IsGlobalCatalog = $true + } + } + + Set-TargetResource @testDefaultParams -DomainName $correctDomainName -IsGlobalCatalog $false -Verbose + + Assert-MockCalled Set-ADObject -Times 1 -ParameterFilter { + $Replace['options'] -eq 0 + } @commonAssertParams + } + + It 'Does not call "Set-ADObject" when "IsGlobalCatalog" matches' { + Mock Get-TargetResource { + return $TargetResource = @{ + Ensure = $true + SiteName = 'PresentSite' + IsGlobalCatalog = $true + } + } + + Set-TargetResource @testDefaultParams -DomainName $correctDomainName -IsGlobalCatalog $true + + Assert-MockCalled Set-ADObject -Times 0 @commonAssertParams + } + + It 'Does not call "Set-ADObject" when "IsGlobalCatalog" is not specified' { + Mock Get-TargetResource { + return $TargetResource = @{ + Ensure = $true + SiteName = 'PresentSite' + IsGlobalCatalog = $false + } + } + + Set-TargetResource @testDefaultParams -DomainName $correctDomainName + + Assert-MockCalled Set-ADObject -Times 0 @commonAssertParams + } + } } #endregion }