diff --git a/SSH Manager/ssh-manager.ps1 b/SSH Manager/ssh-manager.ps1 new file mode 100644 index 0000000..90eda98 --- /dev/null +++ b/SSH Manager/ssh-manager.ps1 @@ -0,0 +1,378 @@ +<# +SSH Manager + +Interactive SSH launcher for Windows PowerShell. + +This portfolio-safe version uses mock server names, documentation-friendly +domains, and example certificate paths. Replace the entries in $servers with +real hosts before using it in an internal environment. +#> + +param ( + [string]$SshUser = "", + [string]$IdentityFile = "", + [string]$CertificateFile = "" +) + +$ErrorActionPreference = "Stop" + +$defaultIdentityFile = Join-Path $PSScriptRoot "..\..\ssh\id_ed25519" +$defaultCertificatePath = Join-Path $PSScriptRoot "certificates" + +if ([string]::IsNullOrWhiteSpace($IdentityFile) -and (Test-Path -LiteralPath $defaultIdentityFile -PathType Leaf)) { + $IdentityFile = (Resolve-Path -LiteralPath $defaultIdentityFile).Path +} + +$servers = @( + @{ + Environment = "DEV" + Name = "Development - arlapi-dev-01.example.com" + Domain = "api-dev-01.example.com" + Host = "192.0.2.11" + Hostname = "arlapi-dev-01" + CertificatePath = $defaultCertificatePath + Port = 22 + }, + @{ + Environment = "DEV" + Name = "Development - arlworker-dev-01.example.com" + Domain = "worker-dev-01.example.com" + Host = "192.0.2.12" + Hostname = "arlworker-dev-01" + CertificatePath = $defaultCertificatePath + Port = 22 + }, + @{ + Environment = "QAS" + Name = "Quality - arlapi-qas-01.example.com" + Domain = "api-qas-01.example.com" + Host = "198.51.100.21" + Hostname = "arlapi-qas-01" + CertificatePath = $defaultCertificatePath + Port = 22 + }, + @{ + Environment = "QAS" + Name = "Quality - arldb-qas-01.example.com" + Domain = "db-qas-01.example.com" + Host = "198.51.100.22" + Hostname = "arldb-qas-01" + CertificatePath = $defaultCertificatePath + Port = 22 + }, + @{ + Environment = "QAS" + Name = "Quality - arlworker-qas-01.example.com" + Domain = "worker-qas-01.example.com" + Host = "198.51.100.23" + Hostname = "arlworker-qas-01" + CertificatePath = $defaultCertificatePath + Port = 22 + }, + @{ + Environment = "PROD" + Name = "Production - arlapi-prd-01.example.com" + Domain = "api-prd-01.example.com" + Host = "203.0.113.31" + Hostname = "arlapi-prd-01" + CertificatePath = $defaultCertificatePath + Port = 22 + }, + @{ + Environment = "PROD" + Name = "Production - arlapi-prd-02.example.com" + Domain = "api-prd-02.example.com" + Host = "203.0.113.32" + Hostname = "arlapi-prd-02" + CertificatePath = $defaultCertificatePath + Port = 22 + }, + @{ + Environment = "PROD" + Name = "Production - arldb-prd-01.example.com" + Domain = "db-prd-01.example.com" + Host = "203.0.113.33" + Hostname = "arldb-prd-01" + CertificatePath = $defaultCertificatePath + Port = 22 + }, + @{ + Environment = "PROD" + Name = "Production - arlmonitor-prd-01.example.com" + Domain = "monitor-prd-01.example.com" + Host = "203.0.113.34" + Hostname = "arlmonitor-prd-01" + CertificatePath = $defaultCertificatePath + Port = 22 + }, + @{ + Environment = "PROD" + Name = "Production - arlworker-prd-01.example.com" + Domain = "worker-prd-01.example.com" + Host = "203.0.113.35" + Hostname = "arlworker-prd-01" + CertificatePath = $defaultCertificatePath + Port = 22 + } +) + +$environments = @( + @{ + Label = "DEV" + Value = "DEV" + }, + @{ + Label = "QAS" + Value = "QAS" + }, + @{ + Label = "PRD" + Value = "PROD" + } +) + +function Show-EnvironmentMenu { + Clear-Host + Write-Host "Select environment:" -ForegroundColor Cyan + Write-Host "" + + for ($i = 0; $i -lt $environments.Count; $i++) { + Write-Host ("[{0:D2}] {1}" -f ($i + 1), $environments[$i].Label) + } + + Write-Host "" + Write-Host "[0] Exit" + Write-Host "" +} + +function Get-EnvironmentLabel { + param ( + [hashtable]$Server + ) + + if ($Server.Name -match "^(?.+?)\s+-\s+") { + return $Matches.EnvironmentLabel + } + + return $Server.Environment +} + +function Get-ShortServerName { + param ( + [hashtable]$Server + ) + + $shortName = $Server.Hostname + + if ([string]::IsNullOrWhiteSpace($shortName)) { + $shortName = $Server.Host + } + + $shortName = $shortName.ToLowerInvariant() + $shortName = $shortName -replace "^arl", "" + $shortName = $shortName -replace "\.(e?corp|lrd)\.cat\.com$", "" + $shortName = $shortName -replace "\.example\.com$", "" + + return $shortName +} + +function Show-ServerMenu { + param ( + [string]$Environment + ) + + Clear-Host + Write-Host ("Select server from {0}:" -f $Environment) -ForegroundColor Cyan + Write-Host "" + + $environmentServers = @( + $servers | + Where-Object { $_.Environment -eq $Environment } | + Sort-Object { Get-ShortServerName -Server $_ } + ) + + $shortNameWidth = 0 + foreach ($server in $environmentServers) { + $shortNameLength = (Get-ShortServerName -Server $server).Length + + if ($shortNameLength -gt $shortNameWidth) { + $shortNameWidth = $shortNameLength + } + } + + for ($i = 0; $i -lt $environmentServers.Count; $i++) { + $server = $environmentServers[$i] + $environmentLabel = Get-EnvironmentLabel -Server $server + $shortName = Get-ShortServerName -Server $server + + Write-Host ("[{0:D2}] {1,-11} {2,-$shortNameWidth} ({3} / {4}:{5})" -f ($i + 1), $environmentLabel, $shortName, $server.Domain, $server.Host, $server.Port) + } + + Write-Host "" + Write-Host "[0] Back" + Write-Host "" +} + +function Resolve-CertificateFile { + param ( + [string]$Path + ) + + if ([string]::IsNullOrWhiteSpace($Path)) { + return "" + } + + if (Test-Path -LiteralPath $Path -PathType Leaf) { + return $Path + } + + if (Test-Path -LiteralPath $Path -PathType Container) { + $certificateFile = Get-ChildItem -LiteralPath $Path -File | + Where-Object { $_.Name -match "(-cert\.pub|\.pem|\.crt|\.cer)$" } | + Sort-Object Name | + Select-Object -First 1 + + if ($null -ne $certificateFile) { + return $certificateFile.FullName + } + } + + return "" +} + +function Get-SshArguments { + param ( + [hashtable]$Server, + [string]$User + ) + + $sshArguments = @() + + if (-not [string]::IsNullOrWhiteSpace($IdentityFile)) { + if (-not (Test-Path -LiteralPath $IdentityFile)) { + throw "Identity file not found: $IdentityFile" + } + + Write-Host ("Using identity file: {0}" -f $IdentityFile) -ForegroundColor DarkGray + $sshArguments += @("-i", $IdentityFile) + } + + if (-not [string]::IsNullOrWhiteSpace($CertificateFile)) { + $resolvedCertificateFile = Resolve-CertificateFile -Path $CertificateFile + + if ([string]::IsNullOrWhiteSpace($resolvedCertificateFile)) { + throw "Certificate file not found: $CertificateFile" + } + + Write-Host ("Using certificate file: {0}" -f $resolvedCertificateFile) -ForegroundColor DarkGray + $sshArguments += @("-o", "CertificateFile=$resolvedCertificateFile") + } + + if ([string]::IsNullOrWhiteSpace($CertificateFile) -and $Server.ContainsKey("CertificatePath")) { + $serverCertificatePath = $Server.CertificatePath + $resolvedServerCertificateFile = Resolve-CertificateFile -Path $serverCertificatePath + + if (-not [string]::IsNullOrWhiteSpace($resolvedServerCertificateFile)) { + Write-Host ("Using certificate file: {0}" -f $resolvedServerCertificateFile) -ForegroundColor DarkGray + $sshArguments += @("-o", "CertificateFile=$resolvedServerCertificateFile") + } + elseif (-not [string]::IsNullOrWhiteSpace($serverCertificatePath)) { + Write-Host ("Certificate not found or not a file: {0}" -f $serverCertificatePath) -ForegroundColor Yellow + } + } + + if ([string]::IsNullOrWhiteSpace($User)) { + throw "SSH user not defined." + } + + $sshArguments += @("-p", $Server.Port, "$User@$($Server.Host)") + + return $sshArguments +} + +if (-not (Get-Command ssh.exe -ErrorAction SilentlyContinue)) { + Write-Host "ssh.exe was not found on Windows." -ForegroundColor Red + Write-Host "Install or enable the OpenSSH Client optional feature." -ForegroundColor Yellow + exit 1 +} + +while ($true) { + Show-EnvironmentMenu + $environmentChoice = Read-Host "Your choice" + + if ($environmentChoice -eq "0") { + Write-Host "Closing..." + exit 0 + } + + $parsedEnvironmentChoice = 0 + + if (-not [int]::TryParse($environmentChoice, [ref]$parsedEnvironmentChoice)) { + Write-Host "Invalid choice." -ForegroundColor Red + Start-Sleep -Seconds 2 + continue + } + + $environmentIndex = $parsedEnvironmentChoice - 1 + + if ($environmentIndex -lt 0 -or $environmentIndex -ge $environments.Count) { + Write-Host "Invalid choice." -ForegroundColor Red + Start-Sleep -Seconds 2 + continue + } + + $selectedEnvironment = $environments[$environmentIndex] + $selectedEnvironmentValue = $selectedEnvironment.Value + $environmentServers = @( + $servers | + Where-Object { $_.Environment -eq $selectedEnvironmentValue } | + Sort-Object { Get-ShortServerName -Server $_ } + ) + + while ($true) { + Show-ServerMenu -Environment $selectedEnvironmentValue + $serverChoice = Read-Host "Your choice" + + if ($serverChoice -eq "0") { + break + } + + $parsedServerChoice = 0 + + if (-not [int]::TryParse($serverChoice, [ref]$parsedServerChoice)) { + Write-Host "Invalid choice." -ForegroundColor Red + Start-Sleep -Seconds 2 + continue + } + + $serverIndex = $parsedServerChoice - 1 + + if ($serverIndex -lt 0 -or $serverIndex -ge $environmentServers.Count) { + Write-Host "Invalid choice." -ForegroundColor Red + Start-Sleep -Seconds 2 + continue + } + + $selectedServer = $environmentServers[$serverIndex] + $sshUserForConnection = $SshUser + + if ([string]::IsNullOrWhiteSpace($sshUserForConnection)) { + $sshUserForConnection = Read-Host "SSH user" + } + + if ([string]::IsNullOrWhiteSpace($sshUserForConnection)) { + Write-Host "SSH user is required." -ForegroundColor Red + Start-Sleep -Seconds 2 + continue + } + + $sshArguments = Get-SshArguments -Server $selectedServer -User $sshUserForConnection + + Write-Host "" + Write-Host ("Connecting to {0} as {1}..." -f $selectedServer.Name, $sshUserForConnection) -ForegroundColor Green + + & ssh.exe @sshArguments + exit $LASTEXITCODE + } +} +