PowerShell Problem Solver: Finding Empty Organizational Units in Active Directory

powershell-hero-img
You might have encountered the problem of finding all of the empty and unused organizational units (OUs) in your Active Directory domain. Depending on the size and complexity of your domain, this could be a thankless job. But let’s see how PowerShell can help. If you missed them, you might take a few minutes to read the previous articles I wrote about managing OUs with PowerShell and the Active Directory module, as we will be using some of the same cmdlets and concepts.

I suppose we might have a differing opinion about what constitutes an empty OU. At the very least, I would define an empty OU as one that doesn’t contain any users, group, computers or other AD objects anywhere in the OU hierarchy. It’s possible that a top-level OU is empty except for a child OU, which contains several user accounts. I wouldn’t want to identify that top-level OU as empty, but I would want to catch another child OU that had nothing in it.
I’m going to start with an OU that I know is completely empty.

Get-ADOrganizationalUnit -filter "Name -like 'Empty*'"

Unfortunately, there’s no property that would help indicate if it contains any child objects. This means I’ll have to manually search the OU for objects. I can use Get-ADObject and specify the OU as the SearchBase.

$ou = Get-ADOrganizationalUnit -filter "Name -like 'Empty*'"
Get-ADObject -filter * -SearchBase $ou

Getting an OU (Image Credit: Jeff Hicks)
Getting an OU (Image Credit: Jeff Hicks)

I’m not sure that will completely help as the command gives me the OU itself. Let’s try on an OU that has a few objects.

$ou = Get-ADOrganizationalUnit -filter "Name -eq 'Test1'"
Get-ADObject -filter * -SearchBase $ou

Getting child objects (Image Credit: Jeff Hicks)
Getting child objects (Image Credit: Jeff Hicks)

This indicates there are indeed some child objects. All of this by the way, assumes that I’m using credentials that have access to see everything in Active Directory.
The Get-ADObject cmdlet has a SearchScope parameter, but none of the options will give me everything but the OU itself. I’ll have to filter.

Get-ADObject -filter * -SearchBase $ou | Where {$_.distinguishedname -ne $ou.distinguishedname}

Filtering out the OU itself
Filtering out the OU itself (Image Credit: Jeff Hicks)

That’s better. In fact, I can take it a step further and filter out the OUs.

Get-ADObject -filter * -SearchBase $ou | Where { ($_.distinguishedname -ne $ou.distinguishedname) -AND ($_.objectclass -ne "organizationalunit")}

Filtering out all OUs
Filtering out all OUs (Image Credit: Jeff Hicks)

This would tell me I shouldn’t delete Test1. It’s most likely that I don’t need the compound filter since filtering out by objectclass would also eliminate the OU itself, but I’ll leave it in as an example of how to create a complex Where filter. Now, let’s try my known empty OU.

$ou = Get-ADOrganizationalUnit -filter "Name -like 'Empty*'"
Get-ADObject -filter * -SearchBase $ou |
Where { ($_.distinguishedname -ne $ou.distinguishedname) -AND ($_.objectclass -ne "organizationalunit")} |
Measure-Object

Measuing and empty OU (Image Credit: Jeff HIcks)
Measuing and empty OU (Image Credit: Jeff HIcks)

I should be able to safely delete this.
But now that I have a set of commands I can use, let’s put them in a pipelined expression. This example will require PowerShell 4.0 or later.

Get-ADOrganizationalUnit -filter "Name -like 'test*'" -Properties Description -PipelineVariable pv |
Select DistinguishedName,Name,Description,
@{Name="Children"; Expression = {
Get-ADObject -filter * -SearchBase $pv.distinguishedname |
Where {$_.objectclass -ne "organizationalunit"} |
Measure-Object | Select -ExpandProperty Count }}


This looks complicated, but it really isn’t. In this example I’m looking for all OUs that start with ‘Test’. The results will be stored temporarily in a pipeline variable, pv. Next, I’ll select a few properties and define a new one called ‘Children’. The value will the count of the number of objects found in the OU by Get-ADObject. The result looks like this:

Measuring OUs
Measuring OUs (Image Credit: Jeff Hicks)

Any OU with value of 0 for Children should be a candidate for deletion.

Get-ADOrganizationalUnit -filter "Name -like 'test*'" -Properties Description -PipelineVariable pv |
Select DistinguishedName,Name,Description,
@{Name="Children"; Expression = {
Get-ADObject -filter * -SearchBase $pv.distinguishedname |
Where { $_.objectclass -ne "organizationalunit"} |
Measure-Object | Select -ExpandProperty Count }} | Where {$_.children -eq 0} |
foreach {
  Remove-ADOrganizationalUnit -Identity $_.distinguishedname -Recursive -WhatIf
}

WhatIf Removal of empty OUs
WhatIf Removal of empty OUs (Image Credit: Jeff Hicks)

Because I’m writing a custom object to the pipeline, I can’t send the output directly to Remove-ADOrganizationalUnit, which is why I’m using Foreach-Object. That looks good so I’ll re-run without –WhatIf.
If you read my other articles, it should come as no surprise that I run into errors.
Errors removing the OU
Errors removing the OU (Image Credit: Jeff Hicks)

I have to reset the accidental deletion protection first.

Get-ADOrganizationalUnit -filter "Name -like 'test*'" -Properties Description -PipelineVariable pv |
Select DistinguishedName,Name,Description,
@{Name="Children"; Expression = {
Get-ADObject -filter * -SearchBase $pv.distinguishedname |
Where { $_.objectclass -ne "organizationalunit"} |
Measure-Object | Select -ExpandProperty Count }} | Where {$_.children -eq 0} |
foreach {
Set-ADOrganizationalUnit -Identity $_.distinguishedname -ProtectedFromAccidentalDeletion $False -PassThru |
Remove-ADOrganizationalUnit -Recursive
}

Now, Set-ADOrganizationalUnit is writing the OU object to the pipeline, so I can send that directly to Remove-ADOrganizationalUnit. If I rerun the command to list the OUs, my 0 children entries are gone.

Verifying OUs
Verifying OUs (Image Credit: Jeff Hicks)

Finally, I can revise the command to search the entire domain.

Get-ADOrganizationalUnit -filter * -Properties Description -PipelineVariable pv |
Select DistinguishedName,Name,Description,
@{Name="Children"; Expression = {
Get-ADObject -filter * -SearchBase $pv.distinguishedname |
Where { $_.objectclass -ne "organizationalunit"} |
Measure-Object | Select -ExpandProperty Count }} | Where {$_.children -eq 0} |
foreach {
 Set-ADOrganizationalUnit -Identity $_.distinguishedname -ProtectedFromAccidentalDeletion $False -PassThru -whatif |
 Remove-ADOrganizationalUnit -Recursive -whatif
}

I’ve used WhatIf for the the last part because I don’t really want to delete them. But at least I can see which ones are empty.

Test removal of all empty OUs in the domain
Test removal of all empty OUs in the domain (Image Credit: Jeff HIcks)


You should be able to use my code examples as the basis for creating your own PowerShell tools. Be sure to test everything in a non-production environment. I hope you’ll let me know what you come up with.