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()15 Function 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-Prereqsnull32 Function 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-DomainForestnull46 Function 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