Welcome to my As Built Report section. Keeping documentation of Omnissa, vSphere and other IT environments up to date, can be a challenge. Changes happen fast and often. Sometimes, if we need to rebuild, getting hold of information about how something was built to begin with, is in best cases difficult. Therefore, having an As Built Report at hand, can be crucial.
I’m not going to go into the specifics about As Built here, as Chris Hildebrand has done that here: As Built Report’s for the Horizon Suite. But, as I often work in air-gapped environments and the fact that doing this offline is a little extra work, I will show below how I do this — now fully automated with PowerShell scripts.
The main use-case for me is Horizon, App Volumes, UAG and vSphere. The modules are available at GitHub here: AsBuiltReport
Overview
The setup has been fully automated into three PowerShell scripts:
- AsBuilt-Offline.ps1 — Downloads and installs all required PowerShell modules and Python (four modes: Download, Install, DownloadPython, InstallPython)
- AsBuilt-CreateJsonConfig.ps1 — Creates the required JSON configuration files on a network share
- AsBuilt-GenerateReports.ps1 — Generates all reports automatically with per-report credentials, file verification and pause between each run
Reports and JSON config files are stored centrally on a network share, making them accessible from any machine in the environment without needing local copies.
Prerequisites
- PowerShell 7.x (pwsh) — required for AsBuiltReport.VMware.vSphere v2.0+
- A machine with internet access for the download phase
- A machine with network line-of-sight to Horizon, App Volumes, UAG and vCenter targets
- Write access to the network share
Folder Structure
The staging folder on the download machine looks like this:
C:\AsBuildOffline\├── PSModules\│ ├── PowerCLI\ (VMware.PowerCLI 13.3)│ ├── PScribo\│ └── AsBuilt\ (all AsBuiltReport modules)├── Python\│ ├── python-3.13.14-amd64.exe│ └── wheels\ (pip packages)└── AsBuilt-Offline.ps1\\<fileserver>\AsBuilt\├── Config\ (JSON config files)└── AsBuiltReports\ ├── Horizon\ ├── AppVolumes\ ├── UAG\ └── vSphere\
Phase 1 — Download Modules (internet machine)
Run the following on a machine with internet access. The script handles NuGet/PSResourceGet automatically depending on whether you’re running PS5.1 or PS7:
.\AsBuilt-Offline.ps1 -Mode Download
This downloads VMware.PowerCLI (locked to 13.3 for compatibility), PScribo, AsBuiltReport.Core, and the Horizon, AppVolumes, UAG and vSphere report modules — all into “C:\AsBuildOffline\PSModules\”.
Optional: Download Python
If you also want to run Python scripts on the air-gapped machine, the same script handles Python 3.13.14 and a minimal base set of packages (requests, jinja2, pyyaml):
# Python must be installed on the internet machine first.\AsBuilt-Offline.ps1 -Mode DownloadPython
Additional packages can be added by editing the “$PythonPackages” list at the top of the script before running DownloadPython.
Phase 2 — Install Modules (air-gapped machine)
Copy the entire “C:\AsBuildOffline\” folder to the air-gapped machine, then run:
pwsh.\AsBuilt-Offline.ps1 -Mode Install
The script automatically detects whether you’re running PS7 or PS5.1 and copies modules to the correct path (C:\Program Files\PowerShell\7\Modules for PS7). It also configures PowerCLI to ignore certificate errors and disable CEIP — things that are easy to forget in air-gapped environments.
A note on the VMware.Sdk.Srm warning you may see during import — this is a known bug in PowerCLI 13.3 on Windows PowerShell 5.1 related to Site Recovery Manager, which we don’t use. It can safely be ignored.
# Optional: install Python offline.\AsBuilt-Offline.ps1 -Mode InstallPython
Phase 3 — Create JSON Configuration Files
Run once to generate the report configuration files directly onto the network share:
.\AsBuilt-CreateJsonConfig.ps1
This creates the following files under “\\<fileserver>\AsBuilt\Config\”:
- AsBuiltHorizon.json
- AsBuiltAppVolumes.json
- AsBuiltUAG.json
- AsBuiltvSphere.json
If a file already exists, the script will ask before overwriting. Edit the JSON files to customise report output, health checks and thresholds — the defaults are good starting points.
Using UNC paths instead of mapped drive letters avoids a common issue where elevated PowerShell sessions don’t inherit network drive mappings from the regular user session.
Phase 4 — Generate Reports
With modules installed and JSON config in place, generating all four reports is a single command:
.\AsBuilt-GenerateReports.ps1
The script prompts for credentials separately for each report at startup — before any report generation begins — so you don’t have to sit and wait between runs. After each report, the script verifies that output files were actually created and shows file size. A 15-second pause with countdown runs between reports to allow sessions to close cleanly before the next one starts.
You can skip individual reports or change output format using parameters:
# Only Horizon and vSphere, in HTML and Word.\AsBuilt-GenerateReports.ps1 -SkipAppVolumes -SkipUAG -Format Html,Word# Only vSphere.\AsBuilt-GenerateReports.ps1 -SkipHorizon -SkipAppVolumes -SkipUAG# Run without interaction (scheduled task).\AsBuilt-GenerateReports.ps1 -NonInteractive
Troubleshooting
| Problem | Solution |
|---|---|
Network share not found (Cannot find drive I:) | Use UNC paths instead of mapped drive letters. Elevated PS sessions don’t inherit drive mappings. |
| VMware.Sdk.Srm warning on import | Known PowerCLI 13.3 / PS5.1 bug. Safe to ignore — does not affect Horizon/UAG/vSphere reports. |
| AsBuiltReport.VMware.vSphere requires PS 7.4 | Run scripts with pwsh (PowerShell 7), not powershell (Windows PowerShell 5.1). |
| No report files generated after run | The script throws an explicit error if no files are found after generation. Check -Verbose output for the underlying API error. |
| pip not found during DownloadPython | Python must be installed on the internet machine before running DownloadPython. Install from the downloaded .exe first, open a new PS session, then re-run. |
Adding More Python Packages
The base set includes requests, jinja2 and pyyaml. To add more packages, edit the “$PythonPackages” list near the top of AsBuilt-Offline.ps1 and re-run DownloadPython/InstallPython:
$PythonPackages = @( 'requests' 'jinja2' 'pyyaml' 'pandas' # add here 'openpyxl' # and here)
The Scripts
Save all three scripts to “C:\AsBuildOffline\” on both the internet machine and the air-gapped PAW.
AsBuilt-Offline.ps1
#Requires -RunAsAdministrator<#.SYNOPSIS AsBuilt Report - Offline Setup Script Last-updated: 2026-06-12.DESCRIPTION Two-phase script for As Built Report in air-gapped environments. PHASE 1 (Download) : Run on a machine with internet access. Downloads NuGet, PowerCLI, PScribo and AsBuiltReport modules to a local staging folder. PHASE 2 (Install) : Run on the air-gapped machine. Copies modules to PSModulePath and imports everything. PHASE 3 (DownloadPython) : Downloads Python installer and pip packages to staging folder. PHASE 4 (InstallPython) : Installs Python and packages from staging folder (offline)..PARAMETER Mode Download - Download modules to $StagingDir (requires internet) Install - Install and import modules from $StagingDir (offline).PARAMETER StagingDir Root folder for download and staging. Can be local path or UNC path. Default: C:\AsBuildOffline.PARAMETER ReportOutputDir Folder where completed reports are stored. Can be local path or UNC path. Default: $StagingDir\AsBuiltReports.PARAMETER JsonConfigDir Folder where JSON configuration files are stored. Can be UNC path. Default: $StagingDir\Config.EXAMPLE # Phase 1 - download on internet machine .\AsBuilt-Offline.ps1 -Mode Download -StagingDir 'C:\AsBuildOffline' # Phase 1 - download directly to network share .\AsBuilt-Offline.ps1 -Mode Download -StagingDir '\\fileserver\AsBuilt\Staging' # Phase 2 - install on air-gapped machine .\AsBuilt-Offline.ps1 -Mode Install -StagingDir 'C:\AsBuildOffline'.NOTES - Copy the entire $StagingDir\PSModules to the air-gapped machine before the Install phase. - PowerCLI is locked to v13.3 for compatibility with Horizon/vCenter. - Use Get-Credential when running New-AsBuiltReport to avoid plaintext passwords.#>[CmdletBinding()]param( [Parameter(Mandatory)] [ValidateSet('Download', 'Install', 'DownloadPython', 'InstallPython')] [string]$Mode, [string]$StagingDir = 'C:\AsBuildOffline', [string]$ReportOutputDir, [string]$JsonConfigDir)Set-StrictMode -Version Latest$ErrorActionPreference = 'Stop'# --- Derived paths -----------------------------------------------------------if (-not $ReportOutputDir) { $ReportOutputDir = Join-Path $StagingDir 'AsBuiltReports' }if (-not $JsonConfigDir) { $JsonConfigDir = Join-Path $StagingDir 'Config' }$ModuleStaging = Join-Path $StagingDir 'PSModules'$NuGetDir = Join-Path $StagingDir 'NuGet'$NuGetDest = 'C:\Program Files\PackageManagement\ProviderAssemblies'$PythonStaging = Join-Path $StagingDir 'Python'$PythonVersion = '3.13.14'$PythonInstaller = "python-$PythonVersion-amd64.exe"$PythonUrl = "https://www.python.org/ftp/python/$PythonVersion/$PythonInstaller"# Minimal base packages - add more as needed$PythonPackages = @( 'requests' 'jinja2' 'pyyaml')# --- Module definitions ------------------------------------------------------$ModuleDefinitions = @( @{ Name = 'VMware.PowerCLI'; MaxVersion = '13.3'; SubDir = 'PowerCLI' } @{ Name = 'PScribo'; MaxVersion = $null; SubDir = 'PScribo' } @{ Name = 'AsBuiltReport.Core'; MaxVersion = $null; SubDir = 'AsBuilt' } @{ Name = 'AsBuiltReport.VMware.Horizon'; MaxVersion = $null; SubDir = 'AsBuilt' } @{ Name = 'AsBuiltReport.VMware.AppVolumes'; MaxVersion = $null; SubDir = 'AsBuilt' } @{ Name = 'AsBuiltReport.VMware.UAG'; MaxVersion = $null; SubDir = 'AsBuilt' } @{ Name = 'AsBuiltReport.VMware.vSphere'; MaxVersion = $null; SubDir = 'AsBuilt' })# AsBuiltReport JSON config mapping$ReportConfigs = @( @{ Report = 'VMware.Horizon'; Filename = 'AsBuiltHorizon' } @{ Report = 'VMware.AppVolumes'; Filename = 'AsBuiltAppVolumes' } @{ Report = 'VMware.UAG'; Filename = 'AsBuiltUAG' } @{ Report = 'VMware.vSphere'; Filename = 'AsBuiltvSphere' })# --- Helper functions --------------------------------------------------------function Write-Step { param([string]$Message) Write-Host "`n[*] $Message" -ForegroundColor Cyan}function Write-OK { param([string]$Message) Write-Host " [OK] $Message" -ForegroundColor Green}function Write-Warn { param([string]$Message) Write-Host " [!] $Message" -ForegroundColor Yellow}function Write-Fail { param([string]$Message) Write-Host " [X] $Message" -ForegroundColor Red}function Ensure-Dir { param([string]$Path) if (-not (Test-Path $Path)) { New-Item -ItemType Directory -Path $Path -Force | Out-Null Write-OK "Created folder: $Path" }}# =============================================================================# PHASE 1 - DOWNLOAD# =============================================================================function Invoke-Download { Write-Host "`n+==============================================+" -ForegroundColor Magenta Write-Host "| AsBuilt Offline - PHASE 1: DOWNLOAD |" -ForegroundColor Magenta Write-Host "+==============================================+`n" -ForegroundColor Magenta # Enforce TLS 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Folders foreach ($dir in @($StagingDir, $ModuleStaging, $NuGetDir, $ReportOutputDir, $JsonConfigDir)) { Ensure-Dir $dir } # NuGet / PSResourceGet Write-Step "Checking package provider..." if ($PSVersionTable.PSVersion.Major -ge 7) { if (-not (Get-Module -Name Microsoft.PowerShell.PSResourceGet -ListAvailable)) { Write-Warn "Microsoft.PowerShell.PSResourceGet not found - installing..." Install-Module -Name Microsoft.PowerShell.PSResourceGet -Force -AllowClobber } Write-OK "PS7 detected - using PSResourceGet (NuGet provider not needed)" } else { Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null Write-OK "NuGet provider installed (PS5.1)" } # Download modules foreach ($mod in $ModuleDefinitions) { Write-Step "Downloading: $($mod.Name)$(if ($mod.MaxVersion) { " (max v$($mod.MaxVersion))" })" $destPath = Join-Path $ModuleStaging $mod.SubDir Ensure-Dir $destPath $saveParams = @{ Name = $mod.Name; Repository = 'PSGallery'; Path = $destPath } if ($mod.MaxVersion) { $saveParams['MaximumVersion'] = $mod.MaxVersion } try { Save-Module @saveParams; Write-OK "$($mod.Name) saved to $destPath" } catch { Write-Warn "ERROR downloading $($mod.Name): $_" } } Write-Host "`n+==================================================================+" -ForegroundColor Green Write-Host "| DOWNLOAD COMPLETE |" -ForegroundColor Green Write-Host "| Copy the entire folder to the air-gapped machine: |" -ForegroundColor Green Write-Host "| $($StagingDir.PadRight(60))|" -ForegroundColor Green Write-Host "| Then run: .\AsBuilt-Offline.ps1 -Mode Install |" -ForegroundColor Green Write-Host "+==================================================================+`n" -ForegroundColor Green}# =============================================================================# PHASE 2 - INSTALL# =============================================================================function Invoke-Install { Write-Host "`n+==============================================+" -ForegroundColor Magenta Write-Host "| AsBuilt Offline - PHASE 2: INSTALL |" -ForegroundColor Magenta Write-Host "+==============================================+`n" -ForegroundColor Magenta [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Set ExecutionPolicy for current user (avoids admin requirement) try { Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force Write-OK "ExecutionPolicy set to RemoteSigned (CurrentUser)" } catch { Write-Warn "Could not set ExecutionPolicy: $_ - continuing anyway" } # Find correct PSModulePath based on PS version if ($PSVersionTable.PSVersion.Major -ge 7) { $SystemModulePath = ($env:PSModulePath -split ';') | Where-Object { $_ -like '*PowerShell\7*' -or $_ -like '*powershell\7*' } | Select-Object -First 1 if (-not $SystemModulePath) { $SystemModulePath = 'C:\Program Files\PowerShell\7\Modules' Write-Warn "PS7 module folder not found, using default: $SystemModulePath" } } else { $SystemModulePath = ($env:PSModulePath -split ';') | Where-Object { $_ -like '*Program Files\WindowsPowerShell\Modules*' } | Select-Object -First 1 if (-not $SystemModulePath) { $SystemModulePath = 'C:\Program Files\WindowsPowerShell\Modules' Write-Warn "PS5.1 module folder not found, using default: $SystemModulePath" } } Write-OK "PSModulePath: $SystemModulePath" Ensure-Dir $SystemModulePath foreach ($mod in $ModuleDefinitions) { Write-Step "Installing: $($mod.Name)" $srcPath = Join-Path $ModuleStaging $mod.SubDir if (-not (Test-Path $srcPath)) { Write-Warn "Staging folder not found: $srcPath - skipping"; continue } if ($mod.Name -eq 'VMware.PowerCLI') { $modFolders = Get-ChildItem -Path $srcPath -Directory } else { $modFolders = Get-ChildItem -Path $srcPath -Directory | Where-Object { $_.Name -like "$($mod.Name)*" } } if (-not $modFolders) { Write-Warn "No module folders found for $($mod.Name) in $srcPath"; continue } foreach ($folder in $modFolders) { $dest = Join-Path $SystemModulePath $folder.Name if (Test-Path $dest) { Write-Warn "$($folder.Name) already exists - skipping copy" } else { Copy-Item -Path $folder.FullName -Destination $dest -Recurse -Force Write-OK "Copied: $($folder.Name) -> $dest" } } Get-ChildItem -Path (Join-Path $SystemModulePath "$($mod.Name)*") -Recurse -ErrorAction SilentlyContinue | Unblock-File -ErrorAction SilentlyContinue } Write-Step "Importing all modules..." $importOrder = @( 'VMware.PowerCLI' 'PScribo' 'AsBuiltReport.Core' 'AsBuiltReport.VMware.Horizon' 'AsBuiltReport.VMware.AppVolumes' 'AsBuiltReport.VMware.UAG' 'AsBuiltReport.VMware.vSphere' ) $importErrors = @() foreach ($modName in $importOrder) { try { Import-Module -Name $modName -ErrorAction Stop $ver = (Get-Module -Name $modName).Version Write-OK "$modName v$ver imported" } catch { $importErrors += $modName; Write-Warn "ERROR importing ${modName}: $_" } } Write-Step "Configuring PowerCLI (ignore certificate errors, CEIP off)..." try { Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -ParticipateInCEIP $false -Confirm:$false | Out-Null Write-OK "PowerCLI configured" } catch { Write-Warn "Could not configure PowerCLI automatically: $_" } foreach ($dir in @($ReportOutputDir, $JsonConfigDir)) { Ensure-Dir $dir } foreach ($rc in $ReportConfigs) { Ensure-Dir (Join-Path $ReportOutputDir ($rc.Report.Split('.')[-1])) } Write-Step "Generating AsBuiltReport JSON configuration files in: $JsonConfigDir" foreach ($rc in $ReportConfigs) { $jsonPath = Join-Path $JsonConfigDir "$($rc.Filename).json" if (Test-Path $jsonPath) { Write-Warn "$($rc.Filename).json already exists - skipping" } else { try { New-AsBuiltReportConfig -Report $rc.Report -FolderPath $JsonConfigDir -Filename $rc.Filename Write-OK "Created: $($rc.Filename).json" } catch { Write-Warn "Could not create $($rc.Filename).json: $_" } } } Write-Host "`n+==================================================================+" -ForegroundColor Green Write-Host "| INSTALL COMPLETE |" -ForegroundColor Green Write-Host "+==================================================================+" -ForegroundColor Green if ($importErrors.Count -gt 0) { Write-Host "`n Modules that failed to import:" -ForegroundColor Yellow $importErrors | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow } } Write-Host "`n JSON config: $JsonConfigDir" -ForegroundColor Cyan Write-Host " Reports: $ReportOutputDir" -ForegroundColor Cyan}# =============================================================================# PHASE 3 - DOWNLOADPYTHON# =============================================================================function Invoke-DownloadPython { Write-Host "`n+==============================================+" -ForegroundColor Magenta Write-Host "| AsBuilt Offline - PHASE 3: DOWNLOAD PYTHON |" -ForegroundColor Magenta Write-Host "+==============================================+`n" -ForegroundColor Magenta [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Ensure-Dir $PythonStaging $wheelDir = Join-Path $PythonStaging 'wheels' Ensure-Dir $wheelDir $installerDest = Join-Path $PythonStaging $PythonInstaller if (Test-Path $installerDest) { Write-Warn "Python installer already exists: $installerDest - skipping download" } else { Write-Step "Downloading Python $PythonVersion..." try { Invoke-WebRequest -Uri $PythonUrl -OutFile $installerDest -UseBasicParsing $sizeMB = [math]::Round((Get-Item $installerDest).Length / 1MB, 1) Write-OK "Python $PythonVersion downloaded ($sizeMB MB): $installerDest" } catch { Write-Warn "ERROR downloading Python: $_"; return } } Write-Step "Downloading pip packages..." if (-not (Get-Command pip -ErrorAction SilentlyContinue)) { Write-Warn "pip not found - Python must be installed on this machine for pip download" Write-Host " Install Python $PythonVersion first, open a new PS session, then re-run DownloadPython." -ForegroundColor Yellow return } foreach ($pkg in $PythonPackages) { Write-Step "Downloading: $pkg (with dependencies)..." try { $result = pip download $pkg --dest $wheelDir --quiet 2>&1 $files = Get-ChildItem $wheelDir -Filter '*.whl' | Where-Object { $_.LastWriteTime -gt (Get-Date).AddSeconds(-30) } Write-OK "$pkg - $($files.Count) wheel file(s) downloaded" } catch { Write-Warn "ERROR downloading $pkg : $_" } } $totalWheels = (Get-ChildItem $wheelDir -Filter '*.whl').Count Write-Host "`n+======================================================================+" -ForegroundColor Green Write-Host "| DOWNLOADPYTHON COMPLETE |" -ForegroundColor Green Write-Host "+======================================================================+" -ForegroundColor Green Write-Host "`n Python installer: $installerDest" -ForegroundColor Cyan Write-Host " Wheel cache: $wheelDir ($totalWheels files)" -ForegroundColor Cyan Write-Host "`n Copy the entire folder to the air-gapped machine:" -ForegroundColor White Write-Host " $PythonStaging" -ForegroundColor DarkCyan Write-Host "`n Then run: .\AsBuilt-Offline.ps1 -Mode InstallPython`n" -ForegroundColor White}# =============================================================================# PHASE 4 - INSTALLPYTHON# =============================================================================function Invoke-InstallPython { Write-Host "`n+==============================================+" -ForegroundColor Magenta Write-Host "| AsBuilt Offline - PHASE 4: INSTALL PYTHON |" -ForegroundColor Magenta Write-Host "+==============================================+`n" -ForegroundColor Magenta $installerPath = Join-Path $PythonStaging $PythonInstaller $wheelDir = Join-Path $PythonStaging 'wheels' if (-not (Test-Path $installerPath)) { Write-Fail "Python installer not found: $installerPath" Write-Host " Run: .\AsBuilt-Offline.ps1 -Mode DownloadPython on an internet machine first.`n" -ForegroundColor Yellow exit 1 } $pythonCmd = Get-Command python -ErrorAction SilentlyContinue if ($pythonCmd) { $installedVer = (python --version 2>&1).ToString().Trim() Write-Warn "Python is already installed: $installedVer" $answer = Read-Host " Install Python $PythonVersion anyway? (y/N)" if ($answer -notmatch '^[yY]$') { Write-Host " Skipping Python installation - continuing with packages...`n" -ForegroundColor Yellow } else { $pythonCmd = $null } } if (-not $pythonCmd) { Write-Step "Installing Python $PythonVersion (silent)..." $installArgs = @('/quiet','InstallAllUsers=1','PrependPath=1','Include_test=0','Include_doc=0','Include_launcher=1','Include_pip=1') try { $proc = Start-Process -FilePath $installerPath -ArgumentList $installArgs -Wait -PassThru if ($proc.ExitCode -eq 0) { Write-OK "Python $PythonVersion installed" } else { Write-Fail "Python installation failed with exit code: $($proc.ExitCode)"; exit 1 } } catch { Write-Fail "ERROR installing Python: $_"; exit 1 } $env:Path = [System.Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path','User') Write-OK "PATH updated in current session" } Write-Step "Verifying Python installation..." try { $ver = python --version 2>&1; Write-OK "$ver" } catch { Write-Fail "python command not found after installation. Restart PowerShell and try again."; exit 1 } if (-not (Test-Path $wheelDir)) { Write-Warn "Wheel cache not found: $wheelDir - skipping package installation" } else { $wheelCount = (Get-ChildItem $wheelDir -Filter '*.whl').Count Write-Step "Installing pip packages from wheel cache ($wheelCount files)..." foreach ($pkg in $PythonPackages) { Write-Host " Installing: $pkg..." -ForegroundColor Gray try { $result = pip install $pkg --no-index --find-links $wheelDir --quiet 2>&1 Write-OK "$pkg installed" } catch { Write-Warn "ERROR installing $pkg : $_" } } Write-Step "Installed Python packages:" pip list --format=columns 2>&1 | Where-Object { $_ -match ($PythonPackages -join '|') } | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGreen } } Write-Host "`n+======================================================================+" -ForegroundColor Green Write-Host "| INSTALLPYTHON COMPLETE |" -ForegroundColor Green Write-Host "+======================================================================+" -ForegroundColor Green Write-Host "`n Python: $(python --version 2>&1)" -ForegroundColor Cyan Write-Host " pip: $(pip --version 2>&1)" -ForegroundColor Cyan Write-Host "`n Add more packages to `$PythonPackages and re-run DownloadPython/InstallPython." -ForegroundColor Gray Write-Host " Or online: pip install <package>`n" -ForegroundColor Gray}# =============================================================================# MAIN# =============================================================================switch ($Mode) { 'Download' { Invoke-Download } 'Install' { Invoke-Install } 'DownloadPython' { Invoke-DownloadPython } 'InstallPython' { Invoke-InstallPython }}
AsBuilt-CreateJsonConfig.ps1
<#.SYNOPSIS AsBuilt Report - Create JSON Configuration Files.DESCRIPTION Creates JSON configuration files for AsBuiltReport on a network share. Run once, or again to reset config to defaults. Note: Use UNC paths (\\server\share) instead of drive letters (I:\) to avoid issues with mapped network drives in elevated PowerShell sessions..NOTES Environment: YourOrg / yourdomain.local Reports: Horizon, AppVolumes, UAG, vSphere Storage: \\fileserver.domain.local\Install\AsBuilt\Config\#>[CmdletBinding()]param( [string]$ConfigDir = '\\fileserver.domain.local\Install\AsBuilt\Config')Set-StrictMode -Version Latest$ErrorActionPreference = 'Stop'# --- Report config definitions -----------------------------------------------$ReportConfigs = @( @{ Report = 'VMware.Horizon'; Filename = 'AsBuiltHorizon' } @{ Report = 'VMware.AppVolumes'; Filename = 'AsBuiltAppVolumes' } @{ Report = 'VMware.UAG'; Filename = 'AsBuiltUAG' } @{ Report = 'VMware.vSphere'; Filename = 'AsBuiltvSphere' })# --- Helper functions --------------------------------------------------------function Write-Step { param([string]$Msg) Write-Host "`n[*] $Msg" -ForegroundColor Cyan }function Write-OK { param([string]$Msg) Write-Host " [OK] $Msg" -ForegroundColor Green }function Write-Warn { param([string]$Msg) Write-Host " [!] $Msg" -ForegroundColor Yellow }# --- Check that required modules are available -------------------------------Write-Step "Checking modules..."$requiredModules = @( 'AsBuiltReport.Core' 'AsBuiltReport.VMware.Horizon' 'AsBuiltReport.VMware.AppVolumes' 'AsBuiltReport.VMware.UAG' 'AsBuiltReport.VMware.vSphere')# Note: AsBuiltReport.VMware.vSphere v1.3.5 - last version with PS5.1 supportforeach ($mod in $requiredModules) { if (-not (Get-Module -Name $mod -ListAvailable)) { Write-Error "Module not found: $mod - run AsBuilt-Offline.ps1 -Mode Install first." } Import-Module -Name $mod -ErrorAction Stop Write-OK "$mod imported"}# --- Create Config folder ---------------------------------------------------Write-Step "Checking Config folder: $ConfigDir"if (-not (Test-Path $ConfigDir)) { New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null Write-OK "Created: $ConfigDir"} else { Write-OK "Already exists: $ConfigDir"}# --- Generate JSON files ----------------------------------------------------Write-Step "Generating JSON configuration files..."foreach ($rc in $ReportConfigs) { $jsonFile = Join-Path $ConfigDir "$($rc.Filename).json" if (Test-Path $jsonFile) { $answer = Read-Host " '$($rc.Filename).json' already exists. Overwrite? (y/N)" if ($answer -notmatch '^[yY]$') { Write-Warn "Skipping: $($rc.Filename).json" continue } Remove-Item $jsonFile -Force } try { New-AsBuiltReportConfig -Report $rc.Report ` -FolderPath $ConfigDir ` -Filename $rc.Filename Write-OK "Created: $jsonFile" } catch { Write-Warn "ERROR creating $($rc.Filename).json: $_" }}# --- Summary -----------------------------------------------------------------Write-Host "`n+==============================================================+" -ForegroundColor GreenWrite-Host "| JSON configuration files ready |" -ForegroundColor GreenWrite-Host "+==============================================================+" -ForegroundColor GreenWrite-Host "`n Location: $ConfigDir" -ForegroundColor CyanWrite-Host "`n Next step: Run AsBuilt-GenerateReports.ps1`n" -ForegroundColor White
AsBuilt-GenerateReports.ps1
<#.SYNOPSIS AsBuilt Report - Generate Reports Automatically.DESCRIPTION Generates As Built reports for Horizon, App Volumes, UAG and vSphere. Reports are stored on your network share with a timestamp in the filename. Note: Use UNC paths (\\server\share) instead of drive letters (I:\) to avoid issues with mapped network drives in elevated PowerShell sessions..PARAMETER SkipHorizon Skip the Horizon report..PARAMETER SkipAppVolumes Skip the App Volumes report..PARAMETER SkipUAG Skip the UAG report..PARAMETER SkipvSphere Skip the vSphere report..PARAMETER Format Report format: Html, Word, Text or combination. Default: Html.EXAMPLE # Generate all reports .\AsBuilt-GenerateReports.ps1 # Only Horizon and UAG, in HTML and Word .\AsBuilt-GenerateReports.ps1 -SkipAppVolumes -Format Html,Word # Run without confirmation prompt (e.g. scheduled task) .\AsBuilt-GenerateReports.ps1 -NonInteractive.NOTES Environment: YourOrg / yourdomain.local Reports: Horizon, AppVolumes, UAG, vSphere Storage: \\fileserver.domain.local\Install\AsBuilt\AsBuiltReports\<type>\ Credentials: Prompted at runtime (Get-Credential)#>[CmdletBinding()]param( [switch]$SkipHorizon, [switch]$SkipAppVolumes, [switch]$SkipUAG, [switch]$SkipvSphere, [ValidateSet('Html','Word','Text')] [string[]]$Format = @('Html'), [switch]$NonInteractive, # Paths - change here if needed [string]$BaseReportDir = '\\fileserver.domain.local\Install\AsBuilt\AsBuiltReports', [string]$ConfigDir = '\\fileserver.domain.local\Install\AsBuilt\Config')Set-StrictMode -Version Latest$ErrorActionPreference = 'Stop'# --- PS version check -------------------------------------------------------$psVersion = $PSVersionTable.PSVersionWrite-Host "`n[i] PowerShell version: $($psVersion.Major).$($psVersion.Minor)" -ForegroundColor Grayif ($psVersion.Major -lt 5 -or ($psVersion.Major -eq 5 -and $psVersion.Minor -lt 1)) { Write-Error "Requires minimum Windows PowerShell 5.1. Please install a newer version." exit 1}if ($psVersion.Major -lt 7) { Write-Host "[i] PS5.1 detected - using AsBuiltReport.VMware.vSphere v1.3.5 (last PS5.1-compatible version)" -ForegroundColor Yellow Write-Host "[i] VMware.Sdk.Srm warning during PowerCLI import can be ignored - does not affect reports" -ForegroundColor Yellow}# --- Timestamp for filenames ------------------------------------------------$Timestamp = Get-Date -Format 'yyyy-MM-dd_HHmm'# --- Report definitions -----------------------------------------------------# Target: Connection Server used for Horizon.# AppVolumes and UAG have their own FQDNs - adjust as needed.$Reports = @( @{ Name = 'Horizon' Report = 'VMware.Horizon' Target = 'horizon-cs.domain.local' # <-- change to your CS FQDN ConfigFile = Join-Path $ConfigDir 'AsBuiltHorizon.json' OutputDir = Join-Path $BaseReportDir 'Horizon' Skip = $SkipHorizon Module = 'AsBuiltReport.VMware.Horizon' CredentialLabel = 'Horizon Connection Server' }, @{ Name = 'AppVolumes' Report = 'VMware.AppVolumes' Target = 'appvolumes.domain.local' # <-- change to your AVM FQDN ConfigFile = Join-Path $ConfigDir 'AsBuiltAppVolumes.json' OutputDir = Join-Path $BaseReportDir 'AppVolumes' Skip = $SkipAppVolumes Module = 'AsBuiltReport.VMware.AppVolumes' CredentialLabel = 'App Volumes Manager' }, @{ Name = 'UAG' Report = 'VMware.UAG' Target = 'uag.example.com' # <-- change to your UAG FQDN ConfigFile = Join-Path $ConfigDir 'AsBuiltUAG.json' OutputDir = Join-Path $BaseReportDir 'UAG' Skip = $SkipUAG Module = 'AsBuiltReport.VMware.UAG' CredentialLabel = 'Unified Access Gateway' }, @{ Name = 'vSphere' Report = 'VMware.vSphere' Target = 'vcenter.domain.local' # <-- change to your vCenter FQDN ConfigFile = Join-Path $ConfigDir 'AsBuiltvSphere.json' OutputDir = Join-Path $BaseReportDir 'vSphere' Skip = $SkipvSphere Module = 'AsBuiltReport.VMware.vSphere' CredentialLabel = 'vCenter Server' })# --- Helper functions -------------------------------------------------------function Write-Step { param([string]$Msg) Write-Host "`n[*] $Msg" -ForegroundColor Cyan }function Write-OK { param([string]$Msg) Write-Host " [OK] $Msg" -ForegroundColor Green }function Write-Warn { param([string]$Msg) Write-Host " [!] $Msg" -ForegroundColor Yellow }function Write-Fail { param([string]$Msg) Write-Host " [X] $Msg" -ForegroundColor Red }# --- Banner -----------------------------------------------------------------Write-Host "`n+==============================================================+" -ForegroundColor MagentaWrite-Host "| AsBuilt Report - Automated report generation |" -ForegroundColor MagentaWrite-Host "| Timestamp: $Timestamp |" -ForegroundColor MagentaWrite-Host "+==============================================================+" -ForegroundColor Magenta# --- Import modules ---------------------------------------------------------Write-Step "Importing modules..."$modulesToLoad = @('AsBuiltReport.Core')foreach ($r in $Reports) { if (-not $r['Skip']) { $modulesToLoad += $r['Module'] }}$modulesToLoad = $modulesToLoad | Select-Object -Uniqueforeach ($mod in $modulesToLoad) { try { Import-Module -Name $mod -ErrorAction Stop Write-OK "$mod" } catch { Write-Fail "Could not load $mod - aborting. Run AsBuilt-Offline.ps1 -Mode Install." exit 1 }}# --- Validate config files --------------------------------------------------Write-Step "Validating JSON configuration files..."$missingConfig = $falseforeach ($r in ($Reports | Where-Object { -not $_['Skip'] })) { if (-not (Test-Path $r['ConfigFile'])) { Write-Fail "Missing: $($r['ConfigFile'])" $missingConfig = $true } else { Write-OK "Found: $($r['ConfigFile'])" }}if ($missingConfig) { Write-Host "`n Run AsBuilt-CreateJsonConfig.ps1 first to create missing files.`n" -ForegroundColor Yellow exit 1}# --- Credentials (one per report) -------------------------------------------Write-Step "Collecting credentials..."Write-Host " You will be prompted for credentials for each report separately." -ForegroundColor Gray$activeReportsForCreds = $Reports | Where-Object { -not $_['Skip'] }foreach ($r in $activeReportsForCreds) { $cred = Get-Credential -Message "Credentials for $($r['CredentialLabel']) ($($r['Target']))" if (-not $cred) { Write-Fail "No credentials provided for $($r['Name']) - aborting." exit 1 } $r['Credential'] = $cred Write-OK "$($r['Name']): $($cred.UserName)"}# --- Pre-run summary --------------------------------------------------------$activeReports = $Reports | Where-Object { -not $_['Skip'] }Write-Host "`n Reports that will be generated:" -ForegroundColor Whiteforeach ($r in $activeReports) { Write-Host " * $($r['Name'].PadRight(12)) -> $($r['Target']) [$($r['Credential'].UserName)]" -ForegroundColor Gray}Write-Host " Format: $($Format -join ', ')" -ForegroundColor GrayWrite-Host " Output: $BaseReportDir\<type>\`n" -ForegroundColor Grayif (-not $NonInteractive) { $confirm = Read-Host " Start report generation? (Y/n)" if ($confirm -match '^[nN]$') { Write-Host " Aborted.`n" -ForegroundColor Yellow exit 0 }}# --- Generate reports -------------------------------------------------------$Results = @()$reportList = @($activeReports) # Convert to array so we can use index$pauseSeconds = 15 # Seconds between reports (adjust as needed)for ($i = 0; $i -lt $reportList.Count; $i++) { $r = $reportList[$i] Write-Step "[$($i+1)/$($reportList.Count)] Generating $($r['Name']) report against $($r['Target'])..." if (-not (Test-Path $r['OutputDir'])) { New-Item -ItemType Directory -Path $r['OutputDir'] -Force | Out-Null Write-OK "Created folder: $($r['OutputDir'])" } $startTime = Get-Date try { New-AsBuiltReport ` -Report $r['Report'] ` -Target $r['Target'] ` -Credential $r['Credential'] ` -Format $Format ` -OutputFolderPath $r['OutputDir'] ` -ReportConfigFilePath $r['ConfigFile'] ` -Timestamp ` -Verbose $duration = [math]::Round(((Get-Date) - $startTime).TotalMinutes, 1) $generatedFiles = @(Get-ChildItem -Path $r['OutputDir'] -File | Where-Object { $_.LastWriteTime -gt $startTime } | Sort-Object LastWriteTime -Descending) if ($generatedFiles.Count -eq 0) { throw "No report files found in $($r['OutputDir']) after generation." } Write-OK "$($r['Name']) completed in $duration min - $($generatedFiles.Count) file(s) created:" foreach ($f in $generatedFiles) { Write-Host " -> $($f.FullName) ($([math]::Round($f.Length/1KB, 1)) KB)" -ForegroundColor DarkGreen } $Results += [PSCustomObject]@{ Report = $r['Name'] Status = 'OK' Files = $generatedFiles.Count Time = "$duration min" Folder = $r['OutputDir'] } } catch { $duration = [math]::Round(((Get-Date) - $startTime).TotalMinutes, 1) Write-Fail "$($r['Name']) failed after $duration min: $_" $Results += [PSCustomObject]@{ Report = $r['Name'] Status = "ERROR: $_" Files = 0 Time = "$duration min" Folder = $r['OutputDir'] } } # Pause between reports (not after the last one) if ($i -lt ($reportList.Count - 1)) { Write-Host "`n Waiting $pauseSeconds seconds before next report..." -ForegroundColor Yellow for ($s = $pauseSeconds; $s -gt 0; $s--) { Write-Host -NoNewline "`r Starting in $s seconds... " Start-Sleep -Seconds 1 } Write-Host "`r Starting next report... " -ForegroundColor Cyan }}# --- Final summary ----------------------------------------------------------Write-Host "`n+==============================================================+" -ForegroundColor GreenWrite-Host "| DONE - Summary |" -ForegroundColor GreenWrite-Host "+==============================================================+" -ForegroundColor Green$Results | Format-Table -AutoSize | Out-String | Write-HostWrite-Host " Reports saved to: $BaseReportDir`n" -ForegroundColor Cyan
As I mentioned to begin with, this is a Must-Have for Lazy Admins and a highly recommended tool for system documentation. I hope this will save someone else as much time as it has done for me…
Disclaimer: Every tips/tricks/posting I have published here, is tried and tested in different IT-solutions. It is not guaranteed to work everywhere, but is meant as a tip for other users out there. Remember, Google is your friend and don’t be afraid to steal with pride! Feel free to comment below as needed.