Files
demo/modules/Get-ServersFromOU.ps1
2026-09-06 02:03:36 +01:00

321 lines
11 KiB
PowerShell

# ============================================================
# Get-ServersFromOU.ps1
# ============================================================
# Queries Active Directory for all enabled Windows Server
# computer objects under a specified OU.
# Resolves hostnames to IPs at runtime via DNS (does NOT rely
# on AD's IPv4Address attribute which is often stale).
# ============================================================
function Get-ServersFromOU {
<#
.SYNOPSIS
Returns all enabled Windows Server computer objects from an AD OU.
.DESCRIPTION
Uses Get-ADComputer to enumerate computers in the target OU.
Filters to Windows Server OS only (excludes workstations/other).
Resolves each hostname to an IP at runtime using DNS.
Servers that cannot be resolved are flagged as Unresolvable
but still returned so they appear in the CSV report.
.PARAMETER OUPath
Distinguished name of the OU to search.
Example: "OU=Servers,OU=Datacenter,DC=corp,DC=local"
.PARAMETER Recurse
If set, searches all sub-OUs under OUPath. Default: $true
.OUTPUTS
Array of PSCustomObjects:
@{ Hostname; ResolvedIP; OS; OperatingSystem; Reachable }
.EXAMPLE
Get-ServersFromOU -OUPath "OU=Servers,DC=corp,DC=local"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$OUPath,
[switch]$NoRecurse
)
# Verify RSAT / ActiveDirectory module is available
if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
throw "ActiveDirectory PowerShell module not found. Install RSAT: " +
"Install-WindowsFeature RSAT-AD-PowerShell (Server) or " +
"Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 (Workstation)"
}
Import-Module ActiveDirectory -ErrorAction Stop
Write-Host " Querying AD OU: $OUPath" -ForegroundColor Cyan
$searchScope = if ($NoRecurse) { "OneLevel" } else { "Subtree" }
try {
$computers = Get-ADComputer -Filter { Enabled -eq $true } `
-SearchBase $OUPath `
-SearchScope $searchScope `
-Properties Name, OperatingSystem, OperatingSystemVersion, DNSHostName `
-ErrorAction Stop
}
catch {
throw "Failed to query AD OU '$OUPath': $($_.Exception.Message)"
}
if ($null -eq $computers -or @($computers).Count -eq 0) {
Write-Warning "No enabled computer objects found in OU: $OUPath"
return @()
}
# Filter to Windows Server OS only
$serverComputers = $computers | Where-Object {
$_.OperatingSystem -like "*Windows Server*"
}
$nonServerCount = @($computers).Count - @($serverComputers).Count
if ($nonServerCount -gt 0) {
Write-Verbose "Excluded $nonServerCount non-server OS objects from results"
}
Write-Host " Found $(@($serverComputers).Count) Windows Server object(s) in OU" -ForegroundColor Cyan
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($computer in $serverComputers) {
# Prefer DNSHostName from AD, fall back to Name
$hostname = if ($computer.DNSHostName) { $computer.DNSHostName } else { $computer.Name }
# Resolve IP at runtime — more reliable than AD's IPv4Address attribute
$resolvedIP = Resolve-ADHostnameToIP -Hostname $hostname
$reachable = $resolvedIP -ne "Unresolvable"
if (-not $reachable) {
Write-Warning " [$hostname] Cannot resolve hostname — will be flagged in report"
}
else {
Write-Verbose " [$hostname] Resolved to $resolvedIP"
}
$results.Add([PSCustomObject]@{
Hostname = $hostname
ResolvedIP = $resolvedIP
OS = "Windows"
OperatingSystem = $computer.OperatingSystem
OSVersion = $computer.OperatingSystemVersion
Reachable = $reachable
Source = "AD"
})
}
Write-Host " Resolved: $(@($results | Where-Object Reachable).Count) | Unresolvable: $(@($results | Where-Object { -not $_.Reachable }).Count)" -ForegroundColor Cyan
return $results.ToArray()
}
function Resolve-ADHostnameToIP {
<#
.SYNOPSIS
Resolves a hostname to its first IPv4 address via DNS.
Returns "Unresolvable" if DNS lookup fails.
#>
param([string]$Hostname)
# Already an IP — return as-is
if ($Hostname -match '^\d{1,3}(\.\d{1,3}){3}$') { return $Hostname }
try {
$addresses = [System.Net.Dns]::GetHostAddresses($Hostname) |
Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork }
if ($addresses) {
return ($addresses | Select-Object -First 1).IPAddressToString
}
return "Unresolvable"
}
catch {
return "Unresolvable"
}
}
function ConvertFrom-ServerListFile {
<#
.SYNOPSIS
Parses a CSV or TXT server list file into a normalized server object array.
.DESCRIPTION
Supports two formats:
CSV: Columns Hostname, OS, Distro (lines starting with # are comments)
TXT: One hostname per line (lines starting with # are comments)
OS and Distro will be auto-detected at runtime.
.PARAMETER Path
Path to the CSV or TXT file.
.OUTPUTS
Array of PSCustomObjects:
@{ Hostname; ResolvedIP; OS; Distro; OperatingSystem; Reachable; Source }
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Path
)
if (-not (Test-Path $Path)) {
throw "Server list file not found: $Path"
}
$extension = [System.IO.Path]::GetExtension($Path).ToLower()
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
if ($extension -eq ".csv") {
# Read CSV, skip comment lines
$rawLines = Get-Content $Path | Where-Object { $_ -notmatch '^\s*#' -and $_.Trim() -ne "" }
# Re-parse as CSV from filtered lines
$csvData = $rawLines | ConvertFrom-Csv
foreach ($row in $csvData) {
$hostname = $row.Hostname.Trim()
if ([string]::IsNullOrWhiteSpace($hostname)) { continue }
$os = if ($row.PSObject.Properties.Name -contains "OS") { $row.OS.Trim() } else { "Unknown" }
$distro = if ($row.PSObject.Properties.Name -contains "Distro") { $row.Distro.Trim() } else { "" }
$resolvedIP = Resolve-ADHostnameToIP -Hostname $hostname
$reachable = $resolvedIP -ne "Unresolvable"
if (-not $reachable) {
Write-Warning " [$hostname] Cannot resolve hostname"
}
$results.Add([PSCustomObject]@{
Hostname = $hostname
ResolvedIP = $resolvedIP
OS = $os
Distro = $distro
OperatingSystem = ""
OSVersion = ""
Reachable = $reachable
Source = "File:$Path"
})
}
}
else {
# TXT format — one hostname per line
$lines = Get-Content $Path | Where-Object { $_ -notmatch '^\s*#' -and $_.Trim() -ne "" }
foreach ($line in $lines) {
$hostname = $line.Trim()
if ([string]::IsNullOrWhiteSpace($hostname)) { continue }
$resolvedIP = Resolve-ADHostnameToIP -Hostname $hostname
$reachable = $resolvedIP -ne "Unresolvable"
if (-not $reachable) {
Write-Warning " [$hostname] Cannot resolve hostname"
}
$results.Add([PSCustomObject]@{
Hostname = $hostname
ResolvedIP = $resolvedIP
OS = "Unknown" # Will be auto-detected at connection time
Distro = ""
OperatingSystem = ""
OSVersion = ""
Reachable = $reachable
Source = "File:$Path"
})
}
}
Write-Host " Parsed $(@($results).Count) server(s) from file: $Path" -ForegroundColor Cyan
return $results.ToArray()
}
function ConvertFrom-InlineServerList {
<#
.SYNOPSIS
Converts an inline array of hostnames into a normalized server object array.
.DESCRIPTION
Accepts a comma-separated string or a string array.
OS and Distro will be auto-detected at connection time.
.PARAMETER Servers
Array of hostnames or a single comma-separated string.
.OUTPUTS
Array of PSCustomObjects with same schema as ConvertFrom-ServerListFile.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string[]]$Servers
)
# Handle comma-separated single string input
if ($Servers.Count -eq 1 -and $Servers[0] -match ",") {
$Servers = $Servers[0] -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
}
$results = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($hostname in $Servers) {
$hostname = $hostname.Trim()
if ([string]::IsNullOrWhiteSpace($hostname)) { continue }
$resolvedIP = Resolve-ADHostnameToIP -Hostname $hostname
$reachable = $resolvedIP -ne "Unresolvable"
if (-not $reachable) {
Write-Warning " [$hostname] Cannot resolve hostname"
}
$results.Add([PSCustomObject]@{
Hostname = $hostname
ResolvedIP = $resolvedIP
OS = "Unknown" # Auto-detected at connection time
Distro = ""
OperatingSystem = ""
OSVersion = ""
Reachable = $reachable
Source = "Inline"
})
}
Write-Host " Parsed $(@($results).Count) server(s) from inline list" -ForegroundColor Cyan
return $results.ToArray()
}
function Merge-ServerLists {
<#
.SYNOPSIS
Merges multiple server lists, deduplicating by Hostname (case-insensitive).
.DESCRIPTION
When -OUPath and -ServerFile are both provided, this function unions
the two lists. The first occurrence of a hostname wins.
.PARAMETER Lists
One or more arrays of server objects to merge.
.OUTPUTS
Deduplicated array of server objects.
#>
param(
[Parameter(Mandatory)]
[object[][]]$Lists
)
$seen = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
$merged = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($list in $Lists) {
foreach ($server in $list) {
if ($seen.Add($server.Hostname)) {
$merged.Add($server)
}
else {
Write-Verbose "Duplicate hostname '$($server.Hostname)' from $($server.Source) — skipped"
}
}
}
return $merged.ToArray()
}