This module follows the nested-folder pattern established by AsBuiltReport.Microsoft.Azure.
AsBuiltReport.VMware.vSphere/ ← repo root
├── AsBuiltReport.VMware.vSphere/ ← module source (published to PSGallery)
│ ├── AsBuiltReport.VMware.vSphere.psd1
│ ├── AsBuiltReport.VMware.vSphere.psm1
│ ├── AsBuiltReport.VMware.vSphere.json ← report configuration schema
│ ├── Language/
│ │ ├── en-US/VMwarevSphere.psd1 ← primary translation template
│ │ ├── en-GB/VMwarevSphere.psd1 ← British English
│ │ ├── es-ES/VMwarevSphere.psd1 ← Spanish
│ │ ├── fr-FR/VMwarevSphere.psd1 ← French
│ │ └── de-DE/VMwarevSphere.psd1 ← German
│ └── Src/
│ ├── Public/
│ │ └── Invoke-AsBuiltReport.VMware.vSphere.ps1 ← entry point
│ └── Private/
│ ├── Convert-DataSize.ps1 ← helper functions
│ ├── Get-ESXiBootDevice.ps1
│ ├── Get-InstallDate.ps1
│ ├── Get-License.ps1
│ ├── Get-PciDeviceDetail.ps1
│ ├── Get-ScsiDeviceDetail.ps1
│ ├── Get-Uptime.ps1
│ ├── Get-vCenterStats.ps1
│ ├── Get-VMHostNetworkAdapterDP.ps1
│ ├── Get-AbrVSpherevCenter.ps1 ← section functions
│ ├── Get-AbrVSphereCluster.ps1
│ ├── Get-AbrVSphereClusterHA.ps1
│ ├── Get-AbrVSphereClusterProactiveHA.ps1
│ ├── Get-AbrVSphereClusterDRS.ps1
│ ├── Get-AbrVSphereClusterVUM.ps1
│ ├── Get-AbrVSphereClusterLCM.ps1
│ ├── Get-AbrVSphereResourcePool.ps1
│ ├── Get-AbrVSphereVMHost.ps1
│ ├── Get-AbrVSphereVMHostHardware.ps1
│ ├── Get-AbrVSphereVMHostSystem.ps1
│ ├── Get-AbrVSphereVMHostStorage.ps1
│ ├── Get-AbrVSphereVMHostNetwork.ps1
│ ├── Get-AbrVSphereVMHostSecurity.ps1
│ ├── Get-AbrVSphereNetwork.ps1
│ ├── Get-AbrVSpherevSAN.ps1
│ ├── Get-AbrVSphereDatastore.ps1
│ ├── Get-AbrVSphereDSCluster.ps1
│ ├── Get-AbrVSphereVM.ps1
│ └── Get-AbrVSphereVUM.ps1
├── Tests/
│ ├── AsBuiltReport.VMware.vSphere.Tests.ps1
│ ├── LocalizationData.Tests.ps1
│ └── Invoke-Tests.ps1
├── CHANGELOG.md
├── README.md
└── LICENSE
| Type | Pattern | Example |
|---|---|---|
| Top-level section function | Get-AbrVSphere{Component} |
Get-AbrVSpherevCenter |
| Sub-section function | Get-AbrVSphere{Parent}{Sub} |
Get-AbrVSphereClusterHA |
| Language key (section) | GetAbrVSphere{Component} |
GetAbrVSpherevCenter |
| Language key (sub-section) | GetAbrVSphere{Parent}{Sub} |
GetAbrVSphereClusterHA |
The language key is derived from the function name by removing the hyphen: Get-AbrVSpherevCenter → GetAbrVSpherevCenter.
Every section function follows this begin/process/end pattern:
function Get-AbrVSphere{Name} {
<#
.SYNOPSIS
Used by As Built Report to retrieve VMware vSphere {Name} information.
.NOTES
Version: 2.0.0
Author: Tim Carman
#>
[CmdletBinding()]
param ()
begin {
$LocalizedData = $reportTranslate.GetAbrVSphere{Name}
Write-PScriboMessage ($LocalizedData.InfoLevel -f $InfoLevel.{Section})
}
process {
Try {
if ($InfoLevel.{Section} -ge 1) {
Write-PScriboMessage $LocalizedData.Collecting
Section -Style Heading2 $LocalizedData.SectionHeading {
Paragraph ($LocalizedData.ParagraphSummary -f $vCenterServerName)
# ... section content
}
}
} Catch {
Write-PScriboMessage -IsWarning $($_.Exception.Message)
}
}
end {}
}Key rules:
- Functions use variables from parent scope (not parameters) —
$vCenter,$InfoLevel,$Report,$Healthcheck,$TextInfo,$vCenterServerName,$reportTranslate,$vcApiUri,$vcApiHeaders, etc. $LocalizedDatais always set inbegin {}from$reportTranslate.{KeyName}- All PSCustomObject property names (column headers), section headings, table names, and user-visible strings use
$LocalizedDatakeys - Table names (
Name =) in$TableParamsalso use$LocalizedDatakeys for localization - PSCustomObject keys must NOT use parentheses around
$LocalizedDataexpressions. Use$LocalizedData.Key = value, never($LocalizedData.Key) = value. The brackets are unnecessary — PowerShell evaluates$LocalizedData.Keyas a key name directly in hashtable/PSCustomObject literals.
The AsBuiltReport.Core module loads Language/{culture}/VMwarevSphere.psd1 automatically. The $reportTranslate variable is set globally by New-AsBuiltReport.ps1 before the Invoke- function runs.
The file name VMwarevSphere is derived from the report name VMware.vSphere by removing the dot.
# culture = 'en-US'
@{
GetAbrVSpherevCenter = ConvertFrom-StringData @'
InfoLevel = Tab:2 vCenter InfoLevel set to {0}.
Collecting = Collecting vCenter Server information.
SectionHeading = vCenter Server
ParagraphSummary = The following sections detail the configuration of vCenter Server {0}.
# ... more keys
'@
GetAbrVSphereCluster = ConvertFrom-StringData @'
# ...
'@
}- Copy
Language/en-US/VMwarevSphere.psd1toLanguage/{culture}/VMwarevSphere.psd1 - Update the culture comment at the top
- Translate string values (keep keys identical)
- Run
Tests/Invoke-Tests.ps1—LocalizationData.Tests.ps1will verify key consistency
# From repo root
Invoke-ScriptAnalyzer -Path .\AsBuiltReport.VMware.vSphere\Src -Recurse -Severity Warning, Error.\Tests\Invoke-Tests.ps1
# With code coverage
.\Tests\Invoke-Tests.ps1 -CodeCoverage
# With JUnit output (for CI)
.\Tests\Invoke-Tests.ps1 -OutputFormat JUnitXmlImport-Module .\AsBuiltReport.VMware.vSphere\AsBuiltReport.VMware.vSphere.psd1 -Force
Get-Command -Module AsBuiltReport.VMware.vSphereRemove-Module AsBuiltReport.VMware.vSphere -ErrorAction SilentlyContinue
Import-Module .\AsBuiltReport.VMware.vSphere\AsBuiltReport.VMware.vSphere.psd1 -Force-
Create the function file in
Src/Private/Get-AbrVSphere{Name}.ps1- Follow the begin/process/end template above
- Use
$InfoLevel.{Section}for the info level check
-
Add the language key to
Language/en-US/VMwarevSphere.psd1:GetAbrVSphere{Name} = ConvertFrom-StringData @' InfoLevel = {Section} InfoLevel set to {0}. Collecting = Collecting {Section} information. SectionHeading = {Section Heading} ParagraphSummary = The following sections detail... '@ -
Copy the key to all other locale files (en-GB, etc.) with identical keys (translate values only)
-
Call the function from
Invoke-AsBuiltReport.VMware.vSphere.ps1inside theSection -Style Heading1block -
Run tests to verify:
LocalizationData.Tests.ps1checks all locale files have matching keysAsBuiltReport.VMware.vSphere.Tests.ps1checks the .ps1 file exists in Private/
| Level | Description |
|---|---|
| 0 | Disabled (section not rendered) |
| 1 | Enabled / Summary |
| 2 | Advanced Summary |
| 3 | Detailed |
| 4 | Advanced Detailed |
| 5 | Comprehensive |
The report configuration is in AsBuiltReport.VMware.vSphere.json. InfoLevel keys correspond to section names: vCenter, Cluster, ResourcePool, VMHost, Network, vSAN, Datastore, DSCluster, VM, VUM.
The -Skip:() expression in It blocks is evaluated at discovery time, before BeforeAll {} runs. Variables set in BeforeAll are not available to -Skip:() conditions. Use BeforeDiscovery {} for variables that control skip logic. Example in AsBuiltReport.VMware.vSphere.Tests.ps1:
# Correct — BeforeDiscovery runs before -Skip:() is evaluated
BeforeDiscovery {
$AnalyzerAvailable = $null -ne (Get-Module -Name PSScriptAnalyzer -ListAvailable | Select-Object -First 1)
}The .psm1 only exports public functions (Export-ModuleMember -Function $Public.BaseName). Private section functions (Get-AbrVSphere*) are dot-sourced and available in module scope without being exported. The FunctionsToExport in the .psd1 manifest acts as the final authoritative allow-list.
The $vcApiUri and $vcApiHeaders variables are set in Invoke-AsBuiltReport.VMware.vSphere.ps1 only when the connected vCenter is vSphere 8.0+. A REST API session is established via POST /api/session using the report $Credential:
$restToken = Invoke-RestMethod -Uri "$vcApiBaseUri/session" -Method Post -Credential $Credential -SkipCertificateCheck
$vcApiHeaders = @{ 'vmware-api-session-id' = $restToken }Do not use $vCenter.SessionId as the session token — that is the PowerCLI SOAP/VMOMI session ID and is rejected by the REST API with HTTP 401.
Functions that call the REST API check if (-not $vcApiUri) { return } to silently skip on unsupported versions or when session establishment failed.
The PowerCLI Get-Compliance cmdlet emits a non-terminating warning alongside its return value. Use -ErrorAction SilentlyContinue (not -ErrorAction Stop) to suppress the warning without discarding the compliance data. The null check on the result variable handles genuine failures:
$Compliances = $Entity | Get-Compliance -ErrorAction SilentlyContinue
if ($Compliances) { ... }Using -ErrorAction Stop converts the warning to a terminating error and the catch block fires, losing the data even though the cmdlet succeeded.
PScribo serializes every PSCustomObject when rendering TEXT/Word/HTML output. VMOMI/PowerCLI objects (e.g., ClusterComputeResource, VIPermission, VirtualMachine) expose a duplicate LinkedView property internally, which causes:
An item with the same key has already been added. Key: LinkedView
Rule: PSCustomObject property values must always be string primitives, never raw PowerCLI objects.
# WRONG — stores raw VMOMI object
[PSCustomObject]@{ ($LocalizedData.Entity) = $ClusterCompliance.Entity }
# CORRECT — store the string name
[PSCustomObject]@{ ($LocalizedData.Entity) = $ClusterCompliance.Entity.Name }This applies to any property that holds a PowerCLI object: use .Name, .Id, .ToString(), or any other string extraction. Using the object in a Where-Object comparison (not as a stored value) is safe.
The en-US locale file is the authoritative template. All other locales must have identical key sets — the LocalizationData.Tests.ps1 Pester test enforces this. To quickly check parity without running Pester, compare key counts:
# Count keys per locale manually
foreach ($locale in 'en-US','en-GB','es-ES','fr-FR','de-DE') {
$count = (Get-Content ".\AsBuiltReport.VMware.vSphere\Language\$locale\VMwarevSphere.psd1" |
Where-Object { $_ -match '^\s+\w+\s*=' -and $_ -notmatch "^'@" }).Count
Write-Host "$locale`: $count keys"
}