Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package nova
package compute

import (
"strings"

api "github.com/cobaltcore-dev/cortex/api/external/nova"
"github.com/cobaltcore-dev/cortex/api/v1alpha1"
"github.com/cobaltcore-dev/cortex/internal/scheduling/compute"
)

// ToSchedulerRequest translates the Nova over-the-wire request into the typed,
// Nova-agnostic compute.PlacementRequest the pipeline consumes. This is the one
// Nova-agnostic PlacementRequest the pipeline consumes. This is the one
// place that reads Nova's loosely-typed scheduler hints and flavor/image extra
// specs; every downstream filter/weigher sees only resolved typed fields.
//
Expand All @@ -22,11 +21,11 @@ func ToSchedulerRequest(
wire api.ExternalSchedulerRequest,
intent v1alpha1.SchedulingIntent,
pipeline string,
) compute.PlacementRequest {
) PlacementRequest {

spec := wire.Spec.Data

req := compute.PlacementRequest{
req := PlacementRequest{
Pipeline: pipeline,
Hosts: toComputeHosts(wire.Hosts),
Weights: wire.Weights,
Expand Down Expand Up @@ -54,7 +53,7 @@ func ToSchedulerRequest(
}

// Flavor sizing.
req.Flavor = compute.FlavorSpec{
req.Flavor = FlavorSpec{
VCPUs: spec.Flavor.Data.VCPUs,
MemoryMB: spec.Flavor.Data.MemoryMB,
}
Expand All @@ -66,7 +65,7 @@ func ToSchedulerRequest(
// preselection) is determined by the hypervisor family: cortex can enumerate
// KVM hypervisors itself, whereas VMware placement relies on the host list
// Nova sent in the request.
req.GatherAllCandidates = req.HypervisorFamily == compute.HypervisorFamilyKVM
req.GatherAllCandidates = req.HypervisorFamily == HypervisorFamilyKVM

// Image.
req.ImageHypervisorType = toImageHypervisorType(spec.Image.Data)
Expand All @@ -83,34 +82,34 @@ func ToSchedulerRequest(
return req
}

func toComputeHosts(hosts []api.ExternalSchedulerHost) []compute.Host {
out := make([]compute.Host, len(hosts))
func toComputeHosts(hosts []api.ExternalSchedulerHost) []Host {
out := make([]Host, len(hosts))
for i, h := range hosts {
out[i] = compute.Host{ComputeHost: h.ComputeHost, HypervisorHostname: h.HypervisorHostname}
out[i] = Host{ComputeHost: h.ComputeHost, HypervisorHostname: h.HypervisorHostname}
}
return out
}

// toHypervisorFamily resolves the coarse family from the wire hypervisor type.
// It is empty when unresolved.
func toHypervisorFamily(wire api.ExternalSchedulerRequest) compute.HypervisorFamily {
func toHypervisorFamily(wire api.ExternalSchedulerRequest) HypervisorFamily {
hv, err := wire.GetHypervisorType()
if err != nil {
return ""
}
switch hv {
case api.HypervisorTypeQEMU, api.HypervisorTypeCH:
return compute.HypervisorFamilyKVM
return HypervisorFamilyKVM
case api.HypervisorTypeVMware:
return compute.HypervisorFamilyVMware
return HypervisorFamilyVMware
}
return ""
}

// toImageHypervisorType maps the image-declared hypervisor type. It returns
// ImageHypervisorTypeUnparseable when the image declared a type that could not
// be parsed, distinct from ImageHypervisorTypeUnset when none was declared.
func toImageHypervisorType(image api.NovaImageMeta) compute.ImageHypervisorType {
func toImageHypervisorType(image api.NovaImageMeta) ImageHypervisorType {
hv, err := image.GetHypervisorType()
if err != nil {
// GetHypervisorType errors both when nothing is declared and when a
Expand All @@ -119,38 +118,38 @@ func toImageHypervisorType(image api.NovaImageMeta) compute.ImageHypervisorType
if image.Properties.Data != nil {
for _, key := range []string{"img_hv_type", "hypervisor_type"} {
if _, ok := image.Properties.Data[key]; ok {
return compute.ImageHypervisorTypeUnparseable
return ImageHypervisorTypeUnparseable
}
}
}
return compute.ImageHypervisorTypeUnset
return ImageHypervisorTypeUnset
}
switch hv {
case api.NovaImageMetaHVTypeVMware:
return compute.ImageHypervisorTypeVMware
return ImageHypervisorTypeVMware
case api.NovaImageMetaHVTypeBaremetal:
return compute.ImageHypervisorTypeBaremetal
return ImageHypervisorTypeBaremetal
case api.NovaImageMetaHVTypeKVM:
return compute.ImageHypervisorTypeKVM
return ImageHypervisorTypeKVM
}
return compute.ImageHypervisorTypeUnset
return ImageHypervisorTypeUnset
}

func toRequestedDestination(rd *api.NovaObject[api.NovaRequestedDestination]) *compute.RequestedDestination {
func toRequestedDestination(rd *api.NovaObject[api.NovaRequestedDestination]) *RequestedDestination {
if rd == nil {
return nil
}
return &compute.RequestedDestination{
return &RequestedDestination{
Host: rd.Data.Host,
Aggregates: rd.Data.Aggregates,
}
}

func toInstanceGroup(ig *api.NovaObject[api.NovaInstanceGroup]) *compute.InstanceGroupSpec {
func toInstanceGroup(ig *api.NovaObject[api.NovaInstanceGroup]) *InstanceGroupSpec {
if ig == nil {
return nil
}
out := &compute.InstanceGroupSpec{
out := &InstanceGroupSpec{
UUID: ig.Data.UUID,
Policy: ig.Data.Policy,
Hosts: ig.Data.Hosts,
Expand Down Expand Up @@ -232,13 +231,13 @@ func SplitTraits(extraSpecs map[string]string) (required, forbidden []string) {
}

// SplitCapabilities extracts a flavor's "capabilities:<key>" extra specs as typed
// compute.Capability equalities (prefix stripped). The "hypervisor_type" value is
// Capability equalities (prefix stripped). The "hypervisor_type" value is
// canonicalized (qemu->QEMU, ch->CH) so it matches what hypervisors advertise
// (filter_capabilities compares case-sensitively); all other values pass through
// unchanged. If any value uses an unsupported comparison operator it returns nil so
// the capabilities filter is skipped entirely (matching Nova behavior).
func SplitCapabilities(extraSpecs map[string]string) []compute.Capability {
var caps []compute.Capability
func SplitCapabilities(extraSpecs map[string]string) []Capability {
var caps []Capability
for key, value := range extraSpecs {
name, ok := strings.CutPrefix(key, "capabilities:")
if !ok {
Expand All @@ -252,7 +251,7 @@ func SplitCapabilities(extraSpecs map[string]string) []compute.Capability {
if name == "hypervisor_type" {
value = CanonicalHypervisorType(value)
}
caps = append(caps, compute.Capability{Name: name, Value: value})
caps = append(caps, Capability{Name: name, Value: value})
}
return caps
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package nova
package compute

import (
"slices"
"strings"
"testing"

"github.com/cobaltcore-dev/cortex/internal/scheduling/compute"
)

func TestSplitTraits(t *testing.T) {
Expand All @@ -27,31 +25,31 @@ func TestSplitTraits(t *testing.T) {
}

func TestSplitCapabilities(t *testing.T) {
sortCaps := func(c []compute.Capability) {
slices.SortFunc(c, func(a, b compute.Capability) int { return strings.Compare(a.Name, b.Name) })
sortCaps := func(c []Capability) {
slices.SortFunc(c, func(a, b Capability) int { return strings.Compare(a.Name, b.Name) })
}
cases := []struct {
name string
extraSpecs map[string]string
want []compute.Capability
want []Capability
}{
{
name: "canonicalizes lowercase hypervisor_type to match hosts",
extraSpecs: map[string]string{"capabilities:hypervisor_type": "qemu"},
want: []compute.Capability{{Name: "hypervisor_type", Value: "QEMU"}},
want: []Capability{{Name: "hypervisor_type", Value: "QEMU"}},
},
{
name: "ch canonicalized",
extraSpecs: map[string]string{"capabilities:hypervisor_type": "ch"},
want: []compute.Capability{{Name: "hypervisor_type", Value: "CH"}},
want: []Capability{{Name: "hypervisor_type", Value: "CH"}},
},
{
name: "non-hypervisor_type capabilities preserved verbatim",
extraSpecs: map[string]string{
"capabilities:hypervisor_type": "QEMU",
"capabilities:cpu_arch": "aarch64",
},
want: []compute.Capability{
want: []Capability{
{Name: "cpu_arch", Value: "aarch64"},
{Name: "hypervisor_type", Value: "QEMU"},
},
Expand Down
7 changes: 4 additions & 3 deletions internal/scheduling/nova/crs/recorder.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import (
"github.com/cobaltcore-dev/cortex/api/v1alpha1"
"github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute"
sched "github.com/cobaltcore-dev/cortex/internal/scheduling/compute"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/accounting"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"

"github.com/prometheus/client_golang/prometheus"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -37,12 +38,12 @@ var errFlavorNotInGroup = errors.New("flavor not in any group")
// Returns errFlavorNotInGroup (PAYG) if the flavor is not in any group.
// Returns a different error for transient failures (Knowledge CRD unavailable, etc).
func (r *Recorder) resolveFlavorGroup(ctx context.Context, flavorName string) (string, *compute.FlavorInGroup, error) {
fgClient := accounting.FlavorGroupKnowledgeClient{Client: r.Client}
fgClient := reservations.FlavorGroupKnowledgeClient{Client: r.Client}
flavorGroups, err := fgClient.GetAllFlavorGroups(ctx, nil)
if err != nil {
return "", nil, err
}
groupName, flavor, err := accounting.FindFlavorInGroups(flavorName, flavorGroups)
groupName, flavor, err := reservations.FindFlavorInGroups(flavorName, flavorGroups)
if err != nil {
return "", nil, errFlavorNotInGroup
}
Expand Down
2 changes: 1 addition & 1 deletion internal/scheduling/nova/external_scheduler_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ func (httpAPI *httpAPI) NovaExternalScheduler(w http.ResponseWriter, r *http.Req
ctx := r.Context()

// Translate the Nova wire request into the typed pipeline request.
schedReq := ToSchedulerRequest(requestData, intent, requestData.Pipeline)
schedReq := compute.ToSchedulerRequest(requestData, intent, requestData.Pipeline)

// Side effects run as a single post-hook after the pipeline, under the
// scheduler lock. It operates on the Nova wire request (which carries the
Expand Down
8 changes: 3 additions & 5 deletions internal/scheduling/reservations/capacity/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ import (
"github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute"
sched "github.com/cobaltcore-dev/cortex/internal/scheduling/compute"
caplib "github.com/cobaltcore-dev/cortex/internal/scheduling/lib/capacity"
"github.com/cobaltcore-dev/cortex/internal/scheduling/nova"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/accounting"
"github.com/cobaltcore-dev/cortex/pkg/multicluster"
"github.com/go-logr/logr"
)
Expand Down Expand Up @@ -227,7 +225,7 @@ func (c *Reconciler) reconcileAll(ctx context.Context) error {
logger := LoggerFromContext(ctx)
startTime := time.Now()

knowledge := &accounting.FlavorGroupKnowledgeClient{Client: c.client}
knowledge := &reservations.FlavorGroupKnowledgeClient{Client: c.client}
flavorGroups, err := knowledge.GetAllFlavorGroups(ctx, nil)
if err != nil {
return fmt.Errorf("failed to get flavor groups: %w", err)
Expand Down Expand Up @@ -829,7 +827,7 @@ func (c *Reconciler) probeScheduler(
// filter_has_requested_traits/filter_capabilities would otherwise see empty
// slices and become no-ops). No hypervisor-type default — only match when the
// flavor names one.
requiredTraits, forbiddenTraits := nova.SplitTraits(flavor.ExtraSpecs)
requiredTraits, forbiddenTraits := sched.SplitTraits(flavor.ExtraSpecs)
out, err := c.scheduler.RunWithoutLock(ctx, sched.PlacementRequest{
Pipeline: pipeline,
GatherAllCandidates: true,
Expand All @@ -844,7 +842,7 @@ func (c *Reconciler) probeScheduler(
RequiredTraits: requiredTraits,
ForbiddenTraits: forbiddenTraits,
RequestedCapabilities: reservations.HypervisorTypeCapability(flavor.ExtraSpecs, false),
AcceleratorsRequested: nova.RequestsAccelerators(flavor.ExtraSpecs),
AcceleratorsRequested: sched.RequestsAccelerators(flavor.ExtraSpecs),
Flavor: sched.FlavorSpec{
VCPUs: flavor.VCPUs,
MemoryMB: flavor.MemoryMB,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (

"github.com/cobaltcore-dev/cortex/api/v1alpha1"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/accounting"
commitments "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments"
"github.com/cobaltcore-dev/cortex/pkg/multicluster"
"github.com/go-logr/logr"
Expand Down Expand Up @@ -181,7 +180,7 @@ func (api *HTTPAPI) HandleChangeCommitments(w http.ResponseWriter, r *http.Reque
}

{
knowledge := &accounting.FlavorGroupKnowledgeClient{Client: api.client}
knowledge := &reservations.FlavorGroupKnowledgeClient{Client: api.client}
if knowledgeCRD, err := knowledge.Get(ctx); err == nil && knowledgeCRD != nil {
var currentVersion int64 = -1
if !knowledgeCRD.Status.LastContentChange.IsZero() {
Expand Down Expand Up @@ -232,7 +231,7 @@ func (api *HTTPAPI) HandleChangeCommitments(w http.ResponseWriter, r *http.Reque
}

func (api *HTTPAPI) processCommitmentChanges(ctx context.Context, w http.ResponseWriter, logger logr.Logger, req liquid.CommitmentChangeRequest, resp *liquid.CommitmentChangeResponse) error {
knowledge := &accounting.FlavorGroupKnowledgeClient{Client: api.client}
knowledge := &reservations.FlavorGroupKnowledgeClient{Client: api.client}
flavorGroups, err := knowledge.GetAllFlavorGroups(ctx, nil)
if err != nil {
logger.Info("failed to get flavor groups from knowledge extractor", "error", err)
Expand Down Expand Up @@ -641,7 +640,7 @@ func rollbackCR(ctx context.Context, logger logr.Logger, k8sClient client.Client
// performDryRun checks whether the net capacity delta in req can be placed by running
// temporary probe CommittedResource CRDs through the controller, then always cleaning them up.
func (api *HTTPAPI) performDryRun(ctx context.Context, logger logr.Logger, req liquid.CommitmentChangeRequest, resp *liquid.CommitmentChangeResponse) {
knowledge := &accounting.FlavorGroupKnowledgeClient{Client: api.client}
knowledge := &reservations.FlavorGroupKnowledgeClient{Client: api.client}
flavorGroups, err := knowledge.GetAllFlavorGroups(ctx, nil)
if err != nil {
resp.RejectionReason = "dry run: caches not ready, please retry later"
Expand Down
3 changes: 1 addition & 2 deletions internal/scheduling/reservations/commitments/api/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"time"

"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/accounting"
commitments "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments"
"github.com/go-logr/logr"
"github.com/google/uuid"
Expand Down Expand Up @@ -106,7 +105,7 @@ func mibToGiB(v *uint64) *uint64 {
// All flavor groups report usage; only those with fixed RAM/core ratio accept commitments.
func (api *HTTPAPI) buildServiceInfo(ctx context.Context, logger logr.Logger) (liquid.ServiceInfo, error) {
// Get all flavor groups from Knowledge CRDs
knowledge := &accounting.FlavorGroupKnowledgeClient{Client: api.client}
knowledge := &reservations.FlavorGroupKnowledgeClient{Client: api.client}
flavorGroups, err := knowledge.GetAllFlavorGroups(ctx, nil)
if err != nil {
// Return -1 as version when knowledge is not ready
Expand Down
4 changes: 2 additions & 2 deletions internal/scheduling/reservations/commitments/api/quota.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"time"

"github.com/cobaltcore-dev/cortex/api/v1alpha1"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/accounting"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
commitments "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments"
"github.com/cobaltcore-dev/cortex/pkg/multicluster"
"github.com/google/uuid"
Expand Down Expand Up @@ -97,7 +97,7 @@ func (api *HTTPAPI) HandleQuota(w http.ResponseWriter, r *http.Request) {
// Fetch flavor groups to determine per-resource RAM unit.
// The ProjectQuota CRD stores RAM values in GiB; Limes sends in the declared unit
// (slots for fixed-ratio groups, GiB for variable-ratio). Convert on receipt.
knowledge := &accounting.FlavorGroupKnowledgeClient{Client: api.client}
knowledge := &reservations.FlavorGroupKnowledgeClient{Client: api.client}
flavorGroups, err := knowledge.GetAllFlavorGroups(r.Context(), nil)
if err != nil {
log.Info("flavor groups not available for quota unit conversion", "error", err.Error())
Expand Down
4 changes: 2 additions & 2 deletions internal/scheduling/reservations/commitments/capacity.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/cobaltcore-dev/cortex/api/v1alpha1"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/accounting"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
)

// ErrCapacityNotReady is returned by CalculateCapacity when one or more FlavorGroupCapacity
Expand Down Expand Up @@ -64,7 +64,7 @@ func NewCapacityCalculator(client client.Client, conf APIConfig) *CapacityCalcul
// - Usage: always None. Limes derives project usage from the separate Report-Usage endpoint, so
// a usage value here is unused and would only invite misinterpretation of reserved capacity.
func (c *CapacityCalculator) CalculateCapacity(ctx context.Context, req liquid.ServiceCapacityRequest) (liquid.ServiceCapacityReport, error) {
knowledge := &accounting.FlavorGroupKnowledgeClient{Client: c.client}
knowledge := &reservations.FlavorGroupKnowledgeClient{Client: c.client}
flavorGroups, err := knowledge.GetAllFlavorGroups(ctx, nil)
if err != nil {
return liquid.ServiceCapacityReport{}, fmt.Errorf("failed to get flavor groups: %w", err)
Expand Down
Loading
Loading