Skip to content

Commit 3b083eb

Browse files
tpcarmanclaude
andcommitted
Fix 'LinkedView' duplicate key error in TEXT format reports (#130)
Convert VMware managed objects stored as PSCustomObject property values to plain strings (.Name or [string] cast) before passing to PScribo's Table cmdlet, preventing .NET's Dictionary<K,V>.Add() from throwing a duplicate-key exception when formatting TEXT output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent acdeb05 commit 3b083eb

5 files changed

Lines changed: 39 additions & 35 deletions

File tree

AsBuiltReport.VMware.vSphere/Src/Private/Get-AbrVSphereCluster.ps1

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@ function Get-AbrVSphereCluster {
3535
BlankLine
3636
$ClusterInfo = foreach ($Cluster in $Clusters) {
3737
[PSCustomObject]@{
38-
($LocalizedData.Cluster) = $Cluster.Name
39-
($LocalizedData.Datacenter) = $Cluster | Get-Datacenter
40-
($LocalizedData.NumHosts) = $Cluster.ExtensionData.Host.Count
41-
($LocalizedData.NumVMs) = $Cluster.ExtensionData.VM.Count
42-
($LocalizedData.HAEnabled) = if ($Cluster.HAEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
43-
($LocalizedData.DRSEnabled) = if ($Cluster.DRSEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
44-
($LocalizedData.VSANEnabled) = if ($Cluster.VsanEnabled -or $VsanCluster.VsanEsaEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
45-
($LocalizedData.EVCMode) = if ($Cluster.EVCMode) { $EvcModeLookup."$($Cluster.EVCMode)" } else { $LocalizedData.Disabled }
46-
($LocalizedData.VMSwapFilePolicy) = switch ($Cluster.VMSwapfilePolicy) {
38+
$LocalizedData.Cluster = $Cluster.Name
39+
$LocalizedData.Datacenter = ($Cluster | Get-Datacenter).Name
40+
$LocalizedData.NumHosts = $Cluster.ExtensionData.Host.Count
41+
$LocalizedData.NumVMs = $Cluster.ExtensionData.VM.Count
42+
$LocalizedData.HAEnabled = if ($Cluster.HAEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
43+
$LocalizedData.DRSEnabled = if ($Cluster.DRSEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
44+
$LocalizedData.VSANEnabled = if ($Cluster.VsanEnabled -or $VsanCluster.VsanEsaEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
45+
$LocalizedData.EVCMode = if ($Cluster.EVCMode) { $EvcModeLookup."$($Cluster.EVCMode)" } else { $LocalizedData.Disabled }
46+
$LocalizedData.VMSwapFilePolicy = switch ($Cluster.VMSwapfilePolicy) {
4747
'WithVM' { $LocalizedData.SwapWithVM }
4848
'InHostDatastore' { $LocalizedData.SwapInHostDatastore }
4949
default { $Cluster.VMSwapfilePolicy }
@@ -87,16 +87,16 @@ function Get-AbrVSphereCluster {
8787
BlankLine
8888
#region Cluster Configuration
8989
$ClusterDetail = [PSCustomObject]@{
90-
($LocalizedData.Cluster) = $Cluster.Name
91-
($LocalizedData.ID) = $Cluster.Id
92-
($LocalizedData.Datacenter) = $Cluster | Get-Datacenter
93-
($LocalizedData.NumberOfHosts) = $Cluster.ExtensionData.Host.Count
94-
($LocalizedData.NumberOfVMs) = ($Cluster | Get-VM).Count
95-
($LocalizedData.HAEnabled) = if ($Cluster.HAEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
96-
($LocalizedData.DRSEnabled) = if ($Cluster.DRSEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
97-
($LocalizedData.VSANEnabled) = if ($Cluster.VsanEnabled -or $Cluster.VsanEsaEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
98-
($LocalizedData.EVCMode) = if ($Cluster.EVCMode) { $EvcModeLookup."$($Cluster.EVCMode)" } else { $LocalizedData.Disabled }
99-
($LocalizedData.VMSwapFilePolicy) = switch ($Cluster.VMSwapfilePolicy) {
90+
$LocalizedData.Cluster = $Cluster.Name
91+
$LocalizedData.ID = $Cluster.Id
92+
$LocalizedData.Datacenter = ($Cluster | Get-Datacenter).Name
93+
$LocalizedData.NumberOfHosts = $Cluster.ExtensionData.Host.Count
94+
$LocalizedData.NumberOfVMs = ($Cluster | Get-VM).Count
95+
$LocalizedData.HAEnabled = if ($Cluster.HAEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
96+
$LocalizedData.DRSEnabled = if ($Cluster.DRSEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
97+
$LocalizedData.VSANEnabled = if ($Cluster.VsanEnabled -or $Cluster.VsanEsaEnabled) { $LocalizedData.Enabled } else { $LocalizedData.Disabled }
98+
$LocalizedData.EVCMode = if ($Cluster.EVCMode) { $EvcModeLookup."$($Cluster.EVCMode)" } else { $LocalizedData.Disabled }
99+
$LocalizedData.VMSwapFilePolicy = switch ($Cluster.VMSwapfilePolicy) {
100100
'WithVM' { $LocalizedData.SwapVMDirectory }
101101
'InHostDatastore' { $LocalizedData.SwapHostDatastore }
102102
default { $Cluster.VMSwapfilePolicy }

AsBuiltReport.VMware.vSphere/Src/Private/Get-AbrVSphereNetwork.ps1

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function Get-AbrVSphereNetwork {
3131
$VDSInfo = foreach ($VDS in $VDSwitches) {
3232
[PSCustomObject]@{
3333
$LocalizedData.VDSwitch = $VDS.Name
34-
$LocalizedData.Datacenter = $VDS.Datacenter
34+
$LocalizedData.Datacenter = $VDS.Datacenter.Name
3535
$LocalizedData.Manufacturer = $VDS.Vendor
3636
$LocalizedData.Version = $VDS.Version
3737
$LocalizedData.NumUplinks = $VDS.NumUplinkPorts
@@ -62,7 +62,7 @@ function Get-AbrVSphereNetwork {
6262
$VDSwitchDetail = [PSCustomObject]@{
6363
$LocalizedData.VDSwitch = $VDS.Name
6464
$LocalizedData.ID = $VDS.Id
65-
$LocalizedData.Datacenter = $VDS.Datacenter
65+
$LocalizedData.Datacenter = $VDS.Datacenter.Name
6666
$LocalizedData.Manufacturer = $VDS.Vendor
6767
$LocalizedData.Version = $VDS.Version
6868
$LocalizedData.NumberOfUplinks = $VDS.NumUplinkPorts
@@ -121,11 +121,11 @@ function Get-AbrVSphereNetwork {
121121
Section -Style Heading4 $LocalizedData.UplinkPorts {
122122
$VdsUplinkDetail = foreach ($VdsUplink in $VdsUplinks) {
123123
[PSCustomObject]@{
124-
$LocalizedData.VDSwitch = $VdsUplink.Switch
125-
$LocalizedData.Host = $VdsUplink.ProxyHost
124+
$LocalizedData.VDSwitch = [string]$VdsUplink.Switch
125+
$LocalizedData.Host = [string]$VdsUplink.ProxyHost
126126
$LocalizedData.UplinkName = $VdsUplink.Name
127-
$LocalizedData.PhysicalNetworkAdapter = $VdsUplink.ConnectedEntity
128-
$LocalizedData.UplinkPortGroup = $VdsUplink.Portgroup
127+
$LocalizedData.PhysicalNetworkAdapter = [string]$VdsUplink.ConnectedEntity
128+
$LocalizedData.UplinkPortGroup = [string]$VdsUplink.Portgroup
129129
}
130130
}
131131
$TableParams = @{
@@ -145,7 +145,7 @@ function Get-AbrVSphereNetwork {
145145
if ($VDSecurityPolicy) {
146146
Section -Style Heading4 $LocalizedData.VDSSecurity {
147147
$VDSecurityPolicyDetail = [PSCustomObject]@{
148-
$LocalizedData.VDSwitch = $VDSecurityPolicy.VDSwitch
148+
$LocalizedData.VDSwitch = $VDSecurityPolicy.VDSwitch.Name
149149
$LocalizedData.AllowPromiscuous = if ($VDSecurityPolicy.AllowPromiscuous) {
150150
$LocalizedData.Accept
151151
} else {
@@ -184,7 +184,7 @@ function Get-AbrVSphereNetwork {
184184
Section -Style Heading4 $LocalizedData.VDSTrafficShaping {
185185
$VDSTrafficShapingDetail = foreach ($VDSTrafficShape in $VDSTrafficShaping) {
186186
[PSCustomObject]@{
187-
$LocalizedData.VDSwitch = $VDSTrafficShape.VDSwitch
187+
$LocalizedData.VDSwitch = $VDSTrafficShape.VDSwitch.Name
188188
$LocalizedData.Direction = $VDSTrafficShape.Direction
189189
$LocalizedData.Status = if ($VDSTrafficShape.Enabled) {
190190
$LocalizedData.Enabled
@@ -216,8 +216,8 @@ function Get-AbrVSphereNetwork {
216216
$VDSPortgroupDetail = foreach ($VDSPortgroup in $VDSPortgroups) {
217217
[PSCustomObject]@{
218218
$LocalizedData.PortGroup = $VDSPortgroup.Name
219-
$LocalizedData.VDSwitch = $VDSPortgroup.VDSwitch
220-
$LocalizedData.Datacenter = $VDSPortgroup.Datacenter
219+
$LocalizedData.VDSwitch = $VDSPortgroup.VDSwitch.Name
220+
$LocalizedData.Datacenter = $VDSPortgroup.Datacenter.Name
221221
$LocalizedData.VLANConfiguration = if ($VDSPortgroup.VlanConfiguration) {
222222
$VDSPortgroup.VlanConfiguration
223223
} else {
@@ -255,7 +255,7 @@ function Get-AbrVSphereNetwork {
255255
Section -Style NOTOCHeading5 -ExcludeFromTOC $LocalizedData.VDSPortGroupSecurity {
256256
$VDSSecurityPolicies = foreach ($VDSSecurityPolicy in $VDSPortgroupSecurity) {
257257
[PSCustomObject]@{
258-
$LocalizedData.PortGroup = $VDSSecurityPolicy.VDPortgroup
258+
$LocalizedData.PortGroup = [string]$VDSSecurityPolicy.VDPortgroup
259259
$LocalizedData.VDSwitch = $VDS.Name
260260
$LocalizedData.AllowPromiscuous = if ($VDSSecurityPolicy.AllowPromiscuous) {
261261
$LocalizedData.Accept
@@ -296,7 +296,7 @@ function Get-AbrVSphereNetwork {
296296
Section -Style NOTOCHeading5 -ExcludeFromTOC $LocalizedData.VDSPortGroupTrafficShaping {
297297
$VDSPortgroupTrafficShapingDetail = foreach ($VDSPortgroupTrafficShape in $VDSPortgroupTrafficShaping) {
298298
[PSCustomObject]@{
299-
$LocalizedData.PortGroup = $VDSPortgroupTrafficShape.VDPortgroup
299+
$LocalizedData.PortGroup = [string]$VDSPortgroupTrafficShape.VDPortgroup
300300
$LocalizedData.VDSwitch = $VDS.Name
301301
$LocalizedData.Direction = $VDSPortgroupTrafficShape.Direction
302302
$LocalizedData.Status = if ($VDSPortgroupTrafficShape.Enabled) {
@@ -328,7 +328,7 @@ function Get-AbrVSphereNetwork {
328328
Section -Style NOTOCHeading5 -ExcludeFromTOC $LocalizedData.VDSPortGroupTeaming {
329329
$VDSPortgroupNICTeaming = foreach ($VDUplink in $VDUplinkTeamingPolicy) {
330330
[PSCustomObject]@{
331-
$LocalizedData.PortGroup = $VDUplink.VDPortgroup
331+
$LocalizedData.PortGroup = [string]$VDUplink.VDPortgroup
332332
$LocalizedData.VDSwitch = $VDS.Name
333333
$LocalizedData.LoadBalancing = switch ($VDUplink.LoadBalancingPolicy) {
334334
'LoadbalanceSrcId' { $LocalizedData.LoadBalanceSrcId }

AsBuiltReport.VMware.vSphere/Src/Private/Get-AbrVSphereResourcePool.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function Get-AbrVSphereResourcePool {
3131
$ResourcePoolInfo = foreach ($ResourcePool in $ResourcePools) {
3232
[PSCustomObject]@{
3333
$LocalizedData.ResourcePool = $ResourcePool.Name
34-
$LocalizedData.Parent = $ResourcePool.Parent
34+
$LocalizedData.Parent = $ResourcePool.Parent.Name
3535
$LocalizedData.CPUSharesLevel = $ResourcePool.CpuSharesLevel
3636
$LocalizedData.CPUReservationMHz = $ResourcePool.CpuReservationMHz
3737
$LocalizedData.CPULimitMHz = switch ($ResourcePool.CpuLimitMHz) {
@@ -65,7 +65,7 @@ function Get-AbrVSphereResourcePool {
6565
$ResourcePoolDetail = [PSCustomObject]@{
6666
$LocalizedData.ResourcePool = $ResourcePool.Name
6767
$LocalizedData.ID = $ResourcePool.Id
68-
$LocalizedData.Parent = $ResourcePool.Parent
68+
$LocalizedData.Parent = $ResourcePool.Parent.Name
6969
$LocalizedData.CPUSharesLevel = $ResourcePool.CpuSharesLevel
7070
$LocalizedData.NumCPUShares = $ResourcePool.NumCpuShares
7171
$LocalizedData.CPUReservation = "$($ResourcePool.CpuReservationMHz) MHz"

AsBuiltReport.VMware.vSphere/Src/Private/Get-AbrVSphereVMHost.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function Get-AbrVSphereVMHost {
3333
$LocalizedData.VMHost = $VMHost.Name
3434
$LocalizedData.Version = $VMHost.Version
3535
$LocalizedData.Build = $VMHost.Build
36-
$LocalizedData.Parent = $VMHost.Parent
36+
$LocalizedData.Parent = $VMHost.Parent.Name
3737
$LocalizedData.ConnectionState = switch ($VMHost.ConnectionState) {
3838
'NotResponding' { $LocalizedData.NotResponding }
3939
'Maintenance' { $LocalizedData.Maintenance }

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [[2.0.0](https://github.com/AsBuiltReport/AsBuiltReport.VMware.vSphere/releases/tag/v2.0.0)] - 2026-03-05
1111

12+
### Fixed
13+
14+
- Fix `An item with the same key has already been added. Key: LinkedView` error when generating TEXT format reports ([#130](https://github.com/AsBuiltReport/AsBuiltReport.VMware.vSphere/issues/130))
15+
1216
### Added
1317

1418
- Modular architecture: each report section is now a dedicated private function (`Get-AbrVSphere*`)

0 commit comments

Comments
 (0)