diff --git a/go.mod b/go.mod index 2d79a67..b8c6387 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ go 1.26 require ( github.com/cert-manager/cert-manager v1.19.4 - github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20260309144200-9c8ed613a94c + github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20260313120621-e3699e2ccab9 github.com/coreos/go-systemd/v22 v22.7.0 github.com/digitalocean/go-libvirt v0.0.0-20260217163227-273eaa321819 github.com/godbus/dbus/v5 v5.2.2 diff --git a/go.sum b/go.sum index 7d38453..2d8a494 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20260309144200-9c8ed613a94c h1:KylfcJikSMWNJnuNfG1Od6fNUw4kQTjseP7khmwVlrM= github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20260309144200-9c8ed613a94c/go.mod h1:b0KmJdxvRI8UXlGe8cRm5BD8Tm2WhF7zSKMSIRGyVL4= +github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20260313120621-e3699e2ccab9 h1:fIQCfP6HTOMu9XqcRLUYeUCK2mPWcOkSqYVF9HUhQyE= +github.com/cobaltcore-dev/openstack-hypervisor-operator v0.0.0-20260313120621-e3699e2ccab9/go.mod h1:b0KmJdxvRI8UXlGe8cRm5BD8Tm2WhF7zSKMSIRGyVL4= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= diff --git a/internal/libvirt/libvirt.go b/internal/libvirt/libvirt.go index fd2bdeb..591a18c 100644 --- a/internal/libvirt/libvirt.go +++ b/internal/libvirt/libvirt.go @@ -274,6 +274,7 @@ func (l *LibVirt) Process(hv v1.Hypervisor) (v1.Hypervisor, error) { l.addCapabilities, l.addDomainCapabilities, l.addAllocationCapacity, + l.addEffectiveCapacity, } var err error for _, processor := range processors { @@ -458,14 +459,14 @@ func (l *LibVirt) addAllocationCapacity(old v1.Hypervisor) (v1.Hypervisor, error cellsById[cell.ID] = v1.Cell{ CellID: cell.ID, - Allocation: map[string]resource.Quantity{ + Allocation: map[v1.ResourceName]resource.Quantity{ // Will be updated below when we look at the domain infos. - "memory": *resource.NewQuantity(0, resource.BinarySI), - "cpu": *resource.NewQuantity(0, resource.DecimalSI), + v1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI), + v1.ResourceCPU: *resource.NewQuantity(0, resource.DecimalSI), }, - Capacity: map[string]resource.Quantity{ - "memory": memoryCapacity, - "cpu": cpuCapacity, + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceMemory: memoryCapacity, + v1.ResourceCPU: cpuCapacity, }, } } @@ -505,14 +506,14 @@ func (l *LibVirt) addAllocationCapacity(old v1.Hypervisor) (v1.Hypervisor, error domInfo.Name, memoryNode.CellID, ) } - memAllocCell := cell.Allocation["memory"] + memAllocCell := cell.Allocation[v1.ResourceMemory] // If a domain is using multiple memory cells, assume the // distribution across cells is even. nCells := int64(len(domInfo.NumaTune.MemNodes)) // is non-zero memAllocPerCell := *resource. NewQuantity(memAlloc.Value()/nCells, resource.BinarySI) memAllocCell.Add(memAllocPerCell) - cell.Allocation["memory"] = memAllocCell + cell.Allocation[v1.ResourceMemory] = memAllocCell cellsById[memoryNode.CellID] = cell } @@ -528,14 +529,14 @@ func (l *LibVirt) addAllocationCapacity(old v1.Hypervisor) (v1.Hypervisor, error domInfo.Name, cpuCell.ID, ) } - cpuAllocCell := cell.Allocation["cpu"] + cpuAllocCell := cell.Allocation[v1.ResourceCPU] // If a domain is using multiple cpu cells, assume the distribution // across cells is even. nCells := int64(len(domInfo.CPU.Numa.Cells)) // is non-zero cpuAllocPerCell := *resource. NewQuantity(cpuAlloc.Value()/nCells, resource.DecimalSI) cpuAllocCell.Add(cpuAllocPerCell) - cell.Allocation["cpu"] = cpuAllocCell + cell.Allocation[v1.ResourceCPU] = cpuAllocCell cellsById[cpuCell.ID] = cell } } @@ -544,12 +545,48 @@ func (l *LibVirt) addAllocationCapacity(old v1.Hypervisor) (v1.Hypervisor, error cellsAsSlice = append(cellsAsSlice, cell) } - newHv.Status.Capacity = make(map[string]resource.Quantity) - newHv.Status.Capacity["memory"] = *totalMemoryCapacity - newHv.Status.Capacity["cpu"] = *totalCpuCapacity - newHv.Status.Allocation = make(map[string]resource.Quantity) - newHv.Status.Allocation["memory"] = *totalMemoryAlloc - newHv.Status.Allocation["cpu"] = *totalCpuAlloc + newHv.Status.Capacity = make(map[v1.ResourceName]resource.Quantity) + newHv.Status.Capacity[v1.ResourceMemory] = *totalMemoryCapacity + newHv.Status.Capacity[v1.ResourceCPU] = *totalCpuCapacity + newHv.Status.Allocation = make(map[v1.ResourceName]resource.Quantity) + newHv.Status.Allocation[v1.ResourceMemory] = *totalMemoryAlloc + newHv.Status.Allocation[v1.ResourceCPU] = *totalCpuAlloc newHv.Status.Cells = cellsAsSlice return newHv, nil } + +// Add the effective capacity to the hypervisor instance. +// +// The effective capacity is calculated as the physical capacity times the +// applied overcommit ratio, or 1.0 by default. In case the resulting values +// are fractional, they are floored. +func (l *LibVirt) addEffectiveCapacity(old v1.Hypervisor) (v1.Hypervisor, error) { + newHv := *old.DeepCopy() + // Always recreate the EffectiveCapacity map to remove stale entries + newHv.Status.EffectiveCapacity = make(map[v1.ResourceName]resource.Quantity) + for resourceName, capacity := range newHv.Status.Capacity { + overcommit, ok := newHv.Spec.Overcommit[resourceName] + if !ok { + overcommit = 1.0 + } + flooredValue := int64(float64(capacity.Value()) * overcommit) + effectiveCapacity := resource.NewQuantity(flooredValue, capacity.Format) + newHv.Status.EffectiveCapacity[resourceName] = *effectiveCapacity + } + // Also apply this to each cell. + for i, cell := range newHv.Status.Cells { + // Always recreate the cell's EffectiveCapacity map to remove stale entries + cell.EffectiveCapacity = make(map[v1.ResourceName]resource.Quantity) + for resourceName, capacity := range cell.Capacity { + overcommit, ok := newHv.Spec.Overcommit[resourceName] + if !ok { + overcommit = 1.0 + } + flooredValue := int64(float64(capacity.Value()) * overcommit) + effectiveCapacity := resource.NewQuantity(flooredValue, capacity.Format) + cell.EffectiveCapacity[resourceName] = *effectiveCapacity + } + newHv.Status.Cells[i] = cell + } + return newHv, nil +} diff --git a/internal/libvirt/libvirt_test.go b/internal/libvirt/libvirt_test.go index e104c58..9e4da3d 100644 --- a/internal/libvirt/libvirt_test.go +++ b/internal/libvirt/libvirt_test.go @@ -1552,3 +1552,349 @@ func TestRunEventLoop_ClosedEventChannel(t *testing.T) { t.Fatal("Event loop did not handle closed channel within timeout") } } + +func TestAddEffectiveCapacity_NoOvercommit(t *testing.T) { + // Test that when no overcommit is specified, effective capacity equals capacity + l := &LibVirt{} + + hv := v1.Hypervisor{ + Status: v1.HypervisorStatus{ + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceMemory: *resource.NewQuantity(64*1024*1024*1024, resource.BinarySI), + v1.ResourceCPU: *resource.NewQuantity(16, resource.DecimalSI), + }, + EffectiveCapacity: make(map[v1.ResourceName]resource.Quantity), + Cells: []v1.Cell{ + { + CellID: 0, + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceMemory: *resource.NewQuantity(64*1024*1024*1024, resource.BinarySI), + v1.ResourceCPU: *resource.NewQuantity(16, resource.DecimalSI), + }, + EffectiveCapacity: make(map[v1.ResourceName]resource.Quantity), + }, + }, + }, + } + + result, err := l.addEffectiveCapacity(hv) + + if err != nil { + t.Fatalf("addEffectiveCapacity() returned unexpected error: %v", err) + } + + // With no overcommit, effective capacity should equal physical capacity + expectedMemory := resource.NewQuantity(64*1024*1024*1024, resource.BinarySI) + memEffective := result.Status.EffectiveCapacity[v1.ResourceMemory] + if !memEffective.Equal(*expectedMemory) { + t.Errorf("Expected effective memory capacity %s, got %s", + expectedMemory.String(), memEffective.String()) + } + + expectedCPU := resource.NewQuantity(16, resource.DecimalSI) + cpuEffective := result.Status.EffectiveCapacity[v1.ResourceCPU] + if !cpuEffective.Equal(*expectedCPU) { + t.Errorf("Expected effective CPU capacity %s, got %s", + expectedCPU.String(), cpuEffective.String()) + } + + // Check cell effective capacity + if len(result.Status.Cells) != 1 { + t.Fatalf("Expected 1 cell, got %d", len(result.Status.Cells)) + } + cellMemEffective := result.Status.Cells[0].EffectiveCapacity[v1.ResourceMemory] + if !cellMemEffective.Equal(*expectedMemory) { + t.Errorf("Cell 0: Expected effective memory capacity %s, got %s", + expectedMemory.String(), cellMemEffective.String()) + } + cellCpuEffective := result.Status.Cells[0].EffectiveCapacity[v1.ResourceCPU] + if !cellCpuEffective.Equal(*expectedCPU) { + t.Errorf("Cell 0: Expected effective CPU capacity %s, got %s", + expectedCPU.String(), cellCpuEffective.String()) + } +} + +func TestAddEffectiveCapacity_WithMemoryOvercommit(t *testing.T) { + // Test memory overcommit ratio of 1.5 + l := &LibVirt{} + + hv := v1.Hypervisor{ + Spec: v1.HypervisorSpec{ + Overcommit: map[v1.ResourceName]float64{ + v1.ResourceMemory: 1.5, + }, + }, + Status: v1.HypervisorStatus{ + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceMemory: *resource.NewQuantity(64*1024*1024*1024, resource.BinarySI), + v1.ResourceCPU: *resource.NewQuantity(16, resource.DecimalSI), + }, + EffectiveCapacity: make(map[v1.ResourceName]resource.Quantity), + Cells: []v1.Cell{}, + }, + } + + result, err := l.addEffectiveCapacity(hv) + + if err != nil { + t.Fatalf("addEffectiveCapacity() returned unexpected error: %v", err) + } + + // Memory should be 64 GiB * 1.5 = 96 GiB + expectedMemory := resource.NewQuantity(96*1024*1024*1024, resource.BinarySI) + memEffective := result.Status.EffectiveCapacity[v1.ResourceMemory] + if !memEffective.Equal(*expectedMemory) { + t.Errorf("Expected effective memory capacity %s, got %s", + expectedMemory.String(), memEffective.String()) + } + + // CPU should remain unchanged (no overcommit specified, defaults to 1.0) + expectedCPU := resource.NewQuantity(16, resource.DecimalSI) + cpuEffective := result.Status.EffectiveCapacity[v1.ResourceCPU] + if !cpuEffective.Equal(*expectedCPU) { + t.Errorf("Expected effective CPU capacity %s, got %s", + expectedCPU.String(), cpuEffective.String()) + } +} + +func TestAddEffectiveCapacity_WithCPUOvercommit(t *testing.T) { + // Test CPU overcommit ratio of 4.0 + l := &LibVirt{} + + hv := v1.Hypervisor{ + Spec: v1.HypervisorSpec{ + Overcommit: map[v1.ResourceName]float64{ + v1.ResourceCPU: 4.0, + }, + }, + Status: v1.HypervisorStatus{ + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceMemory: *resource.NewQuantity(64*1024*1024*1024, resource.BinarySI), + v1.ResourceCPU: *resource.NewQuantity(16, resource.DecimalSI), + }, + EffectiveCapacity: make(map[v1.ResourceName]resource.Quantity), + Cells: []v1.Cell{}, + }, + } + + result, err := l.addEffectiveCapacity(hv) + + if err != nil { + t.Fatalf("addEffectiveCapacity() returned unexpected error: %v", err) + } + + // Memory should remain unchanged (no overcommit specified, defaults to 1.0) + expectedMemory := resource.NewQuantity(64*1024*1024*1024, resource.BinarySI) + memEffective := result.Status.EffectiveCapacity[v1.ResourceMemory] + if !memEffective.Equal(*expectedMemory) { + t.Errorf("Expected effective memory capacity %s, got %s", + expectedMemory.String(), memEffective.String()) + } + + // CPU should be 16 * 4.0 = 64 + expectedCPU := resource.NewQuantity(64, resource.DecimalSI) + cpuEffective := result.Status.EffectiveCapacity[v1.ResourceCPU] + if !cpuEffective.Equal(*expectedCPU) { + t.Errorf("Expected effective CPU capacity %s, got %s", + expectedCPU.String(), cpuEffective.String()) + } +} + +func TestAddEffectiveCapacity_WithBothOvercommit(t *testing.T) { + // Test both memory and CPU overcommit + l := &LibVirt{} + + hv := v1.Hypervisor{ + Spec: v1.HypervisorSpec{ + Overcommit: map[v1.ResourceName]float64{ + v1.ResourceMemory: 2.0, + v1.ResourceCPU: 8.0, + }, + }, + Status: v1.HypervisorStatus{ + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceMemory: *resource.NewQuantity(32*1024*1024*1024, resource.BinarySI), + v1.ResourceCPU: *resource.NewQuantity(8, resource.DecimalSI), + }, + EffectiveCapacity: make(map[v1.ResourceName]resource.Quantity), + Cells: []v1.Cell{}, + }, + } + + result, err := l.addEffectiveCapacity(hv) + + if err != nil { + t.Fatalf("addEffectiveCapacity() returned unexpected error: %v", err) + } + + // Memory should be 32 GiB * 2.0 = 64 GiB + expectedMemory := resource.NewQuantity(64*1024*1024*1024, resource.BinarySI) + memEffective := result.Status.EffectiveCapacity[v1.ResourceMemory] + if !memEffective.Equal(*expectedMemory) { + t.Errorf("Expected effective memory capacity %s, got %s", + expectedMemory.String(), memEffective.String()) + } + + // CPU should be 8 * 8.0 = 64 + expectedCPU := resource.NewQuantity(64, resource.DecimalSI) + cpuEffective := result.Status.EffectiveCapacity[v1.ResourceCPU] + if !cpuEffective.Equal(*expectedCPU) { + t.Errorf("Expected effective CPU capacity %s, got %s", + expectedCPU.String(), cpuEffective.String()) + } +} + +func TestAddEffectiveCapacity_FractionalValuesFloored(t *testing.T) { + // Test that fractional values are floored (not rounded) + // 11 * 1.5 = 16.5, which should be floored to 16 + l := &LibVirt{} + + hv := v1.Hypervisor{ + Spec: v1.HypervisorSpec{ + Overcommit: map[v1.ResourceName]float64{ + v1.ResourceCPU: 1.5, // 11 * 1.5 = 16.5 + }, + }, + Status: v1.HypervisorStatus{ + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: *resource.NewQuantity(11, resource.DecimalSI), + }, + EffectiveCapacity: make(map[v1.ResourceName]resource.Quantity), + Cells: []v1.Cell{}, + }, + } + + result, err := l.addEffectiveCapacity(hv) + + if err != nil { + t.Fatalf("addEffectiveCapacity() returned unexpected error: %v", err) + } + + // CPU should be floor(11 * 1.5) = floor(16.5) = 16 + expectedCPU := resource.NewQuantity(16, resource.DecimalSI) + cpuEffective := result.Status.EffectiveCapacity[v1.ResourceCPU] + if !cpuEffective.Equal(*expectedCPU) { + t.Errorf("Expected effective CPU capacity %s, got %s", + expectedCPU.String(), cpuEffective.String()) + } +} + +func TestAddEffectiveCapacity_FractionalValuesFlooredDown(t *testing.T) { + // Test that fractional values are floored down, not rounded up + // 3 * 1.9 = 5.7, which should be floored to 5 (not rounded to 6) + l := &LibVirt{} + + hv := v1.Hypervisor{ + Spec: v1.HypervisorSpec{ + Overcommit: map[v1.ResourceName]float64{ + v1.ResourceCPU: 1.9, // 3 * 1.9 = 5.7 + }, + }, + Status: v1.HypervisorStatus{ + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: *resource.NewQuantity(3, resource.DecimalSI), + }, + EffectiveCapacity: make(map[v1.ResourceName]resource.Quantity), + Cells: []v1.Cell{}, + }, + } + + result, err := l.addEffectiveCapacity(hv) + + if err != nil { + t.Fatalf("addEffectiveCapacity() returned unexpected error: %v", err) + } + + // CPU should be floor(3 * 1.9) = floor(5.7) = 5 + expectedCPU := resource.NewQuantity(5, resource.DecimalSI) + cpuEffective := result.Status.EffectiveCapacity[v1.ResourceCPU] + if !cpuEffective.Equal(*expectedCPU) { + t.Errorf("Expected effective CPU capacity %s, got %s", + expectedCPU.String(), cpuEffective.String()) + } +} + +func TestAddEffectiveCapacity_MultipleCells(t *testing.T) { + // Test that overcommit is applied to each cell individually + l := &LibVirt{} + + hv := v1.Hypervisor{ + Spec: v1.HypervisorSpec{ + Overcommit: map[v1.ResourceName]float64{ + v1.ResourceMemory: 1.5, + v1.ResourceCPU: 2.0, + }, + }, + Status: v1.HypervisorStatus{ + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceMemory: *resource.NewQuantity(128*1024*1024*1024, resource.BinarySI), + v1.ResourceCPU: *resource.NewQuantity(32, resource.DecimalSI), + }, + EffectiveCapacity: make(map[v1.ResourceName]resource.Quantity), + Cells: []v1.Cell{ + { + CellID: 0, + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceMemory: *resource.NewQuantity(64*1024*1024*1024, resource.BinarySI), + v1.ResourceCPU: *resource.NewQuantity(16, resource.DecimalSI), + }, + EffectiveCapacity: make(map[v1.ResourceName]resource.Quantity), + }, + { + CellID: 1, + Capacity: map[v1.ResourceName]resource.Quantity{ + v1.ResourceMemory: *resource.NewQuantity(64*1024*1024*1024, resource.BinarySI), + v1.ResourceCPU: *resource.NewQuantity(16, resource.DecimalSI), + }, + EffectiveCapacity: make(map[v1.ResourceName]resource.Quantity), + }, + }, + }, + } + + result, err := l.addEffectiveCapacity(hv) + + if err != nil { + t.Fatalf("addEffectiveCapacity() returned unexpected error: %v", err) + } + + // Check total effective capacity + // Memory: 128 GiB * 1.5 = 192 GiB + expectedTotalMemory := resource.NewQuantity(192*1024*1024*1024, resource.BinarySI) + totalMemEffective := result.Status.EffectiveCapacity[v1.ResourceMemory] + if !totalMemEffective.Equal(*expectedTotalMemory) { + t.Errorf("Expected total effective memory capacity %s, got %s", + expectedTotalMemory.String(), totalMemEffective.String()) + } + + // CPU: 32 * 2.0 = 64 + expectedTotalCPU := resource.NewQuantity(64, resource.DecimalSI) + totalCpuEffective := result.Status.EffectiveCapacity[v1.ResourceCPU] + if !totalCpuEffective.Equal(*expectedTotalCPU) { + t.Errorf("Expected total effective CPU capacity %s, got %s", + expectedTotalCPU.String(), totalCpuEffective.String()) + } + + // Check each cell's effective capacity + if len(result.Status.Cells) != 2 { + t.Fatalf("Expected 2 cells, got %d", len(result.Status.Cells)) + } + + for i, cell := range result.Status.Cells { + // Cell memory: 64 GiB * 1.5 = 96 GiB + expectedCellMemory := resource.NewQuantity(96*1024*1024*1024, resource.BinarySI) + cellMemEffective := cell.EffectiveCapacity[v1.ResourceMemory] + if !cellMemEffective.Equal(*expectedCellMemory) { + t.Errorf("Cell %d: Expected effective memory capacity %s, got %s", + i, expectedCellMemory.String(), cellMemEffective.String()) + } + + // Cell CPU: 16 * 2.0 = 32 + expectedCellCPU := resource.NewQuantity(32, resource.DecimalSI) + cellCpuEffective := cell.EffectiveCapacity[v1.ResourceCPU] + if !cellCpuEffective.Equal(*expectedCellCPU) { + t.Errorf("Cell %d: Expected effective CPU capacity %s, got %s", + i, expectedCellCPU.String(), cellCpuEffective.String()) + } + } +}