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