Managing hundreds or thousands of computer objects across different Organizational Units (OUs). In this post, I'll share two PowerShell scripts I developed to solve common AD management tasks: finding where computer objects are located and moving them to new OUs in bulk.
The Misson
I recently faced a scenario where I needed to:
- Identify the current OU location of multiple servers
- Move these servers to new OUs based on specific requirements
- Ensure the moves were completed successfully with proper validation
Manual processes through Active Directory Users and Computers would have been time-consuming and error-prone. PowerShell automation was the clear solution.
Script 1: Finding Computer Object OU Locations
The first script I created reads a list of server names from a text file and reports their current OU locations.
Script Features
- Reads server names from servers.txt
- Queries Active Directory for each computer object
- Extracts OU information from Distinguished Names
- Creates timestamped log files with results
- Handles errors gracefully (servers not found, permission issues)
Script : Get-ServerOUs.ps1
# Get-ServerOUs.ps1
# Script to retrieve OU locations for servers listed in servers.txt
# Import Active Directory module
try {
Import-Module ActiveDirectory -ErrorAction Stop
Write-Host "Active Directory module loaded successfully" -ForegroundColor Green
}
catch {
Write-Error "Failed to import Active Directory module. Please install RSAT tools."
exit 1
}
# Define file paths
$serverListFile = "servers.txt"
$logFile = "server_ou_results_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
# Check if servers.txt exists
if (-not (Test-Path $serverListFile)) {
Write-Error "File '$serverListFile' not found in current directory."
exit 1
}
# Initialize log file with header
$logHeader = @"
Server OU Location Report
Generated: $(Get-Date)
========================================
"@
$logHeader | Out-File -FilePath $logFile -Encoding UTF8
Write-Host "Starting server OU lookup..." -ForegroundColor Yellow
Write-Host "Results will be saved to: $logFile" -ForegroundColor Yellow
# Read server list
$servers = Get-Content $serverListFile | Where-Object { $_.Trim() -ne "" }
if ($servers.Count -eq 0) {
Write-Warning "No servers found in $serverListFile"
exit 0
}
Write-Host "Processing $($servers.Count) servers..." -ForegroundColor Cyan
# Process each server
$successCount = 0
$errorCount = 0
foreach ($server in $servers) {
$serverName = $server.Trim()
if ($serverName -eq "") { continue }
Write-Host "Processing: $serverName" -ForegroundColor White
try {
# Search for the computer object in AD
$adComputer = Get-ADComputer -Identity $serverName -Properties DistinguishedName
-ErrorAction Stop
# Extract OU from Distinguished Name
$dn = $adComputer.DistinguishedName
$ou = ($dn -split ',', 2)[1] # Get everything after the first comma
# Log the result
$result = "SUCCESS: $serverName | OU: $ou"
$result | Add-Content -Path $logFile
Write-Host " Found in: $ou" -ForegroundColor Green
$successCount++
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
$errorResult = "ERROR: $serverName | Status: Computer object not found in
Active Directory"
$errorResult | Add-Content -Path $logFile
Write-Host " Not found in AD" -ForegroundColor Red
$errorCount++
}
catch {
$errorResult = "ERROR: $serverName | Status: $($_.Exception.Message)"
$errorResult | Add-Content -Path $logFile
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
$errorCount++
}
}
# Summary
$summary = @"
========================================
SUMMARY
========================================
Total servers processed: $($servers.Count)
Successful lookups: $successCount
Errors encountered: $errorCount
Completion time: $(Get-Date)
"@
$summary | Add-Content -Path $logFile
Write-Host "`nScript completed!" -ForegroundColor Green
Write-Host "Successfully processed: $successCount servers" -ForegroundColor Green
Write-Host "Errors encountered: $errorCount servers" -ForegroundColor Yellow
Write-Host "Full results saved to: $logFile" -ForegroundColor Cyan
Usage
- Create
servers.txt
with one server name per line - Run:
.\Get-ServerOUs.ps1
- Check the generated timestamped log file
Script 2: Moving Computer Objects Between OUs
Building on the first script, I created a more advanced solution for moving computer objects to new OUs based on CSV input.
Script Features
- Reads from CSV with
Server
andDestinationOU
columns - Validates all moves before execution
- Interactive confirmation prompt
- Batch processing with replication wait
- Post-move verification
- Comprehensive error handling and logging
Script : Move-ServersToOU.ps1
# Move-ServersToOU.ps1
# Script to move servers to specified OUs based on CSV input
# Import Active Directory module
try {
Import-Module ActiveDirectory -ErrorAction Stop
Write-Host "Active Directory module loaded successfully" -ForegroundColor Green
}
catch {
Write-Error "Failed to import Active Directory module. Please install RSAT tools."
exit 1
}
# Define file paths
$csvFile = "servers.csv"
$logFile = "server_move_results_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
# Check if CSV file exists
if (-not (Test-Path $csvFile)) {
Write-Error "File '$csvFile' not found in current directory."
Write-Host "Expected CSV format:" -ForegroundColor Yellow
Write-Host "Server,DestinationOU" -ForegroundColor Yellow
Write-Host "SERVER01,`"OU=Production,DC=bear,DC=local`""
-ForegroundColor Yellow
exit 1
}
# Initialize log file with header
$logHeader = @"
Server Move Operation Report
Generated: $(Get-Date)
========================================
"@
$logHeader | Out-File -FilePath $logFile -Encoding UTF8
Write-Host "Starting server move validation..." -ForegroundColor Yellow
Write-Host "Results will be saved to: $logFile" -ForegroundColor Yellow
# Read and validate CSV
try {
$serverMoves = Import-Csv $csvFile
# Validate CSV headers
if (-not ($serverMoves | Get-Member -Name "Server" -MemberType NoteProperty) -or
-not ($serverMoves | Get-Member -Name "DestinationOU" -MemberType NoteProperty)) {
Write-Error "CSV must have 'Server' and 'DestinationOU' columns"
exit 1
}
}
catch {
Write-Error "Failed to read CSV file: $($_.Exception.Message)"
exit 1
}
if ($serverMoves.Count -eq 0) {
Write-Warning "No server move requests found in $csvFile"
exit 0
}
Write-Host "`nValidating $($serverMoves.Count) server move requests..."
-ForegroundColor Cyan
# Validation phase
$validMoves = @()
$validationErrors = @()
foreach ($moveRequest in $serverMoves) {
$serverName = if ($moveRequest.Server) { $moveRequest.Server.Trim() } else { "" }
$destinationOU = if ($moveRequest.DestinationOU) { $moveRequest.DestinationOU.Trim()
} else { "" }
if ([string]::IsNullOrEmpty($serverName) -or [string]::IsNullOrEmpty($destinationOU)) {
$validationErrors += "VALIDATION ERROR: Empty server name or destination OU in row
- Server: '$serverName' | DestinationOU: '$destinationOU'"
continue
}
Write-Host "Validating: $serverName" -ForegroundColor White
try {
# Check if computer exists in AD
$adComputer = Get-ADComputer -Identity $serverName -Properties DistinguishedName
-ErrorAction Stop
$currentOU = ($adComputer.DistinguishedName -split ',', 2)[1]
# Check if destination OU exists
$null = Get-ADOrganizationalUnit -Identity $destinationOU -ErrorAction Stop
# Check if computer is already in the destination OU
if ($currentOU -eq $destinationOU) {
Write-Host " Already in target OU - skipping" -ForegroundColor Yellow
$validationErrors += "SKIP: $serverName | Already in destination OU:
$destinationOU"
continue
}
# Add to valid moves
$validMoves += [PSCustomObject]@{
ServerName = $serverName
Computer = $adComputer
CurrentOU = $currentOU
DestinationOU = $destinationOU
}
Write-Host " Current OU: $currentOU" -ForegroundColor Gray
Write-Host " Target OU: $destinationOU" -ForegroundColor Green
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
if ($_.Exception.Message -like "*computer*") {
$validationErrors += "ERROR: $serverName | Computer object not found in
Active Directory"
Write-Host " Computer not found in AD" -ForegroundColor Red
} else {
$validationErrors += "ERROR: $serverName | Destination OU not found:
$destinationOU"
Write-Host " Destination OU not found" -ForegroundColor Red
}
}
catch {
$validationErrors += "ERROR: $serverName | Validation failed:
$($_.Exception.Message)"
Write-Host " Validation error: $($_.Exception.Message)" -ForegroundColor Red
}
}
# Log validation errors
if ($validationErrors.Count -gt 0) {
"VALIDATION ERRORS:" | Add-Content -Path $logFile
$validationErrors | Add-Content -Path $logFile
"" | Add-Content -Path $logFile
}
# Display summary and confirmation
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "MOVE OPERATION SUMMARY" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Valid moves to perform: $($validMoves.Count)" -ForegroundColor Green
Write-Host "Validation errors: $($validationErrors.Count)" -ForegroundColor Yellow
if ($validMoves.Count -eq 0) {
Write-Host "No valid moves to perform. Exiting." -ForegroundColor Yellow
exit 0
}
Write-Host "`nServers to be moved:" -ForegroundColor White
foreach ($move in $validMoves) {
Write-Host " $($move.ServerName)" -ForegroundColor White
Write-Host " FROM: $($move.CurrentOU)" -ForegroundColor Red
Write-Host " TO: $($move.DestinationOU)" -ForegroundColor Green
Write-Host ""
}
# Confirmation prompt
do {
$confirmation = Read-Host "Do you want to proceed with these moves? (yes/no)"
$confirmation = $confirmation.ToLower().Trim()
} while ($confirmation -notin @('yes', 'y', 'no', 'n'))
if ($confirmation -in @('no', 'n')) {
Write-Host "Operation cancelled by user." -ForegroundColor Yellow
"OPERATION CANCELLED BY USER at $(Get-Date)" | Add-Content -Path $logFile
exit 0
}
# Execute moves
Write-Host "`nExecuting server moves..." -ForegroundColor Yellow
"MOVE OPERATIONS:" | Add-Content -Path $logFile
$successCount = 0
$errorCount = 0
foreach ($move in $validMoves) {
Write-Host "Moving $($move.ServerName)..." -ForegroundColor White
try {
# Perform the move
Move-ADObject -Identity $move.Computer.DistinguishedName -TargetPath
$move.DestinationOU -ErrorAction Stop
Write-Host " Successfully moved!" -ForegroundColor Green
# Log success
$moveResult = "SUCCESS: $($move.ServerName) | FROM: $($move.CurrentOU) |
TO: $($move.DestinationOU) | Time: $(Get-Date)"
$moveResult | Add-Content -Path $logFile
$successCount++
}
catch {
Write-Host " Move failed: $($_.Exception.Message)" -ForegroundColor Red
# Log error
$errorResult = "ERROR: $($move.ServerName) | Move failed: $($_.Exception.Message) |
Time: $(Get-Date)"
$errorResult | Add-Content -Path $logFile
$errorCount++
}
}
# Wait for replication
if ($successCount -gt 0) {
Write-Host "`nWaiting for AD replication..." -ForegroundColor Yellow
Start-Sleep -Seconds 30
# Verify moves
Write-Host "Verifying moves..." -ForegroundColor Yellow
"VERIFICATION RESULTS:" | Add-Content -Path $logFile
Write-Host "`n========================================" -ForegroundColor Green
Write-Host "SERVERS SUCCESSFULLY MOVED:" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
foreach ($move in $validMoves) {
if ($successCount -eq 0) { break }
try {
$verifyComputer = Get-ADComputer -Identity $move.ServerName -Properties
DistinguishedName -ErrorAction Stop
$newOU = ($verifyComputer.DistinguishedName -split ',', 2)[1]
if ($newOU -eq $move.DestinationOU) {
Write-Host "$($move.ServerName):" -ForegroundColor White
Write-Host " FROM: $($move.CurrentOU)" -ForegroundColor Red
Write-Host " TO: $newOU" -ForegroundColor Green
Write-Host " Status: ✓ VERIFIED" -ForegroundColor Green
Write-Host ""
"MOVED: $($move.ServerName) | FROM: $($move.CurrentOU) | TO: $newOU |
STATUS: Verified" | Add-Content -Path $logFile
} else {
Write-Host "$($move.ServerName): Move verification failed"
-ForegroundColor Red
Write-Host " Expected: $($move.DestinationOU)" -ForegroundColor Yellow
Write-Host " Actual: $newOU" -ForegroundColor Red
Write-Host ""
"MOVE_ISSUE: $($move.ServerName) | FROM: $($move.CurrentOU) |
EXPECTED: $($move.DestinationOU) | ACTUAL: $newOU" | Add-Content
-Path $logFile
}
}
catch {
Write-Host "$($move.ServerName): Verification error - $($_.Exception.Message)"
-ForegroundColor Red
Write-Host ""
"VERIFY_ERROR: $($move.ServerName) | FROM: $($move.CurrentOU) |
TO: $($move.DestinationOU) | ERROR: $($_.Exception.Message)" |
Add-Content -Path $logFile
}
}
}
# Final summary
$finalSummary = @"
========================================
FINAL SUMMARY
========================================
Total move requests: $($serverMoves.Count)
Validation errors: $($validationErrors.Count)
Successful moves: $successCount
Failed moves: $errorCount
Completion time: $(Get-Date)
"@
$finalSummary | Add-Content -Path $logFile
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "OPERATION COMPLETED" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Successfully moved: $successCount servers" -ForegroundColor Green
Write-Host "Failed moves: $errorCount servers" -ForegroundColor $(if($errorCount -gt 0)
{'Red'}else{'Green'})
Write-Host "Full results saved to: $logFile" -ForegroundColor Cyan
Expected CSV Format
Server,DestinationOU
SERVER01,"OU=WebServers,OU=Production,DC=domain,DC=com"
SERVER02,"OU=DatabaseServers,OU=Production,DC=domain,DC=com"
Troubleshooting and Lessons Learnt
During development and testing, I ran into several challenges that required troubleshooting:
1. CSV Formatting Issues
Problem: The script was failing with "Empty server name or destination OU" errors.
Root Cause: The CSV file had formatting issues where server names and OU paths were split across multiple lines instead of being on the same row.
Solution: I added null-safe property handling:
$serverName = if ($moveRequest.Server) { $moveRequest.Server.Trim() } else { "" }
$destinationOU = if ($moveRequest.DestinationOU) { $moveRequest.DestinationOU.Trim() }
else { "" }
Key Learning: Always validate CSV structure and handle null values gracefully when working with imported data.
2. Spaces in OU Distinguished Names
Problem: OUs with spaces in their names (like "Secured Servers") were initially causing "OU not found" errors.
Root Cause: Initially, I thought the spaces needed escaping, but this wasn't the actual issue.
Solution: The problem was actually malformed CSV data, not the spaces themselves. PowerShell handles spaces in DN components correctly when the OU path is properly formatted.
Key Learning: Don't assume escaping is needed for spaces in AD DNs when using PowerShell cmdlets - they handle this automatically.
3. OU Path Validation
Problem: Script reported OUs as "not found" even when they existed.
Root Cause: The OU paths in the CSV didn't match the actual OU structure in Active Directory.
Solution: I added validation logic to verify both computer objects and destination OUs exist before attempting moves.
Key Learning: Always validate that destination paths exist before attempting bulk operations.
4. Batch Processing vs. Individual Waits
Problem: Initially, the script had delays between each individual move operation.
Root Cause: I mistakenly added sleep delays between each computer move, which was inefficient.
Solution: Removed individual delays and implemented a single replication wait after all moves completed:
# Execute all moves without delays
foreach ($move in $validMoves) {
Move-ADObject -Identity $move.Computer.DistinguishedName -TargetPath $move.DestinationOU
}
# Wait once for replication
Start-Sleep -Seconds 30
# Then verify all moves
Key Learning: Batch operations should process all items first, then wait once for replication, rather than waiting after each individual operation.
Conclusion
These scripts have significantly improved my efficiency when managing Active Directory computer objects. The first script provides quick OU location reports, while the second enables safe, bulk movements with proper validation and confirmation.
The key to success was implementing robust error handling, proper validation, and user-friendly confirmation processes. The troubleshooting process taught me valuable lessons about CSV data handling, AD OU path validation, and efficient batch processing techniques.