Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add domain_child module #118

Merged
merged 3 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 3 additions & 29 deletions plugins/action/domain.py
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):
...
8 changes: 8 additions & 0 deletions plugins/action/domain_child.py
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):
...
32 changes: 3 additions & 29 deletions plugins/action/domain_controller.py
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):
...
1 change: 1 addition & 0 deletions plugins/modules/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
bypass_host_loop:
support: none
seealso:
- module: microsoft.ad.domain_child
- module: microsoft.ad.domain_controller
- module: microsoft.ad.group
- module: microsoft.ad.membership
Expand Down
242 changes: 242 additions & 0 deletions plugins/modules/domain_child.ps1
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()
Loading