1 #!powershell
2 
3 # Copyright: (c) 2020, Brian Scholer (@briantist)
4 # Copyright: (c) 2017, AMTEGA - Xunta de Galicia
5 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 
7 #Requires -Module Ansible.ModuleUtils.Legacy
8 #Requires -Module Ansible.ModuleUtils.ArgvParser
9 #Requires -Module Ansible.ModuleUtils.CommandUtil
10 
11 
12 # ------------------------------------------------------------------------------
13 $ErrorActionPreference = "Stop"
14 
15 # Preparing result
16 $result = @{}
17 $result.changed = $false
18 
19 # Parameter ingestion
20 $params = Parse-Args $args -supports_check_mode $true
21 
22 $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool"  -default $false
23 $diff_support = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
24 $temp = Get-AnsibleParam -obj $params -name '_ansible_remote_tmp' -type 'path' -default $env:TEMP
25 
26 $name = Get-AnsibleParam -obj $params -name "name" -failifempty $true -resultobj $result
27 $sam_account_name = Get-AnsibleParam -obj $params -name "sam_account_name" -default "${name}$"
28 If (-not $sam_account_name.EndsWith("$")) {
29   $sam_account_name = "${sam_account_name}$"
30 }
31 $enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool" -default $true
32 $description = Get-AnsibleParam -obj $params -name "description" -default $null
33 $domain_username = Get-AnsibleParam -obj $params -name "domain_username" -type "str"
34 $domain_password = Get-AnsibleParam -obj $params -name "domain_password" -type "str" -failifempty ($null -ne $domain_username)
35 $domain_server = Get-AnsibleParam -obj $params -name "domain_server" -type "str"
36 $state = Get-AnsibleParam -obj $params -name "state" -ValidateSet "present","absent" -default "present"
37 $managed_by = Get-AnsibleParam -obj $params -name "managed_by" -type "str"
38 
39 $odj_action = Get-AnsibleParam -obj $params -name "offline_domain_join" -type "str" -ValidateSet "none","output","path" -default "none"
40 $_default_blob_path = Join-Path -Path $temp -ChildPath ([System.IO.Path]::GetRandomFileName())
41 $odj_blob_path = Get-AnsibleParam -obj $params -name "odj_blob_path" -type "str" -default $_default_blob_path
42 
43 $extra_args = @{}
44 if ($null -ne $domain_username) {
45     $domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force
46     $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $domain_password
47     $extra_args.Credential = $credential
48 }
49 if ($null -ne $domain_server) {
50     $extra_args.Server = $domain_server
51 }
52 
53 If ($state -eq "present") {
54   $dns_hostname = Get-AnsibleParam -obj $params -name "dns_hostname" -failifempty $true -resultobj $result
55   $ou = Get-AnsibleParam -obj $params -name "ou" -failifempty $true -resultobj $result
56   $distinguished_name = "CN=$name,$ou"
57 
58   $desired_state = [ordered]@{
59     name = $name
60     sam_account_name = $sam_account_name
61     dns_hostname = $dns_hostname
62     ou = $ou
63     distinguished_name = $distinguished_name
64     description = $description
65     enabled = $enabled
66     state = $state
67     managed_by = $managed_by
68   }
69 } Else {
70   $desired_state = [ordered]@{
71     name = $name
72     sam_account_name = $sam_account_name
73     state = $state
74   }
75 }
76 
77 # ------------------------------------------------------------------------------
Get-InitialState($desired_state)78 Function Get-InitialState($desired_state) {
79   # Test computer exists
80   $computer = Try {
81     Get-ADComputer `
82       -Identity $desired_state.sam_account_name `
83       -Properties DistinguishedName,DNSHostName,Enabled,Name,SamAccountName,Description,ObjectClass,ManagedBy `
84       @extra_args
85   } Catch { $null }
86   If ($computer) {
87       $null,$current_ou = $computer.DistinguishedName -split '(?<=[^\\](?:\\\\)*),'
88       $current_ou = $current_ou -join ','
89 
90       $initial_state = [ordered]@{
91         name = $computer.Name
92         sam_account_name = $computer.SamAccountName
93         dns_hostname = $computer.DNSHostName
94         ou = $current_ou
95         distinguished_name = $computer.DistinguishedName
96         description = $computer.Description
97         enabled = $computer.Enabled
98         state = "present"
99         managed_by = $computer.ManagedBy
100       }
101   } Else {
102     $initial_state = [ordered]@{
103       name = $desired_state.name
104       sam_account_name = $desired_state.sam_account_name
105       state = "absent"
106     }
107   }
108 
109   return $initial_state
110 }
111 
112 # ------------------------------------------------------------------------------
Set-ConstructedState($initial_state, $desired_state)113 Function Set-ConstructedState($initial_state, $desired_state) {
114   Try {
115     Set-ADComputer `
116       -Identity $desired_state.name `
117       -SamAccountName $desired_state.name `
118       -DNSHostName $desired_state.dns_hostname `
119       -Enabled $desired_state.enabled `
120       -Description $desired_state.description `
121       -ManagedBy $desired_state.managed_by `
122       -WhatIf:$check_mode `
123       @extra_args
124   } Catch {
125     Fail-Json -obj $result -message "Failed to set the AD object $($desired_state.name): $($_.Exception.Message)"
126   }
127 
128   If ($initial_state.distinguished_name -cne $desired_state.distinguished_name) {
129     # Move computer to OU
130     Try {
131       Get-ADComputer -Identity $desired_state.sam_account_name @extra_args |
132           Move-ADObject `
133             -TargetPath $desired_state.ou `
134             -Confirm:$False `
135             -WhatIf:$check_mode `
136             @extra_args
137     } Catch {
138       Fail-Json -obj $result -message "Failed to move the AD object $($initial_state.distinguished_name) to $($desired_state.distinguished_name): $($_.Exception.Message)"
139     }
140   }
141   $result.changed = $true
142 }
143 
144 # ------------------------------------------------------------------------------
Add-ConstructedState($desired_state)145 Function Add-ConstructedState($desired_state) {
146   Try {
147     New-ADComputer `
148       -Name $desired_state.name `
149       -SamAccountName $desired_state.sam_account_name `
150       -DNSHostName $desired_state.dns_hostname `
151       -Path $desired_state.ou `
152       -Enabled $desired_state.enabled `
153       -Description $desired_state.description `
154       -ManagedBy $desired_state.managed_by `
155       -WhatIf:$check_mode `
156       @extra_args
157     } Catch {
158       Fail-Json -obj $result -message "Failed to create the AD object $($desired_state.name): $($_.Exception.Message)"
159     }
160 
161   $result.changed = $true
162 }
163 
Invoke-OfflineDomainJoin()164 Function Invoke-OfflineDomainJoin {
165   [CmdletBinding(SupportsShouldProcess=$true)]
166   param(
167     [Parameter(Mandatory=$true)]
168     [System.Collections.IDictionary]
169     $desired_state ,
170 
171     [Parameter(Mandatory=$true)]
172     [ValidateSet('none','output','path')]
173     [String]
174     $Action ,
175 
176     [Parameter()]
177     [System.IO.FileInfo]
178     $BlobPath
179   )
180 
181   End {
182     if ($Action -eq 'none') {
183       return
184     }
185 
186     $dns_domain = $desired_state.dns_hostname -replace '^[^.]+\.'
187 
188     $output = $Action -eq 'output'
189 
190     $arguments = @(
191       'djoin.exe'
192       '/PROVISION'
193       '/REUSE'  # we're pre-creating the machine normally to set other fields, then overwriting it with this
194       '/DOMAIN'
195       $dns_domain
196       '/MACHINE'
197       $desired_state.sam_account_name.TrimEnd('$')  # this machine name is the short name
198       '/MACHINEOU'
199       $desired_state.ou
200       '/SAVEFILE'
201       $BlobPath.FullName
202     )
203 
204     $invocation = Argv-ToString -arguments $arguments
205     $result.djoin = @{
206       invocation = $invocation
207     }
208     $result.odj_blob = ''
209 
210     if ($Action -eq 'path') {
211       $result.odj_blob_path = $BlobPath.FullName
212     }
213 
214     if (-not $BlobPath.Directory.Exists) {
215       Fail-Json -obj $result -message "BLOB path directory '$($BlobPath.Directory.FullName)' doesn't exist."
216     }
217 
218     if ($PSCmdlet.ShouldProcess($argstring)) {
219       try {
220         $djoin_result = Run-Command -command $invocation
221         $result.djoin.rc = $djoin_result.rc
222         $result.djoin.stdout = $djoin_result.stdout
223         $result.djoin.stderr = $djoin_result.stderr
224 
225         if ($djoin_result.rc) {
226           Fail-Json -obj $result -message "Problem running djoin.exe. See returned values."
227         }
228 
229         if ($output) {
230           $bytes = [System.IO.File]::ReadAllBytes($BlobPath.FullName)
231           $data = [Convert]::ToBase64String($bytes)
232           $result.odj_blob = $data
233         }
234       }
235       finally {
236         if ($output -and $BlobPath.Exists) {
237           $BlobPath.Delete()
238         }
239       }
240     }
241   }
242 }
243 
244 # ------------------------------------------------------------------------------
Remove-ConstructedState($initial_state)245 Function Remove-ConstructedState($initial_state) {
246   Try {
247     Get-ADComputer -Identity $initial_state.sam_account_name @extra_args |
248       Remove-ADObject `
249         -Recursive `
250         -Confirm:$False `
251         -WhatIf:$check_mode `
252         @extra_args
253   } Catch {
254     Fail-Json -obj $result -message "Failed to remove the AD object $($desired_state.name): $($_.Exception.Message)"
255   }
256 
257   $result.changed = $true
258 }
259 
260 # ------------------------------------------------------------------------------
Test-HashtableEquality($x, $y)261 Function Test-HashtableEquality($x, $y) {
262   # Compare not nested HashTables
263   Foreach ($key in $x.Keys) {
264       If (($y.Keys -notcontains $key) -or ($x[$key] -cne $y[$key])) {
265           Return $false
266       }
267   }
268   foreach ($key in $y.Keys) {
269       if (($x.Keys -notcontains $key) -or ($x[$key] -cne $y[$key])) {
270           Return $false
271       }
272   }
273   Return $true
274 }
275 
276 # ------------------------------------------------------------------------------
277 $initial_state = Get-InitialState($desired_state)
278 
279 If ($desired_state.state -eq "present") {
280     If ($initial_state.state -eq "present") {
281       $in_desired_state = Test-HashtableEquality -X $initial_state -Y $desired_state
282 
283       If (-not $in_desired_state) {
284         Set-ConstructedState -initial_state $initial_state -desired_state $desired_state
285       }
286     } Else { # $desired_state.state = "Present" & $initial_state.state = "Absent"
287       Add-ConstructedState -desired_state $desired_state
288       Invoke-OfflineDomainJoin -desired_state $desired_state -Action $odj_action -BlobPath $odj_blob_path -WhatIf:$check_mode
289     }
290   } Else { # $desired_state.state = "Absent"
291     If ($initial_state.state -eq "present") {
292       Remove-ConstructedState -initial_state $initial_state
293     }
294   }
295 
296 If ($diff_support) {
297   $diff = @{
298     before = $initial_state
299     after = $desired_state
300   }
301   $result.diff = $diff
302 }
303 
304 Exit-Json -obj $result
305