366 lines
27 KiB
PowerShell
366 lines
27 KiB
PowerShell
#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 '^(?<Address>\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
|