Files
demo/Invoke-DnsMigration.ps1
2026-09-06 02:03:36 +01:00

648 lines
28 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Automates DNS server reconfiguration across a mixed Windows/Linux fleet.
.DESCRIPTION
Orchestrates DNS migration from legacy Domain Controller IPs to a new DC.
Supports three input modes (combinable):
-OUPath : Discover Windows servers from an Active Directory OU
-ServerFile : Read a CSV or TXT file containing Windows and/or Linux server hostnames
-Servers : Inline comma-separated or array of hostnames
For each server the script:
1. Resolves the hostname to an IP via DNS
2. Connects remotely (WinRM/PSSession/PsExec for Windows, SSH/plink for Linux)
3. Reads and records the current DNS configuration
4. Writes a rollback JSON file before making any changes
5. Applies the new DNS IP(s)
6. Validates DNS resolution using the new server
7. Writes a detailed CSV report
Use -DryRun to probe connectivity and show proposed changes without
modifying any server.
.PARAMETER OUPath
Distinguished name of the AD OU to query for Windows servers.
Example: "OU=Servers,OU=DC,DC=corp,DC=local"
Can be combined with -ServerFile to cover Windows + Linux in one run.
.PARAMETER ServerFile
Path to a CSV or TXT file containing Windows and/or Linux servers to process.
CSV format: Hostname, OS, Distro (OS = Windows or Linux; Distro is optional)
TXT format: one hostname per line (OS auto-detected at runtime via WinRM/SSH probe)
Lines starting with # are treated as comments.
Both OS types can coexist in the same file — the OS column controls routing.
.PARAMETER Servers
Inline list of hostnames. Accepts a comma-separated string or an array.
OS is auto-detected at runtime via WinRM probe (Windows) or SSH (Linux).
Example: -Servers "srv01,srv02,ubuntu-01"
.PARAMETER ConfigFile
Path to config.psd1. Defaults to .\config.psd1 in the script directory.
.PARAMETER OutputDir
Directory for CSV reports, rollback JSON, and log files.
Defaults to .\logs relative to the script directory.
.PARAMETER NewDNS
Override the NewDNS value from config.psd1 at runtime.
Example: -NewDNS "10.35.40.1"
.PARAMETER LinuxSSHPassword
Override the LinuxSSHPassword from config.psd1 at runtime.
Avoids storing the password in the config file.
.PARAMETER WindowsCredential
Optional PSCredential for Windows remote connections (WinRM / PSSession / PsExec).
If not provided, the script uses implicit authentication — the identity of the
user running this script (Kerberos / current logged-in domain account).
Use this when you need to connect as a different domain or local account.
Example: -WindowsCredential (Get-Credential)
.PARAMETER WindowsUsername
Optional Windows username as an alternative to -WindowsCredential.
Must be used together with -WindowsPassword.
Example: -WindowsUsername "CORP\migration-svc" -WindowsPassword "P@ssw0rd"
.PARAMETER DryRun
Read-only mode. Connects to each server, reads current DNS, shows a
diff table of what would change — makes zero modifications.
.PARAMETER Parallel
Process servers concurrently. Uses ForEach-Object -Parallel (PS7+) or
Start-Job (PS5). Controlled by ThrottleLimit in config.psd1.
.PARAMETER NoRecurse
When using -OUPath, search only the immediate OU (not sub-OUs).
.EXAMPLE
# Dry run — Windows servers from AD OU
.\Invoke-DnsMigration.ps1 -OUPath "OU=Servers,DC=corp,DC=local" -DryRun
.EXAMPLE
# Full migration — servers from CSV (Windows and Linux mixed)
.\Invoke-DnsMigration.ps1 -ServerFile ".\servers.csv"
.EXAMPLE
# Combined run — AD for Windows + CSV for mixed fleet
.\Invoke-DnsMigration.ps1 -OUPath "OU=Servers,DC=corp,DC=local" `
-ServerFile ".\servers.csv"
.EXAMPLE
# Inline list, dry run
.\Invoke-DnsMigration.ps1 -Servers "srv01,srv02,ubuntu-01" -DryRun
.EXAMPLE
# Explicit Windows credentials (different account)
.\Invoke-DnsMigration.ps1 -OUPath "OU=Servers,DC=corp,DC=local" `
-WindowsCredential (Get-Credential)
.EXAMPLE
# Explicit Windows credentials via username/password params
.\Invoke-DnsMigration.ps1 -OUPath "OU=Servers,DC=corp,DC=local" `
-WindowsUsername "CORP\migration-svc" `
-WindowsPassword "P@ssw0rd"
#>
[CmdletBinding(SupportsShouldProcess)]
param(
# ── Input sources ─────────────────────────────────────────────────────────
[string] $OUPath,
[string] $ServerFile,
[string[]] $Servers,
# ── Configuration ─────────────────────────────────────────────────────────
[string] $ConfigFile = "",
[string] $OutputDir = "",
[string] $NewDNS = "",
[string] $LinuxSSHPassword = "",
# ── Windows authentication (all optional — omit to use implicit Kerberos) ─
[System.Management.Automation.PSCredential]
[System.Management.Automation.Credential()]
$WindowsCredential = [System.Management.Automation.PSCredential]::Empty,
[string] $WindowsUsername = "",
[string] $WindowsPassword = "",
# ── Execution flags ───────────────────────────────────────────────────────
[switch] $DryRun,
[switch] $Parallel,
[switch] $NoRecurse
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# ── Resolve script root reliably in both PS5 and PS7 ─────────────────────────
$ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path $MyInvocation.MyCommand.Path }
# ── Load modules ─────────────────────────────────────────────────────────────
$modulePaths = @(
"modules\Get-ServersFromOU.ps1",
"modules\Set-WindowsDns.ps1",
"modules\Invoke-LinuxDns.ps1",
"modules\Write-MigrationReport.ps1"
)
foreach ($mp in $modulePaths) {
$fullPath = Join-Path $ScriptDir $mp
if (-not (Test-Path $fullPath)) {
throw "Required module not found: $fullPath"
}
. $fullPath
}
# ── Load configuration ────────────────────────────────────────────────────────
if (-not $ConfigFile) {
$ConfigFile = Join-Path $ScriptDir "config.psd1"
}
if (-not (Test-Path $ConfigFile)) {
throw "Configuration file not found: $ConfigFile`nExpected at: $ConfigFile"
}
$config = Import-PowerShellDataFile $ConfigFile
# Apply runtime overrides
if ($NewDNS) { $config.NewDNS = @($NewDNS) }
if ($LinuxSSHPassword) { $config.LinuxSSHPassword = $LinuxSSHPassword }
if ($OutputDir) { $config.OutputDir = $OutputDir }
# ── Resolve Windows credential ────────────────────────────────────────────────
# Priority: -WindowsCredential > -WindowsUsername/-WindowsPassword > config.psd1 > implicit
$winCredential = [System.Management.Automation.PSCredential]::Empty
if ($WindowsCredential -ne [System.Management.Automation.PSCredential]::Empty -and $null -ne $WindowsCredential) {
# Explicit PSCredential object passed directly
$winCredential = $WindowsCredential
Write-Verbose "Windows auth: using -WindowsCredential ($($winCredential.UserName))"
}
elseif ($WindowsUsername -and $WindowsPassword) {
# Username + password params
$securePass = ConvertTo-SecureString $WindowsPassword -AsPlainText -Force
$winCredential = New-Object System.Management.Automation.PSCredential($WindowsUsername, $securePass)
Write-Verbose "Windows auth: using -WindowsUsername/$WindowsPassword ($WindowsUsername)"
}
elseif ($config.WindowsUsername -and $config.WindowsPassword) {
# Credentials from config.psd1
$securePass = ConvertTo-SecureString $config.WindowsPassword -AsPlainText -Force
$winCredential = New-Object System.Management.Automation.PSCredential($config.WindowsUsername, $securePass)
Write-Verbose "Windows auth: using config.psd1 credentials ($($config.WindowsUsername))"
}
else {
Write-Verbose "Windows auth: implicit (current user / Kerberos)"
}
# Store resolved credential in config for passing to sub-functions
$config._WinCredential = $winCredential
# Resolve OutputDir to absolute path
if (-not [System.IO.Path]::IsPathRooted($config.OutputDir)) {
$config.OutputDir = Join-Path $ScriptDir $config.OutputDir
}
if (-not (Test-Path $config.OutputDir)) {
New-Item -ItemType Directory -Path $config.OutputDir -Force | Out-Null
}
# ── Validate inputs ───────────────────────────────────────────────────────────
if (-not $OUPath -and -not $ServerFile -and (-not $Servers -or $Servers.Count -eq 0)) {
throw "No input source specified. Use -OUPath, -ServerFile, or -Servers."
}
if (-not $config.NewDNS -or $config.NewDNS.Count -eq 0) {
throw "NewDNS is not configured. Set it in config.psd1 or pass -NewDNS."
}
# ── Banner ────────────────────────────────────────────────────────────────────
$runMode = if ($DryRun) { "DRY RUN — no changes will be made" } else { "LIVE RUN — DNS settings WILL be changed" }
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ DNS MIGRATION ORCHESTRATOR ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host " Mode : " -NoNewline
if ($DryRun) { Write-Host $runMode -ForegroundColor Yellow }
else { Write-Host $runMode -ForegroundColor Red }
Write-Host " New DNS : $($config.NewDNS -join ', ')"
Write-Host " Old DNS : $($config.OldDNS -join ', ')"
Write-Host " Domain : $($config.Domain)"
Write-Host " Win auth : " -NoNewline
if ($winCredential -ne [System.Management.Automation.PSCredential]::Empty) {
Write-Host "Explicit ($($winCredential.UserName))" -ForegroundColor Yellow
} else {
Write-Host "Implicit (current user / Kerberos)" -ForegroundColor Gray
}
Write-Host " Output dir : $($config.OutputDir)"
Write-Host " Started at : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
# ── Build server list ─────────────────────────────────────────────────────────
Write-Host "── Discovering servers ──────────────────────────────────────────" -ForegroundColor Cyan
$allServers = [System.Collections.Generic.List[object]]::new()
$sourceLists = [System.Collections.Generic.List[object[]]]::new()
# Source 1: Active Directory OU
if ($OUPath) {
Write-Host " [AD] Querying OU: $OUPath"
try {
$ouServers = Get-ServersFromOU -OUPath $OUPath -NoRecurse:$NoRecurse
if ($ouServers -and $ouServers.Count -gt 0) {
$sourceLists.Add($ouServers)
Write-Host " [AD] Found $($ouServers.Count) Windows server(s)" -ForegroundColor Green
}
else {
Write-Warning " [AD] No servers found in OU: $OUPath"
}
}
catch {
Write-Warning " [AD] Failed to query OU '$OUPath': $($_.Exception.Message)"
}
}
# Source 2: Server file (mixed Windows + Linux)
if ($ServerFile) {
Write-Host " [File] Parsing: $ServerFile"
try {
$fileServers = ConvertFrom-ServerListFile -Path $ServerFile
if ($fileServers -and $fileServers.Count -gt 0) {
$sourceLists.Add($fileServers)
Write-Host " [File] Found $($fileServers.Count) server(s)" -ForegroundColor Green
}
}
catch {
Write-Warning " [File] Failed to parse '$ServerFile': $($_.Exception.Message)"
}
}
# Source 3: Inline servers
if ($Servers -and $Servers.Count -gt 0) {
Write-Host " [Inline] Parsing inline server list..."
try {
$inlineServers = ConvertFrom-InlineServerList -Servers $Servers
if ($inlineServers -and $inlineServers.Count -gt 0) {
$sourceLists.Add($inlineServers)
Write-Host " [Inline] Found $($inlineServers.Count) server(s)" -ForegroundColor Green
}
}
catch {
Write-Warning " [Inline] Failed to parse server list: $($_.Exception.Message)"
}
}
# Merge and deduplicate all sources
if ($sourceLists.Count -gt 1) {
$mergedArr = $sourceLists.ToArray()
$allServerArr = Merge-ServerLists -Lists $mergedArr
}
elseif ($sourceLists.Count -eq 1) {
$allServerArr = $sourceLists[0]
}
else {
throw "No servers could be discovered from the provided input sources."
}
foreach ($s in $allServerArr) { $allServers.Add($s) }
if ($allServers.Count -eq 0) {
throw "No servers to process after discovery and deduplication."
}
Write-Host ""
Write-Host " Total unique servers to process: $($allServers.Count)" -ForegroundColor Cyan
# ── Auto-detect OS for servers with OS = "Unknown" ────────────────────────────
$unknownOS = @($allServers | Where-Object { $_.OS -eq "Unknown" -or $_.OS -eq "" })
if ($unknownOS.Count -gt 0) {
Write-Host ""
Write-Host "── Auto-detecting OS for $($unknownOS.Count) server(s) ──────────────────" -ForegroundColor Cyan
foreach ($srv in $unknownOS) {
$detectedOS = Detect-ServerOS -Hostname $srv.Hostname -Config $config
$srv.OS = $detectedOS
Write-Host " $($srv.Hostname)$detectedOS"
}
}
# ── Dry run preview table ─────────────────────────────────────────────────────
if ($DryRun) {
Write-Host ""
Write-Host "── DRY RUN PREVIEW ─────────────────────────────────────────────" -ForegroundColor Yellow
Write-Host " The following changes WOULD be made:" -ForegroundColor Yellow
Write-Host ""
$previewHeader = "{0,-35} {1,-10} {2,-12} {3,-28} {4,-15} {5}" `
-f "Hostname", "OS", "Connectivity", "Current DNS", "Proposed DNS", "WouldChange"
Write-Host $previewHeader -ForegroundColor White
Write-Host ("-" * 115)
}
# ── Timestamp for output files ────────────────────────────────────────────────
$runTimestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$csvOutputPath = Join-Path $config.OutputDir "dns-migration-report-$runTimestamp.csv"
# ── Process servers ───────────────────────────────────────────────────────────
Write-Host ""
if (-not $DryRun) {
Write-Host "── Processing servers ──────────────────────────────────────────" -ForegroundColor Cyan
}
$allResults = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
# Script block used for parallel execution — captures needed functions
$processServer = {
param($server, $config, $DryRun, $ScriptDir)
# Re-dot-source modules in parallel runspace (each runspace is isolated)
. (Join-Path $ScriptDir "modules\Set-WindowsDns.ps1")
. (Join-Path $ScriptDir "modules\Invoke-LinuxDns.ps1")
$hostname = $server.Hostname
try {
if ($server.OS -eq "Windows") {
$result = Set-WindowsDns -Hostname $hostname -NewDNS $config.NewDNS `
-Config $config -DryRun:$DryRun
}
elseif ($server.OS -eq "Linux") {
$result = Invoke-LinuxDns -Hostname $hostname -Config $config `
-DryRun:$DryRun -Distro $server.Distro
}
else {
$result = [PSCustomObject]@{
Hostname = $hostname
ResolvedIP = $server.ResolvedIP
OS = $server.OS
Distro = ""
OldDNS = ""
NewDNS = $config.NewDNS -join ";"
ConnectionMethod = "None"
ChangeStatus = "Skipped"
ValidationStatus = "Skipped"
ModifiedFiles = ""
ServiceRestarted = ""
ErrorMessage = "OS could not be determined (got: '$($server.OS)')"
DryRun = $DryRun
Timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss")
_RollbackData = $null
}
}
return $result
}
catch {
return [PSCustomObject]@{
Hostname = $hostname
ResolvedIP = $server.ResolvedIP
OS = $server.OS
Distro = ""
OldDNS = ""
NewDNS = $config.NewDNS -join ";"
ConnectionMethod = "Error"
ChangeStatus = "Failed"
ValidationStatus = "NotRun"
ModifiedFiles = ""
ServiceRestarted = ""
ErrorMessage = "Unhandled exception: $($_.Exception.Message)"
DryRun = $DryRun
Timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss")
_RollbackData = $null
}
}
}
if ($Parallel -and $allServers.Count -gt 1) {
$throttle = if ($config.ThrottleLimit) { [int]$config.ThrottleLimit } else { 10 }
# PS7+: ForEach-Object -Parallel
if ($PSVersionTable.PSVersion.Major -ge 7) {
Write-Host " Running parallel (PS7, throttle: $throttle)..." -ForegroundColor Gray
$serverArray = $allServers.ToArray()
$results = $serverArray | ForEach-Object -Parallel {
$server = $_
$config = $using:config
$DryRun = $using:DryRun
$ScriptDir = $using:ScriptDir
. (Join-Path $ScriptDir "modules\Set-WindowsDns.ps1")
. (Join-Path $ScriptDir "modules\Invoke-LinuxDns.ps1")
$hostname = $server.Hostname
try {
if ($server.OS -eq "Windows") {
Set-WindowsDns -Hostname $hostname -NewDNS $config.NewDNS `
-Config $config -Credential $config._WinCredential -DryRun:$DryRun
}
elseif ($server.OS -eq "Linux") {
Invoke-LinuxDns -Hostname $hostname -Config $config `
-DryRun:$DryRun -Distro $server.Distro
}
else {
[PSCustomObject]@{
Hostname="$hostname"; ResolvedIP=$server.ResolvedIP
OS=$server.OS; Distro=""; OldDNS=""; NewDNS=$config.NewDNS -join ";"
ConnectionMethod="None"; ChangeStatus="Skipped"
ValidationStatus="Skipped"; ModifiedFiles=""; ServiceRestarted=""
ErrorMessage="Unknown OS: $($server.OS)"; DryRun=$DryRun
Timestamp=(Get-Date -Format "yyyy-MM-ddTHH:mm:ss"); _RollbackData=$null
}
}
}
catch {
[PSCustomObject]@{
Hostname="$hostname"; ResolvedIP=$server.ResolvedIP
OS=$server.OS; Distro=""; OldDNS=""; NewDNS=$config.NewDNS -join ";"
ConnectionMethod="Error"; ChangeStatus="Failed"
ValidationStatus="NotRun"; ModifiedFiles=""; ServiceRestarted=""
ErrorMessage="Exception: $($_.Exception.Message)"; DryRun=$DryRun
Timestamp=(Get-Date -Format "yyyy-MM-ddTHH:mm:ss"); _RollbackData=$null
}
}
} -ThrottleLimit $throttle
foreach ($r in $results) { $allResults.Add($r) }
}
else {
# PS5: Start-Job fallback
Write-Host " Running parallel (PS5 jobs, throttle: $throttle)..." -ForegroundColor Gray
$jobs = [System.Collections.Generic.List[System.Management.Automation.Job]]::new()
$serverArr = $allServers.ToArray()
$idx = 0
while ($idx -lt $serverArr.Count) {
# Throttle: wait if max active jobs reached
while (@($jobs | Where-Object { $_.State -eq "Running" }).Count -ge $throttle) {
Start-Sleep -Milliseconds 500
}
$srv = $serverArr[$idx]
$job = Start-Job -ScriptBlock $processServer `
-ArgumentList $srv, $config, $DryRun.IsPresent, $ScriptDir
$jobs.Add($job)
$idx++
}
# Collect results
foreach ($job in $jobs) {
$result = $job | Wait-Job | Receive-Job
if ($result) { $allResults.Add($result) }
Remove-Job $job -Force
}
}
}
else {
# Sequential execution
$i = 0
foreach ($server in $allServers) {
$i++
$hostname = $server.Hostname
Write-Host " [$i/$($allServers.Count)] $hostname ($($server.OS)) ..." -NoNewline
$result = $null
try {
if ($server.OS -eq "Windows") {
$result = Set-WindowsDns -Hostname $hostname -NewDNS $config.NewDNS `
-Config $config -Credential $config._WinCredential -DryRun:$DryRun
}
elseif ($server.OS -eq "Linux") {
$result = Invoke-LinuxDns -Hostname $hostname -Config $config `
-DryRun:$DryRun -Distro $server.Distro
}
else {
$result = [PSCustomObject]@{
Hostname="$hostname"; ResolvedIP=$server.ResolvedIP
OS=$server.OS; Distro=""; OldDNS=""; NewDNS=($config.NewDNS -join ";")
ConnectionMethod="None"; ChangeStatus="Skipped"
ValidationStatus="Skipped"; ModifiedFiles=""; ServiceRestarted=""
ErrorMessage="Unknown OS: $($server.OS)"; DryRun=$DryRun.IsPresent
Timestamp=(Get-Date -Format "yyyy-MM-ddTHH:mm:ss"); _RollbackData=$null
}
}
}
catch {
$result = [PSCustomObject]@{
Hostname="$hostname"; ResolvedIP=$server.ResolvedIP
OS=$server.OS; Distro=""; OldDNS=""; NewDNS=($config.NewDNS -join ";")
ConnectionMethod="Error"; ChangeStatus="Failed"
ValidationStatus="NotRun"; ModifiedFiles=""; ServiceRestarted=""
ErrorMessage="Exception: $($_.Exception.Message)"; DryRun=$DryRun.IsPresent
Timestamp=(Get-Date -Format "yyyy-MM-ddTHH:mm:ss"); _RollbackData=$null
}
}
# Inline status indicator
$statusColor = switch ($result.ChangeStatus) {
"Success" { "Green" }
"DryRun" { "Cyan" }
"Failed" { "Red" }
"ConnectionFailed" { "Red" }
"Skipped" { "Yellow" }
default { "White" }
}
Write-Host " [$($result.ChangeStatus)]" -ForegroundColor $statusColor -NoNewline
Write-Host " (Validation: $($result.ValidationStatus))"
# Dry run table row
if ($DryRun) {
$wouldChange = if ($result.OldDNS -and $result.OldDNS -ne $result.NewDNS) { "Yes" } else { "No" }
$connStatus = if ($result.ChangeStatus -eq "DryRun") { "OK ($($result.ConnectionMethod))" }
elseif ($result.ChangeStatus -eq "Failed") { "FAILED" }
else { $result.ConnectionMethod }
$row = "{0,-35} {1,-10} {2,-12} {3,-28} {4,-15} {5}" -f `
$hostname.Substring(0, [Math]::Min($hostname.Length, 34)),
$result.OS,
$connStatus,
($result.OldDNS ?? "Unknown").Substring(0, [Math]::Min(($result.OldDNS ?? "Unknown").Length, 27)),
($result.NewDNS ?? "").Substring(0, [Math]::Min(($result.NewDNS ?? "").Length, 14)),
$wouldChange
Write-Host " $row"
}
$allResults.Add($result)
}
}
# ── Collect results ───────────────────────────────────────────────────────────
$resultsArray = @($allResults)
if ($resultsArray.Count -eq 0) {
Write-Warning "No results collected. Nothing to report."
exit 0
}
# ── Write rollback file (before/after data is in results) ─────────────────────
if (-not $DryRun) {
Write-Host ""
Write-Host "── Writing rollback file ────────────────────────────────────────" -ForegroundColor Cyan
$rollbackPath = Write-RollbackFile -Results $resultsArray -OutputDir $config.OutputDir
Write-Host " Rollback file: $rollbackPath" -ForegroundColor Green
}
# ── Write CSV report ──────────────────────────────────────────────────────────
Write-Host ""
Write-Host "── Writing migration report ─────────────────────────────────────" -ForegroundColor Cyan
Write-MigrationReport -Results $resultsArray -OutputPath $csvOutputPath
Write-Host ""
Write-Host " Run completed at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Gray
if (-not $DryRun) {
Write-Host " To rollback: .\Invoke-DnsRollback.ps1 -RollbackFile `"$rollbackPath`"" -ForegroundColor Yellow
}
Write-Host ""
# ── Helper: auto-detect OS via connection probe ───────────────────────────────
function Detect-ServerOS {
<#
.SYNOPSIS
Probes a server to determine if it is Windows or Linux.
.DESCRIPTION
Tries WinRM first (Windows indicator). Falls back to SSH (Linux indicator).
Used when OS column is missing or "Unknown" in the server list.
#>
param([string]$Hostname, [hashtable]$Config)
# Try WinRM — quick test-connection style probe
try {
$sessionOpt = New-PSSessionOption -OpenTimeout 5000 -OperationTimeout 5000
$invokeParams = @{
ComputerName = $Hostname
ScriptBlock = { $true }
SessionOption = $sessionOpt
ErrorAction = "Stop"
}
$cred = $Config._WinCredential
if ($cred -and $cred -ne [System.Management.Automation.PSCredential]::Empty) {
$invokeParams.Credential = $cred
}
$null = Invoke-Command @invokeParams
return "Windows"
}
catch { }
# Try SSH via plink — just attempt a connection with immediate exit
if (Test-Path $Config.PlinkPath) {
try {
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo.FileName = $Config.PlinkPath
$proc.StartInfo.Arguments = "-ssh -pw `"$($Config.LinuxSSHPassword)`" " +
"-o StrictHostKeyChecking=no -batch " +
"`"$($Config.LinuxSSHUser)@$Hostname`" `"echo LINUX_OK`""
$proc.StartInfo.UseShellExecute = $false
$proc.StartInfo.RedirectStandardOutput = $true
$proc.StartInfo.RedirectStandardError = $true
$proc.StartInfo.CreateNoWindow = $true
$null = $proc.Start()
$output = $proc.StandardOutput.ReadToEnd()
$proc.WaitForExit(($Config.SSHConnectTimeout + 5) * 1000) | Out-Null
if ($output -match "LINUX_OK") { return "Linux" }
}
catch { }
}
return "Unknown"
}