Copy-GPOLinks.ps1
Function Copy-LinkedGPOs { <# .SYNOPSIS Copies the GPLink attribute from a specified source OU to a target OU in the same domain. Optionally appends or prepends to existing GPO links. Requires $CopyMode and @ServerConnection from caller context. .PARAMETER SourceOU Active Directory Organizational Unit to copy the GPLink attribute from. .PARAMETER TargetOU Active Directory Organizational Unit to copy the GPLink attribute to. #> [ CmdletBinding( SupportsShouldProcess = $True ) ] Param ( [ Parameter( Position = 1, Mandatory = $True ) ] [ Microsoft.ActiveDirectory.Management.ADOrganizationalUnit ] $deSourceOU, [ Parameter( Position = 2, Mandatory = $True ) ] [ Microsoft.ActiveDirectory.Management.ADOrganizationalUnit ] $deTargetOU ) Write-Verbose ( 'Retrieving GPLink for "{0}"...' -f $deSourceOU.DistinguishedName ) # The GPLink attribute contains all linked GPOs from bottom (highest number in GPMC) to top (number 1 in GPMC) # First entry is last GPO in GPMC, last entry is GPO #1 in GPMC :-) $SourceLinks = ( Get-ADOrganizationalUnit -Identity $deSourceOU -Properties GPLink @ServerConnection ).GPLink If ( $SourceLinks ) { If ( $ResolveGPONames ) { ForEach ( $Link in $SourceLinks.Trim( ']' ).Split( ']' ) ) { $Link -match '.*{(.*)}.*' | Out-Null Write-Verbose ( 'Linked GPO {0}: "{1}"' -f $Matches[1], ( Get-GPO -Guid $Matches[1] ).DisplayName ) } } Else { Write-Verbose $SourceLinks } $TargetLinks = ( Get-ADOrganizationalUnit -Identity $deTargetOU -Properties GPLink @ServerConnection ).GPLink switch ( $CopyMode ) { 'Append' { $GPLinks = $SourceLinks + $TargetLinks } 'Prepend' { $GPLinks = $TargetLinks + $SourceLinks } default { $GPLinks = $SourceLinks } } # If GPLink is already present on the target, -Replace is required instead of -Add If( $TargetLinks ) { Write-Verbose ( 'Replacing GPLink at target "{0}"...' -f $deTargetOU ) Set-ADOrganizationalUnit -Identity $deTargetOU -Replace @{ GPLink=$GPLinks } @ServerConnection } Else { Write-Verbose ( 'Adding GPLink at target "{0}"...' -f $TargetOU ) Set-ADOrganizationalUnit -Identity $deTargetOU -Add @{ GPLink=$GPLinks } @ServerConnection } } Else { Write-Verbose ( 'Source {0} has no linked GPOs.' -f $deSourceOU ) If ( $CopyMode -EQ 'Replace' ) { # If the source OU has no GPOs linked and we are in replace mode, # the target OU GPLink needs to be cleared. Set-ADOrganizationalUnit -Identity $deTargetOU -Clear 'GPLink' @ServerConnection } } } Function Process-OU { <# .SYNOPSIS Processes a single OU target and source, and optionally calls itself recursively to process childs. Enumerates child OUs in the source OU and searches for a matching child (by name) in the target OU. Requires $CopyMode, $Recurse and @ServerConnection from caller context. .PARAMETER deSourceOU Active Directory Organizational Unit to copy GPO Links from and to start enumerating childs. .PARAMETER deTargetOU Active Directory Organizational Unit to copy GPO Links to and to search for matching childs. #> [ CmdletBinding( SupportsShouldProcess = $True ) ] Param ( [ Parameter( Position = 1, Mandatory = $True ) ] [ Microsoft.ActiveDirectory.Management.ADOrganizationalUnit ] $deSourceOU, [ Parameter( Position = 2, Mandatory = $True ) ] [ Microsoft.ActiveDirectory.Management.ADOrganizationalUnit ] $deTargetOU ) If ( $CopyMode -NE 'None' ) { Copy-LinkedGPOs -deSourceOU $deSourceOU -deTargetOU $deTargetOU } If ( $Recurse ) { Write-Verbose ( 'Enumerating source childs in "{0}"' -f $deSourceOU ) Foreach ( $deSourceChildOU in ( Get-ADOrganizationalUnit -SearchBase $deSourceOU -SearchScope OneLevel -Filter * @ServerConnection ) ) { Write-Verbose ( 'Processing source child "{0}"...' -f $deSourceChildOU.Name ) $deTargetChildOU = Get-ADOrganizationalUnit -SearchBase $deTargetOU -SearchScope OneLevel -Filter { Name -EQ $deSourceChildOU.Name } @ServerConnection If ( $deTargetChildOU ) { Write-Verbose ( 'Found matching target child "{0}"' -f $deTargetChildOU ) } Else { Write-Verbose ( 'No matching target child found for "{0}".' -f $deSourceChildOU.Name ) If ( $CreateMissingChilds ) { Write-Verbose ( 'Creating missing target child "OU={0},{1}"...' -f $deSourceChildOU.Name, $deTargetOU ) $deTargetChildOU = New-ADOrganizationalUnit -Name $deSourceChildOU.Name -Path $deTargetOU -ProtectedFromAccidentalDeletion $ProtectedFromAccidentalDeletion -PassThru -Verbose @ServerConnection } } If ( $deTargetChildOU ) { Process-OU -deSourceOU $deSourceChildOU -deTargetOU $deTargetChildOU } } Write-Verbose ( 'Finished processing "{0}".' -f $deSourceOU ) } } Function Copy-GPOLinks { <# .SYNOPSIS Copies the GPLink attribute from a specified source OU to a target OU in the same domain. Optionally appends or prepends to existing GPO links and recurses through child OUs. .DESCRIPTION When staging environments in a single AD, it is common to create a new identical OU structure for testing purposes. This structure should match the original one, including all child OUs and their linked GPOs. Copy-GPOLinks copies all linked GPOs from a source OU to a target OU in a given domain. Optionally, it recurses through child OUs and copies their GPOs, too. It also can create missing target child OUs automatically. Note: By default, Copy-GPOLinks will not produce any screen output. If you want on-screen information, run it -verbose. If you need logging, intercept output streams or pipe to a file. .PARAMETER SourceOU Distinguished name of the OU to copy the GPLink attribute from. .PARAMETER TargetOU Distinguished name of the OU to copy the GPLink attribute to. Regardless of the CreateMissingChild switch, this OU must already exist or the cmdlet will fail. .PARAMETER CopyMode The copy mode for GPLink: Replace (overwrite), append or prepend to existing, or None. If you append or prepend and you run the command multiple times, you will create multiple links of the same set of GPOs. Within GPMC this cannot be done (GPMC has builtin logic that prevents this), but technically it is possible and valid. The 'None' value is useful when combined with -CreateMissingChilds and -Whatif, see the samples section for more information. .PARAMETER Recurse Process child OUs, too. The target child OU names must match the names of the respective source child OUs. Child OUs that are found in source, but not in target, are ignored and will not raise an error. .PARAMETER CreateMissingChilds If the recurse switch is specified and for a given source child OU no matching target child OU is found, the target child OU is created automatically. ACLs are not copied. .PARAMETER ProtectedFromAccidentalDeletion If missing child OUs are created, this parameter specifies whether they should be protected from accidental deletion or not. The default value is $True. .PARAMETER ResolveGPONames By default Copy-GPOLinks operates on the GPLink attribute. This attribute only contains GPO GUIDs, so if you want to know what was copied, you'll need to look either in GPMC or resolve those GUIDs. If you specify this switch, the source GPO names will be resolved and listed in the verbose output. .PARAMETER TargetDomain The Domain where the operation takes place. Can be a different domain or forest. Defaults to the callers domain. .PARAMETER Credential If the target domain requires different credentials, a credential object can be passed in. .EXAMPLE Copy-GPOLinks -SourceOU 'OU=Corp,DC=Corp,DC=Contoso,DC=Com' -TargetOU 'OU=Corp-Test,DC=Corp,DC=Contoso,DC=Com' This command copies all linked GPOs from OU=Corp to OU=Corp-Test. .EXAMPLE $VerbosePreference = 'Continue' $SourceOU = 'OU=Corp,DC=Corp,DC=Contoso,DC=Com' $TargetOU = 'OU=Corp-Test,DC=Corp,DC=Contoso,DC=Com' Copy-GPOLinks -SourceOU $SourceOU -TargetOU $TargetOU -CopyMode Replace -Recurse -CreateMissingChilds -Whatif This command recursively travels down OU=Corp. It would copy all GPOs linked, and it would create all missing OUs. Due to -WhatIf, for missing OUs it will write out that it would create them. But it will not write out that it would copy their GPOs, because copying is a different step. Since the target OU was not really created, the copy function will exit silently. .EXAMPLE Copy-GPOLinks -SourceOU 'OU=Corp,DC=Corp,DC=Contoso,DC=Com' -TargetOU 'OU=Corp-Test,DC=Corp,DC=Contoso,DC=Com' -CreateMissingChilds -Recurse -CopyMode None This Command works almost the same as above. But due to not trying to copy the GPLink attribute, it will only create some missing OUs. This can then be used with the next example. .EXAMPLE Copy-GPOLinks -SourceOU 'OU=Corp,DC=Corp,DC=Contoso,DC=Com' -TargetOU 'OU=Corp-Test,DC=Corp,DC=Contoso,DC=Com' -Recurse -WhatIf This again works almost the same as example #2 above. But if you first ran example #3, now all OUs are present and -WhatIf will be able to fully show what GPLinks it would copy. #> [ CmdletBinding( SupportsShouldProcess = $True ) ] Param ( [ Parameter( Position = 1, Mandatory = $True ) ][ String ] $SourceOU, [ Parameter( Position = 2, Mandatory = $True ) ][ String ] $TargetOU, [ ValidateSet ( 'Replace','Append','Prepend','None' ) ][ String ] $CopyMode = 'Replace', [ Switch ] $Recurse, [ Switch ] $CreateMissingChilds, [ Bool ] $ProtectedFromAccidentalDeletion = $True, [ Switch ] $ResolveGPONames, [ String ] $TargetDomain = ( Get-ADDomain ).DNSRoot, [ PsCredential ] $Credential = $null ) # Splatting the AD cmdlets to make $Credential a truely optional parameter # Get-ADDomain requires -Identity whereas all other AD cmdlets require -Server $IdentityConnection = @{ Identity = $TargetDomain } $ServerConnection = @{} If ( $Credential ) { $IdentityConnection.Credential = $Credential $ServerConnection.Credential = $Credential } $ServerConnection.Server = $( Get-ADDomain @IdentityConnection ).PDCEmulator Try { $deSourceOU = Get-ADOrganizationalUnit -Identity $SourceOU @ServerConnection $deTargetOU = Get-ADOrganizationalUnit -Identity $TargetOU @ServerConnection } Catch [ Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException ] { Write-Warning ( 'A directory entry could not be found. Make sure that the following OU exists and you have access to it:' ) Write-Warning ( '{0}' -f $_.CategoryInfo.TargetName ) Return $_.Exception.HResult } Process-OU -deSourceOU $deSourceOU -deTargetOU $deTargetOU } |