1 #!powershell
2
3 # Copyright: (c) 2017, Red Hat, Inc.
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6 #Requires -Module Ansible.ModuleUtils.Legacy
7
8 Set-StrictMode -Version 2
9
10 $ErrorActionPreference = "Stop"
11 $ConfirmPreference = "None"
12
13 $log_path = $null
14
Write-DebugLog()15 Function Write-DebugLog {
16 Param(
17 [string]$msg
18 )
19
20 $DebugPreference = "Continue"
21 $ErrorActionPreference = "Continue"
22 $date_str = Get-Date -Format u
23 $msg = "$date_str $msg"
24
25 Write-Debug $msg
26 if($log_path) {
27 Add-Content $log_path $msg
28 }
29 }
30
31 $required_features = @("AD-Domain-Services","RSAT-ADDS")
32
Get-MissingFeatures()33 Function Get-MissingFeatures {
34 Write-DebugLog "Checking for missing Windows features..."
35
36 $features = @(Get-WindowsFeature $required_features)
37
38 If($features.Count -ne $required_features.Count) {
39 Throw "One or more Windows features required for a domain controller are unavailable"
40 }
41
42 $missing_features = @($features | Where-Object InstallState -ne Installed)
43
44 return ,$missing_features # no, the comma's not a typo- allows us to return an empty array
45 }
46
Ensure-FeatureInstallationnull47 Function Ensure-FeatureInstallation {
48 # ensure RSAT-ADDS and AD-Domain-Services features are installed
49
50 Write-DebugLog "Ensuring required Windows features are installed..."
51 $feature_result = Install-WindowsFeature $required_features
52 $result.reboot_required = $feature_result.RestartNeeded
53
54 If(-not $feature_result.Success) {
55 Exit-Json -message ("Error installing AD-Domain-Services and RSAT-ADDS features: {0}" -f ($feature_result | Out-String))
56 }
57 }
58
59 # return the domain we're a DC for, or null if not a DC
Get-DomainControllerDomainnull60 Function Get-DomainControllerDomain {
61 Write-DebugLog "Checking for domain controller role and domain name"
62
63 $sys_cim = Get-CIMInstance Win32_ComputerSystem
64
65 $is_dc = $sys_cim.DomainRole -in (4,5) # backup/primary DC
66 # this will be our workgroup or joined-domain if we're not a DC
67 $domain = $sys_cim.Domain
68
69 Switch($is_dc) {
70 $true { return $domain }
71 Default { return $null }
72 }
73 }
74
Create-Credentialnull75 Function Create-Credential {
76 Param(
77 [string] $cred_user,
78 [string] $cred_password
79 )
80
81 $cred = New-Object System.Management.Automation.PSCredential($cred_user, $($cred_password | ConvertTo-SecureString -AsPlainText -Force))
82
83 Return $cred
84 }
85
Get-OperationMasterRoles()86 Function Get-OperationMasterRoles {
87 $assigned_roles = @((Get-ADDomainController -Server localhost).OperationMasterRoles)
88
89 Return ,$assigned_roles # no, the comma's not a typo- allows us to return an empty array
90 }
91
92 $result = @{
93 changed = $false
94 reboot_required = $false
95 }
96
97 $params = Parse-Args -arguments $args -supports_check_mode $true
98
99 $dns_domain_name = Get-AnsibleParam -obj $params -name "dns_domain_name"
100 $safe_mode_password= Get-AnsibleParam -obj $params -name "safe_mode_password"
101 $domain_admin_user = Get-AnsibleParam -obj $params -name "domain_admin_user" -failifempty $result
102 $domain_admin_password= Get-AnsibleParam -obj $params -name "domain_admin_password" -failifempty $result
103 $local_admin_password= Get-AnsibleParam -obj $params -name "local_admin_password"
104 $database_path = Get-AnsibleParam -obj $params -name "database_path" -type "path"
105 $sysvol_path = Get-AnsibleParam -obj $params -name "sysvol_path" -type "path"
106 $read_only = Get-AnsibleParam -obj $params -name "read_only" -type "bool" -default $false
107 $site_name = Get-AnsibleParam -obj $params -name "site_name" -type "str" -failifempty $read_only
108
109 $state = Get-AnsibleParam -obj $params -name "state" -validateset ("domain_controller", "member_server") -failifempty $result
110
111 $log_path = Get-AnsibleParam -obj $params -name "log_path"
112 $_ansible_check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false
113
114 $global:log_path = $log_path
115
116 Try {
117 # ensure target OS support; < 2012 doesn't have cmdlet support for DC promotion
118 If(-not (Get-Command Install-WindowsFeature -ErrorAction SilentlyContinue)) {
119 Fail-Json -message "win_domain_controller requires at least Windows Server 2012"
120 }
121
122 # validate args
123 If($state -eq "domain_controller") {
124 If(-not $dns_domain_name) {
125 Fail-Json -message "dns_domain_name is required when desired state is 'domain_controller'"
126 }
127
128 If(-not $safe_mode_password) {
129 Fail-Json -message "safe_mode_password is required when desired state is 'domain_controller'"
130 }
131
132 # ensure that domain admin user is in UPN or down-level domain format (prevent hang from https://support.microsoft.com/en-us/kb/2737935)
133 If(-not $domain_admin_user.Contains("\") -and -not $domain_admin_user.Contains("@")) {
134 Fail-Json -message "domain_admin_user must be in domain\user or user@domain.com format"
135 }
136 }
137 Else { # member_server
138 If(-not $local_admin_password) {
139 Fail-Json -message "local_admin_password is required when desired state is 'member_server'"
140 }
141 }
142
143 # short-circuit "member server" check, since we don't need feature checks for this...
144
145 $current_dc_domain = Get-DomainControllerDomain
146
147 If($state -eq "member_server" -and -not $current_dc_domain) {
148 Exit-Json $result
149 }
150
151 # all other operations will require the AD-DS and RSAT-ADDS features...
152
153 $missing_features = Get-MissingFeatures
154
155 If($missing_features.Count -gt 0) {
156 Write-DebugLog ("Missing Windows features ({0}), need to install" -f ($missing_features -join ", "))
157 $result.changed = $true # we need to install features
158 If($_ansible_check_mode) {
159 # bail out here- we can't proceed without knowing the features are installed
160 Write-DebugLog "check-mode, exiting early"
161 Exit-Json $result
162 }
163
164 Ensure-FeatureInstallation | Out-Null
165 }
166
167 $domain_admin_cred = Create-Credential -cred_user $domain_admin_user -cred_password $domain_admin_password
168
169 switch($state) {
170 domain_controller {
171 If(-not $safe_mode_password) {
172 Fail-Json -message "safe_mode_password is required for state=domain_controller"
173 }
174
175 If($current_dc_domain) {
176 # FUTURE: implement managed Remove/Add to change domains?
177
178 If($current_dc_domain -ne $dns_domain_name) {
179 Fail-Json "$(hostname) is a domain controller for domain $current_dc_domain; changing DC domains is not implemented"
180 }
181 }
182
183 # need to promote to DC
184 If(-not $current_dc_domain) {
185 Write-DebugLog "Not currently a domain controller; needs promotion"
186 $result.changed = $true
187 If($_ansible_check_mode) {
188 Write-DebugLog "check-mode, exiting early"
189 Fail-Json -message $result
190 }
191
192 $result.reboot_required = $true
193
194 $safe_mode_secure = $safe_mode_password | ConvertTo-SecureString -AsPlainText -Force
195 Write-DebugLog "Installing domain controller..."
196 $install_params = @{
197 DomainName = $dns_domain_name
198 Credential = $domain_admin_cred
199 SafeModeAdministratorPassword = $safe_mode_secure
200 }
201 if ($database_path) {
202 $install_params.DatabasePath = $database_path
203 }
204 if ($sysvol_path) {
205 $install_params.SysvolPath = $sysvol_path
206 }
207 if ($read_only) {
208 # while this is a switch value, if we set on $false site_name is required
209 # https://github.com/ansible/ansible/issues/35858
210 $install_params.ReadOnlyReplica = $true
211 }
212 if ($site_name) {
213 $install_params.SiteName = $site_name
214 }
215 try
216 {
217 $null = Install-ADDSDomainController -NoRebootOnCompletion -Force @install_params
218 } catch [Microsoft.DirectoryServices.Deployment.DCPromoExecutionException] {
219 # ExitCode 15 == 'Role change is in progress or this computer needs to be restarted.'
220 # DCPromo exit codes details can be found at https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/troubleshooting-domain-controller-deployment
221 if ($_.Exception.ExitCode -eq 15) {
222 $result.reboot_required = $true
223 } else {
224 Fail-Json -obj $result -message "Failed to install ADDSDomainController with DCPromo: $($_.Exception.Message)"
225 }
226 }
227 # If $_.FullyQualifiedErrorId -eq 'Test.VerifyUserCredentialPermissions.DCPromo.General.25,Microsoft.DirectoryServices.Deployment.PowerShell.Commands.InstallADDSDomainControllerCommand'
228 # the module failed to resolve the given dns domain name
229
230 Write-DebugLog "Installation complete, trying to start the Netlogon service"
231 # The Netlogon service is set to auto start but is not started. This is
232 # required for Ansible to connect back to the host and reboot in a
233 # later task. Even if this fails Ansible can still connect but only
234 # with ansible_winrm_transport=basic so we just display a warning if
235 # this fails.
236 try {
237 Start-Service -Name Netlogon
238 } catch {
239 Write-DebugLog "Failed to start the Netlogon service: $($_.Exception.Message)"
240 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)"
241 }
242
243 Write-DebugLog "Domain Controller setup completed, needs reboot..."
244 }
245 }
246 member_server {
247 If(-not $local_admin_password) {
248 Fail-Json -message "local_admin_password is required for state=domain_controller"
249 }
250 # at this point we already know we're a DC and shouldn't be...
251 Write-DebugLog "Need to uninstall domain controller..."
252 $result.changed = $true
253
254 Write-DebugLog "Checking for operation master roles assigned to this DC..."
255
256 $assigned_roles = Get-OperationMasterRoles
257
258 # FUTURE: figure out a sane way to hand off roles automatically (designated recipient server, randomly look one up?)
259 If($assigned_roles.Count -gt 0) {
260 Fail-Json -message ("This domain controller has operation master role(s) ({0}) assigned; they must be moved to other DCs before demotion (see Move-ADDirectoryServerOperationMasterRole)" -f ($assigned_roles -join ", "))
261 }
262
263 If($_ansible_check_mode) {
264 Write-DebugLog "check-mode, exiting early"
265 Exit-Json $result
266 }
267
268 $result.reboot_required = $true
269
270 $local_admin_secure = $local_admin_password | ConvertTo-SecureString -AsPlainText -Force
271
272 Write-DebugLog "Uninstalling domain controller..."
273 Uninstall-ADDSDomainController -NoRebootOnCompletion -LocalAdministratorPassword $local_admin_secure -Credential $domain_admin_cred
274 Write-DebugLog "Uninstallation complete, needs reboot..."
275 }
276 default { throw ("invalid state {0}" -f $state) }
277 }
278
279 Exit-Json $result
280 }
281 Catch {
282 $excep = $_
283
284 Write-DebugLog "Exception: $($excep | out-string)"
285
286 Throw
287 }
288