Last Update: Sep 04, 2024 | Published: Apr 13, 2016
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
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
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}
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")}
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
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:
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 }
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.
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.
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.
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.