As Built Report in Air-Gapped Environments

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:

PowerShell
.\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):

PowerShell
# 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:

PowerShell
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.

PowerShell
# 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:

PowerShell
.\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:

PowerShell
.\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:

PowerShell
# 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

ProblemSolution
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 importKnown PowerCLI 13.3 / PS5.1 bug. Safe to ignore — does not affect Horizon/UAG/vSphere reports.
AsBuiltReport.VMware.vSphere requires PS 7.4Run scripts with pwsh (PowerShell 7), not powershell (Windows PowerShell 5.1).
No report files generated after runThe script throws an explicit error if no files are found after generation. Check -Verbose output for the underlying API error.
pip not found during DownloadPythonPython 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:

PowerShell
$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

PowerShell
#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

PowerShell
<#
.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 support
foreach ($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 Green
Write-Host "| JSON configuration files ready |" -ForegroundColor Green
Write-Host "+==============================================================+" -ForegroundColor Green
Write-Host "`n Location: $ConfigDir" -ForegroundColor Cyan
Write-Host "`n Next step: Run AsBuilt-GenerateReports.ps1`n" -ForegroundColor White

AsBuilt-GenerateReports.ps1

PowerShell
<#
.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.PSVersion
Write-Host "`n[i] PowerShell version: $($psVersion.Major).$($psVersion.Minor)" -ForegroundColor Gray
if ($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 Magenta
Write-Host "| AsBuilt Report - Automated report generation |" -ForegroundColor Magenta
Write-Host "| Timestamp: $Timestamp |" -ForegroundColor Magenta
Write-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 -Unique
foreach ($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 = $false
foreach ($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 White
foreach ($r in $activeReports) {
Write-Host " * $($r['Name'].PadRight(12)) -> $($r['Target']) [$($r['Credential'].UserName)]" -ForegroundColor Gray
}
Write-Host " Format: $($Format -join ', ')" -ForegroundColor Gray
Write-Host " Output: $BaseReportDir\<type>\`n" -ForegroundColor Gray
if (-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 Green
Write-Host "| DONE - Summary |" -ForegroundColor Green
Write-Host "+==============================================================+" -ForegroundColor Green
$Results | Format-Table -AutoSize | Out-String | Write-Host
Write-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.

Leave a Reply