Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
509f84b
Renovate: Update kube-prometheus-stack Docker tag to v92.17.1 (#1465)
renovate[bot] Nov 3, 2026
7e1dd2d
Renovate: Update github.com/sapcc/go-bits digest to 0cf0269 (#1468)
renovate[bot] Nov 4, 2026
4f13e8c
Renovate: Update module github.com/cobaltcore-dev/openstack-hyperviso…
renovate[bot] Nov 4, 2026
8dff7f2
fix(quota): unify CR state filter — confirmed-only default (#1461)
juliusclausnitzer Nov 4, 2026
cc9ba72
fix: move CR reservation allocations from spec to status, refine CRD …
mblos Nov 4, 2026
33f74e7
refactor: clean up pipeline (#1464)
SoWieMarkus Nov 4, 2026
87c93f3
Renovate: Update External dependencies (#1477)
renovate[bot] Nov 5, 2026
10b7f11
refactor(quota): drive CR state filter from a single Helm value (#1472)
juliusclausnitzer Nov 5, 2026
b7d8b6f
Remove deprecated failover package in failoverhost as dependency (#1479)
PhilippMatthes Nov 5, 2026
821050c
chore: Move nova specifics in api directory (#1480)
SoWieMarkus Nov 5, 2026
cf019ca
fix: location of flavor group client (#1481)
SoWieMarkus Nov 5, 2026
e2e2980
Renovate: Update debian:trixie-slim Docker digest to a72b443 (#1482)
renovate[bot] Nov 5, 2026
06024c6
Polish failover host controller and fix probe reservation handling (#…
PhilippMatthes Nov 5, 2026
fa81b9f
Don't enforce image properties to be set inside compute pipeline (#1483)
PhilippMatthes Nov 5, 2026
fdb552f
Release cortex 0.8.0 (#1486)
cortex-ai-agents[bot] Nov 5, 2026
126fba3
bump app version [skip ci] (#1466)
github-actions[bot] Nov 5, 2026
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
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,70 @@
# Changelog

## Release PR [#1485](https://github.com/cobaltcore-dev/cortex/pull/1485) — 2026-11-05

### Bumped Versions

| Package | Previous | New |
|---|---|---|
| cortex | 0.7.3 | 0.8.0 |
| cortex-postgres | 0.6.30 | 0.6.31 |
| cortex-shim | 0.1.35 | 0.1.36 |
| bundles | 0.0.110 | 0.0.111 |
| cortex-placement-shim | 0.1.35 | 0.1.36 |

### ⚠️ Breaking Changes

- **`Decision` CRD removed:** The `cortex.cloud_decisions` custom resource definition has been fully deleted (type file, CRD YAML, all controller references). Any tooling using `kubectl get decisions` or importing the Decision type must be removed. Decision state KPI metrics for Cinder, Manila, and Nova are no longer emitted.
- **`CommittedResource` CRD schema change** ([#1438](https://github.com/cobaltcore-dev/cortex/pull/1438)) — Allocation tracking has moved from `Spec.CommittedResourceReservation.Allocations` to `Status.CommittedResourceReservation.Allocation`. Billing fields have been consolidated into a new `Status.Billing` sub-struct (`AssignedInstances`, `UsedResources`, `LastReconcileAt`, `ObservedGeneration`). The flat billing fields on `CommittedResourceStatus` (`AssignedInstances`, `UsedResources`, `LastUsageReconcileAt`, `UsageObservedGeneration`) are retained but deprecated. Generation tracking has been migrated from `ParentGeneration`/`ObservedParentGeneration` to standard Kubernetes conditions with `ObservedGeneration` signaling.
- **`Reservation` CRD schema expanded** ([#1438](https://github.com/cobaltcore-dev/cortex/pull/1438)) — New `Status.CommittedResourceReservation.Allocation` field replaces spec-level allocations. `PrimaryResource` field added to `CommittedResourceReservationSpec`. `CreationTimestamp` on `CommittedResourceAllocation` is deprecated. Resource keys are normalised to canonical `hv1.ResourceName` constants.
- **Pipeline CRD trimmed** — Several `spec` fields have been removed from the Pipeline CRD.
- **Helm values** — New required key `kvm.crStateFilter` in `cortex-nova/values.yaml` (defaults to `["confirmed"]`). Removed KPI alert templates for Cinder/Manila/Nova decision-state metrics.

### Bug Fixes

- **Quota: unify CR state filter to confirmed-only default** ([#1461](https://github.com/cobaltcore-dev/cortex/pull/1461)) — Aligned `FilterQuotaEnforcement` and the quota controller to count only `confirmed` CRs toward capacity, dropping `guaranteed` from the default filter. This prepares for the future removal of the guaranteed state.

- **Move CR reservation allocations from spec to status** ([#1438](https://github.com/cobaltcore-dev/cortex/pull/1438)) — Major CRD refactor: allocation tracking moved to `Status.Allocation`, allocation accounting rewritten for multi-slot spanning, grace period replaced with live `VMSource.GetVM()` lookup, and billing fields grouped into `Status.Billing`.

- **Fix location of flavor group client** ([#1481](https://github.com/cobaltcore-dev/cortex/pull/1481)) — Corrected the import path for the flavor group client after the Nova API directory reorganisation.

- **Polish failover host controller & fix probe reservation handling** ([#1484](https://github.com/cobaltcore-dev/cortex/pull/1484)) — The path finder now ignores *all* reservation types during capacity probes (not just failover), so capacity is no longer understated when probing. Alert window bumped to 60 minutes; partition descriptions now show the full flavor list; progress logging added for long passes.

- **Don't enforce image properties when unset in compute pipeline** ([#1483](https://github.com/cobaltcore-dev/cortex/pull/1483)) — Boot-from-volume VMs no longer cause spurious image property alignment errors. Image property checks are now only applied when an image is actually specified, simplifying failover scheduling calls.

### Chores & Refactoring

- **Clean up scheduling pipeline** ([#1464](https://github.com/cobaltcore-dev/cortex/pull/1464)) — General scheduling pipeline code clean-up: the `Decision` type is fully removed, test helpers are deduplicated, and pipeline controller signatures are simplified to use the new `compute.Request`/`compute.Placement` types.

- **Drive CR state filter from a single Helm value** ([#1472](https://github.com/cobaltcore-dev/cortex/pull/1472)) — Eliminated duplicated Go-level defaults for the CR state filter. `kvm.crStateFilter` is now the single source of truth, wired into both the quota controller and the enforcement filter via a YAML anchor.

- **Remove deprecated failover package** ([#1479](https://github.com/cobaltcore-dev/cortex/pull/1479)) — Removed the deprecated failover package as a dependency from the failover host controller; reuses shared message-parsing code for pipeline name inference.

- **Move Nova-specific types into `api/external/nova/`** ([#1480](https://github.com/cobaltcore-dev/cortex/pull/1480)) — Introduced `placement_types.go` (typed `FlavorSpec`, `Capability`, `HypervisorFamily`, `RequestedDestination`, `InstanceGroupSpec`, and trait/capability parsing helpers) and `messages.go` (Nova wire types, intent constants, request/response structs) with full test coverage. This separates Nova wire-format types from the internal scheduling API.

### Dependency Updates

| Dependency | Old | New |
|---|---|---|
| `kube-prometheus-stack` | — | **v92.17.1** |
| `github.com/sapcc/go-bits` | — | digest `0cf0269` |
| `github.com/cobaltcore-dev/openstack-hypervisor-operator` | — | **v1.4.0** |
| External dependencies | — | batch update |
| `debian:trixie-slim` | — | digest `a72b443` |

### ⚠️ Operational Notes

1. **CRD migration — `cortex.cloud_decisions` deleted:** The Decision CRD YAML has been removed from the Helm CRDs folder. Ensure any existing Decision resources are cleaned up before or during upgrade. If your CI/CD pipeline applies CRDs separately, remove the old `cortex.cloud_decisions` CRD manifest.
2. **CRD migration — `cortex.cloud_committedresources`:** The new `Status.Allocation` and `Status.Billing` fields will be empty after upgrade until the controllers reconcile. The deprecated flat billing fields are still populated during a transition period — migrate consumers to `Status.Billing` before the next major release.
3. **CRD migration — `cortex.cloud_reservations`:** Spec-level `Allocations` are deprecated; the controller now writes to `Status.CommittedResourceReservation.Allocation`. Existing spec-level allocations will be migrated automatically.
4. **Helm values — `kvm.crStateFilter`:** A new required key. The default is `["confirmed"]`, which changes behavior if your deployment previously counted `guaranteed` CRs toward quota. Review quota behavior in staging before rollout.
5. **Dashboard/alert updates:** KPI templates for Cinder, Manila, and Nova decision-state metrics have been removed. Remove any dashboards or alert rules referencing these metrics.
6. **Major version bump (0.7.x → 0.8.0):** This release carries a minor version bump reflecting the breaking CRD schema changes and Decision type removal.

**Contributors:** @SoWieMarkus, @PhilippMatthes, @mblos, @renovate[bot], @cortex-ai-agents[bot]

**PR:** [#1485](https://github.com/cobaltcore-dev/cortex/pull/1485) | **Commits:** 14 | **Files changed:** 100 | **+3,728 / −7,181**

## Release PR [#1458](https://github.com/cobaltcore-dev/cortex/pull/1458) — 2026-11-02

### Bumped Versions
Expand Down
21 changes: 18 additions & 3 deletions api/external/nova/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,19 @@ const (
FlavorTypeHANA FlavorType = "hana"
)

// GetFlavorType determines the flavor type based on the requested flavor's extra specs.
func (r ExternalSchedulerRequest) GetFlavorType() (FlavorType, error) {
extraSpecs := r.Spec.Data.Flavor.Data.ExtraSpecs
// FlavorTypeFromExtraSpecs determines the flavor type from a flavor's extra specs.
//
// The trait:CUSTOM_HANA_EXCLUSIVE_HOST extra spec is the canonical signal that a
// flavor targets HANA workloads: when set to "required" the VM must land on a
// HANA-exclusive host, so the flavor is a HANA flavor; when set to "forbidden" (or
// absent) it is a general-purpose flavor. The value is compared case-insensitively.
// Any other explicit value is rejected as an error, since it indicates a malformed
// flavor rather than a type we can safely default.
//
// Callers that only hold the raw extra specs map (e.g. failover reservation
// scheduling) should use this helper instead of re-implementing the trait check, so
// the HANA-detection logic stays in one place.
func FlavorTypeFromExtraSpecs(extraSpecs map[string]string) (FlavorType, error) {
val, ok := extraSpecs["trait:CUSTOM_HANA_EXCLUSIVE_HOST"]
if !ok {
return FlavorTypeGeneralPurpose, nil
Expand All @@ -183,6 +193,11 @@ func (r ExternalSchedulerRequest) GetFlavorType() (FlavorType, error) {
}
}

// GetFlavorType determines the flavor type based on the requested flavor's extra specs.
func (r ExternalSchedulerRequest) GetFlavorType() (FlavorType, error) {
return FlavorTypeFromExtraSpecs(r.Spec.Data.Flavor.Data.ExtraSpecs)
}

type HypervisorType string

const (
Expand Down
33 changes: 33 additions & 0 deletions api/external/nova/messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,39 @@ func TestGetFlavorType(t *testing.T) {
}
}

func TestFlavorTypeFromExtraSpecs(t *testing.T) {
tests := []struct {
name string
extraSpecs map[string]string
expected FlavorType
expectError bool
}{
{name: "hana required", extraSpecs: map[string]string{"trait:CUSTOM_HANA_EXCLUSIVE_HOST": "required"}, expected: FlavorTypeHANA},
{name: "hana required mixed case", extraSpecs: map[string]string{"trait:CUSTOM_HANA_EXCLUSIVE_HOST": "Required"}, expected: FlavorTypeHANA},
{name: "general purpose forbidden", extraSpecs: map[string]string{"trait:CUSTOM_HANA_EXCLUSIVE_HOST": "forbidden"}, expected: FlavorTypeGeneralPurpose},
{name: "general purpose trait absent", extraSpecs: map[string]string{}, expected: FlavorTypeGeneralPurpose},
{name: "general purpose nil specs", extraSpecs: nil, expected: FlavorTypeGeneralPurpose},
{name: "unsupported value", extraSpecs: map[string]string{"trait:CUSTOM_HANA_EXCLUSIVE_HOST": "optional"}, expectError: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := FlavorTypeFromExtraSpecs(tt.extraSpecs)
if tt.expectError {
if err == nil {
t.Fatalf("expected error but got none")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, got)
}
})
}
}

func TestNovaSpecUnmarshal(t *testing.T) {
var jsonData = `{
"spec": {
Expand Down
195 changes: 195 additions & 0 deletions api/external/nova/placement_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright SAP SE
// SPDX-License-Identifier: Apache-2.0

package api

import "strings"

// HypervisorFamily is the coarse hypervisor family a request targets. It is the
// distinction the pipeline and request routing care about. The adapter resolves
// the family from Nova's "capabilities:hypervisor_type" flavor extra spec.
type HypervisorFamily string

const (
// HypervisorFamilyKVM covers KVM-based hypervisors (QEMU and CH).
HypervisorFamilyKVM HypervisorFamily = "kvm"
// HypervisorFamilyVMware covers VMware hypervisors.
HypervisorFamilyVMware HypervisorFamily = "vmware"
)

// ImageHypervisorType is the hypervisor type declared by the image. The adapter
// resolves it from Nova's image properties ("img_hv_type"/"hypervisor_type").
type ImageHypervisorType string

const (
// ImageHypervisorTypeVMware indicates a VMware image.
ImageHypervisorTypeVMware ImageHypervisorType = "vmware"
// ImageHypervisorTypeBaremetal indicates a baremetal image.
ImageHypervisorTypeBaremetal ImageHypervisorType = "baremetal"
// ImageHypervisorTypeKVM indicates a KVM image.
ImageHypervisorTypeKVM ImageHypervisorType = "kvm"
// ImageHypervisorTypeUnset indicates no image hypervisor type was declared.
ImageHypervisorTypeUnset ImageHypervisorType = ""
// ImageHypervisorTypeUnparseable indicates an image hypervisor type was declared but could not be parsed.
ImageHypervisorTypeUnparseable ImageHypervisorType = "unparseable"
)

// Capability is a single required hypervisor capability as a typed key/value
// equality, e.g. {Name: "cpu_arch", Value: "x86_64"}. The adapter resolves these
// from Nova's "capabilities:<key>" flavor extra specs and rejects any entry that
// uses an unsupported comparison operator before it reaches the pipeline.
type Capability struct {
// Name is the capability key without the "capabilities:" prefix.
Name string
// Value is the required exact value.
Value string
}

// FlavorSpec is the typed, Nova-agnostic description of the requested flavor.
// It carries no extra-spec map: the adapter resolves every extra spec the
// pipeline needs into the typed fields on this struct and on PlacementRequest.
type FlavorSpec struct {
// MemoryMB is the requested memory per instance. 0 means a zero-memory
// instance to capacity/bin-pack steps (fits anywhere), so the adapter sets it.
MemoryMB uint64
// VCPUs is the requested vCPUs per instance. 0 means a zero-CPU instance to
// capacity/bin-pack steps (fits anywhere), so the adapter sets it.
VCPUs uint64
}

// RequestedDestination mirrors the subset of Nova's requested_destination that
// the pipeline reads. A nil *RequestedDestination on the request means no
// destination constraint was requested.
type RequestedDestination struct {
// Host is the requested target host. Empty means no host was pinned (only an
// aggregate constraint applies).
Host string
// Aggregates are the host-aggregate names the destination must belong to.
// Empty/nil means no aggregate constraint.
Aggregates []string
}

// InstanceGroupSpec is the typed subset of a Nova server group the pipeline
// reads. A nil *InstanceGroupSpec on the request means the instance is not part
// of a server group, so group affinity/anti-affinity filters are skipped.
type InstanceGroupSpec struct {
// UUID identifies the server group. Empty if unknown.
UUID string
// Policy is the group policy (e.g. "anti-affinity", "soft-affinity"). Empty
// means no policy-specific filter/weigher applies.
Policy string
// Hosts are the hosts already occupied by members of this group. Empty/nil
// means the group currently occupies no hosts.
Hosts []string
// Members are the instance UUIDs already in the group. Empty/nil means the
// group has no members yet, so anti-affinity has nothing to exclude.
Members []string
// MaxServerPerHost caps how many group members may share a host. 0 means
// unset, which the anti-affinity filter treats as the Nova default of 1.
MaxServerPerHost int
}

// The helpers below resolve Nova's loosely-typed flavor extra-spec string
// conventions into the typed values the scheduling pipeline consumes. They live
// here, alongside the typed values they produce, because the
// "trait:"/"capabilities:"/"accel:" key conventions are Nova's — not the
// pipeline's. Both the Nova spec adapter and the in-process reservation
// controllers (which hold a flavor's extra-spec map directly) call them, so the
// conventions are understood in exactly one place.

// unsupportedCapabilityOps are the Nova extra-spec comparison operators the
// capabilities filter does not implement. A capability value containing any of
// these means the request uses an advanced match the pipeline can't honor, so
// SplitCapabilities drops capabilities entirely (the filter then becomes a
// no-op), matching Nova's "skip" behavior.
// See: https://github.com/sapcc/nova/blob/3ebf80/nova/scheduler/filters/extra_specs_ops.py#L23
var unsupportedCapabilityOps = []string{
"=", "<in>", "<all-in>", "==", "!=", ">=", "<=",
"s==", "s!=", "s<", "s<=", "s>", "s>=", "<or>",
}

// SplitTraits splits a flavor's "trait:<name>" extra specs into required and
// forbidden trait names, classified by the "required"/"forbidden" value.
func SplitTraits(extraSpecs map[string]string) (required, forbidden []string) {
for key, value := range extraSpecs {
name, ok := strings.CutPrefix(key, "trait:")
if !ok {
continue
}
switch value {
case "required":
required = append(required, name)
case "forbidden":
forbidden = append(forbidden, name)
}
}
return required, forbidden
}

// SplitCapabilities extracts a flavor's "capabilities:<key>" extra specs as typed
// 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) []Capability {
var caps []Capability
for key, value := range extraSpecs {
name, ok := strings.CutPrefix(key, "capabilities:")
if !ok {
continue
}
for _, op := range unsupportedCapabilityOps {
if strings.Contains(value, op) {
return nil // Unsupported operator: skip the whole filter.
}
}
if name == "hypervisor_type" {
value = CanonicalHypervisorType(value)
}
caps = append(caps, Capability{Name: name, Value: value})
}
return caps
}

// CanonicalHypervisorType normalizes a flavor's capabilities:hypervisor_type value
// to the casing hypervisors advertise (QEMU/CH), since filter_capabilities compares
// values case-sensitively. Unrecognized values pass through unchanged.
func CanonicalHypervisorType(value string) string {
switch strings.ToLower(value) {
case "qemu":
return "QEMU"
case "ch":
return "CH"
default:
return value
}
}

// RequestsAccelerators reports whether a flavor requests a hardware accelerator,
// resolved from the presence of the "accel:device_profile" extra spec.
func RequestsAccelerators(extraSpecs map[string]string) bool {
_, ok := extraSpecs["accel:device_profile"]
return ok
}

// HypervisorTypeCapability returns the required (key=value) hypervisor capabilities
// for a reservation scheduler request, so filter_capabilities keeps a workload off
// incompatible hosts. The reservation controllers build the typed placement request
// directly (not via the Nova wire adapter), yet their flavors still carry Nova's
// extra-spec string conventions, so they reuse SplitCapabilities here and layer the
// reservation-specific defaultQEMU seeding on top. It extracts ALL of the flavor's
// "capabilities:<key>" extra specs (canonicalizing the hypervisor_type casing and
// dropping everything on an unsupported operator) — not just hypervisor_type.
//
// When the flavor names no hypervisor type and defaultQEMU is set (the failover
// controller seeds from running VMs whose flavor may omit it), the canonical "QEMU"
// hypervisor_type is added. Without defaultQEMU, a missing hypervisor type adds no
// hypervisor_type constraint.
func HypervisorTypeCapability(extraSpecs map[string]string, defaultQEMU bool) []Capability {
caps := SplitCapabilities(extraSpecs)
if extraSpecs["capabilities:hypervisor_type"] == "" && defaultQEMU {
caps = append(caps, Capability{Name: "hypervisor_type", Value: "QEMU"})
}
return caps
}
Loading
Loading