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