Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

win_domain_computer - add offline domain join support #93

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
105 changes: 96 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}$"
jborean93 marked this conversation as resolved.
Show resolved Hide resolved
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,82 @@ 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 {
jborean93 marked this conversation as resolved.
Show resolved Hide resolved
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 ($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) {
$BlobPath.Delete()
briantist marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
}

# ------------------------------------------------------------------------------
Function Remove-ConstructedState($initial_state) {
Try {
Expand All @@ -163,7 +249,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 +269,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
97 changes: 92 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,29 @@
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.
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.
jborean93 marked this conversation as resolved.
Show resolved Hide resolved
- 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 +121,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 +137,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