diff --git a/internal/scheduling/reservations/commitments/api/info.go b/internal/scheduling/reservations/commitments/api/info.go index 8f1a7b194..fbdd474d8 100644 --- a/internal/scheduling/reservations/commitments/api/info.go +++ b/internal/scheduling/reservations/commitments/api/info.go @@ -76,7 +76,7 @@ func (api *HTTPAPI) recordInfoMetrics(statusCode int, startTime time.Time) { } // resourceAttributes holds the custom attributes for a resource in the info API response. -// Ratio values are in GiB per vCPU, matching the RAM resource unit (UnitGibibytes). +// Ratio values are in GiB per vCPU. type resourceAttributes struct { RamCoreRatio *uint64 `json:"ramCoreRatio,omitempty"` RamCoreRatioMin *uint64 `json:"ramCoreRatioMin,omitempty"` @@ -137,26 +137,22 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l // === 1. RAM Resource === ramResourceName := liquid.ResourceName(commitments.ResourceNameRAM(groupName)) - // Determine topology: AZSeparatedTopology only for groups that accept commitments - // (AZSeparatedTopology means quota is also AZ-aware, required when HasQuota=true) - ramTopology := liquid.AZAwareTopology - if resCfg.RAM.HandlesCommitments { - ramTopology = liquid.AZSeparatedTopology + // Fixed-ratio groups: unit = smallest flavor's RAM in MiB (e.g. "480 GiB" for hana); + // variable-ratio groups: unit = 1 GiB. RAMUnitMiB() encodes both cases. + ramUnit, err := liquid.UnitMebibytes.MultiplyBy(groupData.RAMUnitMiB()) + if err != nil { + return liquid.ServiceInfo{}, fmt.Errorf("failed to create RAM unit for flavor group %q: %w", groupName, err) } - // Fixed-ratio groups: unit is 1 slot (= 1 smallest-flavor instance); variable-ratio: GiB. - var ramUnit liquid.Unit var ramDisplayName string - if groupData.HasFixedRamCoreRatio() { - ramUnit = liquid.UnitNone + if groupData.HasFixedRamCoreRatio() && groupData.SmallestFlavor.MemoryMB > 0 { ramDisplayName = fmt.Sprintf("multiples of %d MiB (usable by: %s)", groupData.SmallestFlavor.MemoryMB, flavorListStr) } else { - ramUnit = liquid.UnitGibibytes ramDisplayName = fmt.Sprintf("GiB of RAM (usable by: %s)", flavorListStr) } resources[ramResourceName] = liquid.ResourceInfo{ DisplayName: ramDisplayName, Unit: ramUnit, - Topology: ramTopology, + Topology: liquid.AZSeparatedTopology, NeedsResourceDemand: false, HasCapacity: resCfg.RAM.HasCapacity, HasQuota: resCfg.RAM.HasQuota, @@ -166,17 +162,13 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l // === 2. Cores Resource === coresResourceName := liquid.ResourceName(commitments.ResourceNameCores(groupName)) - coresTopology := liquid.AZAwareTopology - if resCfg.Cores.HandlesCommitments { - coresTopology = liquid.AZSeparatedTopology - } resources[coresResourceName] = liquid.ResourceInfo{ DisplayName: fmt.Sprintf( "CPU cores (usable by: %s)", flavorListStr, ), Unit: liquid.UnitNone, - Topology: coresTopology, + Topology: liquid.AZSeparatedTopology, NeedsResourceDemand: false, HasCapacity: resCfg.Cores.HasCapacity, HasQuota: resCfg.Cores.HasQuota, @@ -186,17 +178,13 @@ func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (l // === 3. Instances Resource === instancesResourceName := liquid.ResourceName(commitments.ResourceNameInstances(groupName)) - instancesTopology := liquid.AZAwareTopology - if resCfg.Instances.HandlesCommitments { - instancesTopology = liquid.AZSeparatedTopology - } resources[instancesResourceName] = liquid.ResourceInfo{ DisplayName: fmt.Sprintf( "instances (usable by: %s)", flavorListStr, ), Unit: liquid.UnitNone, - Topology: instancesTopology, + Topology: liquid.AZSeparatedTopology, NeedsResourceDemand: false, HasCapacity: resCfg.Instances.HasCapacity, HasQuota: resCfg.Instances.HasQuota, diff --git a/internal/scheduling/reservations/commitments/api/info_test.go b/internal/scheduling/reservations/commitments/api/info_test.go index c01261f6c..fed48bcf2 100644 --- a/internal/scheduling/reservations/commitments/api/info_test.go +++ b/internal/scheduling/reservations/commitments/api/info_test.go @@ -247,7 +247,7 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { t.Error("hw_version_hana_fixed_ram: expected HasQuota=true (fixed ratio groups accept quotas)") } - // Test Cores resource: hw_version_hana_fixed_cores (HandlesCommitments=false → AZAwareTopology) + // Test Cores resource: hw_version_hana_fixed_cores coresResource, ok := serviceInfo.Resources["hw_version_hana_fixed_cores"] if !ok { t.Fatal("expected hw_version_hana_fixed_cores resource to exist") @@ -258,14 +258,14 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { if coresResource.HandlesCommitments { t.Error("hw_version_hana_fixed_cores: expected HandlesCommitments=false") } - if coresResource.Topology != liquid.AZAwareTopology { - t.Errorf("hw_version_hana_fixed_cores: expected Topology=%q, got %q", liquid.AZAwareTopology, coresResource.Topology) + if coresResource.Topology != liquid.AZSeparatedTopology { + t.Errorf("hw_version_hana_fixed_cores: expected Topology=%q, got %q", liquid.AZSeparatedTopology, coresResource.Topology) } if coresResource.HasQuota { t.Error("hw_version_hana_fixed_cores: expected HasQuota=false") } - // Test Instances resource: hw_version_hana_fixed_instances (HandlesCommitments=false → AZAwareTopology) + // Test Instances resource: hw_version_hana_fixed_instances instancesResource, ok := serviceInfo.Resources["hw_version_hana_fixed_instances"] if !ok { t.Fatal("expected hw_version_hana_fixed_instances resource to exist") @@ -276,8 +276,8 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { if instancesResource.HandlesCommitments { t.Error("hw_version_hana_fixed_instances: expected HandlesCommitments=false") } - if instancesResource.Topology != liquid.AZAwareTopology { - t.Errorf("hw_version_hana_fixed_instances: expected Topology=%q, got %q", liquid.AZAwareTopology, instancesResource.Topology) + if instancesResource.Topology != liquid.AZSeparatedTopology { + t.Errorf("hw_version_hana_fixed_instances: expected Topology=%q, got %q", liquid.AZSeparatedTopology, instancesResource.Topology) } if instancesResource.HasQuota { t.Error("hw_version_hana_fixed_instances: expected HasQuota=false") @@ -294,8 +294,8 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { if v2RamResource.HandlesCommitments { t.Error("hw_version_v2_variable_ram: expected HandlesCommitments=false (not in config)") } - if v2RamResource.Topology != liquid.AZAwareTopology { - t.Errorf("hw_version_v2_variable_ram: expected Topology=%q, got %q", liquid.AZAwareTopology, v2RamResource.Topology) + if v2RamResource.Topology != liquid.AZSeparatedTopology { + t.Errorf("hw_version_v2_variable_ram: expected Topology=%q, got %q", liquid.AZSeparatedTopology, v2RamResource.Topology) } if v2RamResource.HasQuota { t.Error("hw_version_v2_variable_ram: expected HasQuota=false (variable ratio)") @@ -311,8 +311,8 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { if v2CoresResource.HandlesCommitments { t.Error("hw_version_v2_variable_cores: expected HandlesCommitments=false") } - if v2CoresResource.Topology != liquid.AZAwareTopology { - t.Errorf("hw_version_v2_variable_cores: expected Topology=%q, got %q", liquid.AZAwareTopology, v2CoresResource.Topology) + if v2CoresResource.Topology != liquid.AZSeparatedTopology { + t.Errorf("hw_version_v2_variable_cores: expected Topology=%q, got %q", liquid.AZSeparatedTopology, v2CoresResource.Topology) } if v2CoresResource.HasQuota { t.Error("hw_version_v2_variable_cores: expected HasQuota=false") @@ -328,8 +328,8 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { if v2InstancesResource.HandlesCommitments { t.Error("hw_version_v2_variable_instances: expected HandlesCommitments=false") } - if v2InstancesResource.Topology != liquid.AZAwareTopology { - t.Errorf("hw_version_v2_variable_instances: expected Topology=%q, got %q", liquid.AZAwareTopology, v2InstancesResource.Topology) + if v2InstancesResource.Topology != liquid.AZSeparatedTopology { + t.Errorf("hw_version_v2_variable_instances: expected Topology=%q, got %q", liquid.AZSeparatedTopology, v2InstancesResource.Topology) } if v2InstancesResource.HasQuota { t.Error("hw_version_v2_variable_instances: expected HasQuota=false") @@ -341,17 +341,22 @@ func TestHandleInfo_ResourceFlagsFromConfig(t *testing.T) { // v2_variable has ramCoreRatioMin=2048 MiB/vCPU, ramCoreRatioMax=16384 MiB/vCPU → expect 2, 16 GiB/vCPU. checkAttrsRatio(t, "hw_version_v2_variable_ram", v2RamResource.Attributes, 0, ptr(uint64(2)), ptr(uint64(16))) - // Verify RAM units: fixed-ratio groups use UnitNone (slot-based), variable-ratio use UnitGibibytes. - if ramResource.Unit != liquid.UnitNone { - t.Errorf("hw_version_hana_fixed_ram: expected Unit=%q (slot-based), got %q", liquid.UnitNone, ramResource.Unit) + // Verify RAM units: fixed-ratio groups use smallest-flavor-based unit (e.g. "16 GiB"), + // variable-ratio groups use UnitGibibytes ("GiB"). + expectedFixedUnit, err := liquid.UnitMebibytes.MultiplyBy(16384) // hana_fixed smallest flavor: 16384 MiB = 16 GiB + if err != nil { + t.Fatalf("failed to create expected fixed unit: %v", err) + } + if ramResource.Unit != expectedFixedUnit { + t.Errorf("hw_version_hana_fixed_ram: expected Unit=%q (slot-based), got %q", expectedFixedUnit, ramResource.Unit) } if v2RamResource.Unit != liquid.UnitGibibytes { t.Errorf("hw_version_v2_variable_ram: expected Unit=%q, got %q", liquid.UnitGibibytes, v2RamResource.Unit) } } -func TestHandleInfo_TopologyFollowsHandlesCommitments(t *testing.T) { - // Verifies that any resource with HandlesCommitments=true gets AZSeparatedTopology, +func TestHandleInfo_AllResourcesAZSeparated(t *testing.T) { + // Verifies that all resources always get AZSeparatedTopology. // regardless of whether it's RAM, Cores, or Instances. scheme := runtime.NewScheme() if err := v1alpha1.AddToScheme(scheme); err != nil { diff --git a/internal/scheduling/reservations/commitments/usage.go b/internal/scheduling/reservations/commitments/usage.go index 61ea40cac..0caf32656 100644 --- a/internal/scheduling/reservations/commitments/usage.go +++ b/internal/scheduling/reservations/commitments/usage.go @@ -509,29 +509,34 @@ func (c *UsageCalculator) buildUsageResponse( // Build ResourceUsageReport for all flavor groups (not just those with fixed ratio) for flavorGroupName := range flavorGroups { // All flavor groups are included in usage reporting. + resCfg := config.ResourceConfigForGroup(flavorGroupName) + + // helper: look up stored quota for a resource in a given AZ (stored in GiB). + // Returns -1 (infinite) if not found. Unit conversion is done by the caller. + lookupQuotaGiB := func(resourceName string, az liquid.AvailabilityZone) int64 { + if quotaByResourceAZ == nil { + return -1 + } + if azMap, ok := quotaByResourceAZ[resourceName]; ok { + if q, ok := azMap[string(az)]; ok { + return q + } + } + return -1 + } // === 1. RAM Resource === ramResourceName := liquid.ResourceName(ResourceNameRAM(flavorGroupName)) ramPerAZ := make(map[liquid.AvailabilityZone]*liquid.AZResourceUsageReport) - // Include per-AZ quota for AZSeparatedTopology resources — same condition as info.go. - ramHasAZQuota := config.ResourceConfigForGroup(flavorGroupName).RAM.HandlesCommitments for _, az := range allAZs { report := &liquid.AZResourceUsageReport{ Usage: 0, Subresources: []liquid.Subresource{}, } - if ramHasAZQuota { - quota := int64(-1) // default: infinite - if quotaByResourceAZ != nil { - if azMap, ok := quotaByResourceAZ[string(ramResourceName)]; ok { - if q, ok := azMap[string(az)]; ok { - // CRD stores quota in GiB; convert to declared unit for Limes. - fg := flavorGroups[flavorGroupName] - quota = fg.GiBToDeclaredUnits(q) - } - } - } - report.Quota = Some(quota) + if resCfg.RAM.HasQuota { + // CRD stores quota in GiB; convert to declared unit for Limes. + fg := flavorGroups[flavorGroupName] + report.Quota = Some(fg.GiBToDeclaredUnits(lookupQuotaGiB(string(ramResourceName), az))) } ramPerAZ[az] = report } @@ -541,7 +546,6 @@ func (c *UsageCalculator) buildUsageResponse( continue // skip VMs in AZs not in allAZs } ramPerAZ[az].Usage = data.ramUsage - // Subresources are only on instances resource } } resources[ramResourceName] = &liquid.ResourceUsageReport{ @@ -552,10 +556,14 @@ func (c *UsageCalculator) buildUsageResponse( coresResourceName := liquid.ResourceName(ResourceNameCores(flavorGroupName)) coresPerAZ := make(map[liquid.AvailabilityZone]*liquid.AZResourceUsageReport) for _, az := range allAZs { - coresPerAZ[az] = &liquid.AZResourceUsageReport{ + report := &liquid.AZResourceUsageReport{ Usage: 0, Subresources: []liquid.Subresource{}, } + if resCfg.Cores.HasQuota { + report.Quota = Some(lookupQuotaGiB(string(coresResourceName), az)) + } + coresPerAZ[az] = report } if azData, exists := usageByFlavorGroupAZ[flavorGroupName]; exists { for az, data := range azData { @@ -563,7 +571,6 @@ func (c *UsageCalculator) buildUsageResponse( continue // skip VMs in AZs not in allAZs } coresPerAZ[az].Usage = data.coresUsage - // Subresources are only on instances resource } } resources[coresResourceName] = &liquid.ResourceUsageReport{ @@ -574,10 +581,14 @@ func (c *UsageCalculator) buildUsageResponse( instancesResourceName := liquid.ResourceName(ResourceNameInstances(flavorGroupName)) instancesPerAZ := make(map[liquid.AvailabilityZone]*liquid.AZResourceUsageReport) for _, az := range allAZs { - instancesPerAZ[az] = &liquid.AZResourceUsageReport{ + report := &liquid.AZResourceUsageReport{ Usage: 0, Subresources: []liquid.Subresource{}, } + if resCfg.Instances.HasQuota { + report.Quota = Some(lookupQuotaGiB(string(instancesResourceName), az)) + } + instancesPerAZ[az] = report } if azData, exists := usageByFlavorGroupAZ[flavorGroupName]; exists { for az, data := range azData {