-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add domain_child module Add the new module microsoft.ad.domain_child which can be used to create a child or tree domain in an existing forest. * Update doc sanity issue * Ignore sidecar error for now
- Loading branch information
Showing
22 changed files
with
921 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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): | ||
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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): | ||
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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): | ||
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
Oops, something went wrong.