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 
Write-DebugLog()12 Function Write-DebugLog {
13     Param(
14         [string]$msg
15     )
16 
17     $DebugPreference = "Continue"
18     $date_str = Get-Date -Format u
19     $msg = "$date_str $msg"
20 
21     Write-Debug $msg
22     $log_path = Get-Variable -Name log_path -Scope Global -ValueOnly -ErrorAction SilentlyContinue
23     if($log_path) {
24         Add-Content -LiteralPath $log_path -Value $msg
25     }
26 }
27 
Get-DomainMembershipMatchnull28 Function Get-DomainMembershipMatch {
29     Param(
30         [string] $dns_domain_name
31     )
32 
33     # FUTURE: add support for NetBIOS domain name?
34 
35     # this requires the DC to be accessible; "DC unavailable" is indistinguishable from "not joined to the domain"...
36     Try {
37         Write-DebugLog "calling GetComputerDomain()"
38         $current_dns_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name
39 
40         $domain_match = $current_dns_domain -eq $dns_domain_name
41 
42         Write-DebugLog ("current domain {0} matches {1}: {2}" -f $current_dns_domain, $dns_domain_name, $domain_match)
43 
44         return $domain_match
45     }
46     catch [System.Security.Authentication.AuthenticationException] {
47         Write-DebugLog "Failed to get computer domain.  Attempting a different method."
48         Add-Type -AssemblyName System.DirectoryServices.AccountManagement
49         $user_principal = [System.DirectoryServices.AccountManagement.UserPrincipal]::Current
50         If ($user_principal.ContextType -eq "Machine") {
51             $current_dns_domain = (Get-CimInstance -ClassName Win32_ComputerSystem -Property Domain).Domain
52 
53             $domain_match = $current_dns_domain -eq $dns_domain_name
54 
55             Write-DebugLog ("current domain {0} matches {1}: {2}" -f $current_dns_domain, $dns_domain_name, $domain_match)
56 
57             return $domain_match
58         }
59         Else {
60             Fail-Json -obj $result -message "Failed to authenticate with domain controller and cannot retrieve the existing domain name: $($_.Exception.Message)"
61         }
62     }
63     Catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] {
64         Write-DebugLog "not currently joined to a reachable domain"
65         return $false
66     }
67 }
68 
New-Credential()69 Function New-Credential {
70     Param(
71         [string] $cred_user,
72         [string] $cred_pass
73     )
74 
75     $cred = New-Object System.Management.Automation.PSCredential($cred_user, $($cred_pass | ConvertTo-SecureString -AsPlainText -Force))
76 
77     return $cred
78 }
79 
Get-HostnameMatchnull80 Function Get-HostnameMatch {
81     Param(
82         [string] $hostname
83     )
84 
85     # Add-Computer will validate the "shape" of the hostname- we just care if it matches...
86 
87     $hostname_match = $env:COMPUTERNAME -eq $hostname
88     Write-DebugLog ("current hostname {0} matches {1}: {2}" -f $env:COMPUTERNAME, $hostname, $hostname_match)
89 
90     return $hostname_match
91 }
92 
Test-DomainJoined()93 Function Test-DomainJoined {
94     return (Get-CIMInstance Win32_ComputerSystem).PartOfDomain
95 }
96 
Join-Domain()97 Function Join-Domain {
98     Param(
99         [string] $dns_domain_name,
100         [string] $new_hostname,
101         [string] $domain_admin_user,
102         [string] $domain_admin_password,
103         [string] $domain_ou_path
104     )
105 
106     Write-DebugLog ("Creating credential for user {0}" -f $domain_admin_user)
107     $domain_cred = New-Credential $domain_admin_user $domain_admin_password
108 
109     $add_args = @{
110         ComputerName="."
111         Credential=$domain_cred
112         DomainName=$dns_domain_name
113         Force=$null
114     }
115 
116     Write-DebugLog "adding hostname set arg to Add-Computer args"
117     If($new_hostname) {
118         $add_args["NewName"] = $new_hostname
119     }
120 
121 
122     if($domain_ou_path){
123         Write-DebugLog "adding OU destination arg to Add-Computer args"
124         $add_args["OUPath"] = $domain_ou_path
125     }
126     $argstr = $add_args | Out-String
127     Write-DebugLog "calling Add-Computer with args: $argstr"
128     try {
129         $add_result = Add-Computer @add_args
130     } catch {
131         Fail-Json -obj $result -message "failed to join domain: $($_.Exception.Message)"
132     }
133 
134     Write-DebugLog ("Add-Computer result was \n{0}" -f $add_result | Out-String)
135 }
136 
Get-Workgroup()137 Function Get-Workgroup {
138     return (Get-CIMInstance Win32_ComputerSystem).Workgroup
139 }
140 
Set-Workgroup()141 Function Set-Workgroup {
142     Param(
143         [string] $workgroup_name
144     )
145 
146     Write-DebugLog ("Calling JoinDomainOrWorkgroup with workgroup {0}" -f $workgroup_name)
147     try {
148         $swg_result = Get-CimInstance Win32_ComputerSystem | Invoke-CimMethod -MethodName JoinDomainOrWorkgroup -Arguments @{Name="$workgroup_name"}
149     } catch {
150         Fail-Json -obj $result -message "failed to call Win32_ComputerSystem.JoinDomainOrWorkgroup($workgroup_name): $($_.Exception.Message)"
151     }
152 
153     if ($swg_result.ReturnValue -ne 0) {
154         Fail-Json -obj $result -message "failed to set workgroup through WMI, return value: $($swg_result.ReturnValue)"
155     }
156 }
157 
Join-Workgroupnull158 Function Join-Workgroup {
159     Param(
160         [string] $workgroup_name,
161         [string] $domain_admin_user,
162         [string] $domain_admin_password
163     )
164 
165     If(Test-DomainJoined) { # if we're on a domain, unjoin it (which forces us to join a workgroup)
166         $domain_cred = New-Credential $domain_admin_user $domain_admin_password
167 
168         # 2012+ call the Workgroup arg WorkgroupName, but seem to accept
169         try {
170             Remove-Computer -Workgroup $workgroup_name -Credential $domain_cred -Force
171         } catch {
172             Fail-Json -obj $result -message "failed to remove computer from domain: $($_.Exception.Message)"
173         }
174     }
175 
176     # we're already on a workgroup- change it.
177     Else {
178         Set-Workgroup $workgroup_name
179     }
180 }
181 
182 
183 $result = @{
184     changed = $false
185     reboot_required = $false
186 }
187 
188 $params = Parse-Args -arguments $args -supports_check_mode $true
189 
190 $state = Get-AnsibleParam $params "state" -validateset @("domain","workgroup") -failifempty $result
191 
192 $dns_domain_name = Get-AnsibleParam $params "dns_domain_name"
193 $hostname = Get-AnsibleParam $params "hostname"
194 $workgroup_name = Get-AnsibleParam $params "workgroup_name"
195 $domain_admin_user = Get-AnsibleParam $params "domain_admin_user" -failifempty $result
196 $domain_admin_password = Get-AnsibleParam $params "domain_admin_password" -failifempty $result
197 $domain_ou_path = Get-AnsibleParam $params "domain_ou_path"
198 
199 $log_path = Get-AnsibleParam $params "log_path"
200 $_ansible_check_mode = Get-AnsibleParam $params "_ansible_check_mode" -default $false
201 
202 If ($state -eq "domain") {
203     If(-not $dns_domain_name) {
204         Fail-Json @{} "dns_domain_name is required when state is 'domain'"
205     }
206 }
207 Else { # workgroup
208     If(-not $workgroup_name) {
209         Fail-Json @{} "workgroup_name is required when state is 'workgroup'"
210     }
211 }
212 
213 Set-Variable -Name log_path -Scope Global -Value $log_path
214 
215 Try {
216 
217     $hostname_match = If($hostname) { Get-HostnameMatch $hostname } Else { $true }
218 
219     $result.changed = $result.changed -or (-not $hostname_match)
220 
221     Switch($state) {
222         domain {
223             $domain_match = Get-DomainMembershipMatch $dns_domain_name
224 
225             $result.changed = $result.changed -or (-not $domain_match)
226 
227             If($result.changed -and -not $_ansible_check_mode) {
228                 If(-not $domain_match) {
229                     If(Test-DomainJoined) {
230                         Write-DebugLog "domain doesn't match, and we're already joined to another domain"
231                         throw "switching domains is not implemented"
232                     }
233 
234                     $join_args = @{
235                         dns_domain_name = $dns_domain_name
236                         domain_admin_user = $domain_admin_user
237                         domain_admin_password = $domain_admin_password
238                     }
239 
240                     Write-DebugLog "not a domain member, joining..."
241 
242                     If(-not $hostname_match) {
243                         Write-DebugLog "adding hostname change to domain-join args"
244                         $join_args.new_hostname = $hostname
245                     }
246                     If($null -ne $domain_ou_path){ # If OU Path is not empty
247                         Write-DebugLog "adding domain_ou_path to domain-join args"
248                         $join_args.domain_ou_path = $domain_ou_path
249                     }
250 
251                     Join-Domain @join_args
252 
253                     # this change requires a reboot
254                     $result.reboot_required = $true
255                 }
256                 ElseIf(-not $hostname_match) { # domain matches but hostname doesn't, just do a rename
257                     Write-DebugLog ("domain matches, setting hostname to {0}" -f $hostname)
258 
259                     $rename_args = @{NewName=$hostname}
260 
261                     If (Test-DomainJoined) {
262                         $domain_cred = New-Credential $domain_admin_user $domain_admin_password
263                         $rename_args.DomainCredential = $domain_cred
264                     }
265 
266                     Rename-Computer @rename_args
267 
268                     # this change requires a reboot
269                     $result.reboot_required = $true
270                 } Else {
271                     # no change is needed
272                 }
273 
274             }
275             Else {
276                 Write-DebugLog "check mode, exiting early..."
277             }
278 
279         }
280 
281         workgroup {
282             $workgroup_match = $(Get-Workgroup) -eq $workgroup_name
283 
284             $result.changed = $result.changed -or (-not $workgroup_match)
285 
286             If(-not $_ansible_check_mode) {
287                 If(-not $workgroup_match) {
288                     Write-DebugLog ("setting workgroup to {0}" -f $workgroup_name)
289                     Join-Workgroup -workgroup_name $workgroup_name -domain_admin_user $domain_admin_user -domain_admin_password $domain_admin_password
290 
291                     # this change requires a reboot
292                     $result.reboot_required = $true
293                 }
294                 If(-not $hostname_match) {
295                     Write-DebugLog ("setting hostname to {0}" -f $hostname)
296                     Rename-Computer -NewName $hostname
297 
298                     # this change requires a reboot
299                     $result.reboot_required = $true
300                 }
301             }
302         }
303         default { throw "invalid state $state" }
304     }
305 
306     Exit-Json $result
307 }
308 Catch {
309     $excep = $_
310 
311     Write-DebugLog "Exception: $($excep | out-string)"
312 
313     Throw
314 }
315