Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9ae8ec4
feat(metalman): support rack manager virtual media installs
jveski Oct 14, 2026
7dd7bc6
fix(metalman): canonicalize the media TFTP server address
jveski Oct 14, 2026
47ccb83
fix(metalman): keep cleaning other targets when one media cleanup fails
jveski Oct 14, 2026
dc2dbb7
fix(metalman): keep a loader start that races the final boot timeout
jveski Oct 14, 2026
2fbf310
fix(metalman): restore frozen boot media images after restart
jveski Oct 14, 2026
90d1b8f
fix(metalman): serve cloud-init from frozen virtual media inputs
jveski Oct 14, 2026
c62cf6a
fix(metalman): protect media finalizer ownership
jveski Oct 14, 2026
bd7587a
fix(metalman): bind media first boot to the direct peer
jveski Oct 14, 2026
0aaef8a
fix(metalman): reject abandoned media callbacks
jveski Oct 14, 2026
ac3afc8
fix(metalman): scope media ownership to the serving site
jveski Oct 14, 2026
dedc81d
fix(metalman): reject failed media cloud-init reports
jveski Oct 14, 2026
9924f4c
fix(metalman): canonicalize paths before authorization
jveski Oct 14, 2026
75f8801
fix(metalman): scope retained media fallback checks by site
jveski Oct 14, 2026
1c868a3
fix(netboot): download the arm64 netboot files from the versioned rel…
jveski Oct 14, 2026
11cf73e
fix(netboot): give each NVMe namespace its own disk alias
jveski Oct 14, 2026
723be94
fix(metalman): order conflicting operations the same way on both sides
jveski Oct 14, 2026
b04f65b
fix(metalman): keep frozen netboot images until first boot ends
jveski Oct 14, 2026
bc8f396
fix(metalman): finish power-on recovery when the blade reports On
jveski Oct 14, 2026
632fd13
fix(metalman): check forwarded addresses against media claims
jveski Oct 14, 2026
1df0072
fix(metalman): retry rack detection when the fallback endpoint does n…
jveski Oct 14, 2026
d7b321b
fix(metalman): reject retained media leases in TFTP config lookups
jveski Oct 14, 2026
cb84b3a
fix(metalman): leave targets with foreign-site media untouched
jveski Oct 14, 2026
ca774b0
fix(metalman): require a real system before accepting the standard fa…
jveski Oct 14, 2026
77abd40
fix(metalman): reject unknown rack manager completion codes
jveski Oct 14, 2026
024b4b7
fix(metalman): attest first boot through retained media ownership
jveski Oct 14, 2026
78cc70c
fix(metalman): treat revoked media lease claims as ambiguous
jveski Oct 14, 2026
1ab21e2
fix(metalman): do not treat accepted rack manager tasks as done
jveski Oct 14, 2026
7b63aea
docs(api): note digest image requirements for virtual media
jveski Oct 14, 2026
fc2522a
fix(metalman): accept rack manager power transition states
jveski Oct 14, 2026
8eb81c0
fix(metalman): finish pending repave power-on before another power cycle
jveski Oct 14, 2026
a4d246c
fix(metalman): scope status reports to the serving site
jveski Oct 14, 2026
5c08d07
ci(release): publish the Azure Linux machine images
jveski Oct 14, 2026
bd22796
fix(netboot): keep the Ubuntu kernel for the amd64 installer
jveski Oct 14, 2026
35c527f
fix(metalman): block legacy installs while virtual media owns the mac…
jveski Oct 14, 2026
414e4a3
fix(netboot): publish the Azure Linux installer kernel as a separate …
jveski Oct 14, 2026
f451d16
ci(release): use the component matrix for the netboot images
jveski Oct 14, 2026
017431e
Merge remote-tracking branch 'origin/main' into metalman-extended
Copilot Oct 15, 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
14 changes: 14 additions & 0 deletions .github/workflows/images.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ jobs:
cache-from: type=gha,scope=netboot
cache-to: type=gha,scope=netboot,mode=max

- name: Build and push Azure Linux netboot dependency
if: needs.resolve.outputs.name == 'metalman'
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: images/netboot/Containerfile
platforms: ${{ needs.resolve.outputs.platforms }}
push: true
build-args: |
INSTALLER_KERNEL=azurelinux
tags: ${{ env.CONTAINER_REGISTRY }}/netboot-azurelinux:${{ needs.resolve.outputs.tag }}
cache-from: type=gha,scope=netboot-azurelinux
cache-to: type=gha,scope=netboot-azurelinux,mode=max

- name: Build and push
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
Expand Down
52 changes: 36 additions & 16 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ name: unbounded release
# -> binaries (GoReleaser; creates draft GitHub release)
# -> net-images (matrix: amd64-native, arm64-native)
# -> net-images-manifest (multi-arch tag stitch + sign + SBOM attest)
# -> netboot-image (PXE boot artifact image)
# -> netboot-image (PXE boot artifact images)
# -> metalman-image (stamped with the matching default netboot image)
# -> component-images (matrix: machina, machine-ops-controller, unbounded-operator,
# orca, gantry, host-ubuntu2404,
# inventory-aggregator, inventory-inspector, inventory-viewer)
# orca, gantry, host-ubuntu2404, host-azurelinux3,
# host-azurelinux3-nvidia, inventory-aggregator,
# inventory-inspector, inventory-viewer)
# -> gantry-chart (OCI chart + signed downloadable archive)
# -> manifests-tarball (make release-manifests + cosign sign)
# -> finalize (attach extras + krew manifest)
Expand Down Expand Up @@ -621,12 +622,22 @@ jobs:
done

# ---------------------------------------------------------------------------
# Build, push, sign, and SBOM-attest the netboot image before metalman is
# Build, push, sign, and SBOM-attest the netboot images before metalman is
# built. The metalman binary is stamped with this exact default netboot tag.
# netboot-azurelinux runs the Azure Linux installer kernel for hosts whose
# NIC driver the Ubuntu kernel lacks.
# ---------------------------------------------------------------------------
netboot-image:
needs: version
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
component:
- name: netboot
kernel: ubuntu
- name: netboot-azurelinux
kernel: azurelinux
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Expand Down Expand Up @@ -661,24 +672,25 @@ jobs:
VERSION=${{ needs.version.outputs.tag }}
GIT_COMMIT=${{ github.sha }}
CONTAINER_REGISTRY=${{ env.REGISTRY }}
tags: ${{ env.REGISTRY }}/netboot:${{ needs.version.outputs.tag }}
cache-from: type=gha,scope=netboot
cache-to: type=gha,scope=netboot,mode=max
INSTALLER_KERNEL=${{ matrix.component.kernel }}
tags: ${{ env.REGISTRY }}/${{ matrix.component.name }}:${{ needs.version.outputs.tag }}
cache-from: type=gha,scope=${{ matrix.component.name }}
cache-to: type=gha,scope=${{ matrix.component.name }},mode=max

- name: Sign container image
run: |
cosign sign --yes \
"${{ env.REGISTRY }}/netboot@${{ steps.push.outputs.digest }}"
"${{ env.REGISTRY }}/${{ matrix.component.name }}@${{ steps.push.outputs.digest }}"

- name: Generate and attach SBOM
uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
with:
image: ${{ env.REGISTRY }}/netboot@${{ steps.push.outputs.digest }}
image: ${{ env.REGISTRY }}/${{ matrix.component.name }}@${{ steps.push.outputs.digest }}
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}
format: spdx-json
artifact-name: sbom-netboot.spdx.json
output-file: sbom-netboot.spdx.json
artifact-name: sbom-${{ matrix.component.name }}.spdx.json
output-file: sbom-${{ matrix.component.name }}.spdx.json
# Defaults to true, and its uploader is neither idempotent nor
# able to cope with a release that does not exist yet. The next
# step attaches this with --clobber instead; see the header.
Expand All @@ -687,9 +699,9 @@ jobs:
- name: Attest SBOM to image
run: |
cosign attest --yes \
--predicate sbom-netboot.spdx.json \
--predicate sbom-${{ matrix.component.name }}.spdx.json \
--type spdxjson \
"${{ env.REGISTRY }}/netboot@${{ steps.push.outputs.digest }}"
"${{ env.REGISTRY }}/${{ matrix.component.name }}@${{ steps.push.outputs.digest }}"

# After the attestation, not before: the attestation is the
# authoritative, digest-bound copy, and it should never be blocked by the
Expand All @@ -702,7 +714,7 @@ jobs:
run: |
set -euo pipefail

gh release upload "$TAG" sbom-netboot.spdx.json --clobber
gh release upload "$TAG" sbom-${{ matrix.component.name }}.spdx.json --clobber

# ---------------------------------------------------------------------------
# Build, scan, push, sign, and SBOM-attest metalman after its default netboot
Expand Down Expand Up @@ -813,8 +825,8 @@ jobs:
# ---------------------------------------------------------------------------
# Build, scan, push, sign, and SBOM-attest the component images that have no
# release-time image ordering requirements.
# Go components cross-compile natively for amd64 and arm64; host-ubuntu2404
# publishes both platforms for PXE architecture support.
# Go components cross-compile natively for amd64 and arm64; the host-* machine
# images publish both platforms for PXE architecture support.
# ---------------------------------------------------------------------------
component-images:
needs: version
Expand Down Expand Up @@ -861,6 +873,14 @@ jobs:
file: images/host-ubuntu2404/Containerfile
platforms: linux/amd64,linux/arm64
trivy: false
- name: host-azurelinux3
file: images/host-azurelinux3/Containerfile
platforms: linux/amd64,linux/arm64
trivy: false
- name: host-azurelinux3-nvidia
file: images/host-azurelinux3-nvidia/Containerfile
platforms: linux/amd64,linux/arm64
trivy: false
- name: inventory-aggregator
file: images/inventory/aggregator/Containerfile
platforms: linux/amd64,linux/arm64
Expand Down
14 changes: 12 additions & 2 deletions api/machina/v1alpha3/machine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,8 @@ type DHCPLease struct {

// RedfishSpec defines Redfish BMC connection details.
type RedfishSpec struct {
// URL is the Redfish endpoint URL.
// URL is the Redfish endpoint URL. It may include a per-system path prefix,
// for example https://manager:8080/7. Do not include /redfish/v1.
// +kubebuilder:validation:Required
URL string `json:"url"`

Expand All @@ -372,6 +373,7 @@ type RedfishSpec struct {
Username string `json:"username"`

// DeviceID is the Redfish system device ID. Defaults to "1".
// It is ignored when the endpoint exposes a single system selected by its URL.
// +kubebuilder:default="1"
DeviceID string `json:"deviceID,omitempty"`

Expand All @@ -381,9 +383,11 @@ type RedfishSpec struct {
}

// PXESpec defines PXE boot configuration for a Machine.
// +kubebuilder:validation:XValidation:rule="!has(self.bootProtocol) || self.bootProtocol != 'VirtualMedia' || has(self.redfish)",message="VirtualMedia requires redfish"
type PXESpec struct {
// Image is an OCI image reference containing the machine disk image.
// The image must contain /disk/disk.img.gz.
// VirtualMedia requires a digest reference (image@sha256:...).
// Example: "ghcr.io/azure/host-ubuntu2404:v1"
// +kubebuilder:validation:Required
Image string `json:"image"`
Expand All @@ -404,6 +408,8 @@ type PXESpec struct {
// NetbootImage is an OCI image reference containing the PXE boot
// artifacts used to install Image. When omitted, metalman uses the
// default netboot image that corresponds to its release.
// VirtualMedia requires this field to be set to a digest reference
// (image@sha256:...).
// +optional
NetbootImage string `json:"netbootImage,omitempty"`

Expand All @@ -418,7 +424,9 @@ type PXESpec struct {
// BootProtocol selects how metalman should trigger network boot for
// repaves. PXE uses DHCP/TFTP bootfile options. HTTP uses Redfish UEFI
// HTTP boot with a URL derived from the netboot image metadata.
// +kubebuilder:validation:Enum=PXE;HTTP
// VirtualMedia mounts an operation-owned bootstrap ISO through Redfish.
// It requires redfish, a digest Image, and a digest NetbootImage.
// +kubebuilder:validation:Enum=PXE;HTTP;VirtualMedia
// +kubebuilder:default=PXE
// +optional
BootProtocol string `json:"bootProtocol,omitempty"`
Expand Down Expand Up @@ -450,6 +458,8 @@ const (
PXEBootProtocolPXE = "PXE"
// PXEBootProtocolHTTP uses Redfish UEFI HTTP boot.
PXEBootProtocolHTTP = "HTTP"
// PXEBootProtocolVirtualMedia uses a Redfish-mounted bootstrap ISO.
PXEBootProtocolVirtualMedia = "VirtualMedia"
// PXEArchitectureAMD64 is the x86_64 target architecture for PXE boot.
PXEArchitectureAMD64 = "amd64"
// PXEArchitectureARM64 is the aarch64 target architecture for PXE boot.
Expand Down
45 changes: 45 additions & 0 deletions api/machina/v1alpha3/machine_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,48 @@ func TestKubernetesSpecOmitsUnsetBootstrapTokenRef(t *testing.T) {
t.Fatalf("marshaled KubernetesSpec = %s, want bootstrapTokenRef omitted", data)
}
}

func TestBootProtocolDefaultsAndVirtualMedia(t *testing.T) {
for _, tc := range []struct {
name string
spec *PXESpec
want string
}{
{"nil", nil, PXEBootProtocolPXE},
{"default", &PXESpec{}, PXEBootProtocolPXE},
{"pxe", &PXESpec{BootProtocol: PXEBootProtocolPXE}, PXEBootProtocolPXE},
{"http", &PXESpec{BootProtocol: PXEBootProtocolHTTP}, PXEBootProtocolHTTP},
{"media", &PXESpec{BootProtocol: PXEBootProtocolVirtualMedia}, PXEBootProtocolVirtualMedia},
} {
t.Run(tc.name, func(t *testing.T) {
if got := tc.spec.TargetBootProtocol(); got != tc.want {
t.Fatalf("protocol = %q, want %q", got, tc.want)
}
})
}
}

func TestBootMediaStatusRoundTrip(t *testing.T) {
original := MachineOperationTargetStatus{MachineRef: "machine", BootMedia: &MachineOperationBootMediaStatus{
ID: "media", SourceServer: "10.0.0.1", Phase: "Installing", MachineUID: "machine-uid",
Netboot: &PXESpec{BootProtocol: PXEBootProtocolVirtualMedia, Image: "disk", Redfish: &RedfishSpec{
URL: "https://bmc", PasswordRef: SecretKeySelector{Name: "credentials", Namespace: "system", Key: "password"},
}},
CertFingerprint: "fingerprint", ImageWritten: true, Cleaned: true,
}}

data, err := json.Marshal(original)
if err != nil {
t.Fatal(err)
}

var restored MachineOperationTargetStatus
if err := json.Unmarshal(data, &restored); err != nil {
t.Fatal(err)
}

b := restored.BootMedia
if b == nil || b.ID != "media" || b.SourceServer != "10.0.0.1" || b.MachineUID != "machine-uid" || b.CertFingerprint != "fingerprint" || !b.ImageWritten || !b.Cleaned || b.Netboot.Redfish.PasswordRef.Name != "credentials" {
t.Fatalf("media status changed during JSON round trip: %s", data)
}
}
41 changes: 41 additions & 0 deletions api/machina/v1alpha3/machineoperation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ type MachineOperationStatus struct {

// MachineOperationTargetStatus records execution state for one target Machine.
type MachineOperationTargetStatus struct {
// BootMedia records immutable media ownership and cleanup progress.
// +optional
BootMedia *MachineOperationBootMediaStatus `json:"bootMedia,omitempty"`

// MachineRef is the name of the targeted Machine.
// +kubebuilder:validation:Required
MachineRef string `json:"machineRef"`
Expand Down Expand Up @@ -339,6 +343,43 @@ type MachineOperationTargetStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// MachineOperationBootMediaStatus persists media ownership before BMC changes.
type MachineOperationBootMediaStatus struct {
ID string `json:"id"`
SourceServer string `json:"sourceServer,omitempty"`
SourceName string `json:"sourceName,omitempty"`
Phase string `json:"phase"`
MachineUID types.UID `json:"machineUID"`
Netboot *PXESpec `json:"netboot"`
CertFingerprint string `json:"certFingerprint,omitempty"`
ImageWritten bool `json:"imageWritten,omitempty"`
Cleaned bool `json:"cleaned,omitempty"`
// StagedDeleted confirms rack media deletion after safe boot is restored.
StagedDeleted bool `json:"stagedDeleted,omitempty"`
// CleanedAt starts the bounded wait for installer reboot handoff.
CleanedAt *metav1.Time `json:"cleanedAt,omitempty"`
// RebootRequestedAt is set by the successful installer ready callback.
RebootRequestedAt *metav1.Time `json:"rebootRequestedAt,omitempty"`
// Released confirms local media removal after Cleaned revokes serving.
Released bool `json:"released,omitempty"`
// Site preserves controller ownership after the Machine is deleted.
Site string `json:"site,omitempty"`
// NodeName preserves the expected Node across Machine edits.
NodeName string `json:"nodeName,omitempty"`
// PhaseStartedAt bounds active progress without spending retry attempts.
PhaseStartedAt *metav1.Time `json:"phaseStartedAt,omitempty"`
// ReadinessStartedAt gives AC recovery one fixed grace window per phase.
ReadinessStartedAt *metav1.Time `json:"readinessStartedAt,omitempty"`
// LoaderStartedAt is set when the bootloader on the media first fetches
// its install config. It proves the host booted the media.
LoaderStartedAt *metav1.Time `json:"loaderStartedAt,omitempty"`
// BootAttempts counts power-on attempts used to boot the media.
BootAttempts int32 `json:"bootAttempts,omitempty"`
// Abandoned ends ownership after the installer never finished its
// handoff within the allowed time.
Abandoned bool `json:"abandoned,omitempty"`
}

// IsTerminal returns true if the operation phase is Complete or Failed.
func (s *MachineOperationStatus) IsTerminal() bool {
return s.Phase == OperationPhaseComplete || s.Phase == OperationPhaseFailed
Expand Down
45 changes: 45 additions & 0 deletions api/machina/v1alpha3/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion deploy/machina/06-metalman-rbac.yaml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ rules:
# MachineOperation controller
- apiGroups: ["unbounded-cloud.io"]
resources: ["machineoperations"]
verbs: ["get", "list", "watch", "delete"]
verbs: ["get", "list", "watch", "delete", "update", "patch"]
- apiGroups: ["unbounded-cloud.io"]
resources: ["machineoperations/finalizers"]
verbs: ["update", "patch"]
- apiGroups: ["unbounded-cloud.io"]
resources: ["machineoperations/status"]
verbs: ["get", "update", "patch"]
Expand Down
Loading
Loading