summaryrefslogtreecommitdiffstats
path: root/ansible_collections/microsoft/ad
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/microsoft/ad')
-rw-r--r--ansible_collections/microsoft/ad/.ansible-lint2
-rw-r--r--ansible_collections/microsoft/ad/.azure-pipelines/azure-pipelines.yml19
-rw-r--r--ansible_collections/microsoft/ad/.gitignore3
-rw-r--r--ansible_collections/microsoft/ad/CHANGELOG.rst26
-rw-r--r--ansible_collections/microsoft/ad/FILES.json197
-rw-r--r--ansible_collections/microsoft/ad/MANIFEST.json4
-rw-r--r--ansible_collections/microsoft/ad/changelogs/changelog.yaml36
-rw-r--r--ansible_collections/microsoft/ad/docs/docsite/extra-docs.yml1
-rw-r--r--ansible_collections/microsoft/ad/docs/docsite/rst/guide_ad_module_authentication.rst120
-rw-r--r--ansible_collections/microsoft/ad/docs/docsite/rst/guide_attributes.rst74
-rw-r--r--ansible_collections/microsoft/ad/docs/docsite/rst/guide_ldap_connection.rst2
-rw-r--r--ansible_collections/microsoft/ad/docs/docsite/rst/guide_migration.rst24
-rw-r--r--ansible_collections/microsoft/ad/plugins/action/domain.py32
-rw-r--r--ansible_collections/microsoft/ad/plugins/action/domain_child.py8
-rw-r--r--ansible_collections/microsoft/ad/plugins/action/domain_controller.py32
-rw-r--r--ansible_collections/microsoft/ad/plugins/doc_fragments/ad_object.py44
-rw-r--r--ansible_collections/microsoft/ad/plugins/doc_fragments/ldap_connection.py1
-rw-r--r--ansible_collections/microsoft/ad/plugins/inventory/ldap.py7
-rw-r--r--ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1340
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/computer.ps174
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/computer.py59
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/domain.py1
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/domain_child.ps1242
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/domain_child.yml184
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/domain_controller.py5
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/group.ps1135
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/group.py57
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/membership.ps12
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/object.py8
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/object_info.py4
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/ou.ps11
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/ou.py15
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/user.ps1108
-rw-r--r--ansible_collections/microsoft/ad/plugins/modules/user.py78
-rw-r--r--ansible_collections/microsoft/ad/plugins/plugin_utils/_module_with_reboot.py37
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml52
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_child/README.md36
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_child/Vagrantfile27
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_child/aliases2
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_child/ansible.cfg4
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_child/inventory.yml28
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_child/setup.yml71
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/cross_domain.yml596
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/main_child.yml98
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/main_tree.yml91
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/domain_child/test.yml146
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml11
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml269
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/tests.yml3
-rw-r--r--ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml15
-rw-r--r--ansible_collections/microsoft/ad/tests/sanity/ignore-2.14.txt1
-rw-r--r--ansible_collections/microsoft/ad/tests/sanity/ignore-2.15.txt1
-rw-r--r--ansible_collections/microsoft/ad/tests/sanity/ignore-2.16.txt1
-rw-r--r--ansible_collections/microsoft/ad/tests/sanity/ignore-2.17.txt1
-rw-r--r--ansible_collections/microsoft/ad/tests/sanity/ignore-2.18.txt1
-rwxr-xr-xansible_collections/microsoft/ad/tests/utils/shippable/lint.sh2
56 files changed, 2962 insertions, 476 deletions
diff --git a/ansible_collections/microsoft/ad/.ansible-lint b/ansible_collections/microsoft/ad/.ansible-lint
index cad3b8e14..d1340f58c 100644
--- a/ansible_collections/microsoft/ad/.ansible-lint
+++ b/ansible_collections/microsoft/ad/.ansible-lint
@@ -5,6 +5,8 @@ exclude_paths:
- changelogs/changelog.yaml
# Incorrect error around supported ansible versions in this file
- meta/runtime.yml
+ # Examples contain duplicate keys due to how we document inventory plugins
+ - plugins/inventory/ldap.py
- tests/integration/
# We skip a rule that has to be skipped
- tests/sanity/ignore-*.txt
diff --git a/ansible_collections/microsoft/ad/.azure-pipelines/azure-pipelines.yml b/ansible_collections/microsoft/ad/.azure-pipelines/azure-pipelines.yml
index 5db3a5531..7de2c9fd6 100644
--- a/ansible_collections/microsoft/ad/.azure-pipelines/azure-pipelines.yml
+++ b/ansible_collections/microsoft/ad/.azure-pipelines/azure-pipelines.yml
@@ -76,6 +76,22 @@ stages:
test: sanity
- name: Units
test: units
+ - name: Lint
+ test: lint
+ - stage: Ansible_2_17
+ displayName: Ansible 2.17
+ dependsOn:
+ - Dependencies
+ jobs:
+ - template: templates/matrix.yml
+ parameters:
+ nameFormat: "{0}"
+ testFormat: "2.17/{0}"
+ targets:
+ - name: Sanity
+ test: sanity
+ - name: Units
+ test: units
- stage: Ansible_2_16
displayName: Ansible 2.16
dependsOn:
@@ -90,8 +106,6 @@ stages:
test: sanity
- name: Units
test: units
- - name: Lint
- test: lint
- stage: Ansible_2_15
displayName: Ansible 2.15
dependsOn:
@@ -139,6 +153,7 @@ stages:
condition: succeededOrFailed()
dependsOn:
- Ansible_devel
+ - Ansible_2_17
- Ansible_2_16
- Ansible_2_15
- Ansible_2_14
diff --git a/ansible_collections/microsoft/ad/.gitignore b/ansible_collections/microsoft/ad/.gitignore
index f77a1cf67..cf70d5ad4 100644
--- a/ansible_collections/microsoft/ad/.gitignore
+++ b/ansible_collections/microsoft/ad/.gitignore
@@ -393,4 +393,5 @@ changelogs/.plugin-cache.yaml
tests/integration/inventory*
tests/integration/targets/domain_controller/.vagrant
tests/integration/targets/membership/.vagrant
-tests/output/ \ No newline at end of file
+tests/output/
+.vagrant/ \ No newline at end of file
diff --git a/ansible_collections/microsoft/ad/CHANGELOG.rst b/ansible_collections/microsoft/ad/CHANGELOG.rst
index 53c63d173..f778721e7 100644
--- a/ansible_collections/microsoft/ad/CHANGELOG.rst
+++ b/ansible_collections/microsoft/ad/CHANGELOG.rst
@@ -4,6 +4,32 @@ Ansible Microsoft Active Directory Release Notes
.. contents:: Topics
+v1.6.0
+======
+
+Release Summary
+---------------
+
+Release summary for v1.6.0
+
+Minor Changes
+-------------
+
+- microsoft.ad AD modules - Added ``domain_credentials`` as a common module option that can be used to specify credentials for specific AD servers.
+- microsoft.ad AD modules - Added ``lookup_failure_action`` on all modules that can specify a list of distinguishedName values to control what should happen if the lookup fails.
+- microsoft.ad.computer - Added the ability to lookup a distinguishedName on a specific domain server for ``delegates`` and ``managed_by``.
+- microsoft.ad.group - Added the ability to lookup a distinguishedName on a specific domain server for ``managed_by`` and ``members``.
+- microsoft.ad.ou - Added the ability to lookup a distinguishedName on a specific domain server for ``managed_by``.
+- microsoft.ad.user - Added the ability to lookup a distinguishedName on a specific domain server for ``delegates``.
+- microsoft.ad.user - Rename the option ``groups.missing_action`` to ``groups.lookup_failure_action`` to make the option more consistent with other modules. The ``missing_action`` option is still supported as an alias.
+- microsoft.ad.user - Support group member lookup on alternative server using the DN lookup syntax. This syntax uses a dictionary where ``name`` defined the group to lookup and ``server`` defines the server to lookup the group on.
+
+Bugfixes
+--------
+
+- microsoft.ad.membership - Fix hostname check to work with hostnames longer than 15 characters long - https://github.com/ansible-collections/microsoft.ad/issues/113
+- microsoft.ad.user - Fix issue when creating a new user account with ``account_locked: false`` - https://github.com/ansible-collections/microsoft.ad/issues/108
+
v1.5.0
======
diff --git a/ansible_collections/microsoft/ad/FILES.json b/ansible_collections/microsoft/ad/FILES.json
index 0dd2d37a2..b5d8ba03c 100644
--- a/ansible_collections/microsoft/ad/FILES.json
+++ b/ansible_collections/microsoft/ad/FILES.json
@@ -102,7 +102,7 @@
"name": ".azure-pipelines/azure-pipelines.yml",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "bd6f3baf66ceaa437b9b6a25f26a4169acb7c08fbacc02b0e7550825ec6d1ec4",
+ "chksum_sha256": "2f25e97b196dbaa78b87a262b6631129769fefac4c58797343e0314b73a348ed",
"format": 1
},
{
@@ -158,7 +158,7 @@
"name": "changelogs/changelog.yaml",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "5a0d5c07afae82f7e211f5f8bf93c2d0a8ffc3cc594bd93f3bc5efc855546cff",
+ "chksum_sha256": "3751f05d4ce0f68f57798e044ecdb504aa8092d5c4a17a0863613b15fabfb26a",
"format": 1
},
{
@@ -190,17 +190,24 @@
"format": 1
},
{
+ "name": "docs/docsite/rst/guide_ad_module_authentication.rst",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a8108762af89062af8a311efe6239224a8878ef7d99136f91dfdc3b3d52388c9",
+ "format": 1
+ },
+ {
"name": "docs/docsite/rst/guide_attributes.rst",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "a42f374f72fec7ecdb4cb7a7d110cede6a29fe62263e3dc48e078b2f1803d9e5",
+ "chksum_sha256": "f50e3d4da85f4b2647f9b374bb94b52873b880b247dfd6ef0ddaf4abfb5981bf",
"format": 1
},
{
"name": "docs/docsite/rst/guide_ldap_connection.rst",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "53537bac987f50ddddd23033632dd28e77022b88f4dcb00a0c3b93ed7c07b925",
+ "chksum_sha256": "35ed448478aef43642299f37cdc9507e5e2bfdec24f97bad939968af4a341f3f",
"format": 1
},
{
@@ -221,14 +228,14 @@
"name": "docs/docsite/rst/guide_migration.rst",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "fe1668d6d2b28633ee9e78394327f846f028e7448901df8040633c5900be9cd4",
+ "chksum_sha256": "af1bcca081b9a0dabcc474ae259dda01030100db31f36ecc03472cb506a2c765",
"format": 1
},
{
"name": "docs/docsite/extra-docs.yml",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "3ba33d70d90d838dee9bb1644d9cabbe184450979613b031507dc1fb11c2bdd8",
+ "chksum_sha256": "9c692e6876d22d1b26c352dd983733861c141ab8afc73b12e7dcd18f0da14753",
"format": 1
},
{
@@ -284,14 +291,21 @@
"name": "plugins/action/domain.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "6e989c9f1f4ecd67419de47b000a05fced5165a4f741d3fdf1bd5bfb9bae7d81",
+ "chksum_sha256": "71d3694b8188e411c19904a7cb93193619ab8a5beb5d1e2c7ae8b6942bd0d2f0",
+ "format": 1
+ },
+ {
+ "name": "plugins/action/domain_child.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "937a4f5e16bab1aa76d3a315964617c462b592bb4886ab9ec6ed5b075e42f9bf",
"format": 1
},
{
"name": "plugins/action/domain_controller.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "6e989c9f1f4ecd67419de47b000a05fced5165a4f741d3fdf1bd5bfb9bae7d81",
+ "chksum_sha256": "71d3694b8188e411c19904a7cb93193619ab8a5beb5d1e2c7ae8b6942bd0d2f0",
"format": 1
},
{
@@ -312,14 +326,14 @@
"name": "plugins/doc_fragments/ad_object.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "6296c1c278de0d5fd4af66393e7e074e9f57b73788888b74856f28a46b1852c8",
+ "chksum_sha256": "e4f6f708c9e7e3d14b16fb6e83cc95ed148bc2b4f74fb28efe6dfd6436c54b9c",
"format": 1
},
{
"name": "plugins/doc_fragments/ldap_connection.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "70c3cef5f7c2a102ab915450e274484939a520a640b82f6d43d9f1ff47b3af6b",
+ "chksum_sha256": "52fd76fcc19d62ee4957866122364df5bf4ae2de161d11f8ecc1470dc6b0f65f",
"format": 1
},
{
@@ -389,7 +403,7 @@
"name": "plugins/inventory/ldap.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "e09952907a3409d3e3730362b2d62d3321b13096f9206ddd10b5f9d0413dc17f",
+ "chksum_sha256": "d7b35e107d4015836c84dca36dcba415b186ae82aff2d2385a35b3d008777c35",
"format": 1
},
{
@@ -403,7 +417,7 @@
"name": "plugins/module_utils/_ADObject.psm1",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "a8e86078dfa4a138807bf4b9d5a7820d017ece515d6dce44800e273f7079260e",
+ "chksum_sha256": "d1aadbe160df42b99a752b2a3d853f67ec3a46683a0bbf8a912958d67e113515",
"format": 1
},
{
@@ -417,14 +431,14 @@
"name": "plugins/modules/computer.ps1",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "54ab2fb411ef15a43c2304f1c9b5f720b727d07c0148db65ff07e9a1a812b57a",
+ "chksum_sha256": "253e81afabef739244cfa13c560fc41cf5ca0b574ff84baa88424bddbb912a63",
"format": 1
},
{
"name": "plugins/modules/computer.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "8072a01ef9ffe97d05a2249c867fd0623cceb2514b436fb32e6bc74fea7c4019",
+ "chksum_sha256": "20de182a119807c7a46e10a987d04e36861a6387e5e7f26f36db8f789304691b",
"format": 1
},
{
@@ -445,7 +459,21 @@
"name": "plugins/modules/domain.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "711592202ac4384d826a4a246fde4f168699e9fdc6bd52a5a54a3e482fc6ee8f",
+ "chksum_sha256": "5acc5cfb5b0cbd6f9c9a4c0542b314c12ed9ed88b09338362dc22eba699db0f6",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/domain_child.ps1",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1d99c67f6a6f99eeda9b5e37d9562977a02fd432f91ca89e096978c1a16110a3",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/domain_child.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "082d40fd1106a83dd5db68aff92f84666d20c58c242157cb85b9b41d924ff32c",
"format": 1
},
{
@@ -459,28 +487,28 @@
"name": "plugins/modules/domain_controller.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "6d1779d52492dc68599a205be7518a84388b6f83322caf23daf4708989cb62f7",
+ "chksum_sha256": "d8e0041bc48b4df5d0f9e734c51e4eacf3b9ccff92e7156cc50915c0c1117ce7",
"format": 1
},
{
"name": "plugins/modules/group.ps1",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "ff138faeab60823fe1deb45c9a40b44691f47710ac074fa62a3f7370536307ac",
+ "chksum_sha256": "81a5a4dba28d1ba50f1142083d6519426c1d1e55faa93b1bc87184b8db1067af",
"format": 1
},
{
"name": "plugins/modules/group.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "ccb8945f218faace36c9fa29bc0180a0e43f35a8ac01015b133e460db82db21c",
+ "chksum_sha256": "95e412dc48e5ec27e4c9dccdbd96a0c3288603db71ff59792e799dc5099c7daf",
"format": 1
},
{
"name": "plugins/modules/membership.ps1",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "acff85cf60f65e36759593d6dc32dd9cdaa8e781bad4ffdce3673fa5a7c3442f",
+ "chksum_sha256": "4c2cd2bc5d4d2650c89f34873c35461718a6e0efec43ca4e2b25eedd96c7a065",
"format": 1
},
{
@@ -501,7 +529,7 @@
"name": "plugins/modules/object.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "4384e7040a815bea61ad11a19a0b65015f418228e980062347a19531012934b7",
+ "chksum_sha256": "b7bf0f56555bccb55b64db5caa83c0d2a76f87c6dfec6e68de71aee1c1a0a5b2",
"format": 1
},
{
@@ -515,7 +543,7 @@
"name": "plugins/modules/object_info.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "403a343f8c36ec12fb991e570aa99b5e58a74329f6e346618d8de557915445ea",
+ "chksum_sha256": "bb8ea5d7aef8c718e29b3a050922fe3385a622b3a5ef3a0056ba42dcda13274d",
"format": 1
},
{
@@ -536,28 +564,28 @@
"name": "plugins/modules/ou.ps1",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "890945f581fbfb1f7a2a9f1d8e9960ec70ca19e731f8fa90e5b91dc52b484e24",
+ "chksum_sha256": "d93529b6c11871d249638d6a62609468e9d90a880a5b27a6d86534bb0b023c78",
"format": 1
},
{
"name": "plugins/modules/ou.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "96b3ce4a4ade31daa6aae80829dab807f513ce0af6728fd3d954866be516b266",
+ "chksum_sha256": "67c53de4b1ae422cee3551bdb8cb8567194a445181ae138a1af7119d77b37f1e",
"format": 1
},
{
"name": "plugins/modules/user.ps1",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "c4c651c30ba1f15b85b6f76f7c2048f6493a1298b8a958a39e5623e16d0f505e",
+ "chksum_sha256": "dfd4b1375677810def9eeaee738c3318893841d2d022e4fc719533ba669c8cdf",
"format": 1
},
{
"name": "plugins/modules/user.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "cd159582daa2ed5b7aeeda556a7218529ef60965d6e6fb657addc9b587dbc62a",
+ "chksum_sha256": "234b6dfe32edbd092299a66e4364a5f66eddd65558fc1c32cdf98449f367bccc",
"format": 1
},
{
@@ -634,7 +662,7 @@
"name": "plugins/plugin_utils/_module_with_reboot.py",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "86be9d66a96d18ba18bd5b65ba042f4b68fcb60283b7fecd6243deac572c4f50",
+ "chksum_sha256": "1f1c8132c7f0e4283266e4e6ecd9fe881d594193cf5825be9c9f79846aaa8128",
"format": 1
},
{
@@ -704,7 +732,7 @@
"name": "tests/integration/targets/computer/tasks/tests.yml",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "9a0987642dce4687f66cba03796f9710be7a2048d54353a274e2edf2d13245ae",
+ "chksum_sha256": "065fc98a925cdae2cc4a8fdcab71938870d1f1974e9ed8faa6907dd0f165fa1e",
"format": 1
},
{
@@ -792,6 +820,90 @@
"format": 1
},
{
+ "name": "tests/integration/targets/domain_child",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/tasks/cross_domain.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0af4c699a160d0bc1d0e396c7100bfa961f45ca3284a36ccf63cedb91b57233e",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/tasks/main_child.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "862600153d9842632f2da4401a6ff560a5c23e7eab57be6d76d2ebf651a3b199",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/tasks/main_tree.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3cba47e4ad9306a68ed63e3cedf1ca61394afb700853c9dabb692f9ba2877667",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6319e7a40152ef8b3b0ce6bf4e48c32d497f84ada01b6e61d1cdce8ceb244900",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/Vagrantfile",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "748db6763788ff2048f1de10720d9210f58457ee62e8bc7f0815e06a45b2b6a6",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/aliases",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "dde1e703301f0d11990651509d20f81e97e080514a7f551493082b247a106901",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/ansible.cfg",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8598f22926ef839805b1c23e17f04b86a0ea1186216682882a441f0601de044f",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/inventory.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "cd824c275c5ad8883ac164a54ae44505992a634a327f858c9872776985d9b3e8",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/setup.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9d717283be6257eeef3ab6a5290cb5711f43d038d175b42b0736a7549d2ec5b5",
+ "format": 1
+ },
+ {
+ "name": "tests/integration/targets/domain_child/test.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4eb27e1346561a4a60faf00ea384690f7a4b9b33016b5f262334285d7624825b",
+ "format": 1
+ },
+ {
"name": "tests/integration/targets/domain_controller",
"ftype": "dir",
"chksum_type": null,
@@ -900,7 +1012,7 @@
"name": "tests/integration/targets/group/tasks/tests.yml",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "508aa71de34afe93bc3d68f856e5b66fc43decbc9b879d346d0be60fc7de6a30",
+ "chksum_sha256": "e9fc5dec76e1536092745fe19c8e749aa199ed2a610cc75e75923623f95ae195",
"format": 1
},
{
@@ -1040,7 +1152,7 @@
"name": "tests/integration/targets/membership/tasks/main.yml",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "ed9037a9950eccef2189e91c0aab66ab2edf5f128c8cc941c150129b1b8f4fca",
+ "chksum_sha256": "89806b126c17a724fd54f8904176692e19f09826d89b2a3254b0568197df728a",
"format": 1
},
{
@@ -1299,7 +1411,7 @@
"name": "tests/integration/targets/ou/tasks/tests.yml",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "7af0a8c25fd6ca592956f33ad115cdc1641ca5346ac85a103b8accf1cb5d40ee",
+ "chksum_sha256": "9c9792a59fefce74504300df3339e3152b5d3acd1bf945b4417a6890f655a836",
"format": 1
},
{
@@ -1390,7 +1502,7 @@
"name": "tests/integration/targets/user/tasks/tests.yml",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "7e74639819afa56c4a630f75600156f3a411b58bb615566f33b905f84baee967",
+ "chksum_sha256": "9649f15ad69b87815255393bb4c35d46b3fc87b3c6be657badf39f0679e7a136",
"format": 1
},
{
@@ -1418,28 +1530,35 @@
"name": "tests/sanity/ignore-2.14.txt",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "chksum_sha256": "1bae2dbdeb7e39b94941d0e9377ae91979b226f2670133fdfc27c7dbe7bf0b05",
"format": 1
},
{
"name": "tests/sanity/ignore-2.15.txt",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "chksum_sha256": "1bae2dbdeb7e39b94941d0e9377ae91979b226f2670133fdfc27c7dbe7bf0b05",
"format": 1
},
{
"name": "tests/sanity/ignore-2.16.txt",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "chksum_sha256": "1bae2dbdeb7e39b94941d0e9377ae91979b226f2670133fdfc27c7dbe7bf0b05",
"format": 1
},
{
"name": "tests/sanity/ignore-2.17.txt",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "chksum_sha256": "1bae2dbdeb7e39b94941d0e9377ae91979b226f2670133fdfc27c7dbe7bf0b05",
+ "format": 1
+ },
+ {
+ "name": "tests/sanity/ignore-2.18.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1bae2dbdeb7e39b94941d0e9377ae91979b226f2670133fdfc27c7dbe7bf0b05",
"format": 1
},
{
@@ -1572,7 +1691,7 @@
"name": "tests/utils/shippable/lint.sh",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "f945142c1487de0996b9bc46bc18c82d5c0b6c9470f0f48b87634c2e4b0eabf5",
+ "chksum_sha256": "c7bc94e43d0337251fa9f22f3ec4516604e109f7cd7fa35c7f1fa2a90f5c2ccc",
"format": 1
},
{
@@ -1614,21 +1733,21 @@
"name": ".ansible-lint",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "33235ea099dc7bd2061e0271afcef60424799834d3023d14652b6296e3f133c2",
+ "chksum_sha256": "2f41de2acdeff3b92cff1f64992fd443ee1fa63a5e21163e8ced110bec121a75",
"format": 1
},
{
"name": ".gitignore",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "0039fe591d9f12f0fb24c7a9a50745d96627f3a8a4cb73d536801dcc220e9b3b",
+ "chksum_sha256": "3df1a3a721c47e43ad0bca664e780fa956cfb209ff897cb3c75250790f896f0a",
"format": 1
},
{
"name": "CHANGELOG.rst",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "fba73adcf8b95de1f2c33cc77b6f8216cb15754dbe34756a8f386f77d9c6db75",
+ "chksum_sha256": "3403e71cab7325c3401111b3de5e8ad8b6eb329f6e4348a6df141dc476c31a0b",
"format": 1
},
{
diff --git a/ansible_collections/microsoft/ad/MANIFEST.json b/ansible_collections/microsoft/ad/MANIFEST.json
index 57ce22ed2..5eb59e544 100644
--- a/ansible_collections/microsoft/ad/MANIFEST.json
+++ b/ansible_collections/microsoft/ad/MANIFEST.json
@@ -2,7 +2,7 @@
"collection_info": {
"namespace": "microsoft",
"name": "ad",
- "version": "1.5.0",
+ "version": "1.6.0",
"authors": [
"Jordan Borean @jborean93",
"Matt Davis @nitzmahone"
@@ -25,7 +25,7 @@
"name": "FILES.json",
"ftype": "file",
"chksum_type": "sha256",
- "chksum_sha256": "4da03dbf6e40118c90aad5d75b02840c99ac9169e450044d11d902a2e3a5f97f",
+ "chksum_sha256": "cbfa3113cd723bbb0b394c37aaed248fd54ab8b280f3dc12ae481283bb35dc3f",
"format": 1
},
"format": 1
diff --git a/ansible_collections/microsoft/ad/changelogs/changelog.yaml b/ansible_collections/microsoft/ad/changelogs/changelog.yaml
index f9d1dc51f..f2a6c6512 100644
--- a/ansible_collections/microsoft/ad/changelogs/changelog.yaml
+++ b/ansible_collections/microsoft/ad/changelogs/changelog.yaml
@@ -153,3 +153,39 @@ releases:
name: parse_dn
namespace: null
release_date: '2024-03-20'
+ 1.6.0:
+ changes:
+ bugfixes:
+ - microsoft.ad.membership - Fix hostname check to work with hostnames longer
+ than 15 characters long - https://github.com/ansible-collections/microsoft.ad/issues/113
+ - 'microsoft.ad.user - Fix issue when creating a new user account with ``account_locked:
+ false`` - https://github.com/ansible-collections/microsoft.ad/issues/108'
+ minor_changes:
+ - microsoft.ad AD modules - Added ``domain_credentials`` as a common module
+ option that can be used to specify credentials for specific AD servers.
+ - microsoft.ad AD modules - Added ``lookup_failure_action`` on all modules that
+ can specify a list of distinguishedName values to control what should happen
+ if the lookup fails.
+ - microsoft.ad.computer - Added the ability to lookup a distinguishedName on
+ a specific domain server for ``delegates`` and ``managed_by``.
+ - microsoft.ad.group - Added the ability to lookup a distinguishedName on a
+ specific domain server for ``managed_by`` and ``members``.
+ - microsoft.ad.ou - Added the ability to lookup a distinguishedName on a specific
+ domain server for ``managed_by``.
+ - microsoft.ad.user - Added the ability to lookup a distinguishedName on a specific
+ domain server for ``delegates``.
+ - microsoft.ad.user - Rename the option ``groups.missing_action`` to ``groups.lookup_failure_action``
+ to make the option more consistent with other modules. The ``missing_action``
+ option is still supported as an alias.
+ - microsoft.ad.user - Support group member lookup on alternative server using
+ the DN lookup syntax. This syntax uses a dictionary where ``name`` defined
+ the group to lookup and ``server`` defines the server to lookup the group
+ on.
+ release_summary: Release summary for v1.6.0
+ fragments:
+ - lookup-dn.yml
+ - membership-long-name.yml
+ - release-1.6.0.yml
+ - user-account-locked.yml
+ - user-groups.yml
+ release_date: '2024-06-10'
diff --git a/ansible_collections/microsoft/ad/docs/docsite/extra-docs.yml b/ansible_collections/microsoft/ad/docs/docsite/extra-docs.yml
index 6a548ed38..a3b4f8ed0 100644
--- a/ansible_collections/microsoft/ad/docs/docsite/extra-docs.yml
+++ b/ansible_collections/microsoft/ad/docs/docsite/extra-docs.yml
@@ -6,6 +6,7 @@
sections:
- title: Scenario Guides
toctree:
+ - guide_ad_module_authentication
- guide_attributes
- guide_ldap_connection
- guide_ldap_inventory
diff --git a/ansible_collections/microsoft/ad/docs/docsite/rst/guide_ad_module_authentication.rst b/ansible_collections/microsoft/ad/docs/docsite/rst/guide_ad_module_authentication.rst
new file mode 100644
index 000000000..632c19515
--- /dev/null
+++ b/ansible_collections/microsoft/ad/docs/docsite/rst/guide_ad_module_authentication.rst
@@ -0,0 +1,120 @@
+.. _ansible_collections.microsoft.ad.docsite.guide_ad_module_authentication:
+
+****************************
+AD Authentication in Modules
+****************************
+
+A key requirement of the modules used inside this collection is being able to authenticate a user to the domain controller when managing a resource. This guide will cover the different options available for this scenario.
+
+.. note::
+ This guide covers authentication to a domain controller when using a module on a Windows host. See :ref:`LDAP Authentication <ansible_collections.microsoft.ad.docsite.guide_ldap_connection.authentication>` for information on how authentication is done when using plugins running on Linux.
+
+.. contents::
+ :local:
+ :depth: 1
+
+.. _ansible_collections.microsoft.ad.docsite.guide_ad_module_authentication.implicit_auth:
+
+Implicit Authentication
+=======================
+
+The first and simplest option is to use the connection user's existing credentials during authentication. This avoids having to specify a username and password in the module's parameters, but it does require that the connection method used by Ansible supports credential delegation. For example using CredSSP authentication with the ``winrm`` and ``psrp`` connection plugin, or using Kerberos delegation. Other authentication options, like NTLM, do not support credential delegation and will not work with implicit authentication.
+
+The only way to test out if implicit authentication is available is to run the module and see if it works. If it does not work then the error will most likely contain the message ``Failed to contact the AD server``.
+
+.. _ansible_collections.microsoft.ad.docsite.guide_ad_module_authentication.become:
+
+Become
+======
+
+If implicit authentication is not available, the module can be run with ``become`` that specifies the username and password to use for authentication.
+
+.. code-block:: yaml
+
+ - name: Use become with connection credentials
+ microsoft.ad.user:
+ name: MyUser
+ state: present
+ become: true
+ become_method: runas
+ become_flags: logon_type=new_credentials logon_flags=netcredentials_only
+ vars:
+ ansible_become_user: '{{ ansible_user }}'
+ ansible_become_pass: '{{ ansible_password }}'
+
+The ``runas`` method is used on Windows and the ``become_flags`` will specify that the credentials should be used for network authentication only. The ``ansible_become_user`` and ``ansible_become_pass`` variables specify the username and password to use for authentication. It is important that both of these variables are set to a valid username and password or else the authentication will fail.
+
+It is also possible to use the ``SYSTEM`` account for become. This will have the module use the AD computer account for that host when authenticating with the target DC rather than an explicit username and password. The AD computer account must still have the required rights to perform the operation requested.
+
+.. code-block:: yaml
+
+ - name: Use machine account for authentication
+ microsoft.ad.user:
+ name: MyUser
+ state: present
+ become: true
+ become_method: runas
+ become_user: SYSTEM
+
+.. _ansible_collections.microsoft.ad.docsite.guide_ad_module_authentication.explicit_creds:
+
+Explicit Credentials
+====================
+
+The final option is to specify the username and password as module options. This can be done in two ways; with the ``domain_username`` and ``domain_password`` options, or with the ``domain_credentials`` option. An example of both methods is shown below.
+
+.. code-block:: yaml
+
+ - name: Use domain_username and domain_password
+ microsoft.ad.user:
+ name: MyUser
+ state: present
+ domain_username: '{{ ansible_user }}'
+ domain_password: '{{ ansible_password }}'
+
+ - name: Use domain_credentials
+ name: MyUser
+ state: present
+ domain_credentials:
+ - username: '{{ ansible_user }}'
+ password: '{{ ansible_password }}'
+
+.. note::
+ The ``domain_credentials`` option was added in version 1.6.0 of this collection.
+
+The ``domain_credentials`` option without the ``name`` key, like in the above example, will be the credentials used for authentication with the default domain controller just like ``domain_username`` and ``domain_password``. Using both options together is not supported and will result in an error.
+
+The ``domain_credentials`` option can also be used to specify server specific credentials. For example when attempting to lookup the identity of an AD object:
+
+.. code-block:: yaml
+
+ - name: Set member with lookup on different server
+ microsoft.ad.group:
+ name: MyGroup
+ state: present
+ members:
+ add:
+ - GroupOnDefaultDC
+ - name: GroupOnDefaultDC2
+ - name: GroupOnOtherDC
+ server: OtherDC
+ - name: GroupOnThirdDC
+ server: ThirdDC
+ domain_credentials:
+ - username: UserForDefaultDC
+ password: PasswordForDefaultDC
+ - name: OtherDC
+ username: UserForOtherDC
+ password: PasswordForOtherDC
+
+In the case above there are three members being added to the group:
+
+* ``GroupOnDefaultDC`` - Will be looked up on the default domain controller using ``UserForDefaultDC`` and ``PasswordForDefaultDC``
+* ``GroupOnDefaultDC2`` - Same as the above just specified as a dictionary
+* ``GroupOnOtherDC`` - Will be looked up on ``OtherDC`` using ``UserForOtherDC`` and ``PasswordForOtherDC``
+* ``GroupOnThirdDC`` - Will be looked up on ``ThirdDC`` using the implicit user authentication context
+
+The value for ``server`` must correspond to a ``name`` entry in ``domain_credentials``. If the server is not specified in ``domain_credentials``, the module will default to using the ``domain_username/domain_password`` or implicit user authentication.
+
+.. note::
+ The default (no ``name`` key) entry in ``domain_credentials`` is only used for lookups without an explicit server set. The ``domain_username`` and ``domain_password`` credential will be used for all connections unless there is an explicit server entry in ``domain_credentials``.
diff --git a/ansible_collections/microsoft/ad/docs/docsite/rst/guide_attributes.rst b/ansible_collections/microsoft/ad/docs/docsite/rst/guide_attributes.rst
index ee53dce64..7ed192ca2 100644
--- a/ansible_collections/microsoft/ad/docs/docsite/rst/guide_attributes.rst
+++ b/ansible_collections/microsoft/ad/docs/docsite/rst/guide_attributes.rst
@@ -310,3 +310,77 @@ SDDL strings can be quite complex so building them manually is ill-advised. It i
$dn = 'CN=ObjectName,DC=domain,DC=test'
$obj = Get-ADObject -Identity $dn -Properties nTSecurityDescriptor
$obj.nTSecurityDescriptor.GetSecurityDescriptorSddlForm('All')
+
+.. _ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes:
+
+DN Lookup Attributes
+====================
+
+Some attributes in Active Directory are stored as a Distinguished Name (``DN``) value that references another AD object. Some modules expose a way to lookup the DN using a more human friendly value, such as ``managed_by``. These option values must either be a string or a dictionary with the key ``name`` and optional key ``server``. The string value or the value of ``name`` is the identity to lookup while ``server`` is the domain server to lookup the identity on. The lookup identity value can be specified as a ``distinguishedName``, ``objectGUID``, ``objectSid``, ``sAMAccountName``, or ``userPrincipalName``. The below is an example of how to lookup a DN using the ``sAMAccountName`` using a string value or in the dictionary form:
+
+.. code-block:: yaml
+
+ - name: Find managed_by using string value
+ microsoft.ad.group:
+ name: My Group
+ scope: global
+ managed_by: Domain Admins
+
+ - name: Find managed_by using dictionary value with a server
+ microsoft.ad.group:
+ name: My Group
+ scope: global
+ managed_by:
+ name: Domain Admins
+ server: OtherDC
+
+There are also module options that can set a list of DN values for an attribute. The list values for these options are the same as the single value attributes where each DN lookup is set as a string or a dictionary with the ``name`` and optional ``server`` key.
+
+.. code-block:: yaml
+
+ - name: Specify a list of DNs to set
+ microsoft.ad.computer:
+ identity: TheComputer
+ delegates:
+ set:
+ - FileShare
+ - name: ServerA
+ server: OtherDC
+
+For list attributes with the ``add/remove/set`` subkey options, the ``lookup_failure_action`` option can also be set to ``fail`` (default), ``ignore``, or ``warn``. The ``fail`` option will fail the task if any of the lookups fail, ``ignore`` will ignore any invalid lookups, and ``warn`` will emit a warning but still continue on a lookup failure.
+
+.. code-block:: yaml
+
+ - name: Specify a list of DNs to set - ignoring lookup failures
+ microsoft.ad.computer:
+ identity: TheComputer
+ delegates:
+ lookup_failure_action: ignore
+ set:
+ - FileShare
+ - MissingUser
+
+When a ``server`` key is provided, the lookup will be done using the server value specified. It is possible to also provide explicit credentials just for that server using the ``domain_credentials`` option.
+
+.. code-block:: yaml
+
+ - name: Set member with lookup on different server
+ microsoft.ad.group:
+ name: MyGroup
+ state: present
+ members:
+ add:
+ - GroupOnDefaultDC
+ - name: GroupOnDefaultDC2
+ - name: GroupOnOtherDC
+ server: OtherDC
+ domain_credentials:
+ - username: UserForDefaultDC
+ password: PasswordForDefaultDC
+ - name: OtherDC
+ username: UserForOtherDC
+ password: PasswordForOtherDC
+
+In the above, the ``GroupOnOtherDC`` will be done with ``OtherDC`` with the username ``UserForOtherDC``.
+
+The documentation for the module option will identify if the option supports the lookup behaviour or whether a DN value must be explicitly provided.
diff --git a/ansible_collections/microsoft/ad/docs/docsite/rst/guide_ldap_connection.rst b/ansible_collections/microsoft/ad/docs/docsite/rst/guide_ldap_connection.rst
index 60755f00c..ed0b290fa 100644
--- a/ansible_collections/microsoft/ad/docs/docsite/rst/guide_ldap_connection.rst
+++ b/ansible_collections/microsoft/ad/docs/docsite/rst/guide_ldap_connection.rst
@@ -7,7 +7,7 @@ LDAP Connection guide
This guide covers information about communicating with an LDAP server, like Microsoft Active Directory, from the Ansible host. Unlike Windows hosts, there are no builtin mechanisms to communicate and authenticate with an LDAP server, so the plugins that run on the Ansible host require some extra configuration to get working.
.. note::
- This guide covers LDAP communication from the Ansible host. This does not apply to the modules that run on the remote Windows hosts.
+ This guide covers LDAP communication from the Ansible host. This does not apply to the modules that run on the remote Windows hosts. See :ref:`AD Authentication in Modules <ansible_collections.microsoft.ad.docsite.guide_ad_module_authentication>` for information on how modules authentication can be configured.
.. contents::
:local:
diff --git a/ansible_collections/microsoft/ad/docs/docsite/rst/guide_migration.rst b/ansible_collections/microsoft/ad/docs/docsite/rst/guide_migration.rst
index c0b01ca5f..d3e3c2d26 100644
--- a/ansible_collections/microsoft/ad/docs/docsite/rst/guide_migration.rst
+++ b/ansible_collections/microsoft/ad/docs/docsite/rst/guide_migration.rst
@@ -129,6 +129,30 @@ Migrated to :ref:`microsoft.ad.group <ansible_collections.microsoft.ad.group_mod
The functionality of this module has been merged with ``microsoft.ad.group``. Use the ``members`` option to ``add``, ``remove``, or ``set`` to add, remove, or set group members respectively.
+One change is ``win_domain_group_membership`` could specify the server to lookup the member using the ``SERVER\member-name`` format. This member format is not supported in ``microsoft.ad.group`` but since v1.6.0 of this collection the same can be achieved by using a dictionary as the member value. For example:
+
+.. code-block:: yaml
+
+ - name: Add a domain user/group from another Domain in the multi-domain forest to a domain group
+ community.windows.win_domain_group_membership:
+ name: GroupinDomainAAA
+ domain_server: DomainAAA.cloud
+ members:
+ - DomainBBB.cloud\UserInDomainBBB
+ state: present
+
+ - name: Add a domain user/group from another Domain in the multi-domain forest to a domain group
+ microsoft.ad.group:
+ name: GroupinDomainAAA
+ domain_server: DomainAAA.cloud
+ members:
+ add:
+ - name: UserInDomainBBB
+ server: DomainBBB.cloud
+ state: present
+
+See :ref:`DN Lookup Attributes <ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes>` for more information.
+
.. _ansible_collections.microsoft.ad.docsite.guide_migration.migrated_modules.win_domain_object_info:
Module ``win_domain_object_info``
diff --git a/ansible_collections/microsoft/ad/plugins/action/domain.py b/ansible_collections/microsoft/ad/plugins/action/domain.py
index 36cdb26e5..803f94d5d 100644
--- a/ansible_collections/microsoft/ad/plugins/action/domain.py
+++ b/ansible_collections/microsoft/ad/plugins/action/domain.py
@@ -1,34 +1,8 @@
# Copyright (c) 2022 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-import typing as t
+from ..plugin_utils._module_with_reboot import DomainPromotionWithReboot
-from ..plugin_utils._module_with_reboot import ActionModuleWithReboot
-
-class ActionModule(ActionModuleWithReboot):
- def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
- super().__init__(*args, **kwargs)
- self._ran_once = False
-
- def _ad_should_rerun(self, result: t.Dict[str, t.Any]) -> bool:
- ran_once = self._ran_once
- self._ran_once = True
-
- if ran_once or not result.get("_do_action_reboot", False):
- return False
-
- if self._task.check_mode:
- # Assume that on a rerun it will not have failed and that it
- # ran successfull.
- result["failed"] = False
- result.pop("msg", None)
- return False
-
- else:
- return True
-
- def _ad_process_result(self, result: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
- result.pop("_do_action_reboot", None)
-
- return result
+class ActionModule(DomainPromotionWithReboot):
+ ...
diff --git a/ansible_collections/microsoft/ad/plugins/action/domain_child.py b/ansible_collections/microsoft/ad/plugins/action/domain_child.py
new file mode 100644
index 000000000..ecc566c5a
--- /dev/null
+++ b/ansible_collections/microsoft/ad/plugins/action/domain_child.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2024 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from ..plugin_utils._module_with_reboot import DomainPromotionWithReboot
+
+
+class ActionModule(DomainPromotionWithReboot):
+ ...
diff --git a/ansible_collections/microsoft/ad/plugins/action/domain_controller.py b/ansible_collections/microsoft/ad/plugins/action/domain_controller.py
index 36cdb26e5..803f94d5d 100644
--- a/ansible_collections/microsoft/ad/plugins/action/domain_controller.py
+++ b/ansible_collections/microsoft/ad/plugins/action/domain_controller.py
@@ -1,34 +1,8 @@
# Copyright (c) 2022 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-import typing as t
+from ..plugin_utils._module_with_reboot import DomainPromotionWithReboot
-from ..plugin_utils._module_with_reboot import ActionModuleWithReboot
-
-class ActionModule(ActionModuleWithReboot):
- def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
- super().__init__(*args, **kwargs)
- self._ran_once = False
-
- def _ad_should_rerun(self, result: t.Dict[str, t.Any]) -> bool:
- ran_once = self._ran_once
- self._ran_once = True
-
- if ran_once or not result.get("_do_action_reboot", False):
- return False
-
- if self._task.check_mode:
- # Assume that on a rerun it will not have failed and that it
- # ran successfull.
- result["failed"] = False
- result.pop("msg", None)
- return False
-
- else:
- return True
-
- def _ad_process_result(self, result: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
- result.pop("_do_action_reboot", None)
-
- return result
+class ActionModule(DomainPromotionWithReboot):
+ ...
diff --git a/ansible_collections/microsoft/ad/plugins/doc_fragments/ad_object.py b/ansible_collections/microsoft/ad/plugins/doc_fragments/ad_object.py
index 3231e2341..5042e1208 100644
--- a/ansible_collections/microsoft/ad/plugins/doc_fragments/ad_object.py
+++ b/ansible_collections/microsoft/ad/plugins/doc_fragments/ad_object.py
@@ -76,9 +76,48 @@ options:
- The display name of the AD object to set.
- This is the value of the C(displayName) LDAP attribute.
type: str
+ domain_credentials:
+ description:
+ - Specifies the credentials that should be used when using the server
+ specified by I(name).
+ - To specify credentials for the default domain server, use an entry
+ without the I(name) key or use the I(domain_username) and
+ I(domain_password) option.
+ - This can be set under the R(play's module defaults,module_defaults_groups)
+ under the C(group/microsoft.ad.domain) group.
+ - See R(AD authentication in modules,ansible_collections.microsoft.ad.docsite.guide_ad_module_authentication)
+ for more information.
+ default: []
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description:
+ - The name of the server these credentials are for.
+ - This value should correspond to the value used in other options that
+ specify a custom server to use, for example an option that references
+ an AD identity located on a different AD server.
+ - This key can be omitted in one entry to specify the default
+ credentials to use when a server is not specified instead of using
+ I(domain_username) and I(domain_password).
+ type: str
+ username:
+ description:
+ - The username to use when connecting to the server specified by
+ I(name).
+ type: str
+ required: true
+ password:
+ description:
+ - The password to use when connecting to the server specified by
+ I(name).
+ type: str
+ required: true
domain_password:
description:
- The password for I(domain_username).
+ - The I(domain_credentials) sub entry without a I(name) key can also be
+ used to specify the credentials for the default domain authentication.
- This can be set under the R(play's module defaults,module_defaults_groups)
under the C(group/microsoft.ad.domain) group.
type: str
@@ -87,6 +126,9 @@ options:
- Specified the Active Directory Domain Services instance to connect to.
- Can be in the form of an FQDN or NetBIOS name.
- If not specified then the value is based on the default domain of the computer running PowerShell.
+ - Custom credentials can be specified under a I(domain_credentials) entry
+ without a I(name) key or through I(domain_username) and
+ I(domain_password).
- This can be set under the R(play's module defaults,module_defaults_groups)
under the C(group/microsoft.ad.domain) group.
type: str
@@ -96,6 +138,8 @@ options:
- If this is not set then the user that is used for authentication will be the connection user.
- Ansible will be unable to use the connection user unless auth is Kerberos with credential delegation or CredSSP,
or become is used on the task.
+ - The I(domain_credentials) sub entry without a I(name) key can also be
+ used to specify the credentials for the default domain authentication.
- This can be set under the R(play's module defaults,module_defaults_groups)
under the C(group/microsoft.ad.domain) group.
type: str
diff --git a/ansible_collections/microsoft/ad/plugins/doc_fragments/ldap_connection.py b/ansible_collections/microsoft/ad/plugins/doc_fragments/ldap_connection.py
index 327c1ba76..79fcbf4d4 100644
--- a/ansible_collections/microsoft/ad/plugins/doc_fragments/ldap_connection.py
+++ b/ansible_collections/microsoft/ad/plugins/doc_fragments/ldap_connection.py
@@ -197,4 +197,5 @@ requirements:
- pyspnego >= 0.8.0
- pyspnego[kerberos] - For Kerberos and server lookup support
- sansldap
+- dpapi-ng - For LAPS decryption support
"""
diff --git a/ansible_collections/microsoft/ad/plugins/inventory/ldap.py b/ansible_collections/microsoft/ad/plugins/inventory/ldap.py
index 0a329daff..ce77a5efb 100644
--- a/ansible_collections/microsoft/ad/plugins/inventory/ldap.py
+++ b/ansible_collections/microsoft/ad/plugins/inventory/ldap.py
@@ -134,6 +134,7 @@ ca_cert: /home/user/certs/ldap.pem
username: '{{ lookup("ansible.builtin.env", "LDAP_USERNAME") }}'
password: '{{ lookup("ansible.builtin.env", "LDAP_PASSWORD") }}'
+
##############################################
# Search Options #
# #
@@ -220,9 +221,9 @@ groups:
# Adds the host to a group site_{{ location }} with the default group of
# site_unknown if the location isn't defined
keyed_groups:
-- key: location | default(omit)
- prefix: site
- default_value: unknown
+ - key: location | default(omit)
+ prefix: site
+ default_value: unknown
"""
import base64
diff --git a/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1 b/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1
index e51c974cb..70868c3e8 100644
--- a/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1
+++ b/ansible_collections/microsoft/ad/plugins/module_utils/_ADObject.psm1
@@ -486,6 +486,7 @@ Function Compare-AnsibleADIdempotentList {
}
[PSCustomObject]@{
+ # $null is explicit here as the AD modules use it to unset a value
Value = if ($value.Count) { $value.ToArray() } else { $null }
# Also returned if the API doesn't support explicitly setting 1 value
ToAdd = $toAdd.ToArray()
@@ -494,6 +495,160 @@ Function Compare-AnsibleADIdempotentList {
}
}
+Function ConvertTo-AnsibleADDistinguishedName {
+ <#
+ .SYNOPSIS
+ Converts the input list into DistinguishedNames for later comparison.
+
+ .PARAMETER InputObject
+ The identity parameter, this can either be a string or a hashtable.
+ If a hashtable it should contain the name and optional server key to
+ identity the object to search and set a specific server to search on.
+
+ .PARAMETER Module
+ The AnsibleModule object associated with the current module execution.
+
+ .PARAMETER Context
+ The context behind this conversion to add to the error message if there
+ is one.
+
+ .PARAMETER Server
+ The default server to search. The Identity server key will override this
+ value is present.
+
+ .PARAMETER Credential
+ The credential to search with. This is ignored if the Identity server key
+ is present.
+
+ .PARAMETER FailureAction
+ The action to take if the lookup fails. Fail will cause the module to
+ exit with an error, ignore will ignore the error, and warn will emit a
+ warning on failure.
+ #>
+ [OutputType([string])]
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory, ValueFromPipeline)]
+ [object[]]
+ $InputObject,
+
+ [Parameter(Mandatory)]
+ [object]
+ $Module,
+
+ [Parameter(Mandatory)]
+ [string]
+ $Context,
+
+ [string]
+ $Server,
+
+ [PSCredential]
+ $Credential,
+
+ [ValidateSet('Fail', 'Ignore', 'Warn')]
+ [string]
+ $FailureAction = 'Fail'
+ )
+
+ begin {
+ $allowedKeys = [string[]]@('name', 'server')
+ $results = [System.Collections.Generic.List[string]]@()
+ $getErrors = [System.Collections.Generic.List[string]]@()
+ $invalidIdentities = [System.Collections.Generic.List[string]]@()
+ }
+
+ process {
+ foreach ($obj in $InputObject) {
+ $getParams = @{}
+ if ($Server) {
+ $getParams.Server = $Server
+ }
+ if ($Credential) {
+ $getParams.Credential = $Credential
+ }
+
+ if ($obj -is [System.Collections.IDictionary]) {
+ # When using a hashtable, the name and server key can be used
+ # to specify the identity and server to use. If no server is
+ # set then it defaults to the default server (if provided) and
+ # it's credentials.
+ $existingKeys = [string[]]$obj.Keys
+
+ if ('name' -notin $existingKeys) {
+ $getErrors.Add("Identity entry does not contain the required name key")
+ continue
+ }
+ $name = [string]$obj.name
+
+ [string[]]$extraKeys = [System.Linq.Enumerable]::Except($existingKeys, $allowedKeys)
+ if ($extraKeys) {
+ $extraKeys = $extraKeys | Sort-Object
+ $getErrors.Add("Identity entry for '$name' contains extra keys: '$($extraKeys -join "', '")'")
+ continue
+ }
+ $getParams.Identity = $name
+
+ if ($obj.server) {
+ # If a custom server is specified we use that and the
+ # credential (if any) associated with that server.
+ $getParams.Server = $obj.server
+
+ if ($Module.ServerCredentials.ContainsKey($obj.server)) {
+ $getParams.Credential = $Module.ServerCredentials[$obj.server]
+ }
+ elseif (-not $Module.DefaultCredentialSet) {
+ $null = $getParams.Remove('Credential')
+ }
+ }
+ }
+ else {
+ # Treat the value as just the identity as a string.
+ $getParams.Identity = [string]$obj
+ }
+
+ if (-not $getParams.Identity) {
+ continue
+ }
+
+ $adDN = Get-AnsibleADObject @getParams |
+ Select-Object -ExpandProperty DistinguishedName
+ if ($adDN) {
+ $results.Add($adDN)
+ }
+ else {
+ $invalidIdentities.Add($getParams.Identity)
+ }
+ }
+ }
+
+ end {
+ # This is a weird workaround as FailJson calls exit which means the
+ # caller won't capture the output causing junk data in the output. By
+ # only outputting the results if no errors occurred we can avoid that
+ # problem.
+ $errorPrefix = "Failed to find the AD object DNs for $Context"
+ if ($getErrors) {
+ $msg = "$errorPrefix. $($getErrors -join '. ')."
+ $Module.FailJson($msg)
+ }
+
+ if ($invalidIdentities) {
+ if ($FailureAction -ne 'Ignore') {
+ $identityString = "'$($invalidIdentities -join "', '")'"
+ if ($FailureAction -eq 'Fail') {
+ $Module.FailJson("$errorPrefix. Invalid identities: $identityString")
+ }
+ else {
+ $module.Warn("$errorPrefix. Ignoring invalid identities: $identityString")
+ }
+ }
+ }
+
+ $results
+ }
+}
+
Function Get-AnsibleADObject {
<#
.SYNOPSIS
@@ -612,9 +767,17 @@ Function Invoke-AnsibleADObject {
Attribute - The ldap attribute name to compare against
CaseInsensitive - The values are case insensitive (defaults to $false)
StateRequired - Set to 'present' or 'absent' if this needs to be defined for either state
+ DNLookup - Whether each value needs to be looked up to get the DN
+ IsRawAttribute - Whether the attribute is a raw LDAP attribute name and not a parameter name
New - Called when the option is to be set on the New-AD* cmdlet splat
Set - Called when the option is to be set on the Set-AD* cmdlet splat
+ The 'type' key in 'Option' should be a valid Ansible.Basic type or
+ 'add_remove_set'. When 'add_remove_set' is used the option type becomes
+ dict with the options subentry for add/remove/set being the Option value
+ specified. This can be combined with DNLookup to set the value as raw that
+ can lookup the DN value from the string or dict specified.
+
If Attribute is set then requested value will be compared with the
attribute specified. The current attribute value is added to the before
diff state for the option it is on. If New is not specified then the
@@ -632,6 +795,10 @@ Function Invoke-AnsibleADObject {
It is up to the scriptblock to set the required splat parameters or call
whatever function is needed.
+ The DNLookup key is used to indicate that the add/remove/set values can
+ either be a string or a dictionary containing the name/server to specify
+ the name and server to lookup the object DN value.
+
Both New and Set must set the $Module.Diff.after results accordingly and/or
mark $Module.Result.changed if it is making a change outside of adjusting
the splat hashtable passed in.
@@ -709,6 +876,25 @@ Function Invoke-AnsibleADObject {
}
}
}
+ domain_credentials = @{
+ default = @()
+ type = 'list'
+ elements = 'dict'
+ options = @{
+ name = @{
+ type = 'str'
+ }
+ username = @{
+ required = $true
+ type = 'str'
+ }
+ password = @{
+ no_log = $true
+ required = $true
+ type = 'str'
+ }
+ }
+ }
domain_password = @{
no_log = $true
type = 'str'
@@ -775,7 +961,44 @@ Function Invoke-AnsibleADObject {
$stateRequiredIf[$propInfo.StateRequired] += $ansibleOption
}
- $spec.options[$ansibleOption] = $propInfo.Option
+ $option = $propInfo.Option
+ if ($option.type -eq 'add_remove_set') {
+ $option.type = 'dict'
+
+ $optionElement = $option.Clone()
+ $optionElement.type = 'list'
+
+ $option = @{
+ type = 'dict'
+ options = @{}
+ }
+
+ if ($propInfo.DNLookup) {
+ $optionElement.elements = 'raw'
+ $option.options.lookup_failure_action = @{
+ choices = @('fail', 'ignore', 'warn')
+ default = 'fail'
+ type = 'str'
+ }
+ }
+ elseif (-not $optionElement.ContainsKey('elements')) {
+ $optionElement.elements = 'str'
+ }
+
+ if ($optionElement.ContainsKey('aliases')) {
+ $option.aliases = $optionElement.aliases
+ $null = $optionElement.Remove('aliases')
+ }
+
+ $option.options.add = $optionElement
+ $option.options.remove = $optionElement
+ $option.options.set = $optionElement
+ }
+ elseif ($propInfo.DNLookup) {
+ $option.type = 'raw'
+ }
+
+ $spec.options[$ansibleOption] = $option
if ($propInfo.Attribute) {
$propInfo.Attribute
@@ -798,15 +1021,39 @@ Function Invoke-AnsibleADObject {
$module.Result.object_guid = $null
$adParams = @{}
+ $serverCredentials = @{}
+ foreach ($domainCred in $module.Params.domain_credentials) {
+ $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @(
+ $domainCred.username,
+ (ConvertTo-SecureString -AsPlainText -Force -String $domainCred.password)
+ )
+
+ if ($domainCred.name) {
+ $serverCredentials[$domainCred.name] = $cred
+ }
+ elseif ($adParams.Credential) {
+ $module.FailJson("Cannot specify default domain_credentials with domain_username and domain_password")
+ }
+ else {
+ $adParams.Credential = $cred
+ }
+ }
+ $module | Add-Member -MemberType NoteProperty -Name ServerCredentials -Value $serverCredentials
+
if ($module.Params.domain_server) {
$adParams.Server = $module.Params.domain_server
}
if ($module.Params.domain_username) {
+ if ($adParams.Credential) {
+ $msg = "Cannot specify domain_username/domain_password and domain_credentials with an entry that has no name."
+ $module.FailJson($msg)
+ }
$adParams.Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @(
$module.Params.domain_username,
(ConvertTo-SecureString -AsPlainText -Force -String $module.Params.domain_password)
)
+ $module | Add-Member -MemberType NoteProperty -Name DefaultCredentialSet -Value $true
}
$defaultObjectPath = & $DefaultPath $module $adParams
@@ -922,8 +1169,7 @@ Function Invoke-AnsibleADObject {
$objectPath = $null
if ($module.Params.path -and $module.Params.path -ne $defaultPathSentinel) {
- $objectPath = $path
- $newParams.Path = $module.Params.path
+ $newParams.Path = $objectPath = $module.Params.path
}
else {
$objectPath = $defaultObjectPath
@@ -953,11 +1199,45 @@ Function Invoke-AnsibleADObject {
$null = & $propInfo.New $module $adParams $newParams
}
elseif ($propInfo.Attribute) {
- if ($propValue -is [System.Collections.IDictionary]) {
- $propValue = @($propValue['add']; $propValue['set']) | Select-Object -Unique
+ # If a dictionary (add/set/remove) and is not a DNLookup single value
+ if ($propValue -is [System.Collections.IDictionary] -and $propInfo.Option.type -ne 'raw') {
+ $propValue = if ($propInfo.DNLookup) {
+ foreach ($actionKvp in $propValue.GetEnumerator()) {
+ if ($null -eq $actionKvp.Value -or $actionKvp.Key -in @('lookup_failure_action', 'remove')) {
+ continue
+ }
+
+ $convertParams = @{
+ Module = $module
+ Context = "$($propInfo.Name).$($actionKvp.Key)"
+ FailureAction = $propValue.lookup_failure_action
+ }
+ $actionKvp.Value | ConvertTo-AnsibleADDistinguishedName @adParams @convertParams
+ }
+ }
+ else {
+ $propValue['add']
+ $propValue['set']
+ }
+
+ $propValue = $propValue | Select-Object -Unique
+ }
+ elseif ($propInfo.DNLookup) {
+ $propValue = $propValue | ConvertTo-AnsibleADDistinguishedName @adParams -Module $module -Context $propInfo.Name
}
- $newParams[$propInfo.Attribute] = $propValue
+ if ($propInfo.IsRawAttribute) {
+ if (-not $newParams.ContainsKey('OtherAttributes')) {
+ $newParams.OtherAttributes = @{}
+ }
+
+ # The AD cmdlets don't like explicitly casted arrays, use
+ # ForEach-Object to get back a vanilla object[] to set.
+ $newParams.OtherAttributes[$propInfo.Attribute] = $propValue | ForEach-Object { "$_" }
+ }
+ else {
+ $newParams[$propInfo.Attribute] = $propValue
+ }
if ($propInfo.Option.no_log) {
$propValue = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
@@ -1043,17 +1323,36 @@ Function Invoke-AnsibleADObject {
$compareParams = @{
Existing = $actualValue
- CaseInsensitive = $propInfo.CaseInsensitive
+ CaseInsensitive = $propInfo.DNLookup -or $propInfo.CaseInsensitive
}
- if ($propValue -is [System.Collections.IDictionary]) {
- $compareParams.Add = $propValue['add']
- $compareParams.Remove = $propValue['remove']
- $compareParams.Set = $propValue['set']
+ # If a dictionary (add/set/remove) and is not a DNLookup single value
+ if ($propValue -is [System.Collections.IDictionary] -and $propInfo.Option.type -ne 'raw') {
+ if ($propInfo.DNLookup) {
+ foreach ($actionKvp in $propValue.GetEnumerator()) {
+ if ($null -eq $actionKvp.Value -or $actionKvp.Key -eq 'lookup_failure_action') { continue }
+
+ $convertParams = @{
+ Module = $module
+ Context = "$($propInfo.Name).$($actionKvp.Key)"
+ FailureAction = $propValue.lookup_failure_action
+ }
+ $dns = $actionKvp.Value | ConvertTo-AnsibleADDistinguishedName @adParams @convertParams
+ $compareParams[$actionKvp.Key] = @($dns)
+ }
+ }
+ else {
+ $compareParams.Add = $propValue['add']
+ $compareParams.Remove = $propValue['remove']
+ $compareParams.Set = $propValue['set']
+ }
}
elseif ([string]::IsNullOrWhiteSpace($propValue)) {
$compareParams.Set = @()
}
+ elseif ($propInfo.DNLookup) {
+ $compareParams.Set = @($propValue | ConvertTo-AnsibleADDistinguishedName @adParams -Module $module -Context $propInfo.Name)
+ }
else {
$compareParams.Set = @($propValue)
}
@@ -1061,7 +1360,23 @@ Function Invoke-AnsibleADObject {
$res = Compare-AnsibleADIdempotentList @compareParams
$newValue = $res.Value
if ($res.Changed) {
- $setParams[$propInfo.Attribute] = $newValue
+ if ($propInfo.IsRawAttribute) {
+ if ($newValue) {
+ if (-not $setParams.ContainsKey('Replace')) {
+ $setParams['Replace'] = @{}
+ }
+ $setParams['Replace'][$propInfo.Attribute] = $newValue
+ }
+ else {
+ if (-not $setParams.ContainsKey('Clear')) {
+ $setParams['Clear'] = [System.Collections.Generic.List[string]]@()
+ }
+ $setParams['Clear'].Add($propInfo.Attribute)
+ }
+ }
+ else {
+ $setParams[$propInfo.Attribute] = $newValue
+ }
}
$noLog = $propInfo.Option.no_log
@@ -1169,6 +1484,7 @@ Function Invoke-AnsibleADObject {
$exportMembers = @{
Function = @(
"Compare-AnsibleADIdempotentList"
+ "ConvertTo-AnsibleADDistinguishedName"
"Get-AnsibleADObject"
"Invoke-AnsibleADObject"
)
diff --git a/ansible_collections/microsoft/ad/plugins/modules/computer.ps1 b/ansible_collections/microsoft/ad/plugins/modules/computer.ps1
index b97bb1062..9010c103d 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/computer.ps1
+++ b/ansible_collections/microsoft/ad/plugins/modules/computer.ps1
@@ -12,15 +12,10 @@ $setParams = @{
Name = 'delegates'
Option = @{
aliases = 'principals_allowed_to_delegate'
- type = 'dict'
- options = @{
- add = @{ type = 'list'; elements = 'str' }
- remove = @{ type = 'list'; elements = 'str' }
- set = @{ type = 'list'; elements = 'str' }
- }
+ type = 'add_remove_set'
}
Attribute = 'PrincipalsAllowedToDelegateToAccount'
- CaseInsensitive = $true
+ DNLookup = $true
}
[PSCustomObject]@{
Name = 'dns_hostname'
@@ -35,24 +30,8 @@ $setParams = @{
[PSCustomObject]@{
Name = 'kerberos_encryption_types'
Option = @{
- type = 'dict'
- options = @{
- add = @{
- choices = 'aes128', 'aes256', 'des', 'rc4'
- type = 'list'
- elements = 'str'
- }
- remove = @{
- choices = 'aes128', 'aes256', 'des', 'rc4'
- type = 'list'
- elements = 'str'
- }
- set = @{
- choices = 'aes128', 'aes256', 'des', 'rc4'
- type = 'list'
- elements = 'str'
- }
- }
+ type = 'add_remove_set'
+ choices = 'aes128', 'aes256', 'des', 'rc4'
}
Attribute = 'KerberosEncryptionType'
CaseInsensitive = $true
@@ -107,8 +86,9 @@ $setParams = @{
}
[PSCustomObject]@{
Name = 'managed_by'
- Option = @{ type = 'str' }
+ Option = @{ type = 'raw' }
Attribute = 'ManagedBy'
+ DNLookup = $true
}
[PSCustomObject]@{
Name = 'sam_account_name'
@@ -119,45 +99,11 @@ $setParams = @{
Name = 'spn'
Option = @{
aliases = 'spns'
- type = 'dict'
- options = @{
- add = @{ type = 'list'; elements = 'str' }
- remove = @{ type = 'list'; elements = 'str' }
- set = @{ type = 'list'; elements = 'str' }
- }
- }
- Attribute = 'ServicePrincipalNames'
- New = {
- param($Module, $ADParams, $NewParams)
-
- $spns = @(
- $Module.Params.spn.add
- $Module.Params.spn.set
- ) | Select-Object -Unique
-
- $NewParams.ServicePrincipalNames = $spns
- $Module.Diff.after.spn = $spns
- }
- Set = {
- param($Module, $ADParams, $SetParams, $ADObject)
-
- $desired = $Module.Params.spn
- $compareParams = @{
- Existing = $ADObject.ServicePrincipalNames
- CaseInsensitive = $true
- }
- $res = Compare-AnsibleADIdempotentList @compareParams @desired
- if ($res.Changed) {
- $SetParams.ServicePrincipalNames = @{}
- if ($res.ToAdd) {
- $SetParams.ServicePrincipalNames.Add = $res.ToAdd
- }
- if ($res.ToRemove) {
- $SetParams.ServicePrincipalNames.Remove = $res.ToRemove
- }
- }
- $module.Diff.after.kerberos_encryption_types = @($res.Value | Sort-Object)
+ type = 'add_remove_set'
}
+ Attribute = 'servicePrincipalName'
+ CaseInsensitive = $true
+ IsRawAttribute = $true
}
[PSCustomObject]@{
Name = 'trusted_for_delegation'
diff --git a/ansible_collections/microsoft/ad/plugins/modules/computer.py b/ansible_collections/microsoft/ad/plugins/modules/computer.py
index ab336d6b4..cf160256a 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/computer.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/computer.py
@@ -15,14 +15,19 @@ options:
description:
- The principal objects that the current AD object can trust for
delegation to either add, remove or set.
- - The values for each sub option must be specified as a distinguished name
- C(CN=shenetworks,CN=Users,DC=ansible,DC=test)
+ - Each subkey value is a list of values in the form of a
+ C(distinguishedName), C(objectGUID), C(objectSid), C(sAMAccountName),
+ or C(userPrincipalName) string or a dictionary with the I(name) and
+ optional I(server) key.
- This is the value set on the C(msDS-AllowedToActOnBehalfOfOtherIdentity)
LDAP attribute.
- This is a highly sensitive attribute as it allows the principals
specified to impersonate any account when authenticating with the AD
computer object being managed.
- To clear all principals, use I(set) with an empty list.
+ - See
+ R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes)
+ for more information on how DN lookups work.
- See R(Setting list option values,ansible_collections.microsoft.ad.docsite.guide_list_values)
for more information on how to add/remove/set list options.
aliases:
@@ -31,29 +36,35 @@ options:
suboptions:
add:
description:
- - The AD objects by their C(DistinguishedName) to add as a principal
- allowed to delegate.
+ - Adds the principals specified as principals allowed to delegate to.
- Any existing principals not specified by I(add) will be untouched
unless specified by I(remove) or not in I(set).
type: list
- elements: str
+ elements: raw
+ lookup_failure_action:
+ description:
+ - Control the action to take when the lookup fails to find the DN.
+ - C(fail) will cause the task to fail.
+ - C(ignore) will ignore the value and continue.
+ - C(warn) will ignore the value and display a warning.
+ choices: ['fail', 'ignore', 'warn']
+ default: fail
+ type: str
remove:
description:
- - The AD objects by their C(DistinguishedName) to remove as a principal
- allowed to delegate.
+ - Removes the principals specified as principals allowed to delegate to.
- Any existing pricipals not specified by I(remove) will be untouched
unless I(set) is defined.
type: list
- elements: str
+ elements: raw
set:
description:
- - The AD objects by their C(DistinguishedName) to set as the only
- principals allowed to delegate.
+ - Sets the principals specified as principals allowed to delegate to.
- This will remove any existing principals if not specified in this
list.
- Specify an empty list to remove all principals allowed to delegate.
type: list
- elements: str
+ elements: raw
dns_hostname:
description:
- Specifies the fully qualified domain name (FQDN) of the computer.
@@ -124,9 +135,13 @@ options:
description:
- The user or group that manages the object.
- The value can be in the form of a C(distinguishedName), C(objectGUID),
- C(objectSid), or sAMAccountName).
+ C(objectSid), C(sAMAccountName), or C(userPrincipalName) string or a
+ dictionary with the I(name) and optional I(server) key.
- This is the value set on the C(managedBy) LDAP attribute.
- type: str
+ - See
+ R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes)
+ for more information on how DN lookups work.
+ type: raw
sam_account_name:
description:
- The C(sAMAccountName) value to set for the group.
@@ -220,7 +235,7 @@ EXAMPLES = r"""
dns_hostname: one_linux_server.my_org.local
path: OU=servers,DC=my_org,DC=local
description: Example of linux server
- enabled: yes
+ enabled: true
state: present
- name: Remove linux computer from Active Directory using a windows machine
@@ -233,26 +248,26 @@ EXAMPLES = r"""
identity: TheComputer
spn:
add:
- - HOST/TheComputer
- - HOST/TheComputer.domain.test
- - HOST/TheComputer.domain.test:1234
+ - HOST/TheComputer
+ - HOST/TheComputer.domain.test
+ - HOST/TheComputer.domain.test:1234
- name: Remove SPNs on the computer
microsoft.ad.computer:
identity: TheComputer
spn:
remove:
- - HOST/TheComputer
- - HOST/TheComputer.domain.test
- - HOST/TheComputer.domain.test:1234
+ - HOST/TheComputer
+ - HOST/TheComputer.domain.test
+ - HOST/TheComputer.domain.test:1234
- name: Set the principals the computer trusts for delegation from
microsoft.ad.computer:
identity: TheComputer
delegates:
set:
- - CN=FileShare,OU=Computers,DC=domain,DC=test
- - CN=DC,OU=Domain Controllers,DC=domain,DC=test
+ - CN=FileShare,OU=Computers,DC=domain,DC=test
+ - OtherServer$ # Lookup by sAMAaccountName
"""
RETURN = r"""
diff --git a/ansible_collections/microsoft/ad/plugins/modules/domain.py b/ansible_collections/microsoft/ad/plugins/modules/domain.py
index 15578f7fd..0d9359242 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/domain.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/domain.py
@@ -99,6 +99,7 @@ attributes:
bypass_host_loop:
support: none
seealso:
+- module: microsoft.ad.domain_child
- module: microsoft.ad.domain_controller
- module: microsoft.ad.group
- module: microsoft.ad.membership
diff --git a/ansible_collections/microsoft/ad/plugins/modules/domain_child.ps1 b/ansible_collections/microsoft/ad/plugins/modules/domain_child.ps1
new file mode 100644
index 000000000..85fe3053d
--- /dev/null
+++ b/ansible_collections/microsoft/ad/plugins/modules/domain_child.ps1
@@ -0,0 +1,242 @@
+#!powershell
+
+# Copyright (c) 2024 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ create_dns_delegation = @{
+ type = 'bool'
+ }
+ database_path = @{
+ type = 'path'
+ }
+ dns_domain_name = @{
+ type = 'str'
+ }
+ domain_admin_password = @{
+ type = 'str'
+ required = $true
+ no_log = $true
+ }
+ domain_admin_user = @{
+ type = 'str'
+ required = $true
+ }
+ domain_mode = @{
+ type = 'str'
+ }
+ domain_type = @{
+ choices = 'child', 'tree'
+ default = 'child'
+ type = 'str'
+ }
+ install_dns = @{
+ type = 'bool'
+ }
+ log_path = @{
+ type = 'path'
+ }
+ parent_domain_name = @{
+ type = 'str'
+ }
+ reboot = @{
+ default = $false
+ type = 'bool'
+ }
+ safe_mode_password = @{
+ type = 'str'
+ required = $true
+ no_log = $true
+ }
+ site_name = @{
+ type = 'str'
+ }
+ sysvol_path = @{
+ type = 'path'
+ }
+ }
+ required_if = @(
+ , @('domain_type', 'tree', @('parent_domain_name'))
+ )
+ required_together = @(
+ , @("domain_admin_user", "domain_admin_password")
+ )
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$module.Result.reboot_required = $false
+$module.Result._do_action_reboot = $false # Used by action plugin
+
+$createDnsDelegation = $module.Params.create_dns_delegation
+$databasePath = $module.Params.database_path
+$dnsDomainName = $module.Params.dns_domain_name
+$domainMode = $module.Params.domain_mode
+$domainType = $module.Params.domain_type
+$installDns = $module.Params.install_dns
+$logPath = $module.Params.log_path
+$parentDomainName = $module.Params.parent_domain_name
+$safeModePassword = $module.Params.safe_mode_password
+$siteName = $module.Params.site_name
+$sysvolPath = $module.Params.sysvol_path
+
+$domainCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @(
+ $module.Params.domain_admin_user,
+ (ConvertTo-SecureString -AsPlainText -Force -String $module.Params.domain_admin_password)
+)
+
+if ($domainType -eq 'child' -and $parentDomainName) {
+ $module.FailJson("parent_domain_name must not be set when domain_type=child")
+}
+
+$requiredFeatures = @("AD-Domain-Services", "RSAT-ADDS")
+$features = Get-WindowsFeature -Name $requiredFeatures
+$unavailableFeatures = Compare-Object -ReferenceObject $requiredFeatures -DifferenceObject $features.Name -PassThru
+
+if ($unavailableFeatures) {
+ $module.FailJson("The following features required for a domain child are unavailable: $($unavailableFeatures -join ',')")
+}
+
+$missingFeatures = $features | Where-Object InstallState -NE Installed
+if ($missingFeatures) {
+ $res = Install-WindowsFeature -Name $missingFeatures -WhatIf:$module.CheckMode
+ $module.Result.changed = $true
+ $module.Result.reboot_required = [bool]$res.RestartNeeded
+
+ # When in check mode and the prereq was "installed" we need to exit early as
+ # the AD cmdlets weren't really installed
+ if ($module.CheckMode) {
+ $module.ExitJson()
+ }
+}
+
+# Check that we got a valid domain_mode
+$validDomainModes = [Enum]::GetNames((Get-Command -Name Install-ADDSDomain).Parameters.DomainMode.ParameterType)
+if (($null -ne $domainMode) -and -not ($domainMode -in $validDomainModes)) {
+ $validModes = $validDomainModes -join ", "
+ $module.FailJson("The parameter 'domain_mode' does not accept '$domainMode', please use one of: $validModes")
+}
+
+$systemRole = Get-CimInstance -ClassName Win32_ComputerSystem -Property Domain, DomainRole
+if ($systemRole.DomainRole -in @(4, 5)) {
+ if ($systemRole.Domain -ne $dnsDomainName) {
+ $module.FailJson("Host is already a domain controller in another domain $($systemRole.Domain)")
+ }
+ $module.ExitJson()
+}
+
+$installParams = @{
+ Confirm = $false
+ Credential = $domainCredential
+ Force = $true
+ NoRebootOnCompletion = $true
+ SafeModeAdministratorPassword = (ConvertTo-SecureString $safeModePassword -AsPlainText -Force)
+ SkipPreChecks = $true
+ WhatIf = $module.CheckMode
+}
+
+if ($domainType -eq 'child') {
+ $newDomainName, $parentDomainName = $dnsDomainName.Split([char[]]".", 2)
+ $installParams.DomainType = 'ChildDomain'
+ $installParams.NewDomainName = $newDomainName
+ $installParams.ParentDomainName = $parentDomainName
+}
+else {
+ $installParams.DomainType = 'TreeDomain'
+ $installParams.NewDomainName = $dnsDomainName
+ $installParams.ParentDomainName = $parentDomainName
+}
+
+if ($null -ne $createDnsDelegation) {
+ $installParams.CreateDnsDelegation = $createDnsDelegation
+}
+if ($databasePath) {
+ $installParams.DatabasePath = $databasePath
+}
+if ($domainMode) {
+ $installParams.DomainMode = $domainMode
+}
+if ($null -ne $installDns) {
+ $installParams.InstallDns = $installDns
+}
+if ($logPath) {
+ $installParams.LogPath = $logPath
+}
+if ($siteName) {
+ $installParams.SiteName = $siteName
+}
+if ($sysvolPath) {
+ $installParams.SysvolPath = $sysvolPath
+}
+
+try {
+ $null = Install-ADDSDomain @installParams
+}
+catch [Microsoft.DirectoryServices.Deployment.DCPromoExecutionException] {
+ # ExitCode 15 == 'Role change is in progress or this computer needs to be restarted.'
+ # DCPromo exit codes details can be found at
+ # https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/troubleshooting-domain-controller-deployment
+ if ($_.Exception.ExitCode -in @(15, 19)) {
+ $module.Result.reboot_required = $true
+ $module.Result._do_action_reboot = $true
+ }
+
+ $module.FailJson("Failed to install ADDSDomain, DCPromo exited with $($_.Exception.ExitCode)", $_)
+}
+finally {
+ # The Netlogon service is set to auto start but is not started. This is
+ # required for Ansible to connect back to the host and reboot in a
+ # later task. Even if this fails Ansible can still connect but only
+ # with ansible_winrm_transport=basic so we just display a warning if
+ # this fails.
+ if (-not $module.CheckMode) {
+ try {
+ Start-Service -Name Netlogon
+ }
+ catch {
+ $msg = -join @(
+ "Failed to start the Netlogon service after promoting the host, "
+ "Ansible may be unable to connect until the host is manually rebooted: $($_.Exception.Message)"
+ )
+ $module.Warn($msg)
+ }
+ }
+}
+
+$module.Result.changed = $true
+$module.Result.reboot_required = $true
+
+if ($module.Result.reboot_required -and $module.Params.reboot -and -not $module.CheckMode) {
+ # Promoting or depromoting puts the server in a very funky state and it may
+ # not be possible for Ansible to connect back without a reboot is done. If
+ # the user requested the action plugin to perform the reboot then start it
+ # here and get the action plugin to continue where this left off.
+
+ $lastBootTime = (Get-CimInstance -ClassName Win32_OperatingSystem -Property LastBootUpTime).LastBootUpTime.ToFileTime()
+ $module.Result._previous_boot_time = $lastBootTime
+
+ $shutdownRegPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\AutoLogonChecked'
+ Remove-Item -LiteralPath $shutdownRegPath -Force -ErrorAction SilentlyContinue
+
+ $comment = 'Reboot initiated by Ansible'
+ $stdout = $null
+ $stderr = . { shutdown.exe /r /t 10 /c $comment | Set-Variable stdout } 2>&1 | ForEach-Object ToString
+ if ($LASTEXITCODE -eq 1190) {
+ # A reboot was already scheduled, abort it and try again
+ shutdown.exe /a
+ $stdout = $null
+ $stderr = . { shutdown.exe /r /t 10 /c $comment | Set-Variable stdout } 2>&1 | ForEach-Object ToString
+ }
+
+ if ($LASTEXITCODE) {
+ $module.Result.rc = $LASTEXITCODE
+ $module.Result.stdout = $stdout
+ $module.Result.stderr = $stderr
+ $module.FailJson("Failed to initiate reboot, see rc, stdout, stderr for more information")
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/microsoft/ad/plugins/modules/domain_child.yml b/ansible_collections/microsoft/ad/plugins/modules/domain_child.yml
new file mode 100644
index 000000000..0f3308098
--- /dev/null
+++ b/ansible_collections/microsoft/ad/plugins/modules/domain_child.yml
@@ -0,0 +1,184 @@
+# Copyright (c) 2024 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION:
+ module: domain_child
+ short_description: Manage domain children in an existing Active Directory forest.
+ description:
+ - Ensure that a Windows Server host is configured as a domain controller as
+ a new domain in an existing forest.
+ - This module may require subsequent use of the
+ M(ansible.windows.win_reboot) action if changes are made.
+ - This module will only check if the domain specified by I(dns_domain_name)
+ exists or not. If the domain already exists under the same name, no other
+ options, other than the domain name will be checked during the run.
+ options:
+ create_dns_delegation:
+ description:
+ - Whether to create a DNS delegation that references the new DNS
+ server that was installed.
+ - Valid for Active Directory-integrated DNS only.
+ - The default is computed automatically based on the environment.
+ type: bool
+ database_path:
+ description:
+ - The path to a directory on a fixed disk of the Windows host where the
+ domain database will be created..
+ - If not set then the default path is C(%SYSTEMROOT%\NTDS).
+ type: path
+ dns_domain_name:
+ description:
+ - The full DNS name of the domain to create.
+ - When I(domain_type=child), the parent DNS domain name is derived
+ from this value.
+ type: str
+ domain_admin_password:
+ description:
+ - Password for the specified I(domain_admin_user).
+ type: str
+ required: true
+ domain_admin_user:
+ description:
+ - Username of a domain admin for the parent domain.
+ type: str
+ required: true
+ domain_mode:
+ description:
+ - Specifies the domain functional level of child/tree.
+ - The domain functional level cannot be lower than the forest
+ functional level, but it can be higher.
+ - The default is automatically computed and set.
+ - Current known modes are C(Win2003), C(Win2008), C(Win2008R2),
+ C(Win2012), C(Win2012R2), or C(WinThreshold).
+ type: str
+ domain_type:
+ description:
+ - Specifies the type of domain to create.
+ - Set to C(child) to create a child of an existing domain as specified
+ by I(dns_domain_name).
+ - Set to C(tree) to create a new domain tree in an existing forest as
+ specified by I(parent_domain_name). The I(dns_domain_name) must be
+ the full domain name of the new domain tree to create.
+ choices:
+ - child
+ - tree
+ default: child
+ type: str
+ install_dns:
+ description:
+ - Whether to install the DNS service when creating the domain
+ controller.
+ - If not specified then the C(-InstallDns) option is not supplied to
+ the C(Install-ADDSDomain) command, see
+ L(Install-ADDSDomain,https://learn.microsoft.com/en-us/powershell/module/addsdeployment/install-addsdomain#-installdns)
+ for more information.
+ type: bool
+ log_path:
+ description:
+ - Specified the fully qualified, non-UNC path to a directory on a fixed
+ disk of the local computer that will contain the domain log files.
+ type: path
+ parent_domain_name:
+ description:
+ - The fully qualified domain name of an existing parent domain to
+ create a new domain tree in.
+ - This can only be set when I(domain_type=tree).
+ type: str
+ reboot:
+ description:
+ - If C(true), this will reboot the host if a reboot was create the
+ domain.
+ - If C(false), this will not reboot the host if a reboot was required
+ and instead sets the I(reboot_required) return value to C(true).
+ - Multiple reboots may occur if the host required a reboot before the
+ domain promotion.
+ - This cannot be used with async mode.
+ type: bool
+ default: false
+ safe_mode_password:
+ description:
+ - Safe mode password for the domain controller.
+ required: true
+ type: str
+ site_name:
+ description:
+ - Specifies the name of an existing site where you can place the new
+ domain controller.
+ type: str
+ sysvol_path:
+ description:
+ - The path to a directory on a fixed disk of the Windows host where the
+ Sysvol folder will be created.
+ - If not set then the default path is C(%SYSTEMROOT%\SYSVOL).
+ type: path
+ notes:
+ - It is highly recommended to set I(reboot=true) to have Ansible manage the
+ host reboot phase as the actions done by this module puts the host in a
+ state where it may not be possible for Ansible to reconnect in a
+ subsequent task without a reboot.
+ - This module must be run on a Windows target host.
+ extends_documentation_fragment:
+ - ansible.builtin.action_common_attributes
+ - ansible.builtin.action_common_attributes.flow
+ attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+ platform:
+ platforms:
+ - windows
+ action:
+ support: full
+ async:
+ support: partial
+ details: Supported for all scenarios except with I(reboot=True).
+ bypass_host_loop:
+ support: none
+ seealso:
+ - module: microsoft.ad.domain
+ - module: microsoft.ad.domain_controller
+ author:
+ - Jordan Borean (@jborean93)
+
+EXAMPLES: |
+ - name: Create a child domain foo.example.com with parent example.com
+ microsoft.ad.domain_child:
+ dns_domain_name: foo.example.com
+ domain_admin_user: testguy@example.com
+ domain_admin_password: password123!
+ safe_mode_password: password123!
+ reboot: true
+
+ - name: Create a domain tree foo.example.com with parent bar.example.com
+ microsoft.ad.domain_child:
+ dns_domain_name: foo.example.com
+ parent_domain_name: bar.example.com
+ domain_type: tree
+ domain_admin_user: testguy@bar.example.com
+ domain_admin_password: password123!
+ local_admin_password: password123!
+ reboot: true
+
+ # This scenario is not recommended, use reboot: true when possible
+ - name: Promote server with custom paths with manual reboot task
+ microsoft.ad.domain_child:
+ dns_domain_name: foo.ansible.vagrant
+ domain_admin_user: testguy@ansible.vagrant
+ domain_admin_password: password123!
+ safe_mode_password: password123!
+ sysvol_path: D:\SYSVOL
+ database_path: D:\NTDS
+ log_path: D:\NTDS
+ register: dc_promotion
+
+ - name: Reboot after promotion
+ microsoft.ad.win_reboot:
+ when: dc_promotion.reboot_required
+
+RETURNS:
+ reboot_required:
+ description: True if changes were made that require a reboot.
+ returned: always
+ type: bool
+ sample: true
diff --git a/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py b/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py
index df4641741..69971243b 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/domain_controller.py
@@ -114,6 +114,7 @@ attributes:
seealso:
- module: microsoft.ad.computer
- module: microsoft.ad.domain
+- module: microsoft.ad.domain_child
- module: microsoft.ad.group
- module: microsoft.ad.membership
- module: microsoft.ad.user
@@ -150,7 +151,7 @@ EXAMPLES = r"""
domain_admin_password: password123!
safe_mode_password: password123!
state: domain_controller
- read_only: yes
+ read_only: true
site_name: London
reboot: true
@@ -168,7 +169,7 @@ EXAMPLES = r"""
register: dc_promotion
- name: Reboot after promotion
- microsoft.ad.win_reboot:
+ ansible.windows.win_reboot:
when: dc_promotion.reboot_required
"""
diff --git a/ansible_collections/microsoft/ad/plugins/modules/group.ps1 b/ansible_collections/microsoft/ad/plugins/modules/group.ps1
index bbb3aa8d7..ed4a52164 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/group.ps1
+++ b/ansible_collections/microsoft/ad/plugins/modules/group.ps1
@@ -26,141 +26,14 @@ $setParams = @{
Name = 'managed_by'
Option = @{ type = 'str' }
Attribute = 'ManagedBy'
+ DNLookup = $true
}
[PSCustomObject]@{
Name = 'members'
- Option = @{
- type = 'dict'
- options = @{
- add = @{
- type = 'list'
- elements = 'str'
- }
- remove = @{
- type = 'list'
- elements = 'str'
- }
- set = @{
- type = 'list'
- elements = 'str'
- }
- }
- }
+ Option = @{ type = 'add_remove_set' }
Attribute = 'member'
- New = {
- param($Module, $ADParams, $NewParams)
-
- $newMembers = @(
- foreach ($actionKvp in $Module.Params.members.GetEnumerator()) {
- if ($null -eq $actionKvp.Value -or $actionKvp.Key -eq 'remove') { continue }
-
- $invalidMembers = [System.Collections.Generic.List[string]]@()
-
- foreach ($m in $actionKvp.Value) {
- $obj = Get-AnsibleADObject -Identity $m @ADParams |
- Select-Object -ExpandProperty DistinguishedName
- if ($obj) {
- $obj
- }
- else {
- $invalidMembers.Add($m)
- }
- }
-
- if ($invalidMembers) {
- $module.FailJson("Failed to find the following ad objects for group members: '$($invalidMembers -join "', '")'")
- }
- }
- )
-
- if ($newMembers) {
- if (-not $NewParams.ContainsKey('OtherAttributes')) {
- $NewParams.OtherAttributes = @{}
- }
- # The AD cmdlets don't like explicitly casted arrays, use
- # ForEach-Object to get back a vanilla object[] to set.
- $NewParams.OtherAttributes.member = $newMembers | ForEach-Object { "$_" }
- }
- $Module.Diff.after.members = @($newMembers | Sort-Object)
- }
- Set = {
- param($Module, $ADParams, $SetParams, $ADObject)
-
- [string[]]$existingMembers = $ADObject.member
-
- $desiredState = @{}
- foreach ($actionKvp in $Module.Params.members.GetEnumerator()) {
- if ($null -eq $actionKvp.Value) { continue }
-
- $invalidMembers = [System.Collections.Generic.List[string]]@()
-
- $dns = foreach ($m in $actionKvp.Value) {
- $obj = Get-AnsibleADObject -Identity $m @ADParams |
- Select-Object -ExpandProperty DistinguishedName
- if ($obj) {
- $obj
- }
- else {
- $invalidMembers.Add($m)
- }
- }
-
- if ($invalidMembers) {
- $module.FailJson("Failed to find the following ad objects for group members: '$($invalidMembers -join "', '")'")
- }
-
- $desiredState[$actionKvp.Key] = @($dns)
- }
-
- $ignoreCase = [System.StringComparer]::OrdinalIgnoreCase
- [string[]]$diffAfter = @()
- if ($desiredState.ContainsKey('set')) {
- [string[]]$desiredMembers = $desiredState.set
- $diffAfter = $desiredMembers
-
- $toAdd = [string[]][System.Linq.Enumerable]::Except($desiredMembers, $existingMembers, $ignoreCase)
- $toRemove = [string[]][System.Linq.Enumerable]::Except($existingMembers, $desiredMembers, $ignoreCase)
-
- if ($toAdd -or $toRemove) {
- if (-not $SetParams.ContainsKey('Replace')) {
- $SetParams.Replace = @{}
- }
- $SetParams.Replace.member = $desiredMembers
- }
- }
- else {
- [string[]]$toAdd = @()
- [string[]]$toRemove = @()
- $diffAfter = $existingMembers
-
- if ($desiredState.ContainsKey('add') -and $desiredState.add) {
- [string[]]$desiredMembers = $desiredState.add
- $toAdd = [string[]][System.Linq.Enumerable]::Except($desiredMembers, $existingMembers, $ignoreCase)
- $diffAfter = [System.Linq.Enumerable]::Union($desiredMembers, $diffAfter, $ignoreCase)
- }
- if ($desiredState.ContainsKey('remove') -and $desiredState.remove) {
-
- [string[]]$desiredMembers = $desiredState.remove
- $toRemove = [string[]][System.Linq.Enumerable]::Intersect($desiredMembers, $existingMembers, $ignoreCase)
- $diffAfter = [System.Linq.Enumerable]::Except($diffAfter, $desiredMembers, $ignoreCase)
- }
-
- if ($toAdd) {
- if (-not $SetParams.ContainsKey('Add')) {
- $SetParams.Add = @{}
- }
- $SetParams.Add.member = $toAdd
- }
- if ($toRemove) {
- if (-not $SetParams.ContainsKey('Remove')) {
- $SetParams.Remove = @{}
- }
- $SetParams.Remove.member = $toRemove
- }
- }
-
- $Module.Diff.after.members = ($diffAfter | Sort-Object)
- }
+ DNLookup = $true
+ IsRawAttribute = $true
}
[PSCustomObject]@{
Name = 'sam_account_name'
diff --git a/ansible_collections/microsoft/ad/plugins/modules/group.py b/ansible_collections/microsoft/ad/plugins/modules/group.py
index 9fb28e819..df2c70440 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/group.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/group.py
@@ -32,19 +32,29 @@ options:
description:
- The user or group that manages the group.
- The value can be in the form of a C(distinguishedName), C(objectGUID),
- C(objectSid), or C(sAMAccountName).
+ C(objectSid), C(sAMAccountName), or C(userPrincipalName) string or a
+ dictionary with the I(name) and optional I(server) key.
- This is the value set on the C(managedBy) LDAP attribute.
- type: str
+ - See
+ R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes)
+ for more information on how DN lookups work.
+ type: raw
members:
description:
- The members of the group to set.
- The value is a dictionary that contains 3 keys, I(add), I(remove), and
I(set).
- - Each subkey is set to a list of AD principal objects to add, remove or
- set as the members of this AD group respectively. A principal can be in
- the form of a C(distinguishedName), C(objectGUID), C(objectSid), or
- C(sAMAccountName).
- - The module will fail if it cannot find any of the members referenced.
+ - Each subkey value is a list of values in the form of a
+ C(distinguishedName), C(objectGUID), C(objectSid), C(sAMAccountName),
+ or C(userPrincipalName) string or a dictionary with the I(name) and
+ optional I(server) key.
+ - The value for each subkey can either be specified as a string or a
+ dictionary with the I(name) and optional I(server) key. The I(name) is
+ the identity to lookup and I(server) is an optional key to override what
+ AD server to lookup the identity on.
+ - See
+ R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes)
+ for more information.
type: dict
suboptions:
add:
@@ -52,13 +62,22 @@ options:
- Adds the principals specified as members of the group, keeping the
existing membership if they are not specified.
type: list
- elements: str
+ elements: raw
+ lookup_failure_action:
+ description:
+ - Control the action to take when the lookup fails to find the DN.
+ - C(fail) will cause the task to fail.
+ - C(ignore) will ignore the value and continue.
+ - C(warn) will ignore the value and display a warning.
+ choices: ['fail', 'ignore', 'warn']
+ default: fail
+ type: str
remove:
description:
- Removes the principals specified as members of the group, keeping the
existing membership if they are not specified.
type: list
- elements: str
+ elements: raw
set:
description:
- Sets only the principals specified as members of the group.
@@ -66,7 +85,7 @@ options:
if not specified in this list.
- Set this to an empty list to remove all members from a group.
type: list
- elements: str
+ elements: raw
sam_account_name:
description:
- The C(sAMAccountName) value to set for the group.
@@ -179,8 +198,8 @@ EXAMPLES = r"""
scope: domainlocal
members:
add:
- - Domain Admins
- - Domain Users
+ - Domain Admins
+ - Domain Users
- name: Remove members from the group, preserving existing membership
microsoft.ad.group:
@@ -188,8 +207,8 @@ EXAMPLES = r"""
scope: domainlocal
members:
remove:
- - Domain Admins
- - Domain Users
+ - Domain Admins
+ - Domain Users
- name: Replace entire membership of group
microsoft.ad.group:
@@ -197,8 +216,14 @@ EXAMPLES = r"""
scope: domainlocal
members:
set:
- - Domain Admins
- - Domain Users
+ - Domain Admins
+ - Domain Users
+ - name: UserInOtherDomain
+ server: OtherDomain
+ domain_credentials:
+ - name: OtherDomain
+ username: OtherDomainUser
+ password: '{{ other_domain_password }}'
"""
RETURN = r"""
diff --git a/ansible_collections/microsoft/ad/plugins/modules/membership.ps1 b/ansible_collections/microsoft/ad/plugins/modules/membership.ps1
index d2be34e9f..963733a97 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/membership.ps1
+++ b/ansible_collections/microsoft/ad/plugins/modules/membership.ps1
@@ -143,7 +143,7 @@ Function Get-CurrentState {
}
[PSCustomObject]@{
- HostName = $env:COMPUTERNAME
+ HostName = [System.Net.Dns]::GetHostName()
PartOfDomain = $cs.PartOfDomain
DnsDomainName = $domainName
WorkgroupName = $cs.Workgroup
diff --git a/ansible_collections/microsoft/ad/plugins/modules/object.py b/ansible_collections/microsoft/ad/plugins/modules/object.py
index c6396619a..6b305afa2 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/object.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/object.py
@@ -128,8 +128,8 @@ EXAMPLES = r"""
attributes:
add:
extensionName:
- - value 1
- - value 2
+ - value 1
+ - value 2
type: container
state: present
@@ -139,8 +139,8 @@ EXAMPLES = r"""
attributes:
remove:
extensionName:
- - value 1
- - value 3
+ - value 1
+ - value 3
type: container
state: present
"""
diff --git a/ansible_collections/microsoft/ad/plugins/modules/object_info.py b/ansible_collections/microsoft/ad/plugins/modules/object_info.py
index 0cdcf06a7..88460979b 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/object_info.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/object_info.py
@@ -130,13 +130,13 @@ EXAMPLES = r"""
microsoft.ad.object_info:
filter: ObjectClass -eq 'user' -and objectCategory -eq 'Person'
properties:
- - objectSid
+ - objectSid
- name: Get the SID for all user accounts as a LDAP filter
microsoft.ad.object_info:
ldap_filter: (&(objectClass=user)(objectCategory=Person))
properties:
- - objectSid
+ - objectSid
- name: Search all computer accounts in a specific path that were added after February 1st
microsoft.ad.object_info:
diff --git a/ansible_collections/microsoft/ad/plugins/modules/ou.ps1 b/ansible_collections/microsoft/ad/plugins/modules/ou.ps1
index 6af68b5ae..909b13cd9 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/ou.ps1
+++ b/ansible_collections/microsoft/ad/plugins/modules/ou.ps1
@@ -22,6 +22,7 @@ $setParams = @{
Name = 'managed_by'
Option = @{ type = 'str' }
Attribute = 'ManagedBy'
+ DNLookup = $true
}
[PSCustomObject]@{
Name = 'postal_code'
diff --git a/ansible_collections/microsoft/ad/plugins/modules/ou.py b/ansible_collections/microsoft/ad/plugins/modules/ou.py
index 5d1d60503..1e31cc890 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/ou.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/ou.py
@@ -26,9 +26,13 @@ options:
description:
- The user or group that manages the object.
- The value can be in the form of a C(distinguishedName), C(objectGUID),
- C(objectSid), or sAMAccountName).
+ C(objectSid), C(sAMAccountName), or C(userPrincipalName) string or a
+ dictionary with the I(name) and optional I(server) key.
- This is the value set on the C(managedBy) LDAP attribute.
- type: str
+ - See
+ R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes)
+ for more information on how DN lookups work.
+ type: raw
postal_code:
description:
- Configures the user's postal code / zip code.
@@ -116,6 +120,13 @@ EXAMPLES = r"""
attributes:
set:
comment: A comment for the OU
+
+- name: Set managedBy using an identity from another DC
+ microsoft.ad.ou:
+ name: MyOU
+ managed_by:
+ name: manager-user
+ server: OtherDC
"""
RETURN = r"""
diff --git a/ansible_collections/microsoft/ad/plugins/modules/user.ps1 b/ansible_collections/microsoft/ad/plugins/modules/user.ps1
index 267c77627..8eef49635 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/user.ps1
+++ b/ansible_collections/microsoft/ad/plugins/modules/user.ps1
@@ -66,6 +66,9 @@ $setParams = @{
type = 'bool'
}
Attribute = 'LockedOut'
+ # We cannot lock a user and creating a user that is unlocked
+ # requires no action.
+ New = {}
Set = {
param($Module, $ADParams, $SetParams, $ADObject)
@@ -100,15 +103,10 @@ $setParams = @{
Name = 'delegates'
Option = @{
aliases = 'principals_allowed_to_delegate'
- type = 'dict'
- options = @{
- add = @{ type = 'list'; elements = 'str' }
- remove = @{ type = 'list'; elements = 'str' }
- set = @{ type = 'list'; elements = 'str' }
- }
+ type = 'add_remove_set'
}
Attribute = 'PrincipalsAllowedToDelegateToAccount'
- CaseInsensitive = $true
+ DNLookup = $true
}
[PSCustomObject]@{
@@ -134,10 +132,11 @@ $setParams = @{
Option = @{
type = 'dict'
options = @{
- add = @{ type = 'list'; elements = 'str' }
- remove = @{ type = 'list'; elements = 'str' }
- set = @{ type = 'list'; elements = 'str' }
- missing_behaviour = @{
+ add = @{ type = 'list'; elements = 'raw' }
+ remove = @{ type = 'list'; elements = 'raw' }
+ set = @{ type = 'list'; elements = 'raw' }
+ lookup_failure_action = @{
+ aliases = @('missing_behaviour')
choices = 'fail', 'ignore', 'warn'
default = 'fail'
type = 'str'
@@ -367,27 +366,19 @@ $setParams = @{
return
}
- $groupMissingBehaviour = $Module.Params.groups.missing_behaviour
- $lookupGroup = {
- try {
- (Get-ADGroup -Identity $args[0] @ADParams).DistinguishedName
- }
- catch {
- if ($groupMissingBehaviour -eq "fail") {
- $module.FailJson("Failed to locate group $($args[0]): $($_.Exception.Message)", $_)
- }
- elseif ($groupMissingBehaviour -eq "warn") {
- $module.Warn("Failed to locate group $($args[0]) but continuing on: $($_.Exception.Message)")
- }
- }
- }
-
[string[]]$existingGroups = @(
# In check mode the ADObject won't be given
if ($ADObject) {
try {
- Get-ADPrincipalGroupMembership -Identity $ADObject.ObjectGUID @ADParams -ErrorAction Stop |
- Select-Object -ExpandProperty DistinguishedName
+ # Get-ADPrincipalGroupMembership doesn't work well with
+ # cross domain membership. It also gets the primary group
+ # so this code reflects that using Get-ADUser instead.
+ $userMembership = Get-ADUser -Identity $ADObject.ObjectGUID @ADParams -Properties @(
+ 'MemberOf',
+ 'PrimaryGroup'
+ ) -ErrorAction Stop
+ $userMembership.memberOf
+ $userMembership.PrimaryGroup
}
catch {
$module.Warn("Failed to enumerate user groups but continuing on: $($_.Exception.Message)")
@@ -403,14 +394,42 @@ $setParams = @{
CaseInsensitive = $true
Existing = $existingGroups
}
- 'add', 'remove', 'set' | ForEach-Object -Process {
- if ($null -ne $Module.Params.groups[$_]) {
- $compareParams[$_] = @(
- foreach ($group in $Module.Params.groups[$_]) {
- & $lookupGroup $group
+ $dnServerParams = @{}
+ foreach ($actionKvp in $Module.Params.groups.GetEnumerator()) {
+ if ($null -eq $actionKvp.Value -or $actionKvp.Key -in @('lookup_failure_action', 'missing_behaviour')) {
+ continue
+ }
+
+ $convertParams = @{
+ Module = $Module
+ Context = "groups.$($actionKvp.Key)"
+ FailureAction = $Module.Params.groups.lookup_failure_action
+ }
+ $dns = foreach ($lookupId in $actionKvp.Value) {
+ $dn = $lookupId | ConvertTo-AnsibleADDistinguishedName @ADParams @convertParams
+ if (-not $dn) {
+ continue # Warning was written
+ }
+
+ # As membership is done on the group server, we need to store
+ # correct server and credentials that was used for the lookup.
+ if ($lookupId -is [System.Collections.IDictionary] -and $lookupId.server) {
+ $dnServerParams[$dn] = @{
+ Server = $lookupId.server
+ }
+
+ if ($Module.ServerCredentials.ContainsKey($lookupId.server)) {
+ $dnServerParams[$dn].Credential = $Module.ServerCredentials[$lookupId.server]
}
- )
+ }
+ else {
+ $dnServerParams[$dn] = $ADParams
+ }
+
+ $dn
}
+
+ $compareParams[$actionKvp.Key] = @($dns)
}
$res = Compare-AnsibleADIdempotentList @compareParams
@@ -422,15 +441,32 @@ $setParams = @{
WhatIf = $Module.CheckMode
}
foreach ($member in $res.ToAdd) {
+ $lookupParams = if ($dnServerParams.ContainsKey($member)) {
+ $dnServerParams[$member]
+ }
+ else {
+ $ADParams
+ }
if ($ADObject) {
- Add-ADGroupMember -Identity $member -Members $ADObject.ObjectGUID @ADParams @commonParams
+ Set-ADObject -Identity $member -Add @{
+ member = $ADObject.DistinguishedName
+ } @lookupParams @commonParams
+
}
$Module.Result.changed = $true
}
foreach ($member in $res.ToRemove) {
+ $lookupParams = if ($dnServerParams.ContainsKey($member)) {
+ $dnServerParams[$member]
+ }
+ else {
+ $ADParams
+ }
if ($ADObject) {
try {
- Remove-ADGroupMember -Identity $member -Members $ADObject.ObjectGUID @ADParams @commonParams
+ Set-ADObject -Identity $member -Remove @{
+ member = $ADObject.DistinguishedName
+ } @lookupParams @commonParams
}
catch [Microsoft.ActiveDirectory.Management.ADException] {
if ($_.Exception.ErrorCode -eq 0x0000055E) {
diff --git a/ansible_collections/microsoft/ad/plugins/modules/user.py b/ansible_collections/microsoft/ad/plugins/modules/user.py
index a3e7d1ecb..81a48b41d 100644
--- a/ansible_collections/microsoft/ad/plugins/modules/user.py
+++ b/ansible_collections/microsoft/ad/plugins/modules/user.py
@@ -40,14 +40,19 @@ options:
description:
- The principal objects that the current AD object can trust for
delegation to either add, remove or set.
- - The values for each sub option must be specified as a distinguished name
- C(CN=shenetworks,CN=Users,DC=ansible,DC=test)
+ - Each subkey value is a list of values in the form of a
+ C(distinguishedName), C(objectGUID), C(objectSid), C(sAMAccountName),
+ or C(userPrincipalName) string or a dictionary with the I(name) and
+ optional I(server) key.
- This is the value set on the C(msDS-AllowedToActOnBehalfOfOtherIdentity)
LDAP attribute.
- This is a highly sensitive attribute as it allows the principals
specified to impersonate any account when authenticating with the AD
computer object being managed.
- To clear all principals, use I(set) with an empty list.
+ - See
+ R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes)
+ for more information on how DN lookups work.
- See R(Setting list option values,ansible_collections.microsoft.ad.docsite.guide_list_values)
for more information on how to add/remove/set list options.
aliases:
@@ -56,29 +61,36 @@ options:
suboptions:
add:
description:
- - The AD objects by their C(DistinguishedName) to add as a principal
- allowed to delegate.
+ - Adds the principals specified as principals allowed to delegate to.
- Any existing principals not specified by I(add) will be untouched
unless specified by I(remove) or not in I(set).
type: list
- elements: str
+ elements: raw
+ lookup_failure_action:
+ description:
+ - Control the action to take when the lookup fails to find the DN.
+ - C(fail) will cause the task to fail.
+ - C(ignore) will ignore the value and continue.
+ - C(warn) will ignore the value and display a warning.
+ choices: ['fail', 'ignore', 'warn']
+ default: fail
+ type: str
remove:
description:
- - The AD objects by their C(DistinguishedName) to remove as a principal
- allowed to delegate.
+ - Removes the principals specified as principals allowed to delegate to.
- Any existing principals not specified by I(remove) will be untouched
unless I(set) is defined.
type: list
- elements: str
+ elements: raw
set:
description:
- - The AD objects by their C(DistinguishedName) to set as the only
+ - Sets the principals specified as principals allowed to delegate to.
principals allowed to delegate.
- This will remove any existing principals if not specified in this
list.
- Specify an empty list to remove all principals allowed to delegate.
type: list
- elements: str
+ elements: raw
email:
description:
- Configures the user's email address.
@@ -104,10 +116,20 @@ options:
- To clear all group memberships, use I(set) with an empty list.
- Note that users cannot be removed from their principal group (for
example, "Domain Users"). Attempting to do so will display a warning.
+ - Adding and removing a user from a group is done on the group AD object.
+ If the group is an object in a different domain, then it may require
+ explicit I(server) and I(domain_credentials) for it to work.
- Each subkey is set to a list of groups objects to add, remove or
set as the membership of this AD user respectively. A group can be in
the form of a C(distinguishedName), C(objectGUID), C(objectSid), or
C(sAMAccountName).
+ - Each subkey value is a list of group objects in the form of a
+ C(distinguishedName), C(objectGUID), C(objectSid), C(sAMAccountName),
+ or C(userPrincipalName) string or a dictionary with the I(name) and
+ optional I(server) key.
+ - See
+ R(DN Lookup Attributes,ansible_collections.microsoft.ad.docsite.guide_attributes.dn_lookup_attributes)
+ for more information on how DN lookups work.
- See R(Setting list option values,ansible_collections.microsoft.ad.docsite.guide_list_values)
for more information on how to add/remove/set list options.
type: dict
@@ -116,20 +138,20 @@ options:
description:
- The groups to add the user to.
type: list
- elements: str
+ elements: raw
remove:
description:
- The groups to remove the user from.
type: list
- elements: str
+ elements: raw
set:
description:
- The only groups the user is a member of.
- This will clear out any existing groups if not in the specified list.
- Set to an empty list to clear all group membership of the user.
type: list
- elements: str
- missing_behaviour:
+ elements: raw
+ lookup_failure_action:
description:
- Controls what happens when a group specified by C(groups) is an
invalid group name.
@@ -138,6 +160,8 @@ options:
- C(ignore) will ignore any groups that does not exist.
- C(warn) will display a warning for any groups that do not exist but
will continue without failing.
+ aliases:
+ - missing_behaviour
choices:
- fail
- ignore
@@ -287,7 +311,7 @@ EXAMPLES = r"""
state: present
groups:
set:
- - Domain Admins
+ - Domain Admins
street: 123 4th St.
city: Sometown
state_province: IN
@@ -316,8 +340,8 @@ EXAMPLES = r"""
path: ou=test,dc=domain,dc=local
groups:
set:
- - Domain Admins
- - Domain Users
+ - Domain Admins
+ - Domain Users
- name: Ensure user bob is absent
microsoft.ad.user:
@@ -329,15 +353,15 @@ EXAMPLES = r"""
identity: liz.kenyon
spn:
set:
- - MSSQLSvc/us99db-svr95:1433
- - MSSQLSvc/us99db-svr95.vmware.com:1433
+ - MSSQLSvc/us99db-svr95:1433
+ - MSSQLSvc/us99db-svr95.vmware.com:1433
- name: Ensure user has spn added
microsoft.ad.user:
identity: liz.kenyon
spn:
add:
- - MSSQLSvc/us99db-svr95:2433
+ - MSSQLSvc/us99db-svr95:2433
- name: Ensure user is created with delegates and spn's defined
microsoft.ad.user:
@@ -346,17 +370,17 @@ EXAMPLES = r"""
state: present
groups:
set:
- - Domain Admins
- - Domain Users
- - Enterprise Admins
+ - Domain Admins
+ - Domain Users
+ - Enterprise Admins
delegates:
set:
- - CN=shenetworks,CN=Users,DC=ansible,DC=test
- - CN=mk.ai,CN=Users,DC=ansible,DC=test
- - CN=jessiedotjs,CN=Users,DC=ansible,DC=test
+ - CN=shenetworks,CN=Users,DC=ansible,DC=test
+ - CN=mk.ai,CN=Users,DC=ansible,DC=test
+ - CN=jessiedotjs,CN=Users,DC=ansible,DC=test
spn:
set:
- - MSSQLSvc/us99db-svr95:2433
+ - MSSQLSvc/us99db-svr95:2433
# The name option is the name of the AD object as seen in dsa.msc and not the
# sAMAccountName. For example, this will change the sAMAccountName of the user
diff --git a/ansible_collections/microsoft/ad/plugins/plugin_utils/_module_with_reboot.py b/ansible_collections/microsoft/ad/plugins/plugin_utils/_module_with_reboot.py
index ebc46ead6..95e23465d 100644
--- a/ansible_collections/microsoft/ad/plugins/plugin_utils/_module_with_reboot.py
+++ b/ansible_collections/microsoft/ad/plugins/plugin_utils/_module_with_reboot.py
@@ -156,7 +156,7 @@ class ActionModuleWithReboot(ActionBase):
if self._ad_should_rerun(module_res) and not self._task.check_mode:
display.vv(
- "Module result has indicated it should rerun after a reboot has occured, rerunning"
+ "Module result has indicated it should rerun after a reboot has occurred, rerunning"
)
continue
@@ -169,3 +169,38 @@ class ActionModuleWithReboot(ActionBase):
result = merge_hash(result, module_res)
return self._ad_process_result(result)
+
+
+class DomainPromotionWithReboot(ActionModuleWithReboot):
+ """Domain Promotion Action Plugin with Auto Reboot.
+
+ An action plugin that runs a task that can promote the target Windows host
+ to a domain controller. It implements the common reboot handling for that
+ particular task.
+ """
+
+ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
+ super().__init__(*args, **kwargs)
+ self._ran_once = False
+
+ def _ad_should_rerun(self, result: t.Dict[str, t.Any]) -> bool:
+ ran_once = self._ran_once
+ self._ran_once = True
+
+ if ran_once or not result.get("_do_action_reboot", False):
+ return False
+
+ if self._task.check_mode:
+ # Assume that on a rerun it will not have failed and that it
+ # ran successful.
+ result["failed"] = False
+ result.pop("msg", None)
+ return False
+
+ else:
+ return True
+
+ def _ad_process_result(self, result: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
+ result.pop("_do_action_reboot", None)
+
+ return result
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml
index 2a403c3d5..3619df40b 100644
--- a/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/computer/tasks/tests.yml
@@ -99,14 +99,41 @@
that:
- not remove_comp_again is changed
+- name: expect failure with invalid DN lookup entry - no name
+ computer:
+ name: MyComputer
+ state: present
+ delegates:
+ set:
+ - CN=krbtgt,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
+ - server: fail
+ register: invalid_dn_lookup_no_name
+ failed_when: >-
+ invalid_dn_lookup_no_name.msg != "Failed to find the AD object DNs for delegates.set. Identity entry does not contain the required name key."
+
+- name: expect failure with invalid DN lookup entry - extra keys
+ computer:
+ name: MyComputer
+ state: present
+ delegates:
+ add:
+ - name: name
+ invalid2: bar
+ invalid1: foo
+ register: invalid_dn_lookup_extra_keys
+ failed_when: >-
+ invalid_dn_lookup_extra_keys.msg != "Failed to find the AD object DNs for delegates.add. Identity entry for 'name' contains extra keys: 'invalid1', 'invalid2'."
+
- name: create computer with custom options
computer:
name: MyComputer
state: present
delegates:
+ lookup_failure_action: ignore
set:
- CN=krbtgt,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
- - CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
+ - name: CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
+ - CN=Missing,{{ setup_domain_info.output[0].defaultNamingContext }}
kerberos_encryption_types:
set:
- aes128
@@ -188,8 +215,11 @@
name: MyComputer
path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
delegates:
+ lookup_failure_action: warn
set:
- - CN=KRBTGT,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
+ - name: CN=KRBTGT,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
+ - ''
+ - CN=Missing,{{ setup_domain_info.output[0].defaultNamingContext }}
dns_hostname: other.domain.com
kerberos_encryption_types:
set:
@@ -236,6 +266,9 @@
assert:
that:
- change_comp is changed
+ - change_comp.warnings | length == 1
+ - >-
+ change_comp.warnings[0] == "Failed to find the AD object DNs for delegates.set. Ignoring invalid identities: 'CN=Missing," ~ setup_domain_info.output[0].defaultNamingContext ~ "'"
- change_comp_actual.objects[0].dnsHostName == 'other.domain.com'
- change_comp_actual.objects[0].location == 'comp location'
- change_comp_actual.objects[0]['msDS-SupportedEncryptionTypes'] == 20
@@ -247,6 +280,17 @@
- '"ADS_UF_TRUSTED_FOR_DELEGATION" not in change_comp_actual.objects[0].userAccountControl_AnsibleFlags'
- change_comp_delegates.output == ["krbtgt"]
+- name: fail with invalid delegate identity
+ computer:
+ name: MyComputer
+ path: CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
+ delegates:
+ set:
+ - CN=Missing,{{ setup_domain_info.output[0].defaultNamingContext }}
+ register: invalid_delegate
+ failed_when: >-
+ invalid_delegate.msg != "Failed to find the AD object DNs for delegates.set. Invalid identities: 'CN=Missing," ~ setup_domain_info.output[0].defaultNamingContext ~ "'"
+
- name: add and remove list options
computer:
name: MyComputer
@@ -254,9 +298,10 @@
delegates:
add:
- CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
+ - ''
remove:
+ - name: ''
- CN=KRBTGT,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
- - CN=Missing,{{ setup_domain_info.output[0].defaultNamingContext }}
kerberos_encryption_types:
add:
- aes128
@@ -305,7 +350,6 @@
- CN=Administrator,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
remove:
- CN=KRBTGT,CN=Users,{{ setup_domain_info.output[0].defaultNamingContext }}
- - CN=Missing,{{ setup_domain_info.output[0].defaultNamingContext }}
kerberos_encryption_types:
add:
- aes128
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/README.md b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/README.md
new file mode 100644
index 000000000..f7bc08ff1
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/README.md
@@ -0,0 +1,36 @@
+# microsoft.ad.domain_child tests
+
+As this cannot be run in CI this is a brief guide on how to run these tests locally.
+Run the following:
+
+```bash
+vagrant up
+
+ansible-playbook setup.yml
+```
+
+It is a good idea to create a snapshot of both hosts before running the tests.
+This allows you to reset the host back to a blank starting state if the tests need to be rerun.
+To create a snapshot do the following:
+
+```bash
+virsh snapshot-create-as --domain "domain_child_PARENT" --name "pretest"
+virsh snapshot-create-as --domain "domain_child_CHILD" --name "pretest"
+virsh snapshot-create-as --domain "domain_child_TREE" --name "pretest"
+```
+
+To restore these snapshots run the following:
+
+```bash
+virsh snapshot-revert --domain "domain_child_PARENT" --snapshotname "pretest" --running
+virsh snapshot-revert --domain "domain_child_CHILD" --snapshotname "pretest" --running
+virsh snapshot-revert --domain "domain_child_TREE" --snapshotname "pretest" --running
+```
+
+Once you are ready to run the tests run the following:
+
+```bash
+ansible-playbook test.yml
+```
+
+Run `vagrant destroy` to remove the test VMs.
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/Vagrantfile b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/Vagrantfile
new file mode 100644
index 000000000..13af40311
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/Vagrantfile
@@ -0,0 +1,27 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+require 'yaml'
+
+inventory = YAML.load_file('inventory.yml')
+
+Vagrant.configure("2") do |config|
+ inventory['all']['children'].each do |group,details|
+ details['hosts'].each do |server,host_details|
+ config.vm.define server do |srv|
+ srv.vm.box = host_details['vagrant_box']
+ srv.vm.hostname = server
+ srv.vm.network :private_network,
+ :ip => host_details['ansible_host'],
+ :libvirt__network_name => 'microsoft.ad',
+ :libvirt__domain_name => inventory['all']['vars']['domain_realm']
+
+ srv.vm.provider :libvirt do |l|
+ l.memory = 8192
+ l.cpus = 4
+ end
+ end
+ end
+ end
+end
+
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/aliases b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/aliases
new file mode 100644
index 000000000..435ff207d
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/aliases
@@ -0,0 +1,2 @@
+windows
+unsupported # can never run in CI, see README.md
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/ansible.cfg b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/ansible.cfg
new file mode 100644
index 000000000..cfedec78f
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/ansible.cfg
@@ -0,0 +1,4 @@
+[defaults]
+callback_result_format = yaml
+inventory = inventory.yml
+retry_files_enabled = False
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/inventory.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/inventory.yml
new file mode 100644
index 000000000..e57f75516
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/inventory.yml
@@ -0,0 +1,28 @@
+all:
+ children:
+ windows:
+ hosts:
+ PARENT:
+ ansible_host: 192.168.11.10
+ vagrant_box: jborean93/WindowsServer2022
+ CHILD:
+ ansible_host: 192.168.11.11
+ vagrant_box: jborean93/WindowsServer2022
+ new_hostname: foo
+ child_domain_name: child.ad.test
+ TREE:
+ ansible_host: 192.168.11.12
+ vagrant_box: jborean93/WindowsServer2022
+ new_hostname: bar
+ child_domain_name: tree.test
+ vars:
+ ansible_port: 5985
+ ansible_connection: psrp
+
+ vars:
+ ansible_user: vagrant
+ ansible_password: vagrant
+ domain_username: vagrant-domain
+ domain_user_upn: '{{ domain_username }}@{{ domain_realm | upper }}'
+ domain_password: VagrantPass1
+ domain_realm: ad.test
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/setup.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/setup.yml
new file mode 100644
index 000000000..de0843829
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/setup.yml
@@ -0,0 +1,71 @@
+- name: setup common Windows information
+ hosts: windows
+ gather_facts: no
+
+ tasks:
+ - name: get network connection names
+ ansible.windows.win_powershell:
+ parameters:
+ IPAddress: '{{ ansible_host }}'
+ script: |
+ param ($IPAddress)
+
+ $Ansible.Changed = $false
+
+ Get-CimInstance -ClassName Win32_NetworkAdapter -Filter "Netenabled='True'" |
+ ForEach-Object -Process {
+ $config = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter "Index='$($_.Index)'"
+ if ($config.IPAddress -contains $IPAddress) {
+ $_.NetConnectionID
+ }
+ }
+ register: connection_name
+
+- name: create parent forest
+ hosts: PARENT
+ gather_facts: no
+
+ tasks:
+ - name: set the DNS for the internal adapters to localhost
+ ansible.windows.win_dns_client:
+ adapter_names:
+ - '{{ connection_name.output[0] }}'
+ dns_servers:
+ - 127.0.0.1
+
+ - name: ensure domain exists and DC is promoted as a domain controller
+ microsoft.ad.domain:
+ dns_domain_name: '{{ domain_realm }}'
+ safe_mode_password: '{{ domain_password }}'
+ reboot: true
+
+ - name: create parent domain username
+ microsoft.ad.user:
+ name: '{{ domain_username }}'
+ upn: '{{ domain_user_upn }}'
+ description: '{{ domain_username }} Domain Account'
+ password: '{{ domain_password }}'
+ password_never_expires: yes
+ update_password: when_changed
+ groups:
+ add:
+ - Domain Admins
+ - Enterprise Admins
+ state: present
+
+- name: setup test host
+ hosts: CHILD,TREE
+ gather_facts: no
+
+ tasks:
+ - name: set DNS for the private adapter to point to the parent forest DC
+ ansible.windows.win_dns_client:
+ adapter_names:
+ - '{{ connection_name.output[0] }}'
+ dns_servers:
+ - '{{ hostvars["PARENT"]["ansible_host"] }}'
+
+ - name: install RSAT tools for debugging purposes
+ ansible.windows.win_feature:
+ name: RSAT-AD-PowerShell
+ state: present
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/cross_domain.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/cross_domain.yml
new file mode 100644
index 000000000..611beedac
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/cross_domain.yml
@@ -0,0 +1,596 @@
+- name: create test object in parent domain with domain_username creds - check mode
+ microsoft.ad.user:
+ name: ParentUser1
+ path: '{{ parent_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_server: '{{ domain_realm }}'
+ domain_username: '{{ domain_user_upn }}'
+ domain_password: '{{ domain_password }}'
+ register: user_with_creds1_check
+ check_mode: true
+ delegate_to: CHILD
+
+- name: get result of create test object in parent domain with domain_username creds - check mode
+ microsoft.ad.object_info:
+ identity: CN=ParentUser1,{{ parent_ou }}
+ register: user_with_creds1_check_actual
+ delegate_to: PARENT
+
+- name: assert create test object in parent domain with domain_username creds - check mode
+ assert:
+ that:
+ - user_with_creds1_check is changed
+ - user_with_creds1_check.distinguished_name == "CN=ParentUser1," ~ parent_ou
+ - user_with_creds1_check_actual.objects == []
+
+- name: create test object in parent domain with domain_username creds
+ microsoft.ad.user:
+ name: ParentUser1
+ path: '{{ parent_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_server: '{{ domain_realm }}'
+ domain_username: '{{ domain_user_upn }}'
+ domain_password: '{{ domain_password }}'
+ register: user_with_creds1
+ delegate_to: CHILD
+
+- name: get result of create test object in parent domain with domain_username creds
+ microsoft.ad.object_info:
+ identity: CN=ParentUser1,{{ parent_ou }}
+ register: user_with_creds1_actual
+ delegate_to: PARENT
+
+- name: assert create test object in parent domain with domain_username creds
+ assert:
+ that:
+ - user_with_creds1 is changed
+ - user_with_creds1.distinguished_name == "CN=ParentUser1," ~ parent_ou
+ - user_with_creds1_actual.objects | count == 1
+ - user_with_creds1_actual.objects[0].ObjectGUID == user_with_creds1.object_guid
+ - user_with_creds1_actual.objects[0].DistinguishedName == user_with_creds1.distinguished_name
+
+- name: create test object in parent domain with domain_username creds - idempotent
+ microsoft.ad.user:
+ name: ParentUser1
+ path: '{{ parent_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_server: '{{ domain_realm }}'
+ domain_username: '{{ domain_user_upn }}'
+ domain_password: '{{ domain_password }}'
+ register: user_with_creds1_again
+ delegate_to: CHILD
+
+- name: assert create test object in parent domain with domain_username creds - idempotent
+ assert:
+ that:
+ - not user_with_creds1_again is changed
+ - user_with_creds1_again.distinguished_name == user_with_creds1.distinguished_name
+ - user_with_creds1_again.object_guid == user_with_creds1.object_guid
+
+- name: create test object in parent domain with domain_credentials creds - check mode
+ microsoft.ad.user:
+ name: ParentUser2
+ path: '{{ parent_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_server: '{{ domain_realm }}'
+ domain_credentials:
+ - username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ register: user_with_creds2_check
+ check_mode: true
+ delegate_to: CHILD
+
+- name: get result of create test object in parent domain with domain_credentials creds - check mode
+ microsoft.ad.object_info:
+ identity: CN=ParentUser2,{{ parent_ou }}
+ register: user_with_creds2_check_actual
+ delegate_to: PARENT
+
+- name: assert create test object in parent domain with domain_credentials creds - check mode
+ assert:
+ that:
+ - user_with_creds2_check is changed
+ - user_with_creds2_check.distinguished_name == "CN=ParentUser2," ~ parent_ou
+ - user_with_creds2_check_actual.objects == []
+
+- name: create test object in parent domain with domain_credentials creds
+ microsoft.ad.user:
+ name: ParentUser2
+ path: '{{ parent_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_server: '{{ domain_realm }}'
+ domain_credentials:
+ - username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ register: user_with_creds2
+ delegate_to: CHILD
+
+- name: get result of create test object in parent domain with domain_credentials creds
+ microsoft.ad.object_info:
+ identity: CN=ParentUser2,{{ parent_ou }}
+ register: user_with_creds2_actual
+ delegate_to: PARENT
+
+- name: assert create test object in parent domain with domain_credentials creds
+ assert:
+ that:
+ - user_with_creds2 is changed
+ - user_with_creds2.distinguished_name == "CN=ParentUser2," ~ parent_ou
+ - user_with_creds2_actual.objects | count == 1
+ - user_with_creds2_actual.objects[0].ObjectGUID == user_with_creds2.object_guid
+ - user_with_creds2_actual.objects[0].DistinguishedName == user_with_creds2.distinguished_name
+
+- name: create test object in parent domain with domain_credentials creds - idempotent
+ microsoft.ad.user:
+ name: ParentUser2
+ path: '{{ parent_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_server: '{{ domain_realm }}'
+ domain_credentials:
+ - username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ register: user_with_creds2_again
+ delegate_to: CHILD
+
+- name: assert create test object in parent domain with domain_credentials creds - idempotent
+ assert:
+ that:
+ - not user_with_creds2_again is changed
+ - user_with_creds2_again.distinguished_name == user_with_creds2.distinguished_name
+ - user_with_creds2_again.object_guid == user_with_creds2.object_guid
+
+- name: edit user with domain_username creds - check mode
+ microsoft.ad.user:
+ name: ParentUser1
+ path: '{{ parent_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_server: '{{ domain_realm }}'
+ domain_username: '{{ domain_user_upn }}'
+ domain_password: '{{ domain_password }}'
+ description: User Description
+ spn:
+ set:
+ - HTTP/ParentUser1
+ attributes:
+ set:
+ comment: My comment
+ register: set_with_creds1_check
+ delegate_to: CHILD
+ check_mode: true
+
+- name: get result of set user with domain_username creds - check mode
+ microsoft.ad.object_info:
+ identity: '{{ user_with_creds1.object_guid }}'
+ properties:
+ - comment
+ - Description
+ - servicePrincipalName
+ register: set_with_creds1_check_actual
+ delegate_to: PARENT
+
+- name: assert set user with domain_username creds - check mode
+ assert:
+ that:
+ - set_with_creds1_check is changed
+ - set_with_creds1_check.distinguished_name == user_with_creds1.distinguished_name
+ - set_with_creds1_check.object_guid == user_with_creds1.object_guid
+ - set_with_creds1_check_actual.objects[0].Description == None
+ - set_with_creds1_check_actual.objects[0].DistinguishedName == user_with_creds1.distinguished_name
+ - set_with_creds1_check_actual.objects[0].Name == 'ParentUser1'
+ - set_with_creds1_check_actual.objects[0].ObjectGUID == user_with_creds1.object_guid
+ - set_with_creds1_check_actual.objects[0].comment == None
+ - set_with_creds1_check_actual.objects[0].servicePrincipalName == None
+
+- name: edit user with domain_username creds
+ microsoft.ad.user:
+ name: ParentUser1
+ path: '{{ parent_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_server: '{{ domain_realm }}'
+ domain_username: '{{ domain_user_upn }}'
+ domain_password: '{{ domain_password }}'
+ description: User Description
+ spn:
+ set:
+ - HTTP/ParentUser1
+ attributes:
+ set:
+ comment: My comment
+ register: set_with_creds1
+ delegate_to: CHILD
+
+- name: get result of set user with domain_username creds
+ microsoft.ad.object_info:
+ identity: '{{ user_with_creds1.object_guid }}'
+ properties:
+ - comment
+ - Description
+ - servicePrincipalName
+ register: set_with_creds1_actual
+ delegate_to: PARENT
+
+- name: assert set user with domain_username creds
+ assert:
+ that:
+ - set_with_creds1 is changed
+ - set_with_creds1.distinguished_name == user_with_creds1.distinguished_name
+ - set_with_creds1.object_guid == user_with_creds1.object_guid
+ - set_with_creds1_actual.objects[0].Description == "User Description"
+ - set_with_creds1_actual.objects[0].DistinguishedName == user_with_creds1.distinguished_name
+ - set_with_creds1_actual.objects[0].Name == 'ParentUser1'
+ - set_with_creds1_actual.objects[0].ObjectGUID == user_with_creds1.object_guid
+ - set_with_creds1_actual.objects[0].comment == "My comment"
+ - set_with_creds1_actual.objects[0].servicePrincipalName == "HTTP/ParentUser1"
+
+- name: edit user with domain_credentials creds - check mode
+ microsoft.ad.user:
+ name: ParentUser2
+ path: '{{ parent_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_server: '{{ domain_realm }}'
+ domain_credentials:
+ - username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ description: User Description
+ spn:
+ set:
+ - HTTP/ParentUser2
+ attributes:
+ set:
+ comment: My comment
+ register: set_with_creds2_check
+ delegate_to: CHILD
+ check_mode: true
+
+- name: get result of set user with domain_credentials creds - check mode
+ microsoft.ad.object_info:
+ identity: '{{ user_with_creds2.object_guid }}'
+ properties:
+ - comment
+ - Description
+ - servicePrincipalName
+ register: set_with_creds2_check_actual
+ delegate_to: PARENT
+
+- name: assert set user with domain_credentials creds - check mode
+ assert:
+ that:
+ - set_with_creds2_check is changed
+ - set_with_creds2_check.distinguished_name == user_with_creds2.distinguished_name
+ - set_with_creds2_check.object_guid == user_with_creds2.object_guid
+ - set_with_creds2_check_actual.objects[0].Description == None
+ - set_with_creds2_check_actual.objects[0].DistinguishedName == user_with_creds2.distinguished_name
+ - set_with_creds2_check_actual.objects[0].Name == 'ParentUser2'
+ - set_with_creds2_check_actual.objects[0].ObjectGUID == user_with_creds2.object_guid
+ - set_with_creds2_check_actual.objects[0].comment == None
+ - set_with_creds2_check_actual.objects[0].servicePrincipalName == None
+
+- name: edit user with domain_credentials creds
+ microsoft.ad.user:
+ name: ParentUser2
+ path: '{{ parent_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_server: '{{ domain_realm }}'
+ domain_credentials:
+ - username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ description: User Description
+ spn:
+ set:
+ - HTTP/ParentUser2
+ attributes:
+ set:
+ comment: My comment
+ register: set_with_creds2
+ delegate_to: CHILD
+
+- name: get result of set user with domain_credentials creds
+ microsoft.ad.object_info:
+ identity: '{{ user_with_creds2.object_guid }}'
+ properties:
+ - comment
+ - Description
+ - servicePrincipalName
+ register: set_with_creds2_actual
+ delegate_to: PARENT
+
+- name: assert set user with domain_credentials creds
+ assert:
+ that:
+ - set_with_creds2 is changed
+ - set_with_creds2.distinguished_name == user_with_creds2.distinguished_name
+ - set_with_creds2.object_guid == user_with_creds2.object_guid
+ - set_with_creds2_actual.objects[0].Description == "User Description"
+ - set_with_creds2_actual.objects[0].DistinguishedName == user_with_creds2.distinguished_name
+ - set_with_creds2_actual.objects[0].Name == 'ParentUser2'
+ - set_with_creds2_actual.objects[0].ObjectGUID == user_with_creds2.object_guid
+ - set_with_creds2_actual.objects[0].comment == "My comment"
+ - set_with_creds2_actual.objects[0].servicePrincipalName == "HTTP/ParentUser2"
+
+- name: set value with DN lookup and creds
+ microsoft.ad.group:
+ name: Group-CHILD
+ path: '{{ child_ou }}'
+ state: present
+ members:
+ add:
+ - User-CHILD
+ - name: User-PARENT
+ server: '{{ domain_realm }}'
+ domain_credentials:
+ - name: '{{ domain_realm }}'
+ username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ register: lookup_with_creds
+ delegate_to: CHILD
+
+- name: get result of set value with DN lookup and creds
+ microsoft.ad.object_info:
+ identity: '{{ lookup_with_creds.object_guid }}'
+ properties:
+ - member
+ register: lookup_with_creds_actual
+ delegate_to: CHILD
+
+- name: assert set value with DN lookup and creds
+ assert:
+ that:
+ - lookup_with_creds is changed
+ - parent_user in lookup_with_creds_actual.objects[0].member
+ - child_user in lookup_with_creds_actual.objects[0].member
+
+- name: set value with DN lookup and creds - idempotent
+ microsoft.ad.group:
+ name: Group-CHILD
+ path: '{{ child_ou }}'
+ state: present
+ members:
+ add:
+ - User-CHILD
+ - name: User-PARENT
+ server: '{{ domain_realm }}'
+ domain_credentials:
+ - name: '{{ domain_realm }}'
+ username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ register: lookup_with_creds_again
+ delegate_to: CHILD
+
+- name: assert set value with DN lookup and creds - idempotent
+ assert:
+ that:
+ - not lookup_with_creds_again is changed
+
+- name: create user group with DN lookup and creds - check mode
+ microsoft.ad.user:
+ name: ChildUser1
+ path: '{{ child_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_credentials:
+ - name: '{{ domain_realm }}'
+ username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ groups:
+ add:
+ - Group-CHILD
+ - name: Group-PARENT
+ server: '{{ domain_realm }}'
+ register: new_user_with_group_check
+ delegate_to: CHILD
+ check_mode: true
+
+- name: get result of create user group with DN lookup and creds - check mode
+ microsoft.ad.object_info:
+ identity: '{{ new_user_with_group_check.distinguished_name }}'
+ properties:
+ - memberOf
+ register: new_user_with_group_check_actual
+ delegate_to: CHILD
+
+- name: assert set value with DN lookup and creds
+ assert:
+ that:
+ - new_user_with_group_check is changed
+ - new_user_with_group_check.distinguished_name == "CN=ChildUser1," ~ child_ou
+ - new_user_with_group_check_actual.objects == []
+
+- name: create user group with DN lookup and creds
+ microsoft.ad.user:
+ name: ChildUser1
+ path: '{{ child_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_credentials:
+ - name: '{{ domain_realm }}'
+ username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ groups:
+ add:
+ - Group-CHILD
+ - name: Group-PARENT
+ server: '{{ domain_realm }}'
+ register: new_user_with_group
+ delegate_to: CHILD
+
+- name: replicate group membership of parent group to child domain after adding group
+ ansible.windows.win_command: >-
+ repadmin.exe
+ /replsingleobj
+ {{ hostvars["CHILD"]["new_hostname"] }}.{{ hostvars["CHILD"]["child_domain_name"] }}
+ parent.{{ domain_realm }}
+ {{ parent_group }}
+ delegate_to: CHILD
+
+- name: get result of create user group with DN lookup and creds
+ microsoft.ad.object_info:
+ identity: '{{ new_user_with_group.distinguished_name }}'
+ properties:
+ - memberOf
+ register: new_user_with_group_actual
+ delegate_to: CHILD
+
+- name: assert create user group with DN lookup and creds
+ assert:
+ that:
+ - new_user_with_group is changed
+ - new_user_with_group.distinguished_name == "CN=ChildUser1," ~ child_ou
+ - new_user_with_group_actual.objects | count == 1
+ - new_user_with_group_actual.objects[0].DistinguishedName == new_user_with_group.distinguished_name
+ - >-
+ "CN=Group-CHILD," ~ child_ou in new_user_with_group_actual.objects[0].memberOf
+ - >-
+ "CN=Group-PARENT," ~ parent_ou in new_user_with_group_actual.objects[0].memberOf
+
+- name: create user group with DN lookup and creds - idempotent
+ microsoft.ad.user:
+ name: ChildUser1
+ path: '{{ child_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_credentials:
+ - name: '{{ domain_realm }}'
+ username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ groups:
+ add:
+ - Group-CHILD
+ - name: Group-PARENT
+ server: '{{ domain_realm }}'
+ register: new_user_with_group_again
+ delegate_to: CHILD
+
+- name: assert create user group with DN lookup and creds - idempotent
+ assert:
+ that:
+ - not new_user_with_group_again is changed
+
+- name: remove user group with DN lookup and creds - check mode
+ microsoft.ad.user:
+ name: ChildUser1
+ path: '{{ child_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_credentials:
+ - name: '{{ domain_realm }}'
+ username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ groups:
+ remove:
+ - Group-CHILD
+ - name: Group-PARENT
+ server: '{{ domain_realm }}'
+ register: remove_user_with_group_check
+ delegate_to: CHILD
+ check_mode: true
+
+- name: get result of remove user group with DN lookup and creds - check mode
+ microsoft.ad.object_info:
+ identity: '{{ remove_user_with_group_check.distinguished_name }}'
+ properties:
+ - memberOf
+ register: remove_user_with_group_check_actual
+ delegate_to: CHILD
+
+- name: assert remove user group with DN lookup and creds - check mode
+ assert:
+ that:
+ - remove_user_with_group_check is changed
+ - remove_user_with_group_check.distinguished_name == "CN=ChildUser1," ~ child_ou
+ - >-
+ "CN=Group-CHILD," ~ child_ou in remove_user_with_group_check_actual.objects[0].memberOf
+ - >-
+ "CN=Group-PARENT," ~ parent_ou in remove_user_with_group_check_actual.objects[0].memberOf
+
+- name: remove user group with DN lookup and creds
+ microsoft.ad.user:
+ name: ChildUser1
+ path: '{{ child_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_credentials:
+ - name: '{{ domain_realm }}'
+ username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ groups:
+ remove:
+ - Group-CHILD
+ - name: Group-PARENT
+ server: '{{ domain_realm }}'
+ register: remove_user_with_group
+ delegate_to: CHILD
+
+- name: replicate group membership of parent group to child domain after removing group
+ ansible.windows.win_command: >-
+ repadmin.exe
+ /replsingleobj
+ {{ hostvars["CHILD"]["new_hostname"] }}.{{ hostvars["CHILD"]["child_domain_name"] }}
+ parent.{{ domain_realm }}
+ {{ parent_group }}
+ delegate_to: CHILD
+
+- name: get result of remove user group with DN lookup and creds
+ microsoft.ad.object_info:
+ identity: '{{ remove_user_with_group.distinguished_name }}'
+ properties:
+ - memberOf
+ register: remove_user_with_group_actual
+ delegate_to: CHILD
+
+- name: assert remove user group with DN lookup and creds
+ assert:
+ that:
+ - remove_user_with_group is changed
+ - remove_user_with_group.distinguished_name == "CN=ChildUser1," ~ child_ou
+ - remove_user_with_group_actual.objects | count == 1
+ - remove_user_with_group_actual.objects[0].DistinguishedName == new_user_with_group.distinguished_name
+ - remove_user_with_group_actual.objects[0].memberOf == None
+
+- name: remove user group with DN lookup and creds - idempotent
+ microsoft.ad.user:
+ name: ChildUser1
+ path: '{{ child_ou }}'
+ state: present
+ password: '{{ domain_password }}'
+ update_password: when_changed
+ domain_credentials:
+ - name: '{{ domain_realm }}'
+ username: '{{ domain_user_upn }}'
+ password: '{{ domain_password }}'
+ groups:
+ remove:
+ - Group-CHILD
+ - name: Group-PARENT
+ server: '{{ domain_realm }}'
+ register: remove_user_with_group_again
+ delegate_to: CHILD
+
+- name: assert remove user group with DN lookup and creds - idempotent
+ assert:
+ that:
+ - not remove_user_with_group_again is changed
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/main_child.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/main_child.yml
new file mode 100644
index 000000000..40f4f2eda
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/main_child.yml
@@ -0,0 +1,98 @@
+- name: create child domain - check mode
+ domain_child:
+ dns_domain_name: '{{ child_domain_name }}'
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ safe_mode_password: '{{ domain_password }}'
+ reboot: true
+ register: to_domain_check
+ check_mode: true
+
+- name: get result of promote to domain - check mode
+ ansible.windows.win_powershell:
+ script: '{{ get_role_script }}'
+ register: to_domain_check_actual
+
+- name: assert promote to domain - check mode
+ assert:
+ that:
+ - to_domain_check is changed
+ - to_domain_check_actual.output[0]["Domain"] == None
+ - to_domain_check_actual.output[0]["DomainRole"] == "StandaloneServer"
+
+- name: change hostname to have a pending change before promotion
+ ansible.windows.win_hostname:
+ name: '{{ new_hostname }}'
+
+- name: create child domain with pending reboot
+ domain_child:
+ dns_domain_name: '{{ child_domain_name }}'
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ safe_mode_password: '{{ domain_password }}'
+ reboot: true
+ register: to_domain
+
+- name: get result of promote to domain with pending reboot
+ ansible.windows.win_powershell:
+ script: '{{ get_role_script }}'
+ register: to_domain_actual
+
+- name: assert promote to domain with pending reboot
+ assert:
+ that:
+ - to_domain is changed
+ - to_domain_actual.output[0]["Domain"] == child_domain_name
+ - to_domain_actual.output[0]["DomainRole"] == "PrimaryDC"
+ - to_domain_actual.output[0]["HostName"] == new_hostname | upper
+
+- name: create child domain - idempotent
+ domain_child:
+ dns_domain_name: '{{ child_domain_name }}'
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ safe_mode_password: '{{ domain_password }}'
+ reboot: true
+ register: to_domain_again
+
+- name: assert create child domain - idempotent
+ assert:
+ that:
+ - not to_domain_again is changed
+
+- name: fail to change domain of host
+ domain_child:
+ dns_domain_name: bogus.local
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ safe_mode_password: '{{ domain_password }}'
+ reboot: true
+ register: change_domain_fail
+ failed_when:
+ - change_domain_fail.msg != "Host is already a domain controller in another domain " ~ child_domain_name
+
+- name: fail with parent_domain_name with domain_type mode
+ domain_child:
+ dns_domain_name: '{{ child_domain_name }}'
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ safe_mode_password: '{{ domain_password }}'
+ parent_domain_name: other
+ reboot: true
+ register: invalid_parent
+ failed_when:
+ - invalid_parent.msg != "parent_domain_name must not be set when domain_type=child"
+
+- name: fail with invalid domain_mode
+ domain_child:
+ dns_domain_name: bogus.local
+ parent_domain_name: '{{ domain_realm }}'
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ safe_mode_password: '{{ domain_password }}'
+ domain_mode: Invalid
+ reboot: true
+ register: change_domain_invalid_mode
+ failed_when:
+ - >-
+ change_domain_invalid_mode.msg.startswith("The parameter 'domain_mode' does not accept 'Invalid', please use one of: ")
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/main_tree.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/main_tree.yml
new file mode 100644
index 000000000..01e5e06b5
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/tasks/main_tree.yml
@@ -0,0 +1,91 @@
+- name: create test folders
+ ansible.windows.win_file:
+ path: 'C:\ansible_testing\{{ item }}'
+ state: directory
+ loop:
+ - DB
+ - LogPath
+ - SysVol
+
+- name: create tree domain - check mode
+ domain_child:
+ dns_domain_name: '{{ child_domain_name }}'
+ parent_domain_name: '{{ domain_realm }}'
+ domain_type: tree
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ safe_mode_password: '{{ domain_password }}'
+ domain_mode: WinThreshold
+ database_path: C:\ansible_testing\DB
+ log_path: C:\ansible_testing\LogPath
+ sysvol_path: C:\ansible_testing\SysVol
+ reboot: true
+ register: to_tree_check
+ check_mode: true
+
+- name: get result of promote to tree domain - check mode
+ ansible.windows.win_powershell:
+ script: '{{ get_role_script }}'
+ register: to_tree_check_actual
+
+- name: assert promote to domain - check mode
+ assert:
+ that:
+ - to_tree_check is changed
+ - not to_tree_check.reboot_required
+ - to_tree_check_actual.output[0]["Domain"] == None
+ - to_tree_check_actual.output[0]["DomainRole"] == "StandaloneServer"
+
+- name: change hostname to have a pending change before promotion
+ ansible.windows.win_hostname:
+ name: '{{ new_hostname }}'
+
+- name: create tree domain with pending reboot
+ domain_child:
+ dns_domain_name: '{{ child_domain_name }}'
+ parent_domain_name: '{{ domain_realm }}'
+ domain_type: tree
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ safe_mode_password: '{{ domain_password }}'
+ domain_mode: WinThreshold
+ database_path: C:\ansible_testing\DB
+ log_path: C:\ansible_testing\LogPath
+ sysvol_path: C:\ansible_testing\SysVol
+ reboot: true
+ register: to_tree
+
+- name: get result of promote to domain with pending reboot
+ ansible.windows.win_powershell:
+ script: '{{ get_role_script }}'
+ register: to_tree_actual
+
+- name: assert promote to domain with pending reboot
+ assert:
+ that:
+ - to_tree is changed
+ - not to_tree.reboot_required
+ - to_tree_actual.output[0]["Domain"] == child_domain_name
+ - to_tree_actual.output[0]["DomainRole"] == "PrimaryDC"
+ - to_tree_actual.output[0]["HostName"] == new_hostname | upper
+
+- name: create tree domain - idempotent
+ domain_child:
+ dns_domain_name: '{{ child_domain_name }}'
+ parent_domain_name: '{{ domain_realm }}'
+ domain_type: tree
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ safe_mode_password: '{{ domain_password }}'
+ domain_mode: WinThreshold
+ database_path: C:\ansible_testing\DB
+ log_path: C:\ansible_testing\LogPath
+ sysvol_path: C:\ansible_testing\SysVol
+ reboot: true
+ register: to_tree_again
+
+- name: assert create tree domain - idempotent
+ assert:
+ that:
+ - not to_tree_again is changed
+ - not to_tree_again.reboot_required
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/test.yml b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/test.yml
new file mode 100644
index 000000000..ba936e1d9
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/domain_child/test.yml
@@ -0,0 +1,146 @@
+- name: ensure time is in sync
+ hosts: windows
+ gather_facts: false
+ tasks:
+ - name: get current host datetime
+ command: date +%s
+ changed_when: False
+ delegate_to: localhost
+ run_once: True
+ register: local_time
+
+ - name: set datetime on Windows
+ ansible.windows.win_powershell:
+ parameters:
+ SecondsSinceEpoch: '{{ local_time.stdout | trim }}'
+ script: |
+ param($SecondsSinceEpoch)
+
+ $utc = [System.DateTimeKind]::Utc
+ $epoch = New-Object -TypeName System.DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0, $utc
+ $date = $epoch.AddSeconds($SecondsSinceEpoch)
+
+ Set-Date -Date $date
+
+ - name: set common test vars
+ ansible.builtin.set_fact:
+ get_role_script: |
+ $Ansible.Changed = $false
+ Get-CimInstance -ClassName Win32_ComputerSystem -Property Domain, DomainRole, PartOfDomain |
+ Select-Object -Property @{
+ N = 'Domain'
+ E = {
+ if ($_.PartOfDomain) {
+ $_.Domain
+ }
+ else {
+ $null
+ }
+ }
+ }, @{
+ N = 'DomainRole'
+ E = {
+ switch ($_.DomainRole) {
+ 0 { "StandaloneWorkstation" }
+ 1 { "MemberWorkstation" }
+ 2 { "StandaloneServer" }
+ 3 { "MemberServer" }
+ 4 { "BackupDC" }
+ 5 { "PrimaryDC" }
+ }
+ }
+ }, @{
+ N = 'HostName'
+ E = { $env:COMPUTERNAME }
+ }
+
+- name: run microsoft.ad.domain_child child tests
+ hosts: CHILD
+ gather_facts: false
+
+ tasks:
+ - name: check domain status to see if test will run
+ ansible.windows.win_powershell:
+ script: '{{ get_role_script }}'
+ register: domain_status
+
+ - ansible.builtin.include_tasks: tasks/main_child.yml
+ when: domain_status.output[0].Domain != child_domain_name
+
+- name: run microsoft.ad.domain_child tree tests
+ hosts: TREE
+ gather_facts: false
+
+ tasks:
+ - name: check domain status to see if test will run
+ ansible.windows.win_powershell:
+ script: '{{ get_role_script }}'
+ register: domain_status
+
+ - ansible.builtin.include_tasks: tasks/main_tree.yml
+ when: domain_status.output[0].Domain != child_domain_name
+
+- name: run extra tests to test out cross domain functionality in other modules
+ hosts: localhost
+ gather_facts: false
+
+ tasks:
+ - name: create test OU in each domain
+ microsoft.ad.ou:
+ name: Ansible-{{ item }}
+ state: present
+ delegate_to: '{{ item }}'
+ register: ou_info
+ loop:
+ - PARENT
+ - CHILD
+
+ - block:
+ - name: set facts for each OU DN
+ ansible.builtin.set_fact:
+ parent_ou: '{{ ou_info.results[0].distinguished_name }}'
+ child_ou: '{{ ou_info.results[1].distinguished_name }}'
+
+ - name: create test users
+ microsoft.ad.user:
+ name: User-{{ item }}
+ state: present
+ password: '{{ domain_password }}'
+ path: '{{ {"PARENT": parent_ou, "CHILD": child_ou}[item] }}'
+ register: user_info
+ delegate_to: '{{ item }}'
+ loop:
+ - PARENT
+ - CHILD
+
+ - name: create test groups
+ microsoft.ad.group:
+ name: Group-{{ item }}
+ state: present
+ path: '{{ {"PARENT": parent_ou, "CHILD": child_ou}[item] }}'
+ scope: universal
+ register: group_info
+ delegate_to: '{{ item }}'
+ loop:
+ - PARENT
+ - CHILD
+
+ - name: set facts for each test user and group DN
+ ansible.builtin.set_fact:
+ parent_user: '{{ user_info.results[0].distinguished_name }}'
+ parent_group: '{{ group_info.results[0].distinguished_name }}'
+ child_user: '{{ user_info.results[1].distinguished_name }}'
+ child_group: '{{ group_info.results[1].distinguished_name }}'
+
+ - name: run cross domain tests
+ ansible.builtin.import_tasks: tasks/cross_domain.yml
+
+ always:
+ - name: remove test OU in each domain
+ microsoft.ad.ou:
+ name: Ansible-{{ item }}
+ state: absent
+ delegate_to: '{{ item }}'
+ loop:
+ - PARENT
+ - CHILD
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml
index b40041b0d..958398a42 100644
--- a/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/group/tasks/tests.yml
@@ -107,7 +107,8 @@
- my_user_2
- another-user
register: fail_invalid_members
- failed_when: 'fail_invalid_members.msg != "Failed to find the following ad objects for group members: ''fake-user'', ''another-user''"'
+ failed_when: >-
+ fail_invalid_members.msg != "Failed to find the AD object DNs for members.add. Invalid identities: 'fake-user', 'another-user'"
- name: add members to a group - check
group:
@@ -141,7 +142,7 @@
members:
add:
- my_user_1
- - '{{ test_users.results[2].sid }}'
+ - name: '{{ test_users.results[2].sid }}'
- MyGroup2-ReallyLongGroupNameHere
register: add_member
@@ -376,7 +377,8 @@
- my_user_2
- another-user
register: fail_invalid_members
- failed_when: 'fail_invalid_members.msg != "Failed to find the following ad objects for group members: ''fake-user'', ''another-user''"'
+ failed_when: >-
+ fail_invalid_members.msg != "Failed to find the AD object DNs for members.add. Invalid identities: 'fake-user', 'another-user'"
- name: create group with custom options
group:
@@ -388,7 +390,8 @@
scope: domainlocal
category: distribution
homepage: www.ansible.com
- managed_by: Domain Admins
+ managed_by:
+ name: Domain Admins
members:
add:
- my_user_1
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml b/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml
index f66985da9..0060179e9 100644
--- a/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/membership/tasks/main.yml
@@ -1,7 +1,7 @@
- set_fact:
get_result_script: |
$Ansible.Changed = $false
- $cs = Get-CimInstance -ClassName Win32_ComputerSystem -Property Domain, PartOfDomain, Workgroup
+ $cs = Get-CimInstance -ClassName Win32_ComputerSystem -Property DNSHostName, Domain, PartOfDomain, Workgroup
$domainName = if ($cs.PartOfDomain) {
try {
[System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name
@@ -15,7 +15,8 @@
}
[PSCustomObject]@{
- HostName = $env:COMPUTERNAME
+ HostName = $cs.DNSHostName
+ NetbiosName = $env:COMPUTERNAME
PartOfDomain = $cs.PartOfDomain
DnsDomainName = $domainName
WorkgroupName = $cs.Workgroup
@@ -23,8 +24,13 @@
get_ad_result_script: |
$Ansible.Changed = $false
- Get-ADComputer -Filter { Name -ne 'DC' } -Properties DistinguishedName, Name, Enabled |
- Select-Object -Property DistinguishedName, Name, Enabled
+ Get-ADComputer -Filter { Name -ne 'DC' } -Properties DistinguishedName, DNSHostName, Name, Enabled |
+ Select-Object -Property @(
+ 'DistinguishedName'
+ @{ N = 'DNSHostName'; E = { $_.DNSHostName.Substring(0, $_.DNSHostName.IndexOf('.')) } }
+ 'Name'
+ 'Enabled'
+ )
- name: join domain invalid OU
membership:
@@ -65,6 +71,7 @@
- join_domain_check.reboot_required == False
- join_domain_check_actual.output[0]["DnsDomainName"] == None
- join_domain_check_actual.output[0]["HostName"] == "TEST"
+ - join_domain_check_actual.output[0]["NetbiosName"] == "TEST"
- join_domain_check_actual.output[0]["PartOfDomain"] == False
- join_domain_check_actual.output[0]["WorkgroupName"] == "WORKGROUP"
@@ -95,9 +102,11 @@
- join_domain.reboot_required == False
- join_domain_actual.output[0]["DnsDomainName"] == domain_realm
- join_domain_actual.output[0]["HostName"] == "TEST"
+ - join_domain_actual.output[0]["NetbiosName"] == "TEST"
- join_domain_actual.output[0]["PartOfDomain"] == True
- join_domain_actual.output[0]["WorkgroupName"] == None
- join_domain_ad_actual.output | length == 1
+ - join_domain_ad_actual.output[0]["DNSHostName"] == "TEST"
- join_domain_ad_actual.output[0]["Name"] == "TEST"
- join_domain_ad_actual.output[0]["Enabled"] == True
@@ -132,7 +141,7 @@
dns_domain_name: '{{ domain_realm }}'
domain_admin_user: '{{ domain_user_upn }}'
domain_admin_password: '{{ domain_password }}'
- hostname: OTHER
+ hostname: TEST1-long-HOSTNAME1
state: domain
reboot: true
register: rename_host_domain_check
@@ -156,9 +165,11 @@
- rename_host_domain_check.reboot_required == False
- rename_host_domain_check_actual.output[0]["DnsDomainName"] == domain_realm
- rename_host_domain_check_actual.output[0]["HostName"] == "TEST"
+ - rename_host_domain_check_actual.output[0]["NetbiosName"] == "TEST"
- rename_host_domain_check_actual.output[0]["PartOfDomain"] == True
- rename_host_domain_check_actual.output[0]["WorkgroupName"] == None
- rename_host_domain_check_ad_actual.output | length == 1
+ - rename_host_domain_check_ad_actual.output[0]["DNSHostName"] == "TEST"
- rename_host_domain_check_ad_actual.output[0]["Name"] == "TEST"
- rename_host_domain_check_ad_actual.output[0]["Enabled"] == True
@@ -167,7 +178,7 @@
dns_domain_name: '{{ domain_realm }}'
domain_admin_user: '{{ domain_user_upn }}'
domain_admin_password: '{{ domain_password }}'
- hostname: OTHER
+ hostname: TEST1-long-HOSTNAME1
state: domain
reboot: true
register: rename_host_domain
@@ -183,19 +194,109 @@
delegate_to: DC
register: rename_host_domain_ad_actual
-- name: assert join domain
+- name: assert rename hostname of domain joined host
assert:
that:
- rename_host_domain is changed
- rename_host_domain.reboot_required == False
- rename_host_domain_actual.output[0]["DnsDomainName"] == domain_realm
- - rename_host_domain_actual.output[0]["HostName"] == "OTHER"
+ - rename_host_domain_actual.output[0]["HostName"] == "TEST1-long-HOSTNAME1"
+ - rename_host_domain_actual.output[0]["NetbiosName"] == "TEST1-LONG-HOST"
- rename_host_domain_actual.output[0]["PartOfDomain"] == True
- rename_host_domain_actual.output[0]["WorkgroupName"] == None
- rename_host_domain_ad_actual.output | length == 1
- - rename_host_domain_ad_actual.output[0]["Name"] == "OTHER"
+ - rename_host_domain_ad_actual.output[0]["DNSHostName"] == "TEST1-long-HOSTNAME1"
+ - rename_host_domain_ad_actual.output[0]["Name"] == "TEST1-LONG-HOST"
- rename_host_domain_ad_actual.output[0]["Enabled"] == True
+- name: rename hostname of domain joined host - idempotent
+ membership:
+ dns_domain_name: '{{ domain_realm }}'
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ hostname: TEST1-long-HOSTNAME1
+ state: domain
+ reboot: true
+ register: rename_host_domain_again
+
+- name: assert rename hostname of domain joined host - idempotent
+ assert:
+ that:
+ - not rename_host_domain_again is changed
+
+- name: rename hostname of domain joined host netbios portion - check mode
+ membership:
+ dns_domain_name: '{{ domain_realm }}'
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ hostname: TEST2-long-HOSTNAME1
+ state: domain
+ reboot: true
+ register: rename_host_domain_netbios_check
+ check_mode: True
+
+- name: get result of rename hostname of domain joined host netbios portion - check mode
+ ansible.windows.win_powershell:
+ script: '{{ get_result_script }}'
+ register: rename_host_domain_netbios_check_actual
+
+- name: get ad result of rename hostname of domain joined host netbios portion - check mode
+ ansible.windows.win_powershell:
+ script: '{{ get_ad_result_script }}'
+ delegate_to: DC
+ register: rename_host_domain_netbios_check_ad_actual
+
+- name: assert rename hostname of domain joined host netbios portion - check mode
+ assert:
+ that:
+ - rename_host_domain_netbios_check is changed
+ - rename_host_domain_netbios_check.reboot_required == False
+ - rename_host_domain_netbios_check_actual.output[0]["DnsDomainName"] == domain_realm
+ - rename_host_domain_netbios_check_actual.output[0]["HostName"] == "TEST1-long-HOSTNAME1"
+ - rename_host_domain_netbios_check_actual.output[0]["NetbiosName"] == "TEST1-LONG-HOST"
+ - rename_host_domain_netbios_check_actual.output[0]["PartOfDomain"] == True
+ - rename_host_domain_netbios_check_actual.output[0]["WorkgroupName"] == None
+ - rename_host_domain_netbios_check_ad_actual.output | length == 1
+ - rename_host_domain_netbios_check_ad_actual.output[0]["DNSHostName"] == "TEST1-long-HOSTNAME1"
+ - rename_host_domain_netbios_check_ad_actual.output[0]["Name"] == "TEST1-LONG-HOST"
+ - rename_host_domain_netbios_check_ad_actual.output[0]["Enabled"] == True
+
+- name: rename hostname of domain joined host netbios portion
+ membership:
+ dns_domain_name: '{{ domain_realm }}'
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ hostname: TEST2-long-HOSTNAME1
+ state: domain
+ reboot: true
+ register: rename_host_domain_netbios
+
+- name: get result of rename hostname of domain joined host netbios portion
+ ansible.windows.win_powershell:
+ script: '{{ get_result_script }}'
+ register: rename_host_domain_netbios_actual
+
+- name: get ad result of rename hostname of domain joined host netbios portion
+ ansible.windows.win_powershell:
+ script: '{{ get_ad_result_script }}'
+ delegate_to: DC
+ register: rename_host_domain_netbios_ad_actual
+
+- name: assert rename hostname of domain joined host
+ assert:
+ that:
+ - rename_host_domain_netbios is changed
+ - rename_host_domain_netbios.reboot_required == False
+ - rename_host_domain_netbios_actual.output[0]["DnsDomainName"] == domain_realm
+ - rename_host_domain_netbios_actual.output[0]["HostName"] == "TEST2-long-HOSTNAME1"
+ - rename_host_domain_netbios_actual.output[0]["NetbiosName"] == "TEST2-LONG-HOST"
+ - rename_host_domain_netbios_actual.output[0]["PartOfDomain"] == True
+ - rename_host_domain_netbios_actual.output[0]["WorkgroupName"] == None
+ - rename_host_domain_netbios_ad_actual.output | length == 1
+ - rename_host_domain_netbios_ad_actual.output[0]["DNSHostName"] == "TEST2-long-HOSTNAME1"
+ - rename_host_domain_netbios_ad_actual.output[0]["Name"] == "TEST2-LONG-HOST"
+ - rename_host_domain_netbios_ad_actual.output[0]["Enabled"] == True
+
- name: change domain to workgroup - check mode
membership:
workgroup_name: TEST
@@ -222,11 +323,13 @@
- to_workgroup_check is changed
- to_workgroup_check.reboot_required == True
- to_workgroup_check_actual.output[0]["DnsDomainName"] == domain_realm
- - to_workgroup_check_actual.output[0]["HostName"] == "OTHER"
+ - to_workgroup_check_actual.output[0]["HostName"] == "TEST2-long-HOSTNAME1"
+ - to_workgroup_check_actual.output[0]["NetbiosName"] == "TEST2-LONG-HOST"
- to_workgroup_check_actual.output[0]["PartOfDomain"] == True
- to_workgroup_check_actual.output[0]["WorkgroupName"] == None
- to_workgroup_check_ad_actual.output | length == 1
- - to_workgroup_check_ad_actual.output[0]["Name"] == "OTHER"
+ - to_workgroup_check_ad_actual.output[0]["DNSHostName"] == "TEST2-long-HOSTNAME1"
+ - to_workgroup_check_ad_actual.output[0]["Name"] == "TEST2-LONG-HOST"
- to_workgroup_check_ad_actual.output[0]["Enabled"] == True
- name: change domain to workgroup
@@ -238,7 +341,7 @@
register: to_workgroup
- set_fact:
- local_user: OTHER\{{ ansible_user }}
+ local_user: TEST2-LONG-HOST\{{ ansible_user }}
- ansible.windows.win_reboot:
when: to_workgroup.reboot_required
@@ -266,16 +369,18 @@
- to_workgroup is changed
- to_workgroup.reboot_required == True
- to_workgroup_actual.output[0]["DnsDomainName"] == None
- - to_workgroup_actual.output[0]["HostName"] == "OTHER"
+ - to_workgroup_actual.output[0]["HostName"] == "TEST2-long-HOSTNAME1"
+ - to_workgroup_actual.output[0]["NetbiosName"] == "TEST2-LONG-HOST"
- to_workgroup_actual.output[0]["PartOfDomain"] == False
- to_workgroup_actual.output[0]["WorkgroupName"] == "TEST"
- to_workgroup_ad_actual.output | length == 1
- - to_workgroup_ad_actual.output[0]["Name"] == "OTHER"
+ - to_workgroup_ad_actual.output[0]["DNSHostName"] == "TEST2-long-HOSTNAME1"
+ - to_workgroup_ad_actual.output[0]["Name"] == "TEST2-LONG-HOST"
- to_workgroup_ad_actual.output[0]["Enabled"] == False
- name: remove orphaned AD account for later tests
microsoft.ad.computer:
- name: OTHER
+ name: TEST2-LONG-HOST
state: absent
delegate_to: DC
@@ -315,7 +420,8 @@
- change_workgroup_check is changed
- change_workgroup_check.reboot_required == False
- change_workgroup_check_actual.output[0]["DnsDomainName"] == None
- - change_workgroup_check_actual.output[0]["HostName"] == "OTHER"
+ - change_workgroup_check_actual.output[0]["HostName"] == "TEST2-long-HOSTNAME1"
+ - change_workgroup_check_actual.output[0]["NetbiosName"] == "TEST2-LONG-HOST"
- change_workgroup_check_actual.output[0]["PartOfDomain"] == False
- change_workgroup_check_actual.output[0]["WorkgroupName"] == "TEST"
@@ -339,7 +445,8 @@
- change_workgroup is changed
- change_workgroup.reboot_required == False
- change_workgroup_actual.output[0]["DnsDomainName"] == None
- - change_workgroup_actual.output[0]["HostName"] == "OTHER"
+ - change_workgroup_actual.output[0]["HostName"] == "TEST2-long-HOSTNAME1"
+ - change_workgroup_actual.output[0]["NetbiosName"] == "TEST2-LONG-HOST"
- change_workgroup_actual.output[0]["PartOfDomain"] == False
- change_workgroup_actual.output[0]["WorkgroupName"] == "TEST2"
@@ -350,7 +457,7 @@
domain_admin_password: '{{ domain_password }}'
state: workgroup
reboot: true
- hostname: FOO
+ hostname: TEST1-long-HOSTNAME2
register: change_hostname_check
check_mode: true
@@ -365,7 +472,8 @@
- change_hostname_check is changed
- change_hostname_check.reboot_required == False
- change_hostname_check_actual.output[0]["DnsDomainName"] == None
- - change_hostname_check_actual.output[0]["HostName"] == "OTHER"
+ - change_hostname_check_actual.output[0]["HostName"] == "TEST2-long-HOSTNAME1"
+ - change_hostname_check_actual.output[0]["NetbiosName"] == "TEST2-LONG-HOST"
- change_hostname_check_actual.output[0]["PartOfDomain"] == False
- change_hostname_check_actual.output[0]["WorkgroupName"] == "TEST2"
@@ -376,7 +484,7 @@
domain_admin_password: '{{ domain_password }}'
state: workgroup
reboot: true
- hostname: FOO
+ hostname: TEST1-long-HOSTNAME2
register: change_hostname
- name: get result of change just the hostname
@@ -384,16 +492,85 @@
script: '{{ get_result_script }}'
register: change_hostname_actual
-- name: assert change just the hostname - check mode
+- name: assert change just the hostname
assert:
that:
- change_hostname is changed
- change_hostname.reboot_required == False
- change_hostname_actual.output[0]["DnsDomainName"] == None
- - change_hostname_actual.output[0]["HostName"] == "FOO"
+ - change_hostname_actual.output[0]["HostName"] == "TEST1-long-HOSTNAME2"
+ - change_hostname_actual.output[0]["NetbiosName"] == "TEST1-LONG-HOST"
- change_hostname_actual.output[0]["PartOfDomain"] == False
- change_hostname_actual.output[0]["WorkgroupName"] == "TEST2"
+- name: change just the hostname - idempotent
+ membership:
+ workgroup_name: TEST2
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ state: workgroup
+ reboot: true
+ hostname: TEST1-long-HOSTNAME2
+ register: change_hostname_again
+
+- name: assert change just the hostname - idempotent
+ assert:
+ that:
+ - not change_hostname_again is changed
+
+- name: change just the hostname netbios portion - check mode
+ membership:
+ workgroup_name: TEST2
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ state: workgroup
+ reboot: true
+ hostname: TEST2-long-HOSTNAME2
+ register: change_hostname_netbios_check
+ check_mode: true
+
+- name: get result of change just the hostname netbios portion - check mode
+ ansible.windows.win_powershell:
+ script: '{{ get_result_script }}'
+ register: change_hostname_netbios_check_actual
+
+- name: assert change just the hostname netbios portion - check mode
+ assert:
+ that:
+ - change_hostname_netbios_check is changed
+ - change_hostname_netbios_check.reboot_required == False
+ - change_hostname_netbios_check_actual.output[0]["DnsDomainName"] == None
+ - change_hostname_netbios_check_actual.output[0]["HostName"] == "TEST1-long-HOSTNAME2"
+ - change_hostname_netbios_check_actual.output[0]["NetbiosName"] == "TEST1-LONG-HOST"
+ - change_hostname_netbios_check_actual.output[0]["PartOfDomain"] == False
+ - change_hostname_netbios_check_actual.output[0]["WorkgroupName"] == "TEST2"
+
+- name: change just the hostname netbios portion
+ membership:
+ workgroup_name: TEST2
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ state: workgroup
+ reboot: true
+ hostname: TEST2-long-HOSTNAME2
+ register: change_hostname_netbios
+
+- name: get result of change just the hostname netbios portion
+ ansible.windows.win_powershell:
+ script: '{{ get_result_script }}'
+ register: change_hostname_netbios_actual
+
+- name: assert change just the hostname
+ assert:
+ that:
+ - change_hostname_netbios is changed
+ - change_hostname_netbios.reboot_required == False
+ - change_hostname_netbios_actual.output[0]["DnsDomainName"] == None
+ - change_hostname_netbios_actual.output[0]["HostName"] == "TEST2-long-HOSTNAME2"
+ - change_hostname_netbios_actual.output[0]["NetbiosName"] == "TEST2-LONG-HOST"
+ - change_hostname_netbios_actual.output[0]["PartOfDomain"] == False
+ - change_hostname_netbios_actual.output[0]["WorkgroupName"] == "TEST2"
+
- name: create custom OU
ansible.windows.win_powershell:
script: |
@@ -407,7 +584,7 @@
dns_domain_name: '{{ domain_realm }}'
domain_admin_user: '{{ domain_user_upn }}'
domain_admin_password: '{{ domain_password }}'
- hostname: BAR
+ hostname: TEST1-long-HOSTNAME3
domain_ou_path: '{{ custom_ou.output[0] }}'
state: domain
register: join_ou_check
@@ -424,7 +601,8 @@
- join_ou_check is changed
- join_ou_check.reboot_required == True
- join_ou_check_actual.output[0]["DnsDomainName"] == None
- - join_ou_check_actual.output[0]["HostName"] == "FOO"
+ - join_ou_check_actual.output[0]["HostName"] == "TEST2-long-HOSTNAME2"
+ - join_ou_check_actual.output[0]["NetbiosName"] == "TEST2-LONG-HOST"
- join_ou_check_actual.output[0]["PartOfDomain"] == False
- join_ou_check_actual.output[0]["WorkgroupName"] == "TEST2"
@@ -433,7 +611,7 @@
dns_domain_name: '{{ domain_realm }}'
domain_admin_user: '{{ domain_user_upn }}'
domain_admin_password: '{{ domain_password }}'
- hostname: BAR
+ hostname: TEST1-long-HOSTNAME3
domain_ou_path: '{{ custom_ou.output[0] }}'
state: domain
register: join_ou
@@ -452,26 +630,28 @@
register: join_ou_ad_actual
delegate_to: DC
-- name: assert change just the hostname
+- name: assert join domain with hostname and OUT
assert:
that:
- join_ou is changed
- join_ou.reboot_required == True
- join_ou_actual.output[0]["DnsDomainName"] == domain_realm
- - join_ou_actual.output[0]["HostName"] == "BAR"
+ - join_ou_actual.output[0]["HostName"] == "TEST1-long-HOSTNAME3"
+ - join_ou_actual.output[0]["NetbiosName"] == "TEST1-LONG-HOST"
- join_ou_actual.output[0]["PartOfDomain"] == True
- join_ou_actual.output[0]["WorkgroupName"] == None
- join_ou_ad_actual.output | length == 1
- - join_ou_ad_actual.output[0]["Name"] == "BAR"
+ - join_ou_ad_actual.output[0]["DNSHostName"] == "TEST1-long-HOSTNAME3"
+ - join_ou_ad_actual.output[0]["Name"] == "TEST1-LONG-HOST"
- join_ou_ad_actual.output[0]["Enabled"] == True
- - join_ou_ad_actual.output[0]["DistinguishedName"] == "CN=BAR," ~ custom_ou.output[0]
+ - join_ou_ad_actual.output[0]["DistinguishedName"] == "CN=TEST1-LONG-HOST," ~ custom_ou.output[0]
- name: change domain to workgroup with hostname change - check mode
membership:
workgroup_name: WORKGROUP
domain_admin_user: '{{ domain_user_upn }}'
domain_admin_password: '{{ domain_password }}'
- hostname: FOO
+ hostname: TEST1-long-HOSTNAME4
state: workgroup
register: to_workgroup_hostname_check
check_mode: true
@@ -493,11 +673,13 @@
- to_workgroup_hostname_check is changed
- to_workgroup_hostname_check.reboot_required == True
- to_workgroup_hostname_check_actual.output[0]["DnsDomainName"] == domain_realm
- - to_workgroup_hostname_check_actual.output[0]["HostName"] == "BAR"
+ - to_workgroup_hostname_check_actual.output[0]["HostName"] == "TEST1-long-HOSTNAME3"
+ - to_workgroup_hostname_check_actual.output[0]["NetbiosName"] == "TEST1-LONG-HOST"
- to_workgroup_hostname_check_actual.output[0]["PartOfDomain"] == True
- to_workgroup_hostname_check_actual.output[0]["WorkgroupName"] == None
- to_workgroup_hostname_check_ad_actual.output | length == 1
- - to_workgroup_hostname_check_ad_actual.output[0]["Name"] == "BAR"
+ - to_workgroup_hostname_check_ad_actual.output[0]["DNSHostName"] == "TEST1-long-HOSTNAME3"
+ - to_workgroup_hostname_check_ad_actual.output[0]["Name"] == "TEST1-LONG-HOST"
- to_workgroup_hostname_check_ad_actual.output[0]["Enabled"] == True
- name: change domain to workgroup with hostname change
@@ -505,7 +687,7 @@
workgroup_name: WORKGROUP
domain_admin_user: '{{ domain_user_upn }}'
domain_admin_password: '{{ domain_password }}'
- hostname: FOO
+ hostname: TEST1-long-HOSTNAME4
state: workgroup
reboot: true
register: to_workgroup_hostname
@@ -527,13 +709,30 @@
- to_workgroup_hostname is changed
- to_workgroup_hostname.reboot_required == False
- to_workgroup_hostname_actual.output[0]["DnsDomainName"] == None
- - to_workgroup_hostname_actual.output[0]["HostName"] == "FOO"
+ - to_workgroup_hostname_actual.output[0]["HostName"] == "TEST1-long-HOSTNAME4"
+ - to_workgroup_hostname_actual.output[0]["NetbiosName"] == "TEST1-LONG-HOST"
- to_workgroup_hostname_actual.output[0]["PartOfDomain"] == False
- to_workgroup_hostname_actual.output[0]["WorkgroupName"] == "WORKGROUP"
- to_workgroup_hostname_ad_actual.output | length == 1
- - to_workgroup_hostname_ad_actual.output[0]["Name"] == "BAR"
+ - to_workgroup_hostname_ad_actual.output[0]["DNSHostName"] == "TEST1-long-HOSTNAME3"
+ - to_workgroup_hostname_ad_actual.output[0]["Name"] == "TEST1-LONG-HOST"
- to_workgroup_hostname_ad_actual.output[0]["Enabled"] == False
+- name: change domain to workgroup with hostname change - idempotent
+ membership:
+ workgroup_name: WORKGROUP
+ domain_admin_user: '{{ domain_user_upn }}'
+ domain_admin_password: '{{ domain_password }}'
+ hostname: TEST1-long-HOSTNAME4
+ state: workgroup
+ reboot: true
+ register: to_workgroup_hostname_again
+
+- name: assert change domain to workgroup with hostname change - idempotent
+ assert:
+ that:
+ - not to_workgroup_hostname_again is changed
+
- name: remove orphaned AD account for later tests
microsoft.ad.computer:
name: BAR
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/tests.yml
index 49d06aefb..b6061b7d5 100644
--- a/ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/tests.yml
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/ou/tasks/tests.yml
@@ -163,7 +163,8 @@
country: US
description: Custom description
display_name: OU display Name
- managed_by: Domain Users
+ managed_by:
+ name: Domain Users
postal_code: 10001
state_province: ''
street: Main
diff --git a/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml b/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml
index 98718da6f..10261afdf 100644
--- a/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml
+++ b/ansible_collections/microsoft/ad/tests/integration/targets/user/tasks/tests.yml
@@ -510,6 +510,7 @@
user:
name: MyUser
state: present
+ account_locked: False
city: Brisbane
company: Red Hat
country: au
@@ -563,6 +564,7 @@
user:
name: MyUser
state: present
+ account_locked: False
city: Brisbane
company: Red Hat
country: au
@@ -685,6 +687,7 @@
user:
name: MyUser
state: present
+ account_locked: False
city: Brisbane
company: Red Hat
country: au
@@ -1095,7 +1098,8 @@
- Invalid
register: fail_missing_group
failed_when:
- - '"Failed to locate group Invalid: Cannot find an object with identity" not in fail_missing_group.msg'
+ - >-
+ "Failed to find the AD object DNs for groups.add. Invalid identities: 'Invalid'" not in fail_missing_group.msg
- name: warn on group that is missing
user:
@@ -1104,7 +1108,7 @@
groups:
add:
- Invalid
- missing_behaviour: warn
+ lookup_failure_action: warn
register: warn_missing_group
- name: assert warn on group that is missing
@@ -1112,7 +1116,8 @@
that:
- not warn_missing_group is changed
- warn_missing_group.warnings | length == 1
- - '"Failed to locate group Invalid but continuing on" in warn_missing_group.warnings[0]'
+ - >-
+ "Failed to find the AD object DNs for groups.add. Ignoring invalid identities: 'Invalid'" in warn_missing_group.warnings[0]
- name: ignore on group that is missing
user:
@@ -1120,7 +1125,7 @@
path: '{{ setup_domain_info.output[0].defaultNamingContext }}'
groups:
add:
- - Invalid
+ - name: Invalid
missing_behaviour: ignore
register: ignore_missing_group
@@ -1136,7 +1141,7 @@
path: '{{ setup_domain_info.output[0].defaultNamingContext }}'
groups:
remove:
- - domain admins
+ - name: domain admins
- Enterprise Admins
register: groups_remove
diff --git a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.14.txt b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.14.txt
index e69de29bb..a3dca9603 100644
--- a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.14.txt
+++ b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.14.txt
@@ -0,0 +1 @@
+plugins/action/domain_child.py action-plugin-docs # ansible-test is ignoring sidecar docs \ No newline at end of file
diff --git a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.15.txt b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.15.txt
index e69de29bb..a3dca9603 100644
--- a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.15.txt
+++ b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.15.txt
@@ -0,0 +1 @@
+plugins/action/domain_child.py action-plugin-docs # ansible-test is ignoring sidecar docs \ No newline at end of file
diff --git a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.16.txt b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.16.txt
index e69de29bb..a3dca9603 100644
--- a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.16.txt
+++ b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.16.txt
@@ -0,0 +1 @@
+plugins/action/domain_child.py action-plugin-docs # ansible-test is ignoring sidecar docs \ No newline at end of file
diff --git a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.17.txt b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.17.txt
index e69de29bb..a3dca9603 100644
--- a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.17.txt
+++ b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.17.txt
@@ -0,0 +1 @@
+plugins/action/domain_child.py action-plugin-docs # ansible-test is ignoring sidecar docs \ No newline at end of file
diff --git a/ansible_collections/microsoft/ad/tests/sanity/ignore-2.18.txt b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.18.txt
new file mode 100644
index 000000000..a3dca9603
--- /dev/null
+++ b/ansible_collections/microsoft/ad/tests/sanity/ignore-2.18.txt
@@ -0,0 +1 @@
+plugins/action/domain_child.py action-plugin-docs # ansible-test is ignoring sidecar docs \ No newline at end of file
diff --git a/ansible_collections/microsoft/ad/tests/utils/shippable/lint.sh b/ansible_collections/microsoft/ad/tests/utils/shippable/lint.sh
index 12b5b4cd2..9d60edb2a 100755
--- a/ansible_collections/microsoft/ad/tests/utils/shippable/lint.sh
+++ b/ansible_collections/microsoft/ad/tests/utils/shippable/lint.sh
@@ -5,6 +5,6 @@ set -o pipefail -eux
# This is aligned with the galaxy-importer used by AH
# https://github.com/ansible/galaxy-importer/blob/d4b5e6d12088ba452f129f4824bd049be5543358/setup.cfg#L22C4-L22C33
python -m pip install \
- 'ansible-lint>=6.2.2,<=6.14.3'
+ 'ansible-lint>=6.2.2,<=6.22.1'
ansible-lint