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
Changes from all 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)
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


# ------------------------------------------------------------------------------
@@ -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
@@ -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
@@ -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
@@ -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 {
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 (-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()
briantist marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
}

# ------------------------------------------------------------------------------
Function Remove-ConstructedState($initial_state) {
Try {
@@ -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])) {
@@ -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
}
}

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)

@@ -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:
@@ -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:
@@ -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.
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
@@ -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"
@@ -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.
'''
441 changes: 424 additions & 17 deletions tests/integration/targets/win_domain_computer/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -2,55 +2,92 @@
# these are here if someone wants to run the module tests locally on their own
# domain.
# Requirements:
# LDAP Base path set in defaults/main.yml like DC=ansible,DC=local
# Custom OU path set in defaults/main.yml like OU=ou1,DC=ansible,DC=local
# Set the names in vars: on the command line, or set the following:
# test_domain_name: The DNS name of the domain like ansible.local
# test_ad_domain_dn: The DN of the domain like DC=ansible,DC=local
# test_ad_computer_ou: The DN of the OU where computers will be created like OU=ou1,DC=ansible,DC=local
#
# This is not a traditional role, and can't be used with ansible-test. This is a playbook. To run ensure:
# - your collections are set up and Ansible knows where to find them ($ANSIBLE_COLLECTIONS_PATHS for example)
# - your inventory contains a host where this can run, like a domain controller
# - connection keywords/options/vars are set properly to connect to the host
# - the variable win_domain_computer_testing_host contains the name of the host or the group that contains it
#
# then call this file with ansible-playbook and any extra vars or other params you need
---
- name: run win_domain_users test
hosts: win_domain_computer_testing_host
hosts: "{{ win_domain_computer_testing_host }}"
gather_facts: no
collections:
- community.windows
vars:
test_win_domain_computer_ldap_base: "{{ test_ad_ou }}"
test_win_domain_computer_ou_path: "{{ test_ad_group_ou }}"
test_win_domain_computer_name: "test_computer.{{ test_domain_name }}"
test_win_domain_computer_ldap_base: "{{ test_ad_domain_dn }}"
test_win_domain_computer_ou_path: "{{ test_ad_computer_ou }}"
test_win_domain_computer_name: "test_computer"
test_win_domain_domain_name: "{{ test_domain_name }}"
test_win_domain_computer_dns_hostname: "{{ test_win_domain_computer_name }}.{{ test_domain_name }}"
test_win_domain_computer_description: "{{ test_computer_description | default('description') }}"
tasks:

- name: ensure the computer is deleted before the test
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
state: absent
tags: always

# --------------------------------------------------------------------------

- name: Test computer with long name and distinct sam_account_name
tags:
- long_name
vars:
test_win_domain_computer_long_name: '{{ test_win_domain_computer_name }}_with_long_name'
test_win_domain_computer_sam_account_name: '{{ test_win_domain_computer_name }}$'
test_win_domain_computer_dns_hostname: "{{ test_win_domain_computer_long_name }}.{{ test_domain_name }}"
block:

# ----------------------------------------------------------------------
- name: create computer with long name and distinct sam_account_name
- name: create computer with long name and distinct sam_account_name (check mode)
win_domain_computer:
name: '{{ test_win_domain_computer_long_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
enabled: yes
state: present
register: create_distinct_sam_account_name
check_mode: yes

- name: get actual computer with long name and distinct sam_account_name
ansible.windows.win_command: powershell.exe "Import-Module ActiveDirectory; Get-ADComputer -Identity '{{ test_win_domain_computer_sam_account_name }}'"
register: create_distinct_sam_account_name_check
ignore_errors: True

- name: assert create computer with long name and distinct sam_account_name
- name: assert create computer with long name and distinct sam_account_name (check mode)
assert:
that:
- create_distinct_sam_account_name is changed
- create_distinct_sam_account_name_check.rc == 1
- create_distinct_sam_account_name is changed

- name: create computer with long name and distinct sam_account_name
win_domain_computer:
name: '{{ test_win_domain_computer_long_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
enabled: yes
state: present
register: create_distinct_sam_account_name

- name: get actual computer with long name and distinct sam_account_name
ansible.windows.win_shell: |
Import-Module ActiveDirectory
$c = Get-ADComputer -Identity '{{ test_win_domain_computer_sam_account_name }}' -ErrorAction Stop
if ($c.Name -ne '{{ test_win_domain_computer_long_name }}') {
throw 'Wrong computer name in relation to sAMAccountName'
}
register: create_distinct_sam_account_name_check

- name: (Idempotence) create computer with long name and distinct sam_account_name
win_domain_computer:
name: '{{ test_win_domain_computer_long_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
enabled: yes
state: present
register: create_distinct_sam_account_name_idempotence
@@ -61,11 +98,381 @@
that:
- create_distinct_sam_account_name_idempotence is not changed

- name: ensure the test group is deleted after the test
always:
- name: ensure the test computer is deleted after the test
win_domain_computer:
name: '{{ test_win_domain_computer_long_name }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
state: absent
ignore_protection: True

# ----------------------------------------------------------------------

- name: Test offline domain join
tags:
- djoin
vars:
test_win_domain_computer_sam_account_name: '{{ test_win_domain_computer_name }}$'
block:
- name: No file with blob return
block:
- name: Create computer with offline domain join and blob return (check mode)
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: output
register: odj_result
check_mode: yes

- name: assert odj (check mode)
assert:
that:
- odj_result is changed
- odj_result.odj_blob is defined
- odj_result.odj_blob == ''
- odj_result.odj_blob_path is not defined
- odj_result.djoin is defined
- odj_result.djoin.invocation is defined

- name: Create computer with offline domain join and blob return
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: output
register: odj_result

- name: assert odj
assert:
that:
- odj_result is changed
- odj_result.odj_blob is defined
- odj_result.odj_blob != ''
- odj_result.odj_blob_path is not defined
- odj_result.djoin is defined
- odj_result.djoin.invocation is defined
- odj_result.djoin.rc is defined
- odj_result.djoin.rc == 0
- odj_result.djoin.stdout is defined
- odj_result.djoin.stderr is defined

- name: Create computer with offline domain join and blob return (idempotence)
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: output
register: odj_result

- name: assert odj
assert:
that:
- odj_result is not changed
- odj_result.odj_blob is not defined
- odj_result.odj_blob_path is not defined
- odj_result.djoin is not defined

always:
- name: ensure the test computer is deleted after the test
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
state: absent


- name: File and blob return
vars:
blob_path: 'C:\Windows\Temp\blob.txt'
block:
- name: Create computer with offline domain join and blob file with return (check mode)
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: output
odj_blob_path: "{{ blob_path }}"
register: odj_result
check_mode: yes

- name: assert odj (check mode)
assert:
that:
- odj_result is changed
- odj_result.odj_blob is defined
- odj_result.odj_blob == ''
- odj_result.odj_blob_path is not defined
- odj_result.djoin is defined
- odj_result.djoin.invocation is defined

- name: Create computer with offline domain join and blob file with return
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: output
odj_blob_path: "{{ blob_path }}"
register: odj_result

- name: assert odj
assert:
that:
- odj_result is changed
- odj_result.odj_blob is defined
- odj_result.odj_blob != ''
- odj_result.odj_blob_path is not defined
- odj_result.djoin is defined
- odj_result.djoin.invocation is defined
- odj_result.djoin.rc is defined
- odj_result.djoin.rc == 0
- odj_result.djoin.stdout is defined
- odj_result.djoin.stderr is defined

- name: Create computer with offline domain join and blob file with return (idempotence)
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: output
odj_blob_path: "{{ blob_path }}"
register: odj_result

- name: assert odj
assert:
that:
- odj_result is not changed
- odj_result.odj_blob is not defined
- odj_result.odj_blob_path is not defined
- odj_result.djoin is not defined

always:
- name: ensure the test computer is deleted after the test
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
state: absent

- name: ensure the blob file is deleted
win_shell: |
Remove-Item -LiteralPath '{{ blob_path }}' -Force -ErrorAction SilentlyContinue
exit 0
- name: Specified file return
vars:
blob_path: 'C:\Windows\Temp\blob.txt'
block:
- name: Create computer with offline domain join and blob file return with specified path (check mode)
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: path
odj_blob_path: "{{ blob_path }}"
register: odj_result
check_mode: yes

- name: assert odj (check mode)
assert:
that:
- odj_result is changed
- odj_result.odj_blob is defined
- odj_result.odj_blob == ''
- odj_result.odj_blob_path is defined
- odj_result.odj_blob_path == blob_path
- odj_result.djoin is defined
- odj_result.djoin.invocation is defined

- name: Create computer with offline domain join and blob file return with specified path
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: path
odj_blob_path: "{{ blob_path }}"
register: odj_result

- name: assert odj
assert:
that:
- odj_result is changed
- odj_result.odj_blob is defined
- odj_result.odj_blob == ''
- odj_result.odj_blob_path is defined
- odj_result.odj_blob_path == blob_path
- odj_result.djoin is defined
- odj_result.djoin.invocation is defined
- odj_result.djoin.rc is defined
- odj_result.djoin.rc == 0
- odj_result.djoin.stdout is defined
- odj_result.djoin.stderr is defined

- name: Test ODJ File
ansible.windows.win_shell: |
$ErrorActionPreference = 'Stop'
$file = '{{ odj_result.odj_blob_path }}'
$content = Get-Content -LiteralPath $file -Raw -Encoding Unicode
$trimmed = $content.TrimEnd("`0")
if ($content.Length -eq $trimmed.Length) { throw 'No terminating null found' }
# try a base64 decode to validate it is the kind of data we expect
$bytes = [Convert]::FromBase64String($trimmed)
- name: Create computer with offline domain join and blob file return with specified path (idempotence)
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: path
odj_blob_path: "{{ blob_path }}"
register: odj_result

- name: assert odj
assert:
that:
- odj_result is not changed
- odj_result.odj_blob is not defined
- odj_result.odj_blob_path is not defined
- odj_result.djoin is not defined

always:
- name: ensure the test computer is deleted after the test
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
state: absent

- name: ensure the blob file is deleted
win_shell: |
Remove-Item -LiteralPath '{{ blob_path }}' -Force -ErrorAction SilentlyContinue
exit 0
- name: Random file return
block:
- name: Create computer with offline domain join and random blob file return (check mode)
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: path
register: odj_result
check_mode: yes

- name: assert odj (check mode)
assert:
that:
- odj_result is changed
- odj_result.odj_blob is defined
- odj_result.odj_blob == ''
- odj_result.odj_blob_path is defined
- odj_result.djoin is defined
- odj_result.djoin.invocation is defined

- name: Create computer with offline domain join and random blob file return
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: path
register: odj_result

- name: assert odj
assert:
that:
- odj_result is changed
- odj_result.odj_blob is defined
- odj_result.odj_blob == ''
- odj_result.odj_blob_path is defined
- odj_result.djoin is defined
- odj_result.djoin.invocation is defined
- odj_result.djoin.rc is defined
- odj_result.djoin.rc == 0
- odj_result.djoin.stdout is defined
- odj_result.djoin.stderr is defined

- name: This file needs to be deleted later
set_fact:
returned_file: "{{ odj_result.odj_blob_path }}"

- name: Test ODJ File
ansible.windows.win_shell: |
$ErrorActionPreference = 'Stop'
$file = '{{ odj_result.odj_blob_path }}'
$content = Get-Content -LiteralPath $file -Raw -Encoding Unicode
$trimmed = $content.TrimEnd("`0")
if ($content.Length -eq $trimmed.Length) { throw 'No terminating null found' }
# try a base64 decode to validate it is the kind of data we expect
$bytes = [Convert]::FromBase64String($trimmed)
- name: Create computer with offline domain join and random blob file return (idempotence)
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
dns_hostname: '{{ test_win_domain_computer_dns_hostname }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
ou: "{{ test_win_domain_computer_ou_path }}"
description: "{{ test_computer_description }}"
enabled: yes
state: present
offline_domain_join: path
register: odj_result

- name: assert odj
assert:
that:
- odj_result is not changed
- odj_result.odj_blob is not defined
- odj_result.odj_blob_path is not defined
- odj_result.djoin is not defined

always:
- name: ensure the test computer is deleted after the test
win_domain_computer:
name: '{{ test_win_domain_computer_name }}'
sam_account_name: '{{ test_win_domain_computer_sam_account_name }}'
state: absent

- name: ensure the blob file is deleted
win_shell: |
Remove-Item -LiteralPath '{{ returned_file }}' -Force -ErrorAction SilentlyContinue
exit 0