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
53 changes: 48 additions & 5 deletions internal/knowledge/extractor/plugins/compute/flavor_groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ type FlavorGroupFeature struct {

// The smallest flavor in the group (used for CR size quantification)
SmallestFlavor FlavorInGroup `json:"smallestFlavor"`

// RAM-to-core ratio in MiB per vCPU (MemoryMB / VCPUs).
// If all flavors have the same ratio: RamCoreRatio is set, RamCoreRatioMin/Max are nil.
// If flavors have different ratios: RamCoreRatio is nil, RamCoreRatioMin/Max are set.
RamCoreRatio *uint64 `json:"ramCoreRatio,omitempty"`
RamCoreRatioMin *uint64 `json:"ramCoreRatioMin,omitempty"`
RamCoreRatioMax *uint64 `json:"ramCoreRatioMax,omitempty"`
}

// HasFixedRamCoreRatio returns true if all flavors in this group have the same RAM/core ratio.
func (f *FlavorGroupFeature) HasFixedRamCoreRatio() bool {
return f.RamCoreRatio != nil
}

// flavorRow represents a row from the SQL query.
Expand Down Expand Up @@ -128,20 +140,51 @@ func (e *FlavorGroupExtractor) Extract() ([]plugins.Feature, error) {
largest := flavors[0]
smallest := flavors[len(flavors)-1]

// Compute RAM/core ratio (MiB per vCPU)
var minRatio, maxRatio uint64 = ^uint64(0), 0
for _, f := range flavors {
if f.VCPUs == 0 {
continue // Skip flavors with 0 vCPUs to avoid division by zero
}
ratio := f.MemoryMB / f.VCPUs
if ratio < minRatio {
minRatio = ratio
}
if ratio > maxRatio {
maxRatio = ratio
}
}

var ramCoreRatio, ramCoreRatioMin, ramCoreRatioMax *uint64
if minRatio == maxRatio && maxRatio != 0 {
// All flavors have the same ratio
ramCoreRatio = &minRatio
} else if maxRatio != 0 {
// Flavors have different ratios
ramCoreRatioMin = &minRatio
ramCoreRatioMax = &maxRatio
}

flavorGroupLog.Info("identified largest and smallest flavors",
"groupName", groupName,
"largestFlavor", largest.Name,
"largestMemoryMB", largest.MemoryMB,
"largestVCPUs", largest.VCPUs,
"smallestFlavor", smallest.Name,
"smallestMemoryMB", smallest.MemoryMB,
"smallestVCPUs", smallest.VCPUs)
"smallestVCPUs", smallest.VCPUs,
"ramCoreRatio", ramCoreRatio,
"ramCoreRatioMin", ramCoreRatioMin,
"ramCoreRatioMax", ramCoreRatioMax)

features = append(features, FlavorGroupFeature{
Name: groupName,
Flavors: flavors,
LargestFlavor: largest,
SmallestFlavor: smallest,
Name: groupName,
Flavors: flavors,
LargestFlavor: largest,
SmallestFlavor: smallest,
RamCoreRatio: ramCoreRatio,
RamCoreRatioMin: ramCoreRatioMin,
RamCoreRatioMax: ramCoreRatioMax,
})
}

Expand Down
118 changes: 118 additions & 0 deletions internal/knowledge/extractor/plugins/compute/flavor_groups_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,122 @@ func TestFlavorGroupExtractor_Extract(t *testing.T) {
if !foundCH {
t.Error("Cloud-Hypervisor flavor should have been included but was not found")
}

// Verify RAM/core ratio for v2 group
// v2 group has flavors with different ratios:
// - hana flavors: 491520/30=16384, 983040/60=16384, 3932160/240=16384 MiB/vCPU
// - gp_c8_m32_v2: 32768/8=4096, gp_c16_m64_v2: 65536/16=4096 MiB/vCPU
// - gp_c12_m32_v2: 32768/12=2730, gp_c12_m32_alt: 32768/12=2730 MiB/vCPU
// So min=2730, max=16384 (variable ratio)
if v2Group.RamCoreRatio != nil {
t.Errorf("expected v2 group to have variable ratio (nil RamCoreRatio), got %d", *v2Group.RamCoreRatio)
}
if v2Group.RamCoreRatioMin == nil || *v2Group.RamCoreRatioMin != 2730 {
var got any = nil
if v2Group.RamCoreRatioMin != nil {
got = *v2Group.RamCoreRatioMin
}
t.Errorf("expected v2 group RamCoreRatioMin=2730, got %v", got)
}
if v2Group.RamCoreRatioMax == nil || *v2Group.RamCoreRatioMax != 16384 {
var got any = nil
if v2Group.RamCoreRatioMax != nil {
got = *v2Group.RamCoreRatioMax
}
t.Errorf("expected v2 group RamCoreRatioMax=16384, got %v", got)
}

// Verify RAM/core ratio for ch group (single flavor = fixed ratio)
// gp_c4_m16_ch: 16384/4=4096 MiB/vCPU
if chGroup.RamCoreRatio == nil || *chGroup.RamCoreRatio != 4096 {
var got any = nil
if chGroup.RamCoreRatio != nil {
got = *chGroup.RamCoreRatio
}
t.Errorf("expected ch group RamCoreRatio=4096, got %v", got)
}
if chGroup.RamCoreRatioMin != nil {
t.Errorf("expected ch group RamCoreRatioMin=nil (fixed ratio), got %d", *chGroup.RamCoreRatioMin)
}
if chGroup.RamCoreRatioMax != nil {
t.Errorf("expected ch group RamCoreRatioMax=nil (fixed ratio), got %d", *chGroup.RamCoreRatioMax)
}
}

func TestFlavorGroupExtractor_RamCoreRatio_FixedRatio(t *testing.T) {
dbEnv := testlibDB.SetupDBEnv(t)
defer dbEnv.Close()
testDB := db.DB{DbMap: dbEnv.DbMap}

if err := testDB.CreateTable(
testDB.AddTable(nova.Flavor{}),
); err != nil {
t.Fatal(err)
}

// Insert flavors with same RAM/core ratio (4096 MiB/vCPU)
flavors := []any{
&nova.Flavor{
ID: "1",
Name: "fixed_c2_m8",
VCPUs: 2,
RAM: 8192, // 8GB
Disk: 50,
ExtraSpecs: `{"capabilities:hypervisor_type":"qemu","quota:hw_version":"fixed"}`,
},
&nova.Flavor{
ID: "2",
Name: "fixed_c4_m16",
VCPUs: 4,
RAM: 16384, // 16GB
Disk: 50,
ExtraSpecs: `{"capabilities:hypervisor_type":"qemu","quota:hw_version":"fixed"}`,
},
&nova.Flavor{
ID: "3",
Name: "fixed_c8_m32",
VCPUs: 8,
RAM: 32768, // 32GB
Disk: 50,
ExtraSpecs: `{"capabilities:hypervisor_type":"qemu","quota:hw_version":"fixed"}`,
},
}

if err := testDB.Insert(flavors...); err != nil {
t.Fatal(err)
}

extractor := &FlavorGroupExtractor{}
if err := extractor.Init(&testDB, nil, v1alpha1.KnowledgeSpec{}); err != nil {
t.Fatal(err)
}

features, err := extractor.Extract()
if err != nil {
t.Fatal(err)
}

if len(features) != 1 {
t.Fatalf("expected 1 flavor group, got %d", len(features))
}

fg := features[0].(FlavorGroupFeature)
if fg.Name != "fixed" {
t.Errorf("expected group name 'fixed', got %s", fg.Name)
}

// All flavors have ratio 4096 MiB/vCPU
if fg.RamCoreRatio == nil || *fg.RamCoreRatio != 4096 {
var got any = nil
if fg.RamCoreRatio != nil {
got = *fg.RamCoreRatio
}
t.Errorf("expected RamCoreRatio=4096, got %v", got)
}
if fg.RamCoreRatioMin != nil {
t.Errorf("expected RamCoreRatioMin=nil for fixed ratio, got %d", *fg.RamCoreRatioMin)
}
if fg.RamCoreRatioMax != nil {
t.Errorf("expected RamCoreRatioMax=nil for fixed ratio, got %d", *fg.RamCoreRatioMax)
}
}
2 changes: 1 addition & 1 deletion internal/scheduling/reservations/commitments/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ func NewAPIWithConfig(client client.Client, config Config) *HTTPAPI {
func (api *HTTPAPI) Init(mux *http.ServeMux, registry prometheus.Registerer) {
registry.MustRegister(&api.monitor)
mux.HandleFunc("/v1/commitments/change-commitments", api.HandleChangeCommitments)
// mux.HandleFunc("/v1/report-capacity", api.HandleReportCapacity)
mux.HandleFunc("/v1/commitments/report-capacity", api.HandleReportCapacity)
mux.HandleFunc("/v1/commitments/info", api.HandleInfo)
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ ProcessLoop:
break ProcessLoop
}

// Reject commitments for flavor groups that don't accept CRs
if !FlavorGroupAcceptsCommitments(&flavorGroup) {
resp.RejectionReason = FlavorGroupCommitmentRejectionReason(&flavorGroup)
requireRollback = true
break ProcessLoop
}

for _, commitment := range resourceChanges.Commitments {
// Additional per-commitment validation if needed
logger.Info("processing commitment change", "commitmentUUID", commitment.UUID, "projectID", projectID, "resourceName", resourceName, "oldStatus", commitment.OldStatus.UnwrapOr("none"), "newStatus", commitment.NewStatus.UnwrapOr("none"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -629,11 +629,37 @@ func (tfg TestFlavorGroup) ToFlavorGroupsKnowledge() FlavorGroupsKnowledge {
smallest := groupFlavors[len(groupFlavors)-1]
largest := groupFlavors[0]

// Compute RAM/core ratio (MiB per vCPU)
var minRatio, maxRatio uint64 = ^uint64(0), 0
for _, f := range groupFlavors {
if f.VCPUs == 0 {
continue
}
ratio := f.MemoryMB / f.VCPUs
if ratio < minRatio {
minRatio = ratio
}
if ratio > maxRatio {
maxRatio = ratio
}
}

var ramCoreRatio, ramCoreRatioMin, ramCoreRatioMax *uint64
if minRatio == maxRatio && maxRatio != 0 {
ramCoreRatio = &minRatio
} else if maxRatio != 0 {
ramCoreRatioMin = &minRatio
ramCoreRatioMax = &maxRatio
}

groups = append(groups, compute.FlavorGroupFeature{
Name: groupName,
Flavors: groupFlavors,
SmallestFlavor: smallest,
LargestFlavor: largest,
Name: groupName,
Flavors: groupFlavors,
SmallestFlavor: smallest,
LargestFlavor: largest,
RamCoreRatio: ramCoreRatio,
RamCoreRatioMin: ramCoreRatioMin,
RamCoreRatioMax: ramCoreRatioMax,
})
}

Expand Down
33 changes: 30 additions & 3 deletions internal/scheduling/reservations/commitments/api_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ func (api *HTTPAPI) HandleInfo(w http.ResponseWriter, r *http.Request) {
}
}

// resourceAttributes holds the custom attributes for a resource in the info API response.
type resourceAttributes struct {
RamCoreRatio *uint64 `json:"ramCoreRatio,omitempty"`
RamCoreRatioMin *uint64 `json:"ramCoreRatioMin,omitempty"`
RamCoreRatioMax *uint64 `json:"ramCoreRatioMax,omitempty"`
}

// buildServiceInfo constructs the ServiceInfo response with metadata for all flavor groups.
func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (liquid.ServiceInfo, error) {
// Get all flavor groups from Knowledge CRDs
Expand Down Expand Up @@ -77,22 +84,42 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l
strings.Join(flavorNames, ", "),
)

// Only handle commitments for groups with a fixed RAM/core ratio
handlesCommitments := FlavorGroupAcceptsCommitments(&groupData)

// Build attributes JSON with ratio info
attrs := resourceAttributes{
RamCoreRatio: groupData.RamCoreRatio,
RamCoreRatioMin: groupData.RamCoreRatioMin,
RamCoreRatioMax: groupData.RamCoreRatioMax,
}
attrsJSON, err := json.Marshal(attrs)
if err != nil {
logger.Error(err, "failed to marshal resource attributes", "resourceName", resourceName)
attrsJSON = nil
}

resources[resourceName] = liquid.ResourceInfo{
DisplayName: displayName,
Unit: liquid.UnitNone, // Countable: multiples of smallest flavor instances
Topology: liquid.AZAwareTopology, // Commitments are per-AZ
NeedsResourceDemand: false, // Capacity planning out of scope for now
HasCapacity: true, // We report capacity via /v1/report-capacity
HasCapacity: handlesCommitments, // We report capacity via /v1/report-capacity only for groups that accept commitments
HasQuota: false, // No quota enforcement as of now
HandlesCommitments: true, // We handle commitment changes via /v1/change-commitments
HandlesCommitments: handlesCommitments, // Only for groups with fixed RAM/core ratio
Attributes: attrsJSON,
}

logger.V(1).Info("registered flavor group resource",
"resourceName", resourceName,
"flavorGroup", groupName,
"displayName", displayName,
"smallestFlavor", groupData.SmallestFlavor.Name,
"smallestRamMB", groupData.SmallestFlavor.MemoryMB)
"smallestRamMB", groupData.SmallestFlavor.MemoryMB,
"handlesCommitments", handlesCommitments,
"ramCoreRatio", groupData.RamCoreRatio,
"ramCoreRatioMin", groupData.RamCoreRatioMin,
"ramCoreRatioMax", groupData.RamCoreRatioMax)
}

// Get last content changed from flavor group knowledge and treat it as version
Expand Down
Loading
Loading