#region Get-ProxmoxInventory Function Get-ProxmoxInventory { <# .SYNOPSIS Collects the compute inventory from a Proxmox Virtual Environment cluster and returns a normalized model. .DESCRIPTION Authentication uses an API token rather than a ticket so that no session state has to be maintained. The token is supplied using the "PVEAPIToken=USER@REALM!TOKENID=SECRET" authorization scheme. The following routes are consumed. /api2/json/cluster/status Cluster name and quorum state /api2/json/nodes Node inventory /api2/json/nodes/{node}/status Per node CPU, memory, and version detail /api2/json/nodes/{node}/network Bridge and interface configuration /api2/json/nodes/{node}/qemu Virtual machine inventory /api2/json/nodes/{node}/qemu/{vmid}/config Virtual machine network interface configuration /api2/json/nodes/{node}/lxc Container inventory /api2/json/nodes/{node}/lxc/{vmid}/config Container network interface configuration Bridges become virtual switches within Rackpad and guest network interfaces become ports attached to those virtual switches, which is what produces a readable virtual topology alongside the physical rack view. .PARAMETER BaseURL The base URL of a Proxmox node or of the cluster virtual address, for example https://proxmox.example.com:8006 .PARAMETER TokenID The API token identifier in USER@REALM!TOKENNAME format. .PARAMETER TokenSecret The API token secret. .PARAMETER SkipCertificateCheck Ignores server certificate validation errors. Usually required because nodes present a self signed certificate. .PARAMETER IncludeTemplates Includes guests that are marked as templates. Excluded by default because templates are not running workloads. .EXAMPLE $ProxmoxInventory = Get-ProxmoxInventory -BaseURL 'https://proxmox.example.com:8006' -TokenID 'automation@pve!rackpad' -TokenSecret $TokenSecret -SkipCertificateCheck .NOTES Author: Grace Solutions Version: 2026.08.02.0000 #> [CmdletBinding()] Param ( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [System.URI]$BaseURL, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String]$TokenID, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String]$TokenSecret, [Parameter(Mandatory=$False)] [Switch]$SkipCertificateCheck, [Parameter(Mandatory=$False)] [Switch]$IncludeTemplates ) Try { $NormalizedBaseURL = [System.URI]"$("$($BaseURL.AbsoluteUri)".TrimEnd('/'))" $BaseAddress = "$($NormalizedBaseURL.AbsoluteUri)".TrimEnd('/') $ProxmoxHeader = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $ProxmoxHeader.Authorization = "PVEAPIToken=$($TokenID)=$($TokenSecret)" $ProxmoxHeader.Accept = 'application/json' #region Define the data retrieval logic [ScriptBlock]$GetProxmoxResult = { Param ( [String]$Path ) $ResultObject = $Null $Response = Invoke-RestAPIRequest -URI ([System.URI]"$($BaseAddress)$($Path)") -Method 'GET' -Header ($ProxmoxHeader) -SkipCertificateCheck:($SkipCertificateCheck.IsPresent) -AcceptableStatusCodeList @('403', '404', '500', '501', '595', '596') Switch (($Response.StatusCode -ge 200) -and ($Response.StatusCode -lt 300)) { {($_ -eq $True)} { $ResultObject = $Response.Content.data } {($_ -eq $False)} { Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - The Proxmox route `"$($Path)`" returned status code $($Response.StatusCode). [$($Response.StatusDescription)] [$($Response.ErrorMessage)]" } } Write-Output -InputObject ($ResultObject) } #endregion #region Define the network interface parsing logic [ScriptBlock]$ConvertToInterfaceObject = { Param ( [String]$InterfaceKey, [String]$InterfaceValue ) $InterfaceSettingTable = New-Object -TypeName 'System.Collections.Generic.Dictionary[[System.String], [System.String]]' ForEach ($InterfaceSegment In "$($InterfaceValue)".Split(',', [System.StringSplitOptions]::RemoveEmptyEntries)) { $SegmentParts = "$($InterfaceSegment)".Split('=', 2) Switch ($SegmentParts.Count -eq 2) { {($_ -eq $True)} { $InterfaceSettingTable["$($SegmentParts[0])".Trim().ToLowerInvariant()] = "$($SegmentParts[1])".Trim() } } } #region Determine the MAC address and adapter model $AdapterModel = '' $MACAddress = '' $KnownAdapterModelList = New-Object -TypeName 'System.Collections.Generic.List[String]' $KnownAdapterModelList.Add('virtio') $KnownAdapterModelList.Add('e1000') $KnownAdapterModelList.Add('e1000e') $KnownAdapterModelList.Add('rtl8139') $KnownAdapterModelList.Add('vmxnet3') ForEach ($KnownAdapterModel In $KnownAdapterModelList) { Switch ($InterfaceSettingTable.ContainsKey($KnownAdapterModel)) { {($_ -eq $True)} { $AdapterModel = "$($KnownAdapterModel)" $MACAddress = "$($InterfaceSettingTable[$KnownAdapterModel])" } } } Switch ($InterfaceSettingTable.ContainsKey('hwaddr')) { {($_ -eq $True)} { $MACAddress = "$($InterfaceSettingTable['hwaddr'])" $AdapterModel = 'veth' } } #endregion $IPAddress = '' Switch ($InterfaceSettingTable.ContainsKey('ip')) { {($_ -eq $True)} { Switch ("$($InterfaceSettingTable['ip'])" -imatch '^(?
\d{1,3}(\.\d{1,3}){3})') { {($_ -eq $True)} { $IPAddress = "$($Matches['Address'])" } } } } $InterfaceProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $InterfaceProperties.Name = Switch ($InterfaceSettingTable.ContainsKey('name')) {{($_ -eq $True)} {"$($InterfaceSettingTable['name'])"} Default {"$($InterfaceKey)"}} $InterfaceProperties.ConfigurationKey = "$($InterfaceKey)" $InterfaceProperties.MACAddress = "$($MACAddress)".ToUpperInvariant() $InterfaceProperties.AdapterModel = "$($AdapterModel)" $InterfaceProperties.BridgeName = Switch ($InterfaceSettingTable.ContainsKey('bridge')) {{($_ -eq $True)} {"$($InterfaceSettingTable['bridge'])"} Default {''}} $InterfaceProperties.VLANID = Switch ($InterfaceSettingTable.ContainsKey('tag')) {{($_ -eq $True)} {"$($InterfaceSettingTable['tag'])"} Default {''}} $InterfaceProperties.IPAddress = "$($IPAddress)" $InterfaceProperties.IsFirewalled = Switch ($InterfaceSettingTable.ContainsKey('firewall')) {{($_ -eq $True)} {"$($InterfaceSettingTable['firewall'])" -ieq '1'} Default {$False}} Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($InterfaceProperties)) } #endregion #region Determine the cluster name $ClusterName = '' $ClusterStatusList = $GetProxmoxResult.InvokeReturnAsIs('/api2/json/cluster/status') ForEach ($ClusterStatusItem In @($ClusterStatusList)) { Switch ("$($ClusterStatusItem.type)" -ieq 'cluster') { {($_ -eq $True)} { $ClusterName = "$($ClusterStatusItem.name)" } } } #endregion #region Enumerate nodes $NodeObjectList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]' $DiscoveredNodeList = @($GetProxmoxResult.InvokeReturnAsIs('/api2/json/nodes')) $DiscoveredNodeListCount = ($DiscoveredNodeList | Measure-Object).Count Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - $($DiscoveredNodeListCount) Proxmox node(s) were discovered." For ($DiscoveredNodeIndex = 0; $DiscoveredNodeIndex -lt $DiscoveredNodeListCount; $DiscoveredNodeIndex++) { $DiscoveredNode = $DiscoveredNodeList[$DiscoveredNodeIndex] $NodeName = "$($DiscoveredNode.node)" $WriteProgressParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $WriteProgressParameters.Activity = 'Collecting the Proxmox inventory' $WriteProgressParameters.Status = "Processing node `"$($NodeName)`" ($($DiscoveredNodeIndex + 1) of $($DiscoveredNodeListCount))" $WriteProgressParameters.PercentComplete = [System.Math]::Round((($DiscoveredNodeIndex + 1) / $DiscoveredNodeListCount) * 100, 2) $WriteProgressParameters.Id = 21 $Null = Write-Progress @WriteProgressParameters $NodeStatus = $GetProxmoxResult.InvokeReturnAsIs("/api2/json/nodes/$($NodeName)/status") #region Bridges $BridgeObjectList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]' ForEach ($NetworkInterface In @($GetProxmoxResult.InvokeReturnAsIs("/api2/json/nodes/$($NodeName)/network"))) { Switch ("$($NetworkInterface.type)" -imatch '^(bridge|OVSBridge)$') { {($_ -eq $True)} { $BridgeProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $BridgeProperties.Name = "$($NetworkInterface.iface)" $BridgeProperties.Type = "$($NetworkInterface.type)" $BridgeProperties.UplinkPortList = "$($NetworkInterface.bridge_ports)".Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) $BridgeProperties.CIDR = "$($NetworkInterface.cidr)" $BridgeProperties.Gateway = "$($NetworkInterface.gateway)" $BridgeProperties.IsVLANAware = ("$($NetworkInterface.bridge_vlan_aware)" -ieq '1') $BridgeProperties.Comment = "$($NetworkInterface.comments)".Trim() $BridgeProperties.IsActive = ("$($NetworkInterface.active)" -ieq '1') $BridgeObjectList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($BridgeProperties))) } } } #endregion #region Guests $GuestObjectList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]' $GuestRouteTable = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $GuestRouteTable.qemu = 'vm' $GuestRouteTable.lxc = 'container' ForEach ($GuestRouteEntry In $GuestRouteTable.GetEnumerator()) { ForEach ($DiscoveredGuest In @($GetProxmoxResult.InvokeReturnAsIs("/api2/json/nodes/$($NodeName)/$($GuestRouteEntry.Key)"))) { $GuestIsTemplate = ("$($DiscoveredGuest.template)" -ieq '1') $GuestIsIncluded = ($GuestIsTemplate -eq $False) -or ($IncludeTemplates.IsPresent -eq $True) Switch ($GuestIsIncluded) { {($_ -eq $True)} { $GuestConfiguration = $GetProxmoxResult.InvokeReturnAsIs("/api2/json/nodes/$($NodeName)/$($GuestRouteEntry.Key)/$($DiscoveredGuest.vmid)/config") $GuestInterfaceList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]' ForEach ($ConfigurationProperty In @($GuestConfiguration.PSObject.Properties)) { Switch ("$($ConfigurationProperty.Name)" -imatch '^net\d+$') { {($_ -eq $True)} { $GuestInterfaceList.Add($ConvertToInterfaceObject.InvokeReturnAsIs("$($ConfigurationProperty.Name)", "$($ConfigurationProperty.Value)")) } } } $GuestTagList = New-Object -TypeName 'System.Collections.Generic.List[String]' ForEach ($GuestTag In "$($DiscoveredGuest.tags)$($GuestConfiguration.tags)".Split(';', [System.StringSplitOptions]::RemoveEmptyEntries)) { Switch ($GuestTagList.Contains("$($GuestTag)".Trim())) { {($_ -eq $False)} { $GuestTagList.Add("$($GuestTag)".Trim()) } } } $GuestProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $GuestProperties.VMID = "$($DiscoveredGuest.vmid)" $GuestProperties.Name = Switch ([String]::IsNullOrWhiteSpace("$($DiscoveredGuest.name)")) {{($_ -eq $False)} {"$($DiscoveredGuest.name)"} Default {"$($GuestRouteEntry.Value)-$($DiscoveredGuest.vmid)"}} $GuestProperties.GuestType = "$($GuestRouteEntry.Value)" $GuestProperties.Status = Switch ("$($DiscoveredGuest.status)") {{($_ -ieq 'running')} {'online'} {($_ -ieq 'stopped')} {'offline'} Default {'unknown'}} $GuestProperties.IsTemplate = $GuestIsTemplate $GuestProperties.CPUCoreCount = Switch ($Null -ine $DiscoveredGuest.cpus) {{($_ -eq $True)} {[Int]"$($DiscoveredGuest.cpus)"} Default {$Null}} $GuestProperties.MemoryGB = Try {[System.Math]::Round(([Double]"$($DiscoveredGuest.maxmem)" / 1GB), 2)} Catch {$Null} $GuestProperties.StorageGB = Try {[System.Math]::Round(([Double]"$($DiscoveredGuest.maxdisk)" / 1GB), 2)} Catch {$Null} $GuestProperties.OperatingSystemType = "$($GuestConfiguration.ostype)" $GuestProperties.Description = "$($GuestConfiguration.description)".Trim() $GuestProperties.TagList = $GuestTagList $GuestProperties.InterfaceList = $GuestInterfaceList $GuestProperties.NodeName = "$($NodeName)" $GuestObjectList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($GuestProperties))) } } } } #endregion $NodeProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $NodeProperties.Name = "$($NodeName)" $NodeProperties.Status = Switch ("$($DiscoveredNode.status)") {{($_ -ieq 'online')} {'online'} {($_ -ieq 'offline')} {'offline'} Default {'unknown'}} $NodeProperties.IPAddress = "$($DiscoveredNode.ip)" $NodeProperties.CPUCoreCount = Switch ($Null -ine $DiscoveredNode.maxcpu) {{($_ -eq $True)} {[Int]"$($DiscoveredNode.maxcpu)"} Default {$Null}} $NodeProperties.MemoryGB = Try {[System.Math]::Round(([Double]"$($DiscoveredNode.maxmem)" / 1GB), 2)} Catch {$Null} $NodeProperties.StorageGB = Try {[System.Math]::Round(([Double]"$($DiscoveredNode.maxdisk)" / 1GB), 2)} Catch {$Null} $NodeProperties.Version = "$($NodeStatus.pveversion)" $NodeProperties.CPUModel = "$($NodeStatus.cpuinfo.model)" $NodeProperties.BridgeList = $BridgeObjectList $NodeProperties.GuestList = $GuestObjectList $NodeObjectList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($NodeProperties))) Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Collected $($BridgeObjectList.Count) bridge(s) and $($GuestObjectList.Count) guest(s) from Proxmox node `"$($NodeName)`"." } $Null = Write-Progress -Activity 'Collecting the Proxmox inventory' -Id 21 -Completed #endregion $InventoryProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' $InventoryProperties.SourceType = 'Proxmox' $InventoryProperties.BaseURL = $NormalizedBaseURL $InventoryProperties.ClusterName = "$($ClusterName)" $InventoryProperties.RetrievedAt = [System.DateTime]::UtcNow $InventoryProperties.NodeList = $NodeObjectList Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($InventoryProperties)) } Catch { Throw } } #endregion