Skip to content

Commit

Permalink
win_domain_computer - add offline domain join support (#93)
Browse files Browse the repository at this point in the history
* Fix up tests, documentation, function calls

* Add offline domain join capability, documentation, tests, examples

* Add changelog for win_domain_computer offline domain join

* minor fixes in changelog

* Add link to PR in changelog

* Changes from review, tests and docs to match

* Handle blob file/dir existence issues
  • Loading branch information
briantist authored Jun 9, 2020
1 parent d6e678b commit 6e8b122
Show file tree
Hide file tree
Showing 4 changed files with 621 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
minor_changes:
- win_domain_computer - add support for offline domain join (https://github.com/ansible-collections/community.windows/pull/93)
- win_domain_computer - ``sam_account_name`` with missing ``$`` will have it added automatically (https://github.com/ansible-collections/community.windows/pull/93)
109 changes: 100 additions & 9 deletions plugins/modules/win_domain_computer.ps1
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#!powershell

# Copyright: (c) 2020, Brian Scholer (@briantist)
# Copyright: (c) 2017, AMTEGA - Xunta de Galicia
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

#Requires -Module Ansible.ModuleUtils.Legacy
#Requires -Module Ansible.ModuleUtils.ArgvParser
#Requires -Module Ansible.ModuleUtils.CommandUtil


# ------------------------------------------------------------------------------
Expand All @@ -18,11 +21,12 @@ $params = Parse-Args $args -supports_check_mode $true

$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
$diff_support = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
$temp = Get-AnsibleParam -obj $params -name '_ansible_remote_tmp' -type 'path' -default $env:TEMP

$name = Get-AnsibleParam -obj $params -name "name" -failifempty $true -resultobj $result
$sam_account_name = Get-AnsibleParam -obj $params -name "sam_account_name" -default "$name$"
$sam_account_name = Get-AnsibleParam -obj $params -name "sam_account_name" -default "${name}$"
If (-not $sam_account_name.EndsWith("$")) {
Fail-Json -obj $result -message "sam_account_name must end in $"
$sam_account_name = "${sam_account_name}$"
}
$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool" -default $true
$description = Get-AnsibleParam -obj $params -name "description" -default $null
Expand All @@ -31,6 +35,10 @@ $domain_password = Get-AnsibleParam -obj $params -name "domain_password" -type "
$domain_server = Get-AnsibleParam -obj $params -name "domain_server" -type "str"
$state = Get-AnsibleParam -obj $params -name "state" -ValidateSet "present","absent" -default "present"

$odj_action = Get-AnsibleParam -obj $params -name "offline_domain_join" -type "str" -ValidateSet "none","output","path" -default "none"
$_default_blob_path = Join-Path -Path $temp -ChildPath ([System.IO.Path]::GetRandomFileName())
$odj_blob_path = Get-AnsibleParam -obj $params -name "odj_blob_path" -type "str" -default $_default_blob_path

$extra_args = @{}
if ($null -ne $domain_username) {
$domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force
Expand Down Expand Up @@ -74,12 +82,14 @@ Function Get-InitialState($desired_state) {
@extra_args
} Catch { $null }
If ($computer) {
$null,$current_ou = $computer.DistinguishedName -split '(?<=[^\\](?:\\\\)*),'
$current_ou = $current_ou -join ','

$initial_state = [ordered]@{
name = $computer.Name
sam_account_name = $computer.SamAccountName
dns_hostname = $computer.DNSHostName
# Get OU from regexp that removes all characters to the first ","
ou = $computer.DistinguishedName -creplace "^[^,]*,",""
ou = $current_ou
distinguished_name = $computer.DistinguishedName
description = $computer.Description
enabled = $computer.Enabled
Expand Down Expand Up @@ -146,6 +156,86 @@ Function Add-ConstructedState($desired_state) {
$result.changed = $true
}

Function Invoke-OfflineDomainJoin {
[CmdletBinding(SupportsShouldProcess=$true)]
param(
[Parameter(Mandatory=$true)]
[System.Collections.IDictionary]
$desired_state ,

[Parameter(Mandatory=$true)]
[ValidateSet('none','output','path')]
[String]
$Action ,

[Parameter()]
[System.IO.FileInfo]
$BlobPath
)

End {
if ($Action -eq 'none') {
return
}

$dns_domain = $desired_state.dns_hostname -replace '^[^.]+\.'

$output = $Action -eq 'output'

$arguments = @(
'djoin.exe'
'/PROVISION'
'/REUSE' # we're pre-creating the machine normally to set other fields, then overwriting it with this
'/DOMAIN'
$dns_domain
'/MACHINE'
$desired_state.sam_account_name.TrimEnd('$') # this machine name is the short name
'/MACHINEOU'
$desired_state.ou
'/SAVEFILE'
$BlobPath.FullName
)

$invocation = Argv-ToString -arguments $arguments
$result.djoin = @{
invocation = $invocation
}
$result.odj_blob = ''

if ($Action -eq 'path') {
$result.odj_blob_path = $BlobPath.FullName
}

if (-not $BlobPath.Directory.Exists) {
Fail-Json -obj $result -message "BLOB path directory '$($BlobPath.Directory.FullName)' doesn't exist."
}

if ($PSCmdlet.ShouldProcess($argstring)) {
try {
$djoin_result = Run-Command -command $invocation
$result.djoin.rc = $djoin_result.rc
$result.djoin.stdout = $djoin_result.stdout
$result.djoin.stderr = $djoin_result.stderr

if ($djoin_result.rc) {
Fail-Json -obj $result -message "Problem running djoin.exe. See returned values."
}

if ($output) {
$bytes = [System.IO.File]::ReadAllBytes($BlobPath.FullName)
$data = [Convert]::ToBase64String($bytes)
$result.odj_blob = $data
}
}
finally {
if ($output -and $BlobPath.Exists) {
$BlobPath.Delete()
}
}
}
}
}

# ------------------------------------------------------------------------------
Function Remove-ConstructedState($initial_state) {
Try {
Expand All @@ -163,7 +253,7 @@ Function Remove-ConstructedState($initial_state) {
}

# ------------------------------------------------------------------------------
Function are_hashtables_equal($x, $y) {
Function Test-HashtableEquality($x, $y) {
# Compare not nested HashTables
Foreach ($key in $x.Keys) {
If (($y.Keys -notcontains $key) -or ($x[$key] -cne $y[$key])) {
Expand All @@ -183,17 +273,18 @@ $initial_state = Get-InitialState($desired_state)

If ($desired_state.state -eq "present") {
If ($initial_state.state -eq "present") {
$in_desired_state = are_hashtables_equal $initial_state $desired_state
$in_desired_state = Test-HashtableEquality -X $initial_state -Y $desired_state

If (-not $in_desired_state) {
Set-ConstructedState $initial_state $desired_state
Set-ConstructedState -initial_state $initial_state -desired_state $desired_state
}
} Else { # $desired_state.state = "Present" & $initial_state.state = "Absent"
Add-ConstructedState($desired_state)
Add-ConstructedState -desired_state $desired_state
Invoke-OfflineDomainJoin -desired_state $desired_state -Action $odj_action -BlobPath $odj_blob_path -WhatIf:$check_mode
}
} Else { # $desired_state.state = "Absent"
If ($initial_state.state -eq "present") {
Remove-ConstructedState($initial_state)
Remove-ConstructedState -initial_state $initial_state
}
}

Expand Down
98 changes: 93 additions & 5 deletions plugins/modules/win_domain_computer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2020, Brian Scholer (@briantist)
# Copyright: (c) 2017, AMTEGA - Xunta de Galicia
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

Expand Down Expand Up @@ -32,7 +33,8 @@
operating systems compatibility.
- The LDAP display name (ldapDisplayName) for this property is sAMAccountName.
- If ommitted the value is the same as C(name).
- Note that all computer SAMAccountNames need to end with a $.
- Note that all computer SAMAccountNames need to end with a C($).
- If C($) is omitted, it will be added to the end.
type: str
enabled:
description:
Expand All @@ -47,6 +49,8 @@
description:
- Specifies the X.500 path of the Organizational Unit (OU) or container
where the new object is created. Required when I(state=present).
- "Special characters must be escaped,
see L(Distinguished Names,https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ldap/distinguished-names) for details."
type: str
description:
description:
Expand Down Expand Up @@ -86,6 +90,30 @@
type: str
choices: [ absent, present ]
default: present
offline_domain_join:
description:
- Provisions a computer in the directory and provides a BLOB file that can be used on the target computer/image to join it to the domain while offline.
- The C(none) value doesn't do any offline join operations.
- C(output) returns the BLOB in output. The BLOB should be treated as secret (it contains the machine password) so use C(no_log) when using this option.
- C(path) preserves the offline domain join BLOB file on the target machine for later use. The path will be returned.
- If the computer already exists, no BLOB will be created/returned, and the module will operate as it would have without offline domain join.
type: str
choices:
- none
- output
- path
default: none
odj_blob_path:
description:
- The path to the file where the BLOB will be saved. If omitted, a temporary file will be used.
- If I(offline_domain_join=output) the file will be deleted after its contents are returned.
- The parent directory for the BLOB file must exist; intermediate directories will not be created.
notes:
- "For more information on Offline Domain Join
see L(the step-by-step guide,https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd392267%28v=ws.10%29)."
- When using the ODJ BLOB to join a computer to the domain, it must be written out to a file.
- The file must be UTF-16 encoded (in PowerShell this encoding is called C(Unicode)), and it must end in a null character. See examples.
- The C(djoin.exe) part of the offline domain join process will not use I(domain_server), I(domain_username), or I(domain_password).
seealso:
- module: win_domain
- module: win_domain_controller
Expand All @@ -94,12 +122,13 @@
- module: win_domain_user
author:
- Daniel Sánchez Fábregas (@Daniel-Sanchez-Fabregas)
- Brian Scholer (@briantist)
'''

EXAMPLES = r'''
- name: Add linux computer to Active Directory OU using a windows machine
win_domain_computer:
name: one_linux_server.my_org.local
community.windows.win_domain_computer:
name: one_linux_server
sam_account_name: linux_server$
dns_hostname: one_linux_server.my_org.local
ou: "OU=servers,DC=my_org,DC=local"
Expand All @@ -109,11 +138,70 @@
delegate_to: my_windows_bridge.my_org.local
- name: Remove linux computer from Active Directory using a windows machine
win_domain_computer:
name: one_linux_server.my_org.local
community.windows.win_domain_computer:
name: one_linux_server
state: absent
delegate_to: my_windows_bridge.my_org.local
- name: Provision a computer for offline domain join
community.windows.win_domain_computer:
name: newhost
dns_hostname: newhost.ansible.local
ou: 'OU=A great\, big organizational unit name,DC=ansible,DC=local'
state: present
offline_domain_join: yes
odj_return_blob: yes
register: computer_status
delegate_to: windc.ansible.local
- name: Join a workgroup computer to the domain
vars:
target_blob_file: 'C:\ODJ\blob.txt'
ansible.windows.win_shell: |
$blob = [Convert]::FromBase64String('{{ computer_status.odj_blob }}')
[IO.File]::WriteAllBytes('{{ target_blob_file }}', $blob)
& djoin.exe --% /RequestODJ /LoadFile '{{ target_blob_file }}' /LocalOS /WindowsPath "%SystemRoot%"
- name: Restart to complete domain join
ansible.windows.win_restart:
'''

RETURN = r'''
odj_blob:
description:
- The offline domain join BLOB. This is an empty string when in check mode or when offline_domain_join is 'path'.
- This field contains the base64 encoded raw bytes of the offline domain join BLOB file.
returned: when offline_domain_join is not 'none' and the computer didn't exist
type: str
sample: <a long base64 string>
odj_blob_file:
description: The path to the offline domain join BLOB file on the target host. If odj_blob_path was specified, this will match that path.
returned: when offline_domain_join is 'path' and the computer didn't exist
type: str
sample: 'C:\Users\admin\AppData\Local\Temp\e4vxonty.rkb'
djoin:
description: Information about the invocation of djoin.exe.
returned: when offline_domain_join is True and the computer didn't exist
type: dict
contains:
invocation:
description: The full command line used to call djoin.exe
type: str
returned: always
sample: djoin.exe /PROVISION /MACHINE compname /MACHINEOU OU=Hosts,DC=ansible,DC=local /DOMAIN ansible.local /SAVEFILE blobfile.txt
rc:
description: The return code from djoin.exe
type: int
returned: when not check mode
sample: 87
stdout:
description: The stdout from djoin.exe
type: str
returned: when not check mode
sample: Computer provisioning completed successfully.
stderr:
description: The stderr from djoin.exe
type: str
returned: when not check mode
sample: Invalid input parameter combination.
'''
Loading

0 comments on commit 6e8b122

Please sign in to comment.