migrate data gathering to libvirtxml parsing

- instead of getting all the data the hard way, use libvirtxml
  to parse the XML from libvirt
- this makes it more accurate, and more future proof when schema
  changes occur
- add pcidb to query devices better
This commit is contained in:
Matthew Stobbs 2024-03-19 15:25:57 -06:00
parent 48bdc94351
commit de93204e3d
8 changed files with 323 additions and 299 deletions

19
cluster/lock/lock.go Normal file
View File

@ -0,0 +1,19 @@
// Package lock implements a locking mechanism on shared resources to ensure
// they don't get used at the same time. There needs to be a lock for the following:
// - VM on Host: A VM must only exist on one host at a time
// - Storage attached to VM: Block storage can only be attached to one VM at a time
package lock
// Locker interface used to lock and unlock Lockable resources
type Locker interface {
Lock(Lockable) error
Unlock(Lockable) error
}
// Lockable interface must be attached to lockable resources, such as
// Virtual Machines, block devices, and host devices that can be attached
// to virtual machines.
type Lockable interface {
Locked() bool
HeldBy() Locker
}

1
cluster/lock/volume.go Normal file
View File

@ -0,0 +1 @@
package lock

View File

@ -1,96 +1,168 @@
package cluster package cluster
// ClusterStats is used to gather stats for the entire cluster // ClusterStats is used to gather stats for the entire cluster
// Combined with StatsDiff, we can get some basic cluster wide stats tracking
type ClusterStats struct { type ClusterStats struct {
CPU struct { // CPU Statistics including number of CPUs
Sockets uint32 CPU CPUStats
Cores uint32 // Memory provides information about the amount of memory, including free and
Threads uint32 // allocated memory
Allocated uint32 Memory MemoryStats
} // Storage provides information about storage pools, Only get's stats for active
Memory struct { // pools, and will not activate pools that are not already active.
Total uint64 // Trys to sort out shared file systems from local filesystems using the Type parameter
Free uint64 // of Host.StoragePoolInfo
Buffers uint64 Storage StorageStats
Cached uint64 // Volume provides information on allocated volumes used in the cluster
Allocated uint64 Volume VolumeStats
} // VM provides VM specific counters for the cluster
Storage struct { VM VMStats
Total uint64 // Host provides Host information for the cluster
Used uint64 Host HostStats
Free uint64 // Network provices available networks, and how many are shared between hosts
Active uint32 Network NetworkStats
Inactive uint32 // NetIF provides information about Libvirt allocated networks, usable by the
Pools uint32 // libvirt cluster
NetIF NetIFStats
Volumes struct {
Total uint32
Active uint32
Inactive uint32
}
}
VM struct {
Count uint32
Started uint32
Stopped uint32
}
Host struct {
Count uint32
Available uint32
}
Network struct {
Count uint32
Active uint32
Inactive uint32
}
old *ClusterStats old *ClusterStats
c *Cluster c *Cluster
} }
// CPUStats provides information about the number of CPUs, Cores,
// Threads, and Speed available to the cluster.
type CPUStats struct {
Sockets uint32
Cores uint32
Threads uint32
Allocated uint32
MHz uint64
}
// MemoryStats provies information about the amount of memory, including free and
// allocated memory. Allocated is the total allocated to Guests
type MemoryStats struct {
Total uint64
Free uint64
Buffers uint64
Cached uint64
Allocated uint64
}
// StorageStats provides information about the available storage pools in the cluster,
// including the amount of space available, allocated, and how many pools are shared
// between hosts
type StorageStats struct {
Total uint64
Used uint64
Free uint64
Active uint32
Inactive uint32
Pools uint32
}
// VolumeStats provides information about the number of volumes on the cluster.
// Counts volumes in shared storage (as detmermined by StorageStats) only once
type VolumeStats struct {
Total uint32
Active uint32
Inactive uint32
}
// VMStats provides information about the defined Virtual Machines on the cluster
type VMStats struct {
Count uint32
Started uint32
Stopped uint32
}
// HostStats provides informatoin about the number of hosts defined, and how many
// are currently available. An unavailable host will not have it's statistics counted
type HostStats struct {
Count uint32
Available uint32
Nodes uint32
}
// NetworkStats provides informatoin about the available Host network connections,
// including bridges and ethernet devices.
type NetworkStats struct {
Count uint32
Active uint32
Inactive uint32
}
// NetIFStats provides information about Libvirt defined networks
type NetIFStats struct {
Count uint32
Common uint32
Active uint32
Inactive uint32
}
// DeviceStats provides information about the number of allocatable devices in the
// cluster. These are PCI and USB devices.
type DeviceStats struct {
Count uint32
}
// SecretStats provides the number of secrets defined throughout the cluster.
// Shared secrets are only counted once, and are recognized by their UUID
type SecretStats struct {
Count uint32
Shared uint32
}
// ClusterStats is used to gather stats for the entire cluster // ClusterStats is used to gather stats for the entire cluster
type StatDiff struct { type StatDiff struct {
CPU struct { CPU CPUDiff
Sockets int Memory MemoryDiff
Cores int Storage StorageStats
Threads int Volume VolumeDiff
Allocated int VM VMDiff
} Host HostDiff
Memory struct { Network NetworkDiff
Total int }
Free int
Buffers int
Cached int
Allocated int
}
Storage struct {
Total int
Used int
Free int
Active int
Inactive int
Pools int
Volumes struct { type CPUDiff struct {
Total int Sockets int
Active int Cores int
Inactive int Threads int
} Allocated int
} }
VM struct { type MemoryDiff struct {
Count int Total int64
Started int Free int64
Stopped int Buffers int64
} Cached int64
Host struct { Allocated int64
Count int }
Available int type StorageDiff struct {
} Total int64
Network struct { Used int64
Count int Free int64
Active int Active int64
Inactive int Inactive int64
} Pools int
}
type VolumeDiff struct {
Total int
Active int
Inactive int
}
type VMDiff struct {
Count int
Started int
Stopped int
}
type HostDiff struct {
Count int
Available int
}
type NetworkDiff struct {
Count int
Active int
Inactive int
} }
// InitStats is given a cluster, which it then uses to load the initial statistics // InitStats is given a cluster, which it then uses to load the initial statistics
@ -144,9 +216,9 @@ func (cs *ClusterStats) Update() {
cs.Storage.Used += sp.Allocation cs.Storage.Used += sp.Allocation
cs.Storage.Free += sp.Capacity - sp.Allocation cs.Storage.Free += sp.Capacity - sp.Allocation
// Volumes in the pool // Volumes in the pool
cs.Storage.Volumes.Total += uint32(len(sp.Volumes)) cs.Volume.Total += uint32(len(sp.Volumes))
for range sp.Volumes { for range sp.Volumes {
cs.Storage.Volumes.Active++ cs.Volume.Active++
} }
} }
@ -225,9 +297,9 @@ func (cs *ClusterStats) Diff() StatDiff {
Active int Active int
Inactive int Inactive int
}{ }{
Total: int(cs.old.Storage.Volumes.Total - cs.Storage.Volumes.Total), Total: int(cs.old.Volume.Total - cs.Volume.Total),
Active: int(cs.old.Storage.Volumes.Active - cs.Storage.Volumes.Active), Active: int(cs.old.Volume.Active - cs.Volume.Active),
Inactive: int(cs.old.Storage.Volumes.Inactive - cs.Storage.Volumes.Inactive), Inactive: int(cs.old.Volume.Inactive - cs.Volume.Inactive),
}, },
}, },
VM: struct { VM: struct {

2
go.mod
View File

@ -14,5 +14,7 @@ require (
require ( require (
github.com/blend/go-sdk v1.20220411.3 // indirect github.com/blend/go-sdk v1.20220411.3 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jaypipes/pcidb v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.0.0 // indirect
golang.org/x/image v0.11.0 // indirect golang.org/x/image v0.11.0 // indirect
) )

4
go.sum
View File

@ -8,6 +8,10 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jaypipes/pcidb v1.0.0 h1:vtZIfkiCUE42oYbJS0TAq9XSfSmcsgo9IdxSm9qzYU8=
github.com/jaypipes/pcidb v1.0.0/go.mod h1:TnYUvqhPBzCKnH34KrIX22kAeEbDCSRJ9cqLRCuNDfk=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE= github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE=
github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14= github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

View File

@ -14,7 +14,6 @@ import (
"git.staur.ca/stobbsm/clustvirt/lib/secret" "git.staur.ca/stobbsm/clustvirt/lib/secret"
"git.staur.ca/stobbsm/clustvirt/lib/storagepool" "git.staur.ca/stobbsm/clustvirt/lib/storagepool"
"git.staur.ca/stobbsm/clustvirt/lib/storagevol" "git.staur.ca/stobbsm/clustvirt/lib/storagevol"
"git.staur.ca/stobbsm/clustvirt/util"
"libvirt.org/go/libvirt" "libvirt.org/go/libvirt"
"libvirt.org/go/libvirtxml" "libvirt.org/go/libvirtxml"
) )
@ -27,20 +26,8 @@ type Host struct {
HostName string HostName string
// SystemHostName is the hostname as reported by the system itself // SystemHostName is the hostname as reported by the system itself
SystemHostName string SystemHostName string
// FreeMemory is the available free memory
FreeMemory uint64
// LibVersion is the version of Libvirt on the host // LibVersion is the version of Libvirt on the host
LibVersion uint32 LibVersion uint32
// HostInfo provides basic HW information about a host
HostInfo NodeInfo
// HostSEVInfo provides informatoin about AMD SEV extentions available on the host
HostSEVInfo SEVInfo
// AvailableCPUTypes are the available types of CPUs that can be used for VM creation
AvailableCPUTypes []string
// NodeMemory provides basic memory information about the Host
NodeMemory NodeMemoryInfo
// StorageCapabilities is the XML representation of the hosts storage capabilities
StorageCapabilities string
// SysInfo is the XML representation of the host system information // SysInfo is the XML representation of the host system information
SysInfo string SysInfo string
// Alive indicates if the connection is alive // Alive indicates if the connection is alive
@ -49,18 +36,24 @@ type Host struct {
Encrypted bool Encrypted bool
// Secure indicates if the connection is secure // Secure indicates if the connection is secure
Secure bool Secure bool
// HostInfo provides basic HW information about a host
HostInfo libvirtxml.CapsHost
// NodeMemory provides basic memory information about the Host
NodeMemory NodeMemoryInfo
// VMList is the list of virtual machines available to the host // VMList is the list of virtual machines available to the host
VMList []VMInfo VMList []libvirtxml.Domain
// NetIfList is the list of network interfaces on the host // NetIfList is the list of network interfaces on the host
NetIfFList []NetIfInfo NetIfFList []libvirtxml.Interface
// NetworkList is the list of defined networks on the host // NetworkList is the list of defined networks on the host
NetworkList []NetworkInfo NetworkList []libvirtxml.Network
// DeviceList is the list of devices on the host // DeviceList is the list of devices on the host
DeviceList []DeviceInfo DeviceList []libvirtxml.NodeDevice
// SecretList provides a list of secrets available to the host // SecretList provides a list of secrets available to the host
SecretList []SecretInfo SecretList []libvirtxml.Secret
// StoragePoolList provides the list of stoarge ppols available to the host // StoragePoolList provides the list of stoarge ppols available to the host
StoragePoolList []StoragePoolInfo StoragePoolList []libvirtxml.StoragePool
// VolumeList is the list of volumes available on the host
VolumeList []libvirtxml.StorageVolume
uri *URI uri *URI
conn *libvirt.Connect conn *libvirt.Connect
@ -68,108 +61,6 @@ type Host struct {
closeErr chan error closeErr chan error
} }
// DeviceInfo holds basic information for host devices
type DeviceInfo struct {
Name string
Capabilities []string
XML string
}
// VMInfo holds basic VM information, like the name and ID
type VMInfo struct {
Name string
ID uint
UUID []byte
XML string
Active bool
VCPUs uint
Memory uint
// States are the current states active on the host
States []guest.VMState
}
// SecretInfo doesn't let you see the contents of secrets, but does let you see what secrets have
// been defined in a simple format.
type SecretInfo struct {
UUID string
XML string
Type string
}
// StoragePoolInfo holds basic information on storage pools
type StoragePoolInfo struct {
Name string
UUID []byte
Type string
XML string
Active bool
Persistent bool
AutoStart bool
State string
Capacity uint64
Allocation uint64
Available uint64
IsNet bool
// HAEnabled indicates if the storage pool has High Availability
HAEnabled bool
// Volumes defined in the storage pool
Volumes []VolumeInfo
}
// VolumeInfo holds basic information about Volumes available in storage pools
type VolumeInfo struct {
Name string
Key string
Path string
Type string
Capacity uint64
Allocation uint64
XML string
}
// NetIfInfo holds basic information about available network interfaces (not their connections, the devices themselves)
type NetIfInfo struct {
Name string
MacAddr string
XML string
}
// NetworkInfo holds basic information about network connections
type NetworkInfo struct {
Name string
UUID []byte
XML string
Active bool
// NetIf is the network interface this connection is applied to
NetIf NetIfInfo
}
// NodeInfo represents the basic HW info for a host node
type NodeInfo struct {
// livirt.NodeInfo section
Model string
Memory uint64
Cpus uint
MHz uint
Nodes uint32
Sockets uint32
Cores uint32
Threads uint32
}
// SEVInfo provides information about AMD SEV support
type SEVInfo struct {
// livirt.NodeSEVParameters section
SEVEnabled bool
PDH string
CertChain string
CBitPos uint
ReducedPhysBits uint
MaxGuests uint
MaxEsGuests uint
CPU0ID string
}
// NodeMemoryInfo provides statistis about node memory usage from libvirt.NodeMemoryStats // NodeMemoryInfo provides statistis about node memory usage from libvirt.NodeMemoryStats
type NodeMemoryInfo struct { type NodeMemoryInfo struct {
Total uint64 Total uint64
@ -234,7 +125,7 @@ func (h *Host) Close() error {
// private methods that load the different informational parts // private methods that load the different informational parts
func (h *Host) getInfo() { func (h *Host) getInfo() {
var wg = new(sync.WaitGroup) wg := new(sync.WaitGroup)
infoFuncs := []func(){ infoFuncs := []func(){
h.getDevicesInfo, h.getDevicesInfo,
@ -242,7 +133,6 @@ func (h *Host) getInfo() {
h.getIfaceInfo, h.getIfaceInfo,
h.getNetsInfo, h.getNetsInfo,
h.getNodeInfo, h.getNodeInfo,
// h.getSEVInfo,
// h.getSecretsInfo, // h.getSecretsInfo,
// h.getStoragePools, // h.getStoragePools,
} }
@ -298,13 +188,13 @@ func (h *Host) getStoragePools() {
log.Println(err) log.Println(err)
} }
h.StoragePoolList[i].Type = spoolXML.Type h.StoragePoolList[i].Type = spoolXML.Type
for _, t := range storagepool.NetTypes { for _, t := range storagepool.NetTypes {
if h.StoragePoolList[i].Type == t { if h.StoragePoolList[i].Type == t {
h.StoragePoolList[i].IsNet = true h.StoragePoolList[i].IsNet = true
h.StoragePoolList[i].HAEnabled = true h.StoragePoolList[i].HAEnabled = true
continue continue
} }
} }
svols, err := s.ListAllStorageVolumes(0) svols, err := s.ListAllStorageVolumes(0)
if err != nil { if err != nil {
@ -432,34 +322,6 @@ func (h *Host) getNodeInfo() {
} }
} }
func (h *Host) getSEVInfo() {
// getSEVInfo
h.HostSEVInfo.SEVEnabled = true
ns, err := h.conn.GetSEVInfo(0)
if err != nil {
log.Println(err)
lverr, ok := err.(libvirt.Error)
if ok {
switch lverr.Code {
case 84:
log.Println("SEV functions not supported")
h.HostSEVInfo.SEVEnabled = false
default:
log.Println("Error encountered", lverr.Error())
}
}
}
if h.HostSEVInfo.SEVEnabled {
h.HostSEVInfo.PDH = util.SetNotSet(ns.PDH, ns.PDHSet)
h.HostSEVInfo.CertChain = util.SetNotSet(ns.CertChain, ns.CertChainSet)
h.HostSEVInfo.CBitPos = ns.CBitPos
h.HostSEVInfo.ReducedPhysBits = ns.ReducedPhysBits
h.HostSEVInfo.MaxGuests = ns.MaxGuests
h.HostSEVInfo.MaxEsGuests = ns.MaxEsGuests
h.HostSEVInfo.CPU0ID = util.SetNotSet(ns.CPU0ID, ns.CPU0IDSet)
}
}
func (h *Host) getDomainInfo() { func (h *Host) getDomainInfo() {
// getDomainInfo // getDomainInfo
doms, err := h.conn.ListAllDomains(0) doms, err := h.conn.ListAllDomains(0)
@ -482,15 +344,15 @@ func (h *Host) getDomainInfo() {
if h.VMList[i].XML, err = d.GetXMLDesc(0); err != nil { if h.VMList[i].XML, err = d.GetXMLDesc(0); err != nil {
log.Println(err) log.Println(err)
} }
vmXML := &libvirtxml.Domain{} vmXML := &libvirtxml.Domain{}
if err = vmXML.Unmarshal(h.VMList[i].XML); err != nil { if err = vmXML.Unmarshal(h.VMList[i].XML); err != nil {
log.Println(err) log.Println(err)
} }
h.VMList[i].VCPUs = vmXML.VCPU.Value h.VMList[i].VCPUs = vmXML.VCPU.Value
h.VMList[i].Memory = vmXML.CurrentMemory.Value h.VMList[i].Memory = vmXML.CurrentMemory.Value
if h.VMList[i].Active, err = d.IsActive(); err != nil { if h.VMList[i].Active, err = d.IsActive(); err != nil {
log.Println(err) log.Println(err)
} }
d.Free() d.Free()
} }
@ -545,9 +407,9 @@ func (h *Host) getNetsInfo() {
if h.NetworkList[i].XML, err = net.GetXMLDesc(0); err != nil { if h.NetworkList[i].XML, err = net.GetXMLDesc(0); err != nil {
log.Println(err) log.Println(err)
} }
if h.NetworkList[i].Active, err = net.IsActive(); err != nil { if h.NetworkList[i].Active, err = net.IsActive(); err != nil {
log.Println(err) log.Println(err)
} }
net.Free() net.Free()
} }
@ -571,6 +433,12 @@ func (h *Host) getDevicesInfo() {
if h.DeviceList[i].XML, err = dev.GetXMLDesc(0); err != nil { if h.DeviceList[i].XML, err = dev.GetXMLDesc(0); err != nil {
log.Println(err) log.Println(err)
} }
dx := &libvirtxml.NodeDevice{}
if err != dx.Unmarshal(h.DeviceList[i].XML); err != nil {
log.Println(err)
}
h.DeviceList[i].Driver = dx.Driver.Name
dx.Capability.PCI.Class
dev.Free() dev.Free()
} }

48
util/pcidb.go Normal file
View File

@ -0,0 +1,48 @@
package util
import (
"log"
"github.com/jaypipes/pcidb"
)
var (
pcidbInitDone = false
db *pcidb.PCIDB
)
const (
pcidbNOTFOUND string = `NOTFOUND`
pcidbNODB string = `NODBFOUND`
)
func initPCIDB() {
var err error
// Attempt to use local sources first, fallback to network if
// local sources aren't found
db, err = pcidb.New()
if err != nil {
log.Printf("warning: couldn't use local pcidb cache: %s", err)
log.Println("falling back to downloading database")
db, err = pcidb.New(pcidb.WithEnableNetworkFetch())
if err != nil {
log.Println("error: couldn't get pcidb. no more fallbacks available, will not be able to query the pcidb")
}
}
pcidbInitDone = true
}
func GetPCIClass(id string) string {
if !pcidbInitDone {
initPCIDB()
}
if pcidbInitDone && db == nil {
log.Println("unable to access pcidb")
return pcidbNODB
}
if class, ok := db.Classes[id]; ok {
return class.Name
}
return pcidbNOTFOUND
}

View File

@ -10,48 +10,58 @@ import (
templ ClusterInfo(cs *cluster.ClusterStats, diff cluster.StatDiff, navbar []components.NavItem) { templ ClusterInfo(cs *cluster.ClusterStats, diff cluster.StatDiff, navbar []components.NavItem) {
@layouts.Manager("ClustVirt", "Cluster Manager", navbar) { @layouts.Manager("ClustVirt", "Cluster Manager", navbar) {
<h3>Cluster Stats</h3> <h3>Cluster Stats</h3>
<table class={ "table-auto", "w-full" }>
<caption class={ "caption-top" }>
CPU stats
</caption>
<thead>
<tr>
<th>Sockets</th>
<th>Cores</th>
<th>Threads</th>
<th>Allocated</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{ fmt.Sprint(cs.CPU.Sockets) }
</td>
<td>
{ fmt.Sprint(cs.CPU.Cores) }
</td>
<td>
{ fmt.Sprint(cs.CPU.Threads) }
</td>
<td>
{ fmt.Sprint(cs.CPU.Allocated) }
</td>
</tr>
<tr>
<td>
{ fmt.Sprint(diff.CPU.Sockets) }
</td>
<td>
{ fmt.Sprint(diff.CPU.Cores) }
</td>
<td>
{ fmt.Sprint(diff.CPU.Threads) }
</td>
<td>
{ fmt.Sprint(diff.CPU.Allocated) }
</td>
</tr>
</tbody>
</table>
} }
} }
templ CPUStats() {
<table class={ "table-auto", "w-full" }>
<caption class={ "caption-top" }>
CPU stats
</caption>
<thead>
<tr>
<th></th>
<th>Sockets</th>
<th>Cores</th>
<th>Threads</th>
<th>Allocated</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Latest
</td>
<td>
{ fmt.Sprint(cs.CPU.Sockets) }
</td>
<td>
{ fmt.Sprint(cs.CPU.Cores) }
</td>
<td>
{ fmt.Sprint(cs.CPU.Threads) }
</td>
<td>
{ fmt.Sprint(cs.CPU.Allocated) }
</td>
</tr>
<tr>
<td>
Change
</td>
<td>
{ fmt.Sprint(diff.CPU.Sockets) }
</td>
<td>
{ fmt.Sprint(diff.CPU.Cores) }
</td>
<td>
{ fmt.Sprint(diff.CPU.Threads) }
</td>
<td>
{ fmt.Sprint(diff.CPU.Allocated) }
</td>
</tr>
</tbody>
</table>
}