1 #!powershell 2 3 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5 #Requires -Module Ansible.ModuleUtils.Legacy 6 7 Set-StrictMode -Version 2 8 $ErrorActionPreference = "Stop" 9 10 # FUTURE: Consider action wrapper to manage reboots and credential changes 11 12 # Set of features required for a domain controller 13 $dc_required_features = @("AD-Domain-Services","RSAT-ADDS") 14 Get-MissingFeatures()15Function Get-MissingFeatures { 16 Param( 17 [string[]]$required_features 18 ) 19 $features = @(Get-WindowsFeature $required_features) 20 # Check for $required_features that are not in $features 21 $unavailable_features = @(Compare-Object -ReferenceObject $required_features -DifferenceObject ($features | Select-Object -ExpandProperty Name) -PassThru) 22 23 if ($unavailable_features) { 24 Throw "The following features required for a domain controller are unavailable: $($unavailable_features -join ',')" 25 } 26 27 $missing_features = @($features | Where-Object InstallState -ne Installed) 28 29 return @($missing_features) 30 } 31 Install-Prereqsnull32Function Install-Prereqs { 33 $missing_features = Get-MissingFeatures $dc_required_features 34 if ($missing_features) { 35 $result.changed = $true 36 37 $awf = Add-WindowsFeature $missing_features -WhatIf:$check_mode 38 $result.reboot_required = $awf.RestartNeeded 39 # FUTURE: Check if reboot necessary 40 41 return $true 42 } 43 return $false 44 } 45 Get-DomainForestnull46Function Get-DomainForest { 47 <# 48 .SYNOPSIS 49 Gets the domain forest similar to Get-ADForest but without requiring credential delegation. 50 51 .PARAMETER DnsName 52 The DNS name of the forest, for example 'sales.corp.fabrikam.com'. 53 #> 54 [CmdletBinding()] 55 param ( 56 [Parameter(Mandatory=$true)] 57 [String]$DnsName 58 ) 59 60 try { 61 $forest_context = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext -ArgumentList @( 62 'Forest', $DnsName 63 ) 64 [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($forest_context) 65 } catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] { 66 Write-Error -Message "AD Object not found: $($_.Exception.Message)" -Exception $_.Exception 67 } catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryOperationException] { 68 Write-Error -Message "AD Operation Exception: $($_.Exception.Message)" -Exception $_.Exception 69 } 70 } 71 72 $params = Parse-Args $args -supports_check_mode $true 73 $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false 74 $dns_domain_name = Get-AnsibleParam -obj $params -name "dns_domain_name" -failifempty $true 75 $domain_netbios_name = Get-AnsibleParam -obj $params -name "domain_netbios_name" 76 $safe_mode_admin_password = Get-AnsibleParam -obj $params -name "safe_mode_password" -failifempty $true 77 $database_path = Get-AnsibleParam -obj $params -name "database_path" -type "path" 78 $sysvol_path = Get-AnsibleParam -obj $params -name "sysvol_path" -type "path" 79 $log_path = Get-AnsibleParam -obj $params -name "log_path" -type "path" 80 $create_dns_delegation = Get-AnsibleParam -obj $params -name "create_dns_delegation" -type "bool" 81 $domain_mode = Get-AnsibleParam -obj $params -name "domain_mode" -type "str" 82 $forest_mode = Get-AnsibleParam -obj $params -name "forest_mode" -type "str" 83 $install_dns = Get-AnsibleParam -obj $params -name "install_dns" -type "bool" -default $true 84 85 # FUTURE: Support down to Server 2012? 86 if ([System.Environment]::OSVersion.Version -lt [Version]"6.3.9600.0") { 87 Fail-Json -message "win_domain requires Windows Server 2012R2 or higher" 88 } 89 90 # Check that domain_netbios_name is less than 15 characters 91 if ($domain_netbios_name -and $domain_netbios_name.length -gt 15) { 92 Fail-Json -message "The parameter 'domain_netbios_name' should not exceed 15 characters in length" 93 } 94 95 $result = @{ 96 changed=$false; 97 reboot_required=$false; 98 } 99 100 # FUTURE: Any sane way to do the detection under check-mode *without* installing the feature? 101 $installed = Install-Prereqs 102 103 # when in check mode and the prereq was "installed" we need to exit early as 104 # the AD cmdlets weren't really installed 105 if ($check_mode -and $installed) { 106 Exit-Json -obj $result 107 } 108 109 # Check that we got a valid domain_mode 110 $valid_domain_modes = [Enum]::GetNames((Get-Command -Name Install-ADDSForest).Parameters.DomainMode.ParameterType) 111 if (($null -ne $domain_mode) -and -not ($domain_mode -in $valid_domain_modes)) { 112 Fail-Json -obj $result -message "The parameter 'domain_mode' does not accept '$domain_mode', please use one of: $valid_domain_modes" 113 } 114 115 # Check that we got a valid forest_mode 116 $valid_forest_modes = [Enum]::GetNames((Get-Command -Name Install-ADDSForest).Parameters.ForestMode.ParameterType) 117 if (($null -ne $forest_mode) -and -not ($forest_mode -in $valid_forest_modes)) { 118 Fail-Json -obj $result -message "The parameter 'forest_mode' does not accept '$forest_mode', please use one of: $valid_forest_modes" 119 } 120 121 $forest = Get-DomainForest -DnsName $dns_domain_name -ErrorAction SilentlyContinue 122 if (-not $forest) { 123 $result.changed = $true 124 125 $sm_cred = ConvertTo-SecureString $safe_mode_admin_password -AsPlainText -Force 126 127 $install_params = @{ 128 DomainName=$dns_domain_name; 129 SafeModeAdministratorPassword=$sm_cred; 130 Confirm=$false; 131 SkipPreChecks=$true; 132 InstallDns=$install_dns; 133 NoRebootOnCompletion=$true; 134 WhatIf=$check_mode; 135 } 136 137 if ($database_path) { 138 $install_params.DatabasePath = $database_path 139 } 140 141 if ($sysvol_path) { 142 $install_params.SysvolPath = $sysvol_path 143 } 144 145 if ($log_path) { 146 $install_params.LogPath = $log_path 147 } 148 149 if ($domain_netbios_name) { 150 $install_params.DomainNetBiosName = $domain_netbios_name 151 } 152 153 if ($null -ne $create_dns_delegation) { 154 $install_params.CreateDnsDelegation = $create_dns_delegation 155 } 156 157 if ($domain_mode) { 158 $install_params.DomainMode = $domain_mode 159 } 160 161 if ($forest_mode) { 162 $install_params.ForestMode = $forest_mode 163 } 164 165 $iaf = $null 166 try { 167 $iaf = Install-ADDSForest @install_params 168 } catch [Microsoft.DirectoryServices.Deployment.DCPromoExecutionException] { 169 # ExitCode 15 == 'Role change is in progress or this computer needs to be restarted.' 170 # DCPromo exit codes details can be found at https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/troubleshooting-domain-controller-deployment 171 if ($_.Exception.ExitCode -in @(15, 19)) { 172 $result.reboot_required = $true 173 } else { 174 Fail-Json -obj $result -message "Failed to install ADDSForest, DCPromo exited with $($_.Exception.ExitCode): $($_.Exception.Message)" 175 } 176 } 177 178 if ($check_mode) { 179 # the return value after -WhatIf does not have RebootRequired populated 180 # manually set to True as the domain would have been installed 181 $result.reboot_required = $true 182 } elseif ($null -ne $iaf) { 183 $result.reboot_required = $iaf.RebootRequired 184 185 # The Netlogon service is set to auto start but is not started. This is 186 # required for Ansible to connect back to the host and reboot in a 187 # later task. Even if this fails Ansible can still connect but only 188 # with ansible_winrm_transport=basic so we just display a warning if 189 # this fails. 190 try { 191 Start-Service -Name Netlogon 192 } catch { 193 Add-Warning -obj $result -message "Failed to start the Netlogon service after promoting the host, Ansible may be unable to connect until the host is manually rebooting: $($_.Exception.Message)" 194 } 195 } 196 } 197 198 Exit-Json $result 199