Skip to content

Commit

Permalink
New Module: win_domain_ou take 2 (ansible-collections#278)
Browse files Browse the repository at this point in the history
Co-authored-by: Jordan Borean <jborean93@gmail.com>
  • Loading branch information
gamethis and jborean93 authored Sep 23, 2021
1 parent a49ed5d commit f3a2c5d
Show file tree
Hide file tree
Showing 8 changed files with 763 additions and 0 deletions.
236 changes: 236 additions & 0 deletions plugins/modules/win_domain_ou.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
#!powershell

# Copyright: (c) 2020 VMware, Inc. All Rights Reserved.
# SPDX-License-Identifier: GPL-3.0-only
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

#AnsibleRequires -CSharpUtil Ansible.Basic
#Requires -Module ActiveDirectory

$spec = @{
options = @{
state = @{ type = "str"; choices = @("absent", "present"); default = "present" }
name = @{ type = "str"; required = $true }
protected = @{ type = "bool"; default = $false }
path = @{ type = "str"; required = $false }
filter = @{type = "str"; default = '*' }
recursive = @{ type = "bool"; default = $false }
domain_username = @{ type = "str"; }
domain_password = @{ type = "str"; no_log = $true }
domain_server = @{ type = "str" }
properties = @{ type = "dict" }
}
required_together = @(
,@('domain_password', 'domain_username')
)
supports_check_mode = $true
}

$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)

$extra_args = @{}
$onboard_extra_args = @{}
if ($null -ne $module.Params.domain_username) {
$domain_password = ConvertTo-SecureString $module.Params.domain_password -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $module.Params.domain_username, $domain_password
$extra_args.Credential = $credential
$onboard_extra_args.Credential = $credential
}
if ($null -ne $module.Params.domain_server) {
$extra_args.Server = $module.Params.domain_server
$onboard_extra_args.Server = $module.Params.domain_server
}
if ($module.Params.properties.count -ne 0){
$Properties = New-Object Collections.Generic.List[string]
$module.Params.properties.Keys | Foreach-Object{
$Properties.Add($_)
}
$extra_args.Properties = $Properties
}else{
$extra_args.Properties = '*'
$Properties = '*'
}

$extra_args.Filter = $module.Params.filter
$check_mode = $module.CheckMode
$name = $module.Params.name
$protected = $module.Params.protected
$path = $module.Params.path
$state = $module.Params.state
$recursive = $module.Params.recursive

# setup Dynamic Params
$params = @{}
if ($module.Params.properties.count -ne 0){
$module.Params.properties.Keys | ForEach-Object{
$params.Add($_,$module.Params.properties.Item($_))
}
}

Function Compare-OuObject {
Param(
[PSObject]$Original,
[PSObject]$Updated,
$properties
)
if (($Null -eq $Original) -or ($Original -eq $false)) { return $false }
if ($properties -ne '*'){
$x = Compare-Object -ReferenceObject $Original -DifferenceObject $Updated -Property $properties
#$module.Result.original = $Original | ConvertTo-Json -Compress
#$module.Result.updated = $Updated | ConvertTo-Json -Compress
#$module.Result.compare_properties = $properties
#$module.FailJson("Testing: failed compare $($x.count) $($properties.GetType().Name)")

}else{
$x = Compare-Object -ReferenceObject $Original -DifferenceObject $Updated
}
return $x.Count -eq 0
}

Function Get-SimulatedOu {
Param($Object)
$params = @{
Name = $Object.name
DistinguishedName = "OU=$($Object.name),$($Object.path)"
ProtectedFromAccidentalDeletion = $Object.protected
}
if ($Object.properties) {
if ($Object.properties.description) { $params.Description = $Object.properties.description }
if ($Object.properties.city) { $params.City = $Object.properties.city }
if ($Object.properties.state) { $params.State = $Object.properties.state }
if ($Object.properties.street_address) { $params.StreetAddress = $Object.properties.street_address }
if ($Object.properties.postal_code) { $params.PostalCode = $Object.properties.postal_code }
if ($Object.properties.country) { $params.Country = $Object.properties.country }
if ($Object.properties.managed_by) { $params.ManagedBy = $Object.properties.managed_by }
}
# convert to psobject & return
[PSCustomObject]$params
}

Function Get-OuObject {
Param([PSObject]$Object)
$obj = $Object | Select-Object -Property * -ExcludeProperty nTSecurityDescriptor | ConvertTo-Json -Depth 1 | ConvertFrom-Json
return $obj
}

# attempt import of module
Try { Import-Module ActiveDirectory }
Catch { $module.FailJson("The ActiveDirectory module failed to load properly: $($_.Exception.Message)", $_) }

Try{
$all_ous = Get-ADOrganizationalUnit @extra_args
}Catch{$module.FailJson("Get-ADOrganizationalUnit failed: $($_.Exception.Message)", $_) }

# set path if not defined to base domain
if ($null -eq $path){
if ($($all_ous | Measure-Object | Select-Object -ExpandProperty Count) -eq 1){
$matched = $all_ous.DistinguishedName -match "DC=.+"
}elseif ($($all_ous | Measure-Object | Select-Object -ExpandProperty Count) -gt 1) {
$matched = $all_ous[0].DistinguishedName -match "DC=.+"
}else{
$module.FailJson("Path was null and unable to determine default domain $($_.Exception.Message)", $_)
}
if ($matched){
$path = $matches.Values[0]
}else{
$module.FailJson("Unable to find default domain $($_.Exception.Message)", $_)
}
}

$module.Result.path = $path

# determine if requested OU exist
Try {
$current_ou = $all_ous | Where-Object {
$_.DistinguishedName -eq "OU=$name,$path"}
$module.Diff.before = Get-OuObject -Object $current_ou
$module.Result.ou = $module.Diff.before
} Catch {
$module.Diff.before = ""
$current_ou = $false
}
if ($state -eq "present") {
# ou does not exist, create object
if(-not $current_ou) {
$params.Name = $name
$params.Path = $path
$module.Result.params = $params
Try {
New-ADOrganizationalUnit @params @onboard_extra_args -ProtectedFromAccidentalDeletion $protected -WhatIf:$check_mode
}Catch {
$module.FailJson("Failed to create organizational unit: $($_.Exception.Message)", $_)
}
$module.Result.Changed = $true
}

# ou exists, update object
if ($current_ou) {
Try {
Set-ADOrganizationalUnit -Identity "OU=$name,$path" @params @onboard_extra_args -WhatIf:$check_mode
}Catch {
$module.FailJson("Failed to update organizational unit: $($_.Exception.Message)", $_)
}
}
}

if ($state -eq "absent") {
# ou exists, delete object
if ($current_ou -and -not $check_mode) {
Try {
# override protected from accidental deletion
Set-ADOrganizationalUnit -Identity "OU=$name,$path" -ProtectedFromAccidentalDeletion $false @onboard_extra_args -Confirm:$False -WhatIf:$check_mode
}Catch{
$module.FailJson("Failed to remove ProtectedFromAccidentalDeletion Lock: $($_.Exception.Message)", $_)
}
try{
# check recursive deletion
if ($recursive) {
Remove-ADOrganizationalUnit -Identity "OU=$name,$path" -Confirm:$False -WhatIf:$check_mode -Recursive @onboard_extra_args
}else {
Remove-ADOrganizationalUnit -Identity "OU=$name,$path" -Confirm:$False -WhatIf:$check_mode @onboard_extra_args
}
$module.Diff.after = ""
} Catch {
$module.FailJson("Failed to remove OU: $($_.Exception.Message)", $_)
}
$module.Result.changed = $true
}
$module.ExitJson()
}

# determine if a change was made
if (-not $check_mode) {
try{
$module.Result.extra_args = $extra_args
$new_ou = Get-ADOrganizationalUnit @extra_args | Where-Object {
$_.DistinguishedName -eq "OU=$name,$path"
}
}catch{
$module.FailJson("Failed to Get-ADOrganizationalUnit: $($_.Exception.Message)", $_)
}

$module.Diff.after = Get-OuObject -Object $new_ou
$module.Result.ou = $module.Diff.after
# compare old/new objects
if(-not (Compare-OuObject -Original $module.Diff.before -Updated $module.Diff.after -properties $module.Result.extra_args.Properties)) {
$module.Result.changed = $true
}
}

# simulate changes
if ($check_mode -and $current_ou) {
$new_ou = @{}
$current_ou.PropertyNames | ForEach-Object {
if ($params[$_.Name]) { $new_ou[$_.Name] = $params[$_.Name] }
else { $new_ou[$_.Name] = $_.Value }
}
$module.Diff.after = Get-OuObject -Object $new_ou
$module.Result.ou = $module.Diff.after
}
# simulate new ou created
if ($check_mode -and -not $current_ou) {
$simulated_ou = Get-SimulatedOu -Object $params
$module.Diff.after = Get-OuObject -Object $simulated_ou
}

$module.ExitJson()
152 changes: 152 additions & 0 deletions plugins/modules/win_domain_ou.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2020 VMware, Inc. All Rights Reserved.
# SPDX-License-Identifier: GPL-3.0-only
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

DOCUMENTATION = r'''
---
module: win_domain_ou
short_description: Manage Active Directory Organizational Units
author: ['Joe Zollo (@joezollo)', 'Larry Lane (@gamethis)']
version_added: 1.7.0
requirements:
- This module requires Windows Server 2012 or Newer
description:
- Manage Active Directory Organizational Units
- Adds, Removes and Modifies Active Directory Organizational Units
- Task should be delegated to a Windows Active Directory Domain Controller
options:
name:
description:
- The name of the Organizational Unit
type: str
required: true
protected:
description:
- Indicates whether to prevent the object from being deleted. When this
l(protected=true), you cannot delete the corresponding object without
changing the value of the property.
type: bool
default: false
path:
description:
- Specifies the X.500 path of the OU or container where the new object is
created.
- defaults to adding ou at base of domain connected to.
type: str
required: false
state:
description:
- Specifies the desired state of the OU.
- When l(state=present) the module will attempt to create the specified
OU if it does not already exist.
- When l(state=absent), the module will remove the specified OU.
- When l(state=absent) and l(recursive=true), the module will remove all
the OU and all child OU's.
type: str
default: present
choices: [ present, absent ]
recursive:
description:
- Removes the OU and any child items it contains.
- You must specify this parameter to remove an OU that is not empty.
type: bool
default: false
domain_server:
description:
- Specifies 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 domain of the computer
running PowerShell.
type: str
domain_username:
description:
- The username to use when interacting with AD.
- If this is not set then the user Ansible used to log in with will be
used instead when using CredSSP or Kerberos with credential delegation.
type: str
domain_password:
type: str
description:
- The password for the domain you are accessing
filter:
type: str
description: filter for lookup of ou.
default: '*'
properties:
type: dict
description:
- Free form dict of properties for the organizational unit. Follows LDAP property names, like StreetAddress or PostalCode.
'''

EXAMPLES = r'''
- name: Ensure OU is present & protected
community.windows.win_domain_ou:
name: AnsibleFest
state: present
- name: Ensure OU is present & protected
community.windows.win_domain_ou:
name: EUC Users
path: "DC=euc,DC=vmware,DC=lan"
state: present
protected: true
delegate_to: win-ad1.euc.vmware.lab
- name: Ensure OU is absent
community.windows.win_domain_ou:
name: EUC Users
path: "DC=euc,DC=vmware,DC=lan"
state: absent
delegate_to: win-ad1.euc.vmware.lab
- name: Ensure OU is present with specific properties
community.windows.win_domain_ou:
name: WS1Users
path: "CN=EUC Users,DC=euc,DC=vmware,DC=lan"
protected: true
properties:
city: Sandy Springs
state: Georgia
StreetAddress: 1155 Perimeter Center West
country: US
description: EUC Business Unit
PostalCode: 30189
delegate_to: win-ad1.euc.vmware.lab
- name: Ensure OU updated with new properties
community.windows.win_domain_ou:
name: WS1Users
path: DC=euc,DC=vmware,DC=lan
protected: false
properties:
city: Atlanta
state: Georgia
managedBy: jzollo@vmware.com
delegate_to: win-ad1.euc.vmware.lab
'''

RETURN = r'''
ou:
description: New/Updated organizational unit parameters
returned: When l(state=present)
type: dict
sample:
name:
guid:
distinguished_name:
canonoical_name:
created:
modified:
protected:
properties:
displayName:
description:
city:
streetAddress:
postalCode:
country:
managedBY:
'''
2 changes: 2 additions & 0 deletions tests/integration/targets/win_domain_ou/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
shippable/windows/group2
skip/windows/2012
Loading

0 comments on commit f3a2c5d

Please sign in to comment.