Created
April 24, 2026 18:50
-
-
Save nctiggy/d05248be9f46878d347be39375eacea4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Automated RVTools export for VMware environment assessment. | |
| .DESCRIPTION | |
| Two-step workflow: | |
| 1. Run against each vCenter/ESXi host to collect exports (run as many times as needed) | |
| 2. Run with -Zip to package everything up for emailing | |
| Downloads and installs RVTools automatically if not already installed. | |
| .PARAMETER Server | |
| vCenter or ESXi hostname/IP to export from. Omit when using -Zip. | |
| .PARAMETER User | |
| vSphere username (e.g. administrator@vsphere.local). | |
| If omitted, uses Windows pass-through authentication. | |
| .PARAMETER Password | |
| vSphere password. If -User is specified without -Password, you will be prompted. | |
| .PARAMETER OutputDir | |
| Directory for exports. Defaults to Desktop\RVTools-Export. | |
| All runs accumulate here until you zip. | |
| .PARAMETER Format | |
| Export format: "xlsx" (default) or "csv". | |
| .PARAMETER Zip | |
| Package all collected exports into a zip file for emailing. No server connection needed. | |
| .EXAMPLE | |
| .\Export-RVTools.ps1 -Server vcenter01.company.com | |
| # Uses Windows pass-through auth (no prompt) and exports a single vCenter. | |
| .EXAMPLE | |
| .\Export-RVTools.ps1 -Server vcenter.company.com -User admin@vsphere.local | |
| # Prompts for password via secure input, then exports. | |
| .EXAMPLE | |
| .\Export-RVTools.ps1 -Server esxi-standalone.company.com -User root | |
| # Targets a standalone ESXi host. | |
| .EXAMPLE | |
| .\Export-RVTools.ps1 -Zip | |
| # After running one or more exports, bundle everything into a zip for email. | |
| .EXAMPLE | |
| .\Export-RVTools.ps1 -Zip -OutputDir C:\Exports | |
| # Zip exports from a custom directory. | |
| #> | |
| param( | |
| [Parameter(Mandatory=$false, HelpMessage="vCenter or ESXi hostname")] | |
| [string]$Server, | |
| [Parameter(Mandatory=$false)] | |
| [string]$User, | |
| [Parameter(Mandatory=$false)] | |
| [string]$Password, | |
| [Parameter(Mandatory=$false)] | |
| [string]$OutputDir = (Join-Path ([Environment]::GetFolderPath('Desktop')) "RVTools-Export"), | |
| [Parameter(Mandatory=$false)] | |
| [ValidateSet("xlsx","csv")] | |
| [string]$Format = "xlsx", | |
| [Parameter(Mandatory=$false)] | |
| [switch]$Zip | |
| ) | |
| $ErrorActionPreference = "Stop" | |
| # --- Configuration --- | |
| $RVToolsInstallerUrl = "https://downloads.dell.com/rvtools/rvtools4.7.1.msi" | |
| $RVToolsPaths = @( | |
| "${env:ProgramFiles(x86)}\Dell\RVTools\RVTools.exe", | |
| "$env:ProgramFiles\Dell\RVTools\RVTools.exe", | |
| "${env:ProgramFiles(x86)}\Robware\RVTools\RVTools.exe", | |
| "$env:ProgramFiles\Robware\RVTools\RVTools.exe" | |
| ) | |
| $TempInstaller = Join-Path $env:TEMP "RVTools-installer.msi" | |
| # --- Functions --- | |
| function Find-RVTools { | |
| foreach ($path in $RVToolsPaths) { | |
| if (Test-Path $path) { return $path } | |
| } | |
| $inPath = Get-Command RVTools.exe -ErrorAction SilentlyContinue | |
| if ($inPath) { return $inPath.Source } | |
| return $null | |
| } | |
| function Get-RVToolsEncryptedPassword { | |
| param( | |
| [Parameter(Mandatory=$true)][string]$PlainPassword | |
| ) | |
| # RVTools uses Windows DPAPI (ConvertFrom-SecureString) with a "_RVToolsV3PWD" prefix. | |
| # The ciphertext is bound to this Windows user + machine, which is fine because | |
| # RVTools.exe runs in the same session immediately after. | |
| # See: C:\Program Files (x86)\Dell\RVTools\RVToolsPasswordEncryption.ps1 | |
| $secure = ConvertTo-SecureString -String $PlainPassword -AsPlainText -Force | |
| $encrypted = $secure | ConvertFrom-SecureString | |
| return "_RVToolsV3PWD" + $encrypted | |
| } | |
| function Install-RVTools { | |
| Write-Host "`n[*] RVTools not found. Downloading installer..." -ForegroundColor Yellow | |
| # Check .NET Framework 4.6.2+ | |
| $ndpKey = "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" | |
| if (Test-Path $ndpKey) { | |
| $release = (Get-ItemProperty $ndpKey).Release | |
| if ($release -lt 394802) { | |
| Write-Host "[!] .NET Framework 4.6.2+ required. Current release: $release" -ForegroundColor Red | |
| Write-Host " Download from: https://dotnet.microsoft.com/download/dotnet-framework" -ForegroundColor Red | |
| exit 1 | |
| } | |
| } else { | |
| Write-Host "[!] .NET Framework 4.x not detected. RVTools requires .NET 4.6.2+" -ForegroundColor Red | |
| exit 1 | |
| } | |
| # Download | |
| try { | |
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | |
| $wc = New-Object System.Net.WebClient | |
| $wc.DownloadFile($RVToolsInstallerUrl, $TempInstaller) | |
| Write-Host "[+] Downloaded to $TempInstaller" -ForegroundColor Green | |
| } catch { | |
| Write-Host "[!] Download failed: $_" -ForegroundColor Red | |
| Write-Host " Download manually from https://www.robware.net/rvtools/" -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| # Install silently (MSI) | |
| Write-Host "[*] Installing RVTools (silent)..." -ForegroundColor Yellow | |
| $msiArgs = @("/i", "`"$TempInstaller`"", "/qn", "REBOOT=ReallySuppress", "/norestart") | |
| $proc = Start-Process -FilePath "msiexec.exe" -ArgumentList $msiArgs -Wait -PassThru | |
| if ($proc.ExitCode -ne 0 -and $proc.ExitCode -ne 3010) { | |
| Write-Host "[!] Install failed (exit code $($proc.ExitCode)). Try running installer manually." -ForegroundColor Red | |
| exit 1 | |
| } | |
| Remove-Item $TempInstaller -Force -ErrorAction SilentlyContinue | |
| $exe = Find-RVTools | |
| if (-not $exe) { | |
| Write-Host "[!] Install completed but RVTools.exe not found at expected paths." -ForegroundColor Red | |
| Write-Host " Check: $($RVToolsPaths -join ', ')" -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| Write-Host "[+] RVTools installed: $exe" -ForegroundColor Green | |
| return $exe | |
| } | |
| # --- Zip Mode --- | |
| if ($Zip) { | |
| Write-Host "============================================" -ForegroundColor Cyan | |
| Write-Host " RVTools Export - Package for Delivery" -ForegroundColor Cyan | |
| Write-Host " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor DarkGray | |
| Write-Host "============================================" -ForegroundColor Cyan | |
| if (-not (Test-Path $OutputDir)) { | |
| Write-Host "`n[!] No export directory found at: $OutputDir" -ForegroundColor Red | |
| Write-Host " Run exports first before zipping." -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| $exports = Get-ChildItem -Path $OutputDir -File | |
| if ($exports.Count -eq 0) { | |
| Write-Host "`n[!] No export files found in: $OutputDir" -ForegroundColor Red | |
| Write-Host " Run exports first before zipping." -ForegroundColor Yellow | |
| exit 1 | |
| } | |
| # Show what we're packaging | |
| Write-Host "`n[*] Files to package:" -ForegroundColor Yellow | |
| $totalSize = 0 | |
| foreach ($f in $exports) { | |
| $sizeKB = [math]::Round($f.Length / 1KB, 1) | |
| $totalSize += $f.Length | |
| Write-Host " $($f.Name) ($sizeKB KB)" -ForegroundColor DarkGray | |
| } | |
| # Create zip | |
| $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" | |
| $zipName = "RVTools-Export_${timestamp}.zip" | |
| $zipPath = Join-Path (Split-Path $OutputDir) $zipName | |
| Compress-Archive -Path "$OutputDir\*" -DestinationPath $zipPath -Force | |
| $zipSizeMB = [math]::Round((Get-Item $zipPath).Length / 1MB, 2) | |
| Write-Host "`n============================================" -ForegroundColor Green | |
| Write-Host " Package Ready" -ForegroundColor Green | |
| Write-Host "============================================" -ForegroundColor Green | |
| Write-Host " Files: $($exports.Count) exports packaged" -ForegroundColor White | |
| Write-Host " Zip: $zipPath" -ForegroundColor White | |
| Write-Host " Size: $zipSizeMB MB" -ForegroundColor White | |
| Write-Host "`n Please email the zip file to:" -ForegroundColor Yellow | |
| Write-Host " craig.smith@spectrocloud.com" -ForegroundColor Cyan | |
| Write-Host "" | |
| exit 0 | |
| } | |
| # --- Export Mode --- | |
| if (-not $Server) { | |
| $desktop = [Environment]::GetFolderPath('Desktop') | |
| Write-Host "Usage:" -ForegroundColor Yellow | |
| Write-Host " Export: .\Export-RVTools.ps1 -Server <vcenter-or-esxi>" -ForegroundColor White | |
| Write-Host " .\Export-RVTools.ps1 -Server <host> -User <user> # prompts for password" -ForegroundColor White | |
| Write-Host " Zip: .\Export-RVTools.ps1 -Zip" -ForegroundColor White | |
| Write-Host "" | |
| Write-Host "Workflow:" -ForegroundColor Yellow | |
| Write-Host " 1. Run export against each vCenter/ESXi host" -ForegroundColor DarkGray | |
| Write-Host " (Windows pass-through auth is used if you omit -User)" -ForegroundColor DarkGray | |
| Write-Host " 2. Run with -Zip to package everything for email" -ForegroundColor DarkGray | |
| Write-Host "" | |
| Write-Host "Output: $desktop\RVTools-Export\ (zip is written next to that folder)" -ForegroundColor DarkGray | |
| Write-Host "Note: RVTools is downloaded and installed automatically if not present." -ForegroundColor DarkGray | |
| Write-Host " Never pass -Password on the command line; let the script prompt instead." -ForegroundColor DarkGray | |
| Write-Host "" | |
| Get-Help $MyInvocation.MyCommand.Path -Examples | |
| exit 0 | |
| } | |
| Write-Host "============================================" -ForegroundColor Cyan | |
| Write-Host " RVTools Export - $Server" -ForegroundColor Cyan | |
| Write-Host " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor DarkGray | |
| Write-Host "============================================" -ForegroundColor Cyan | |
| # Find or install RVTools | |
| $rvtools = Find-RVTools | |
| if ($rvtools) { | |
| $version = (Get-Item $rvtools).VersionInfo.FileVersion | |
| Write-Host "[+] RVTools found: $rvtools (v$version)" -ForegroundColor Green | |
| } else { | |
| $rvtools = Install-RVTools | |
| } | |
| # Prompt for password if user specified without password | |
| if ($User -and -not $Password) { | |
| $secPwd = Read-Host -Prompt "Password for $User" -AsSecureString | |
| $Password = [Runtime.InteropServices.Marshal]::PtrToStringAuto( | |
| [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secPwd) | |
| ) | |
| } | |
| # Create output directory | |
| if (-not (Test-Path $OutputDir)) { | |
| New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null | |
| } | |
| # Test connectivity | |
| Write-Host "[*] Testing connectivity to $Server on port 443..." -ForegroundColor Yellow | |
| $test = Test-NetConnection -ComputerName $Server -Port 443 -WarningAction SilentlyContinue | |
| if (-not $test.TcpTestSucceeded) { | |
| Write-Host "[!] Cannot reach $Server on port 443. Check network/firewall." -ForegroundColor Red | |
| exit 1 | |
| } | |
| Write-Host "[+] $Server reachable" -ForegroundColor Green | |
| # Build export arguments | |
| $sanitized = $Server -replace "[:/\\]", "_" | |
| $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" | |
| $filename = "${sanitized}_${timestamp}" | |
| $exportCmd = if ($Format -eq "xlsx") { "ExportAll2xlsx" } else { "ExportAll2csv" } | |
| $rvArgs = @( | |
| "-s", $Server, | |
| "-c", $exportCmd, | |
| "-d", $OutputDir, | |
| "-f", "$filename.$Format" | |
| ) | |
| if ($User) { | |
| Write-Host "[*] Encrypting password for RVTools..." -ForegroundColor Yellow | |
| $encryptedPassword = Get-RVToolsEncryptedPassword -PlainPassword $Password | |
| $rvArgs += "-u", $User, "-p", $encryptedPassword | |
| } else { | |
| $rvArgs += "-passthroughAuth" | |
| } | |
| # Mask password in display | |
| $displayArgs = $rvArgs -join ' ' | |
| if ($Password) { $displayArgs = $displayArgs -replace [regex]::Escape($Password), '****' } | |
| if ($encryptedPassword) { $displayArgs = $displayArgs -replace [regex]::Escape($encryptedPassword), '****' } | |
| Write-Host "[*] Exporting from $Server..." -ForegroundColor Cyan | |
| Write-Host " RVTools.exe $displayArgs" -ForegroundColor DarkGray | |
| # Run export | |
| $proc = Start-Process -FilePath $rvtools -ArgumentList $rvArgs -Wait -PassThru -NoNewWindow | |
| if ($proc.ExitCode -ne 0) { | |
| Write-Host "[!] Export failed for $Server (exit code $($proc.ExitCode))" -ForegroundColor Red | |
| exit 1 | |
| } | |
| # Verify output | |
| $exported = Get-ChildItem -Path $OutputDir -Filter "${filename}*" | Sort-Object LastWriteTime -Descending | |
| if ($exported.Count -eq 0) { | |
| Write-Host "[!] No export files found for $Server" -ForegroundColor Red | |
| exit 1 | |
| } | |
| # Show results | |
| Write-Host "`n[+] Export complete for $Server" -ForegroundColor Green | |
| foreach ($f in $exported) { | |
| $sizeKB = [math]::Round($f.Length / 1KB, 1) | |
| Write-Host " $($f.Name) ($sizeKB KB)" -ForegroundColor DarkGray | |
| } | |
| # Show accumulated exports | |
| $allExports = Get-ChildItem -Path $OutputDir -File | |
| Write-Host "`n[*] Total exports in $OutputDir`: $($allExports.Count) file(s)" -ForegroundColor Yellow | |
| Write-Host " Run against more servers, or package with:" -ForegroundColor DarkGray | |
| Write-Host " .\Export-RVTools.ps1 -Zip" -ForegroundColor White | |
| Write-Host "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment