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