Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions internal/libvirt/libvirt.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,13 +444,32 @@ func (l *LibVirt) addAllocationCapacity(old v1.Hypervisor) (v1.Hypervisor, error
totalCpuCapacity := resource.NewQuantity(0, resource.DecimalSI)
cellsById := make(map[uint64]v1.Cell)
for _, cell := range caps.Host.Topology.CellSpec.Cells {
memoryCapacity, err := MemoryToResource(
cell.Memory.Value,
cell.Memory.Unit,
)
// Report the stable, hugepage-backed allocatable memory as the cell's
// memory capacity. All KVM nova flavors use hw:mem_page_size=large, so
// this reflects what can actually be allocated to VMs and, unlike the
// cell's total physical memory, does not fluctuate between hosts.
memoryCapacity, err := HugepageMemoryToResource(cell.Pages)
if err != nil {
return old, err
}
// Fall back to the cell's total physical memory if libvirt reports no
// hugepages for the cell. This should not happen on a correctly
// configured KVM host, but guards against reporting a spurious zero
// capacity (e.g. before hugepages are allocated at boot).
if memoryCapacity.IsZero() {
logger.Log.Info(
"no hugepages reported for numa cell, "+
"falling back to total physical memory",
"cell", cell.ID,
)
memoryCapacity, err = MemoryToResource(
cell.Memory.Value,
cell.Memory.Unit,
)
if err != nil {
return old, err
}
}
totalMemoryCapacity.Add(memoryCapacity)

cpuCapacity := *resource.NewQuantity(
Expand Down
80 changes: 80 additions & 0 deletions internal/libvirt/libvirt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,11 @@ func TestAddAllocationCapacity_InstanceSpreadAcrossMultipleNumaCells(t *testing.
Unit: "GiB",
Value: 256,
},
// 131072 * 2 MiB hugepages = 256 GiB allocatable.
Pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 1000000},
{Unit: "KiB", Size: 2048, Value: 131072},
},
CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
Num: 64,
},
Expand All @@ -500,6 +505,10 @@ func TestAddAllocationCapacity_InstanceSpreadAcrossMultipleNumaCells(t *testing.
Unit: "GiB",
Value: 256,
},
Pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 1000000},
{Unit: "KiB", Size: 2048, Value: 131072},
},
CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
Num: 64,
},
Expand All @@ -510,6 +519,10 @@ func TestAddAllocationCapacity_InstanceSpreadAcrossMultipleNumaCells(t *testing.
Unit: "GiB",
Value: 256,
},
Pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 1000000},
{Unit: "KiB", Size: 2048, Value: 131072},
},
CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
Num: 64,
},
Expand All @@ -520,6 +533,10 @@ func TestAddAllocationCapacity_InstanceSpreadAcrossMultipleNumaCells(t *testing.
Unit: "GiB",
Value: 256,
},
Pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 1000000},
{Unit: "KiB", Size: 2048, Value: 131072},
},
CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
Num: 64,
},
Expand Down Expand Up @@ -706,6 +723,12 @@ func TestAddAllocationCapacity_Success(t *testing.T) {
Unit: "GiB",
Value: 64,
},
// 32768 * 2 MiB hugepages = 64 GiB allocatable.
Pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 1000000},
{Unit: "KiB", Size: 2048, Value: 32768},
{Unit: "KiB", Size: 1048576, Value: 0},
},
CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
Num: 16,
},
Expand Down Expand Up @@ -792,6 +815,63 @@ func TestAddAllocationCapacity_Success(t *testing.T) {
}
}

func TestAddAllocationCapacity_FallsBackToPhysicalMemoryWithoutHugepages(t *testing.T) {
// When libvirt reports no hugepages for a cell, the memory capacity should
// fall back to the cell's total physical memory instead of reporting zero.
caps := capabilities.Capabilities{
Host: capabilities.CapabilitiesHost{
Topology: capabilities.CapabilitiesHostTopology{
CellSpec: capabilities.CapabilitiesHostTopologyCells{
Num: 1,
Cells: []capabilities.CapabilitiesHostTopologyCell{
{
ID: 0,
Memory: capabilities.CapabilitiesHostTopologyCellMemory{
Unit: "GiB",
Value: 64,
},
// Only the base page, no hugepages configured.
Pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 16777216},
},
CPUs: capabilities.CapabilitiesHostTopologyCellCPUs{
Num: 16,
},
},
},
},
},
},
}

l := &LibVirt{
capabilitiesClient: &mockCapabilitiesClient{caps: caps},
domainInfoClient: &mockDomInfoClient{infos: []dominfo.DomainInfo{}},
}

result, err := l.addAllocationCapacity(v1.Hypervisor{})
if err != nil {
t.Fatalf("addAllocationCapacity() returned unexpected error: %v", err)
}

// Capacity should fall back to the 64 GiB physical memory, not 0.
expectedMemCapacity := resource.NewQuantity(64*1024*1024*1024, resource.BinarySI)
memCap := result.Status.Capacity["memory"]
if !memCap.Equal(*expectedMemCapacity) {
t.Errorf("Expected fallback memory capacity %s, got %s",
expectedMemCapacity.String(), memCap.String())
}

if len(result.Status.Cells) != 1 {
t.Fatalf("Expected 1 cell, got %d", len(result.Status.Cells))
}
cellMemCap := result.Status.Cells[0].Capacity["memory"]
if !cellMemCap.Equal(*expectedMemCapacity) {
t.Errorf("Cell 0: Expected fallback memory capacity %s, got %s",
expectedMemCapacity.String(), cellMemCap.String())
}
}

func TestProcess_Success(t *testing.T) {
caps := capabilities.Capabilities{
Host: capabilities.CapabilitiesHost{
Expand Down
54 changes: 54 additions & 0 deletions internal/libvirt/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ package libvirt
import (
"encoding/hex"
"fmt"
"math"

"k8s.io/apimachinery/pkg/api/resource"

"github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt/capabilities"
)

const (
Expand Down Expand Up @@ -81,3 +84,54 @@ func MemoryToResource(value int64, unit string) (resource.Quantity, error) {
// Set the value
return *quantity, nil
}

// HugepageMemoryToResource returns the allocatable hugepage-backed memory of a
// NUMA cell as a resource.Quantity.
//
// Unlike the cell's total physical memory, which fluctuates between otherwise
// identical hosts because linux reserves/frees varying amounts during boot, the
// number of hugepages is explicitly configured and therefore stable. All KVM
// nova flavors use hw:mem_page_size=large, so only non-base (hugepage) page
// sizes are allocatable to VMs. The base page size is the smallest page size
// reported for the cell and is excluded. A cell with no pages, or only base
// pages, yields 0.
func HugepageMemoryToResource(pages []capabilities.CapabilitiesHostTopologyCellPages) (resource.Quantity, error) {
total := resource.NewQuantity(0, resource.BinarySI)
if len(pages) == 0 {
return *total, nil
}

// The base page size is the smallest reported page size. Do not assume it
// is the first entry.
baseSize := pages[0].Size
for _, page := range pages[1:] {
if page.Size < baseSize {
baseSize = page.Size
}
}

for _, page := range pages {
// Skip the base page size, only hugepages are allocatable to VMs.
if page.Size == baseSize {
continue
}
// Libvirt always reports page sizes in KiB. Be defensive in case this
// ever changes, so we don't silently report a wrong capacity.
if page.Unit != UnitKiB {
return resource.Quantity{}, fmt.Errorf(
"unexpected hugepage unit %s", page.Unit,
)
}
// Guard the uint64 -> int64 conversion. Real hugepage counts are far
// below this bound, but guard anyway so an implausible value cannot
// wrap into a negative capacity.
if page.Value > math.MaxInt64 {
return resource.Quantity{}, fmt.Errorf(
"hugepage count %d exceeds int64", page.Value,
)
}
bytes := int64(page.Value) * int64(page.Size) * 1024
total.Add(*resource.NewQuantity(bytes, resource.BinarySI))
}
return *total, nil
}
88 changes: 87 additions & 1 deletion internal/libvirt/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"testing"

"k8s.io/apimachinery/pkg/api/resource"

"github.com/cobaltcore-dev/kvm-node-agent/internal/libvirt/capabilities"
)

func TestMemoryToResourceKiB(t *testing.T) {
Expand Down Expand Up @@ -385,8 +387,92 @@ func TestMemoryToResourceRealWorldScenarios(t *testing.T) {
}
}

func TestHugepageMemoryToResource(t *testing.T) {
tests := []struct {
name string
pages []capabilities.CapabilitiesHostTopologyCellPages
expectedBytes int64
expectError bool
}{
{
name: "base page plus 2 MiB hugepages",
pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 1000000},
{Unit: "KiB", Size: 2048, Value: 10},
{Unit: "KiB", Size: 1048576, Value: 0},
},
// 10 * 2 MiB
expectedBytes: 10 * 2048 * 1024,
},
{
name: "1 GiB hugepages",
pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 1000000},
{Unit: "KiB", Size: 1048576, Value: 10},
},
expectedBytes: 10 * 1024 * 1024 * 1024,
},
{
name: "mixed 2 MiB and 1 GiB hugepages",
pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 1000000},
{Unit: "KiB", Size: 2048, Value: 100},
{Unit: "KiB", Size: 1048576, Value: 2},
},
expectedBytes: 100*2048*1024 + 2*1024*1024*1024,
},
{
name: "no pages",
pages: nil,
expectedBytes: 0,
},
{
name: "only base page",
pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 1000000},
},
expectedBytes: 0,
},
{
name: "base page not first element",
pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 2048, Value: 10},
{Unit: "KiB", Size: 4, Value: 1000000},
},
expectedBytes: 10 * 2048 * 1024,
},
{
name: "unexpected hugepage unit",
pages: []capabilities.CapabilitiesHostTopologyCellPages{
{Unit: "KiB", Size: 4, Value: 1000000},
{Unit: "MiB", Size: 2048, Value: 10},
},
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := HugepageMemoryToResource(tt.pages)
if tt.expectError {
if err == nil {
t.Fatalf("Expected error, got result: %s", result.String())
}
return
}
if err != nil {
t.Fatalf("HugepageMemoryToResource() returned unexpected error: %v", err)
}

expectedQuantity := resource.NewQuantity(tt.expectedBytes, resource.BinarySI)
if !result.Equal(*expectedQuantity) {
t.Errorf("Expected quantity %s, got %s", expectedQuantity.String(), result.String())
}
})
}
}

func TestMemoryToResourceNegativeValues(t *testing.T) {
// Test behavior with negative values (edge case)
tests := []struct {
name string
value int64
Expand Down
Loading