diff --git a/lib/ansible/modules/windows/win_domain_membership.ps1 b/lib/ansible/modules/windows/win_domain_membership.ps1 new file mode 100644 index 0000000000..f6071f5b81 --- /dev/null +++ b/lib/ansible/modules/windows/win_domain_membership.ps1 @@ -0,0 +1,279 @@ +#!powershell + +# (c) 2017, Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +Set-StrictMode -Version 2 + +$ErrorActionPreference = "Stop" + +$log_path = $null + +Function Write-DebugLog { + Param( + [string]$msg + ) + + $DebugPreference = "Continue" + $date_str = Get-Date -Format u + $msg = "$date_str $msg" + + Write-Debug $msg + + if($log_path) { + Add-Content $log_path $msg + } +} + +Function Get-DomainMembershipMatch { + Param( + [string] $dns_domain_name + ) + + # FUTURE: add support for NetBIOS domain name? + + # this requires the DC to be accessible; "DC unavailable" is indistinguishable from "not joined to the domain"... + Try { + Write-DebugLog "calling GetComputerDomain()" + $current_dns_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name + + $domain_match = $current_dns_domain -eq $dns_domain_name + + Write-DebugLog ("current domain {0} matches {1}: {2}" -f $current_dns_domain, $dns_domain_name, $domain_match) + + return $domain_match + } + Catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] { + Write-DebugLog "not currently joined to a reachable domain" + return $false + } +} + +Function Create-Credential { + Param( + [string] $cred_user, + [string] $cred_pass + ) + + $cred = New-Object System.Management.Automation.PSCredential($cred_user, $($cred_pass | ConvertTo-SecureString -AsPlainText -Force)) + + return $cred +} + +Function Get-HostnameMatch { + Param( + [string] $hostname + ) + + # Add-Computer will validate the "shape" of the hostname- we just care if it matches... + + $hostname_match = $env:COMPUTERNAME -eq $hostname + Write-DebugLog ("current hostname {0} matches {1}: {2}" -f $env:COMPUTERNAME, $hostname, $hostname_match) + + return $hostname_match +} + +Function Is-DomainJoined { + return (Get-WmiObject Win32_ComputerSystem).PartOfDomain +} + +Function Join-Domain { + Param( + [string] $dns_domain_name, + [string] $new_hostname, + [string] $domain_admin_user, + [string] $domain_admin_password + ) + + Write-DebugLog ("Creating credential for user {0}" -f $domain_admin_user) + $domain_cred = Create-Credential $domain_admin_user $domain_admin_password + + $add_args = @{ + ComputerName="." + Credential=$domain_cred + DomainName=$dns_domain_name + Force=$null + } + + Write-DebugLog "adding hostname set arg to Add-Computer args" + If($new_hostname) { + $add_args["NewName"] = $new_hostname + } + + Write-DebugLog "calling Add-Computer" + $add_result = Add-Computer @add_args + + Write-DebugLog ("Add-Computer result was \n{0}" -f $add_result | Out-String) +} + +Function Get-Workgroup { + return (Get-WmiObject Win32_ComputerSystem).Workgroup +} + +Function Set-Workgroup { + Param( + [string] $workgroup_name + ) + + Write-DebugLog ("Calling JoinDomainOrWorkgroup with workgroup {0}" -f $workgroup_name) + + return (Get-WmiObject Win32_ComputerSystem).JoinDomainOrWorkgroup($workgroup_name) +} + +Function Join-Workgroup { + Param( + [string] $workgroup_name, + [string] $domain_admin_user, + [string] $domain_admin_password + ) + + If(Is-DomainJoined) { # if we're on a domain, unjoin it (which forces us to join a workgroup) + $domain_cred = Create-Credential $domain_admin_user $domain_admin_password + + # 2012+ call the Workgroup arg WorkgroupName, but seem to accept + $rc_result = Remove-Computer -Workgroup $workgroup_name -Credential $domain_cred -Force + } + + # we're already on a workgroup- change it. + Else { + $swg_result = Set-Workgroup $workgroup_name + } +} + + +$result = @{ + changed = $false + reboot_required = $false +} + +$params = Parse-Args -arguments $args -supports_check_mode $true + +$state = Get-AnsibleParam $params "state" -validateset @("domain","workgroup") -failifempty $result + +$dns_domain_name = Get-AnsibleParam $params "dns_domain_name" +$hostname = Get-AnsibleParam $params "hostname" +$workgroup_name = Get-AnsibleParam $params "workgroup_name" +$domain_admin_user = Get-AnsibleParam $params "domain_admin_user" -failifempty $result +$domain_admin_password = Get-AnsibleParam $params "domain_admin_password" -failifempty $result + +$log_path = Get-AnsibleParam $params "log_path" +$_ansible_check_mode = Get-AnsibleParam $params "_ansible_check_mode" -default $false + +If ($state -eq "domain") { + If(-not $dns_domain_name) { + Fail-Json @{} "dns_domain_name is required when state is 'domain'" + } +} +Else { # workgroup + If(-not $workgroup_name) { + Fail-Json @{} "workgroup_name is required when state is 'workgroup'" + } +} + + +$global:log_path = $log_path + +Try { + + $hostname_match = If($hostname) { Get-HostnameMatch $hostname } Else { $true } + + $result.changed = $result.changed -or (-not $hostname_match) + + Switch($state) { + domain { + $domain_match = Get-DomainMembershipMatch $dns_domain_name + + $result.changed = $result.changed -or (-not $domain_match) + + If($result.changed -and -not $_ansible_check_mode) { + If(-not $domain_match) { + If(Is-DomainJoined) { + Write-DebugLog "domain doesn't match, and we're already joined to another domain" + throw "switching domains is not implemented" + } + + $join_args = @{ + dns_domain_name = $dns_domain_name + domain_admin_user = $domain_admin_user + domain_admin_password = $domain_admin_password + } + + Write-DebugLog "not a domain member, joining..." + + If(-not $hostname_match) { + Write-DebugLog "adding hostname change to domain-join args" + $join_args.new_hostname = $hostname + } + + $join_result = Join-Domain @join_args + } + ElseIf(-not $hostname_match) { # domain matches but hostname doesn't, just do a rename + Write-DebugLog ("domain matches, setting hostname to {0}" -f $hostname) + + $rename_args = @{NewName=$hostname} + + If (Is-DomainJoined) { + $domain_cred = Create-Credential $domain_admin_user $domain_admin_password + $rename_args.DomainCredential = $domain_cred + } + + $rename_result = Rename-Computer @rename_args + } + + # all these changes require a reboot + $result.reboot_required = $true + } + Else { + Write-DebugLog "check mode, exiting early..." + } + + } + + workgroup { + $workgroup_match = $(Get-Workgroup) -eq $workgroup_name + + $result.changed = $result.changed -or (-not $workgroup_match) + + If(-not $_ansible_check_mode) { + If(-not $workgroup_match) { + Write-DebugLog ("setting workgroup to {0}" -f $workgroup_name) + $join_wg_result = Join-Workgroup -workgroup_name $workgroup_name -domain_admin_user $domain_admin_user -domain_admin_password $domain_admin_password + $result.reboot_required = $true + } + If(-not $hostname_match) { + Write-DebugLog ("setting hostname to {0}" -f $hostname) + $rename_result = Rename-Computer -NewName $hostname + $result.reboot_required = $true + } + } + } + default { throw "invalid state $state" } + } + + Exit-Json $result +} +Catch { + $excep = $_ + + Write-DebugLog "Exception: $($excep | out-string)" + + Throw +} + diff --git a/lib/ansible/modules/windows/win_domain_membership.py b/lib/ansible/modules/windows/win_domain_membership.py new file mode 100644 index 0000000000..535e698162 --- /dev/null +++ b/lib/ansible/modules/windows/win_domain_membership.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + +DOCUMENTATION=''' +module: win_domain_membership +short_description: Manage domain/workgroup membership for a Windows host +version_added: 2.3 +description: + - Manages domain membership or workgroup membership for a Windows host. Also supports hostname changes. This module may require + subsequent use of the M(win_reboot) action if changes are made. +options: + dns_domain_name: + description: + - when C(state) is C(domain), the DNS name of the domain to which the targeted Windows host should be joined + domain_admin_user: + description: + - username of a domain admin for the target domain (required to join or leave the domain) + required: true + domain_admin_password: + description: + - password for the specified C(domain_admin_user) + hostname: + description: + - the desired hostname for the Windows host + state: + description: + - whether the target host should be a member of a domain or workgroup + choices: + - domain + - workgroup + workgroup_name: + description: + - when C(state) is C(workgroup), the name of the workgroup that the Windows host should be in +author: + - Matt Davis (@nitzmahone) +''' + +RETURN=''' +reboot_required: + description: True if changes were made that require a reboot. + returned: always + type: boolean + sample: true +''' + +EXAMPLES=''' + +# host should be a member of domain ansible.vagrant; module will ensure the hostname is mydomainclient +# and will use the passed credentials to join domain if necessary. +# Ansible connection should use local credentials if possible. +# If a reboot is required, the second task will trigger one and wait until the host is available. +- hosts: winclient + gather_facts: no + tasks: + - win_domain_membership: + dns_domain_name: ansible.vagrant + hostname: mydomainclient + domain_admin_user: testguy@ansible.vagrant + domain_admin_password: password123! + state: domain + register: domain_state + + - win_reboot: + when: domain_state.reboot_required + + + +# Host should be in workgroup mywg- module will use the passed credentials to clean-unjoin domain if possible. +# Ansible connection should use local credentials if possible. +# The domain admin credentials can be sourced from a vault-encrypted variable +- hosts: winclient + gather_facts: no + tasks: + - win_domain_membership: + workgroup_name: mywg + domain_admin_user: '{{ win_domain_admin_user }}' + domain_admin_password: '{{ win_domain_admin_password }}' + state: workgroup +'''