diff --git a/Makefile b/Makefile
index b21c3c053..634790562 100644
--- a/Makefile
+++ b/Makefile
@@ -170,6 +170,7 @@ install-system:
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" \
nbdkit nbdkit-plugin-dev pkg-config $(QEMU_PACKAGES) qemu-utils qemu-kvm \
+ ovmf qemu-efi-aarch64 \
libvirt-daemon-system libvirt-clients libvirt-dev make gcc jq curl \
iproute2 netcat-openbsd openssh-client wget git unzip sudo xz-utils file \
ovn-central ovn-host openvswitch-switch dhcpcd-base
diff --git a/cmd/spinifex/cmd/admin.go b/cmd/spinifex/cmd/admin.go
index f220a2644..34bef4be4 100644
--- a/cmd/spinifex/cmd/admin.go
+++ b/cmd/spinifex/cmd/admin.go
@@ -84,6 +84,14 @@ var supportedPlatforms = map[string]bool{
"Windows": true,
}
+// Mirrors the gateway RegisterImage allowlist so admin imports can't write an
+// AMI with a boot mode that RegisterImage would reject.
+var supportedBootModes = map[string]bool{
+ "bios": true,
+ "uefi": true,
+ "uefi-preferred": true,
+}
+
var adminCmd = &cobra.Command{
Use: "admin",
Short: "Administrative commands for Spinifex platform management",
@@ -304,6 +312,7 @@ func init() {
imagesImportCmd.Flags().String("version", "", "Specified distro version (e.g 12)")
imagesImportCmd.Flags().String("arch", "", "Specified distro arch (e.g aarch64, arm64, x86_64)")
imagesImportCmd.Flags().String("platform", "Linux/UNIX", "Specified platform (e.g Linux/UNIX, Windows)")
+ imagesImportCmd.Flags().String("boot-mode", "", "Boot mode for the imported AMI (bios|uefi|uefi-preferred). Required with --file. Overrides the catalog value when used with --name.")
imagesImportCmd.Flags().StringSlice("tag", nil, "Tag to apply to the imported AMI as key=value (repeatable; e.g. --tag spinifex:managed-by=elbv2)")
imagesImportCmd.Flags().Bool("force", false, "Force command execution (overwrites existing files)")
imagesImportCmd.Flags().Bool("skip-verify", false, "Skip catalog-image checksum verification (INSECURE; operator assumes integrity responsibility)")
@@ -376,6 +385,22 @@ func runimagesImportCmd(cmd *cobra.Command, args []string) {
}
}
+ // --file imports have no catalog metadata to inherit from, so the operator
+ // must declare the boot mode explicitly — guessing would silently produce
+ // a BIOS AMI from a UEFI-only image (or vice versa) and fail at launch.
+ // --name imports inherit from the catalog; the flag overrides when set.
+ bootModeFlag, _ := cmd.Flags().GetString("boot-mode")
+ if bootModeFlag != "" {
+ if !supportedBootModes[bootModeFlag] {
+ fmt.Fprintf(os.Stderr, "Unsupported --boot-mode %q (expected bios|uefi|uefi-preferred)\n", bootModeFlag)
+ os.Exit(1)
+ }
+ image.BootMode = bootModeFlag
+ } else if imageName == "" {
+ fmt.Fprintf(os.Stderr, "--boot-mode is required when importing via --file (expected bios|uefi|uefi-preferred)\n")
+ os.Exit(1)
+ }
+
// --tag k=v (repeatable) merges user-supplied tags into the AMI. Overrides
// catalog tags on key collision so operators can re-tag a known image.
tagFlags, _ := cmd.Flags().GetStringSlice("tag")
diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md
index 8002643a9..4d45dbd45 100644
--- a/docs/COMMANDS.md
+++ b/docs/COMMANDS.md
@@ -89,7 +89,7 @@ Operational commands for inspecting cluster state. These fan out NATS requests t
| Command | Flags | Prerequisites | Basic Logic | Test Cases | Status |
|---------|-------|---------------|-------------|------------|--------|
-| `spx admin images import` | `--name`, `--file`, `--force`, `--skip-verify` | Cluster must be running; either `--name` (catalog download) or `--file` (operator-supplied media) | Catalog imports (`--name`) download the image, fetch the catalog `Checksum` URL, and verify the SHA-256/SHA-512 digest before extraction. Mismatch fails closed; the cached file is left on disk and `--force` re-downloads. `--file` imports skip verification — operator-supplied media is outside Spinifex's trust boundary, and the skip is logged at INFO for audit. `--skip-verify` bypasses verification for catalog imports and emits a WARN slog + stderr notice; use only for debugging or when upstream mirrors are confirmed-broken. | 1. Import valid catalog image (verifies checksum)
2. Tampered cache hit fails with `ErrChecksumMismatch`
3. `--force` recovers after a mismatch
4. `--file` import skips verification
5. `--skip-verify` bypasses checksum with WARN | **DONE** |
+| `spx admin images import` | `--name`, `--file`, `--distro`, `--version`, `--arch`, `--platform`, `--boot-mode` (bios/uefi/uefi-preferred), `--tag`, `--force`, `--skip-verify` | Cluster must be running; either `--name` (catalog download) or `--file` (operator-supplied media). `--file` mode additionally requires `--distro`, `--version`, `--arch`, `--boot-mode`. | Catalog imports (`--name`) download the image, fetch the catalog `Checksum` URL, verify the SHA-256/SHA-512 digest, and inherit `BootMode` from the catalog entry. `--boot-mode` overrides the catalog value when set. Mismatch fails closed; the cached file is left on disk and `--force` re-downloads. `--file` imports skip checksum verification (operator-supplied media is outside Spinifex's trust boundary, the skip is logged at INFO for audit) and require an explicit `--boot-mode` because there is no catalog metadata to inherit from. `--skip-verify` bypasses verification for catalog imports and emits a WARN slog + stderr notice; use only for debugging or when upstream mirrors are confirmed-broken. | 1. Import valid catalog image (verifies checksum)
2. Tampered cache hit fails with `ErrChecksumMismatch`
3. `--force` recovers after a mismatch
4. `--file` import skips verification
5. `--skip-verify` bypasses checksum with WARN
6. `--file` without `--boot-mode` rejected
7. `--boot-mode=invalid` rejected | **DONE** |
| `spx admin images list` | — | None | Lists available OS images that can be imported or downloaded | 1. List available images | **DONE** |
#### Image integrity verification (CMMC SI.L1-3.14.2)
@@ -168,7 +168,7 @@ file, deferred to a later phase.
|---------|-------------------|---------------|---------------|-------------|------------|--------|
| `describe-images` | `--image-ids` (format validation only), `--owners` (self, account ID, alias), `--filters` (name, state, architecture, image-id, is-public, owner-id, description, image-type, tag:\*) | `--executable-users`, `--include-deprecated`, `--include-disabled`, `--max-results`, `--next-token`, `--dry-run` | None | NATS `ec2.DescribeImages` → daemon reads AMI metadata from Predastore S3 buckets (ami-*) → filters by ImageIds, Owners, and Filters → returns image list with state, architecture, block device mappings | 1. List all images
2. Filter by image ID
3. Filter by owner (self/amazon)
4. Non-existent AMI returns empty
5. Verify metadata fields (architecture, state, rootDeviceName)
6. Filter by name wildcard
7. Unknown filter returns InvalidParameterValue | **DONE** |
| `create-image` | `--instance-id`, `--name`, `--description`, `--tag-specifications` | `--no-reboot`, `--block-device-mappings`, `--dry-run` | Instance must exist (running or stopped) | Gateway validates input → NATS `ec2.{instanceId}.CreateImage` (per-instance topic) → daemon extracts root volume → snapshots via `ebs.snapshot` NATS (running) or offline S3 copy (stopped) → creates AMI metadata in S3 (`{amiId}/config.json`) → stores tags → returns ami-ID. Duplicate AMI name validation enforced. | 1. Create image from running instance
2. Create image from stopped instance
3. Invalid instance ID (error)
4. Duplicate AMI name (error)
5. Verify new AMI appears in describe-images
6. Launch new instance from created AMI | **DONE** |
-| `register-image` | `--name`, `--description`, `--architecture` (x86_64/arm64/i386), `--root-device-name`, `--virtualization-type` (hvm only), `--block-device-mappings` (root with `Ebs.SnapshotId`+optional `VolumeSize`), `--tag-specifications` | `--billing-products`, `--uefi-data` | Backing snapshot must exist in Predastore (`{snapshotId}/metadata.json`); caller must own snapshot or it must be system-owned | Gateway validates name length (3–128), `snap-` prefix, architecture/virtualization values → NATS `ec2.RegisterImage` → daemon checks AMI name uniqueness, reads snapshot metadata, verifies snapshot ownership, builds `viperblock.AMIMetadata` (defaults: `Architecture=x86_64`, `Virtualization=hvm`, `PlatformDetails=Linux/UNIX`, `RootDeviceType=ebs`), writes `{amiId}/config.json`. Pointer-only — never touches block data. `BootMode`, `KernelId`, `RamdiskId`, `TpmSupport`, `ImdsSupport`, `EnaSupport`, `SriovNetSupport`, `ImageLocation`, `paravirtual` virtualization rejected with `InvalidParameterValue`. `VolumeSize` smaller than snapshot rejected. | 1. Register with valid snapshot
2. Missing name/snapshot (error)
3. Duplicate AMI name (`InvalidAMIName.Duplicate`)
4. Snapshot not found (`InvalidSnapshot.NotFound`)
5. Cross-account snapshot (`UnauthorizedOperation`)
6. Tags from `TagSpecifications` persisted
7. Verify registered image in describe-images | **DONE** |
+| `register-image` | `--name`, `--description`, `--architecture` (x86_64/arm64/i386), `--root-device-name`, `--virtualization-type` (hvm only), `--boot-mode` (bios/uefi/uefi-preferred), `--block-device-mappings` (root with `Ebs.SnapshotId`+optional `VolumeSize`), `--tag-specifications` | `--billing-products`, `--uefi-data` | Backing snapshot must exist in Predastore (`{snapshotId}/metadata.json`); caller must own snapshot or it must be system-owned | Gateway validates name length (3–128), `snap-` prefix, architecture/virtualization/boot-mode values → NATS `ec2.RegisterImage` → daemon checks AMI name uniqueness, reads snapshot metadata, verifies snapshot ownership, builds `viperblock.AMIMetadata` (defaults: `Architecture=x86_64`, `Virtualization=hvm`, `PlatformDetails=Linux/UNIX`, `RootDeviceType=ebs`), writes `{amiId}/config.json`. Pointer-only — never touches block data. `BootMode` outside `bios|uefi|uefi-preferred`, `KernelId`, `RamdiskId`, `TpmSupport`, `ImdsSupport`, `EnaSupport`, `SriovNetSupport`, `ImageLocation`, `paravirtual` virtualization rejected with `InvalidParameterValue`. `uefi-preferred` is treated as `uefi` at launch. `VolumeSize` smaller than snapshot rejected. | 1. Register with valid snapshot
2. Missing name/snapshot (error)
3. Duplicate AMI name (`InvalidAMIName.Duplicate`)
4. Snapshot not found (`InvalidSnapshot.NotFound`)
5. Cross-account snapshot (`UnauthorizedOperation`)
6. Tags from `TagSpecifications` persisted
7. Verify registered image in describe-images
8. `--boot-mode=uefi` accepted, returned by `describe-images`
9. `--boot-mode=invalid` rejected (`InvalidParameterValue`) | **DONE** |
| `deregister-image` | `--image-id` | `--dry-run` | AMI must exist; caller must own it (system AMIs immutable via this API) | Gateway validates `ami-` prefix → NATS `ec2.DeregisterImage` → daemon hard-deletes `{amiId}/config.json` from Predastore. Backing snapshot is left intact (matches AWS — operators run `delete-snapshot` separately to reclaim block storage). Cross-account/system-AMI mutations rejected with `UnauthorizedOperation`. Re-deregister returns `InvalidAMIID.NotFound` (no tombstone). | 1. Deregister existing AMI
2. Deregister non-existent AMI (`InvalidAMIID.NotFound`)
3. Re-deregister already-deleted AMI (`InvalidAMIID.NotFound`)
4. Cross-account AMI (`UnauthorizedOperation`)
5. System AMI (`UnauthorizedOperation`)
6. Verify deregistered AMI not in describe-images
7. Backing snapshot untouched | **DONE** |
| `copy-image` | `--source-image-id`, `--source-region` (must equal gateway region), `--name`, `--description`, `--client-token` (accepted, not honoured), `--copy-image-tags`, `--tag-specifications` (image only) | `--encrypted`, `--kms-key-id`, `--destination-outpost-arn`, `--dry-run` | Source AMI must exist and be owned by caller OR be a system AMI; backing snapshot must still exist | Gateway validates `ami-` prefix, name length (3–128), same-region — cross-region / `Encrypted` / `KmsKeyId` / `DestinationOutpostArn` rejected with `InvalidParameterValue`. NATS `ec2.CopyImage` → daemon checks name uniqueness, reads source `ami-xxx/config.json`, visibility-filters cross-account sources as `InvalidAMIID.NotFound`, reads source `{snap-xxx}/metadata.json` (missing or empty SnapshotID → `InvalidAMIID.NotFound`), writes new `snap-yyy/metadata.json` that inherits source `VolumeID` (no block copy), writes new `ami-yyy/config.json` owned by caller. `Description` inherits from source when unset. Tag merge: `CopyImageTags=true` seeds tags from source, explicit image-resource `TagSpecifications` override colliding keys and append new ones; non-image tag specs ignored. | 1. Same-region copy produces new `ami-`/`snap-` pair with distinct IDs
2. New snap shares source `VolumeID` (metadata-only)
3. New AMI owned by caller, source untouched
4. System AMI copied into caller's account
5. Cross-account source (`InvalidAMIID.NotFound`)
6. Missing source AMI (`InvalidAMIID.NotFound`)
7. Orphaned source (missing/empty `SnapshotID`) → `InvalidAMIID.NotFound`
8. Duplicate name (`InvalidAMIName.Duplicate`)
9. Cross-region / `Encrypted` / `KmsKeyId` rejected (`InvalidParameterValue`)
10. `CopyImageTags` true/false tag inheritance semantics | **DONE** |
| `import-image` | — | `--disk-containers` (Format, Url/S3Bucket+S3Key), `--description`, `--architecture`, `--platform` | S3 bucket with disk image | Download disk image from S3 → convert format (VMDK/VHD/RAW→QCOW2) → create viperblock volume → register as AMI | 1. Import QCOW2 image
2. Import RAW image
3. Invalid format (error)
4. Verify imported image launchable | **NOT STARTED** |
diff --git a/docs/getting-started/install-airgapped/README.md b/docs/getting-started/install-airgapped/README.md
index 196a766d1..98ae17a67 100644
--- a/docs/getting-started/install-airgapped/README.md
+++ b/docs/getting-started/install-airgapped/README.md
@@ -57,6 +57,7 @@ sudo apt update
sudo apt install --download-only -y \
nbdkit nbdkit-plugin-dev pkg-config \
qemu-system-x86 qemu-utils qemu-kvm \
+ ovmf qemu-efi-aarch64 \
libvirt-daemon-system libvirt-clients libvirt-dev \
ovn-central ovn-host openvswitch-switch \
dhcpcd-base make gcc jq curl iproute2 netcat-openbsd \
@@ -141,7 +142,7 @@ Register the pre-staged image with Spinifex:
```bash
sudo spx admin images import --file /mnt/usb/images/debian-13-amd64.qcow2 \
- --distro debian --version 13 --arch x86_64
+ --distro debian --version 13 --arch x86_64 --boot-mode uefi
```
## Step 9. Verify
@@ -183,4 +184,4 @@ sudo INSTALL_SPINIFEX_TARBALL=/mnt/usb/tarball/spinifex-...tar.gz \
### Image Import Fails
-`spx admin images import --file` requires the distro/version/arch flags so the image registers in the catalogue. If the import succeeds but `aws ec2 describe-images` returns empty, check `journalctl -u spinifex-daemon -f` for predastore upload errors.
+`spx admin images import --file` requires the distro/version/arch/boot-mode flags so the image registers in the catalogue. If the import succeeds but `aws ec2 describe-images` returns empty, check `journalctl -u spinifex-daemon -f` for predastore upload errors.
diff --git a/docs/getting-started/setting-up-your-cluster/README.md b/docs/getting-started/setting-up-your-cluster/README.md
index 7c899e2aa..20a372d22 100644
--- a/docs/getting-started/setting-up-your-cluster/README.md
+++ b/docs/getting-started/setting-up-your-cluster/README.md
@@ -58,10 +58,10 @@ spx admin images list
```
NAME | DISTRO | VERSION | ARCH | BOOT
-debian-13-arm64 | debian | 13 | arm64 | bios
-debian-13-x86_64 | debian | 13 | x86_64 | bios
-ubuntu-26.04-arm64 | ubuntu | 26.04 | arm64 | bios
-ubuntu-26.04-x86_64 | ubuntu | 26.04 | x86_64 | bios
+debian-13-arm64 | debian | 13 | arm64 | uefi
+debian-13-x86_64 | debian | 13 | x86_64 | uefi
+ubuntu-26.04-arm64 | ubuntu | 26.04 | arm64 | uefi
+ubuntu-26.04-x86_64 | ubuntu | 26.04 | x86_64 | uefi
```
Import an image:
@@ -73,7 +73,7 @@ spx admin images import --name ubuntu-26.04-x86_64
Or import a local image file:
```bash
-spx admin images import --file ~/images/ubuntu-26.04.img --distro ubuntu --version 26.04 --arch x86_64
+spx admin images import --file ~/images/ubuntu-26.04.img --distro ubuntu --version 26.04 --arch x86_64 --boot-mode uefi
```
Verify the import and note the AMI ID:
diff --git a/scripts/ansible/roles/smoketest/tasks/main.yml b/scripts/ansible/roles/smoketest/tasks/main.yml
index e30bcb0b4..ea0d62719 100644
--- a/scripts/ansible/roles/smoketest/tasks/main.yml
+++ b/scripts/ansible/roles/smoketest/tasks/main.yml
@@ -61,6 +61,7 @@
--distro {{ spinifex_smoketest_distro }}
--version {{ spinifex_smoketest_version }}
--arch {{ smoketest_arch }}
+ --boot-mode uefi
register: smoketest_import_local
until: >-
smoketest_import_local.rc == 0 or
diff --git a/scripts/build-system-image.sh b/scripts/build-system-image.sh
index c55c2e819..8005ddd4d 100755
--- a/scripts/build-system-image.sh
+++ b/scripts/build-system-image.sh
@@ -75,6 +75,7 @@ case "$DISTRO" in
OUTPUT_RAW="${BUILD_DIR}/${IMAGE_NAME}-alpine.raw"
DISTRO_VERSION="${ALPINE_VERSION}"
ROOT_PART="${NBD_DEV}"
+ BOOT_MODE="bios"
;;
ubuntu)
if [[ -z "${UBUNTU_VERSION:-}" ]]; then
@@ -87,6 +88,7 @@ case "$DISTRO" in
OUTPUT_RAW="${BUILD_DIR}/${IMAGE_NAME}-ubuntu.raw"
DISTRO_VERSION="${UBUNTU_VERSION}"
ROOT_PART="${NBD_DEV}p1"
+ BOOT_MODE="uefi"
;;
*)
echo "ERROR: Unknown DISTRO: $DISTRO (supported: alpine, ubuntu)"
@@ -189,7 +191,7 @@ if [[ -f "$OUTPUT_RAW" ]] && [[ $(( $(date +%s) - $(stat -c %Y "$OUTPUT_RAW") ))
if [[ "$DO_IMPORT" == true ]]; then
echo "Importing as AMI..."
rm -f "$OUTPUT_IMAGE"
- IMPORT_ARGS=(--file "$OUTPUT_RAW" --distro "${DISTRO}" --version "${DISTRO_VERSION}" --arch x86_64)
+ IMPORT_ARGS=(--file "$OUTPUT_RAW" --distro "${DISTRO}" --version "${DISTRO_VERSION}" --arch x86_64 --boot-mode "${BOOT_MODE}")
if [[ -n "${SYSTEM_TAG:-}" ]]; then
IMPORT_ARGS+=(--tag "$SYSTEM_TAG")
fi
@@ -423,7 +425,7 @@ echo ""
if [[ "$DO_IMPORT" == true ]]; then
echo "Importing as AMI..."
rm -f "$OUTPUT_IMAGE"
- IMPORT_ARGS=(--file "$OUTPUT_RAW" --distro "${DISTRO}" --version "${DISTRO_VERSION}" --arch x86_64)
+ IMPORT_ARGS=(--file "$OUTPUT_RAW" --distro "${DISTRO}" --version "${DISTRO_VERSION}" --arch x86_64 --boot-mode "${BOOT_MODE}")
if [[ -n "${SYSTEM_TAG:-}" ]]; then
IMPORT_ARGS+=(--tag "$SYSTEM_TAG")
fi
@@ -433,9 +435,9 @@ else
echo " spx admin images import \\"
echo " --file $OUTPUT_RAW \\"
if [[ -n "${SYSTEM_TAG:-}" ]]; then
- echo " --distro ${DISTRO} --version ${DISTRO_VERSION} --arch x86_64 \\"
+ echo " --distro ${DISTRO} --version ${DISTRO_VERSION} --arch x86_64 --boot-mode ${BOOT_MODE} \\"
echo " --tag ${SYSTEM_TAG}"
else
- echo " --distro ${DISTRO} --version ${DISTRO_VERSION} --arch x86_64"
+ echo " --distro ${DISTRO} --version ${DISTRO_VERSION} --arch x86_64 --boot-mode ${BOOT_MODE}"
fi
fi
diff --git a/scripts/setup.sh b/scripts/setup.sh
index e734b7a28..9abeef2bd 100755
--- a/scripts/setup.sh
+++ b/scripts/setup.sh
@@ -214,7 +214,7 @@ install_apt_deps() {
DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y -qq \
nbdkit \
- $QEMU_PACKAGES qemu-utils qemu-kvm ovmf less \
+ $QEMU_PACKAGES qemu-utils qemu-kvm ovmf qemu-efi-aarch64 less \
libvirt-daemon-system libvirt-clients \
pciutils \
jq curl iproute2 netcat-openbsd wget unzip xz-utils file \
diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh
index 2217fc9ac..83e1bb8c1 100755
--- a/scripts/smoke-test.sh
+++ b/scripts/smoke-test.sh
@@ -126,7 +126,7 @@ else
if [ -f "$LOCAL_IMAGE" ]; then
echo " Using local image: $LOCAL_IMAGE"
sudo /usr/local/bin/spx admin images import \
- --file "$LOCAL_IMAGE" --distro ubuntu --version 26.04 --arch "$IMG_ARCH"
+ --file "$LOCAL_IMAGE" --distro ubuntu --version 26.04 --arch "$IMG_ARCH" --boot-mode uefi
else
echo " Downloading image: $IMAGE_NAME"
sudo /usr/local/bin/spx admin images import --name "$IMAGE_NAME"
diff --git a/spinifex/gateway/ec2/image/RegisterImage.go b/spinifex/gateway/ec2/image/RegisterImage.go
index 5f221c15f..7870056e7 100644
--- a/spinifex/gateway/ec2/image/RegisterImage.go
+++ b/spinifex/gateway/ec2/image/RegisterImage.go
@@ -17,6 +17,14 @@ var validRegisterImageArchitectures = map[string]bool{
"i386": true,
}
+// uefi-preferred is accepted and treated as uefi at launch (single host, single
+// firmware path — the BIOS-fallback ambiguity AWS resolves doesn't apply here).
+var validRegisterImageBootModes = map[string]bool{
+ "bios": true,
+ "uefi": true,
+ "uefi-preferred": true,
+}
+
// ValidateRegisterImageInput rejects inputs that ask for behaviour spinifex
// doesn't have (PV, S3 bundles, kernel/ramdisk, IMDS/TPM/ENA hints) rather
// than silently discarding them. Registration is pointer-only: caller supplies
@@ -48,7 +56,9 @@ func ValidateRegisterImageInput(input *ec2.RegisterImageInput) error {
}
if input.BootMode != nil && *input.BootMode != "" {
- return errors.New(awserrors.ErrorInvalidParameterValue)
+ if !validRegisterImageBootModes[*input.BootMode] {
+ return errors.New(awserrors.ErrorInvalidParameterValue)
+ }
}
if input.KernelId != nil && *input.KernelId != "" {
return errors.New(awserrors.ErrorInvalidParameterValue)
diff --git a/spinifex/gateway/ec2/image/RegisterImage_test.go b/spinifex/gateway/ec2/image/RegisterImage_test.go
index a8b026979..32b8a3982 100644
--- a/spinifex/gateway/ec2/image/RegisterImage_test.go
+++ b/spinifex/gateway/ec2/image/RegisterImage_test.go
@@ -124,8 +124,23 @@ func TestValidateRegisterImageInput(t *testing.T) {
errMsg: awserrors.ErrorInvalidParameterValue,
},
{
- name: "BootModeRejected",
+ name: "BootModeAcceptedBios",
+ mutate: func(i *ec2.RegisterImageInput) { i.BootMode = aws.String("bios") },
+ wantErr: false,
+ },
+ {
+ name: "BootModeAcceptedUEFI",
mutate: func(i *ec2.RegisterImageInput) { i.BootMode = aws.String("uefi") },
+ wantErr: false,
+ },
+ {
+ name: "BootModeAcceptedUEFIPreferred",
+ mutate: func(i *ec2.RegisterImageInput) { i.BootMode = aws.String("uefi-preferred") },
+ wantErr: false,
+ },
+ {
+ name: "BootModeRejectedInvalidValue",
+ mutate: func(i *ec2.RegisterImageInput) { i.BootMode = aws.String("legacy-bios") },
wantErr: true,
errMsg: awserrors.ErrorInvalidParameterValue,
},
diff --git a/spinifex/utils/images.go b/spinifex/utils/images.go
index 8533e3718..f19a98722 100644
--- a/spinifex/utils/images.go
+++ b/spinifex/utils/images.go
@@ -285,7 +285,7 @@ var AvailableImages = map[string]Images{
URL: "https://cloud.debian.org/images/cloud/trixie/20260518-2482/debian-13-genericcloud-amd64-20260518-2482.tar.xz",
Checksum: "https://cloud.debian.org/images/cloud/trixie/20260518-2482/SHA512SUMS",
ChecksumType: "sha512",
- BootMode: "bios",
+ BootMode: "uefi",
},
"debian-13-arm64": {
@@ -299,7 +299,7 @@ var AvailableImages = map[string]Images{
URL: "https://cloud.debian.org/images/cloud/trixie/20260518-2482/debian-13-genericcloud-arm64-20260518-2482.tar.xz",
Checksum: "https://cloud.debian.org/images/cloud/trixie/20260518-2482/SHA512SUMS",
ChecksumType: "sha512",
- BootMode: "bios",
+ BootMode: "uefi",
},
"ubuntu-26.04-x86_64": {
@@ -313,7 +313,7 @@ var AvailableImages = map[string]Images{
URL: "https://cloud-images.ubuntu.com/resolute/20260421/resolute-server-cloudimg-amd64.img",
Checksum: "https://cloud-images.ubuntu.com/resolute/20260421/SHA256SUMS",
ChecksumType: "sha256",
- BootMode: "bios",
+ BootMode: "uefi",
},
"ubuntu-26.04-arm64": {
@@ -327,41 +327,36 @@ var AvailableImages = map[string]Images{
URL: "https://cloud-images.ubuntu.com/resolute/20260421/resolute-server-cloudimg-arm64.img",
Checksum: "https://cloud-images.ubuntu.com/resolute/20260421/SHA256SUMS",
ChecksumType: "sha256",
- BootMode: "bios",
+ BootMode: "uefi",
},
- "alpine-3.22.2-x86_64": {
- Name: "alpine-3.22.2-x86_64",
- Description: "Alpine Linux 3.22.2 x86_64 cloud image",
+ "alpine-3.22.4-x86_64": {
+ Name: "alpine-3.22.4-x86_64",
+ Description: "Alpine Linux 3.22.4 x86_64 cloud image",
Distro: "alpine",
- Version: "3.22.2",
+ Version: "3.22.4",
Arch: "x86_64",
Platform: "Linux/UNIX",
- CreatedAt: time.Date(2025, 10, 6, 0, 0, 0, 0, time.UTC),
- URL: "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/generic_alpine-3.22.2-x86_64-bios-cloudinit-r0.qcow2",
- Checksum: "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/generic_alpine-3.22.2-x86_64-bios-cloudinit-r0.qcow2.sha512",
+ CreatedAt: time.Date(2026, 4, 17, 0, 0, 0, 0, time.UTC),
+ URL: "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/generic_alpine-3.22.4-x86_64-uefi-cloudinit-r0.qcow2",
+ Checksum: "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/generic_alpine-3.22.4-x86_64-uefi-cloudinit-r0.qcow2.sha512",
ChecksumType: "sha512",
- BootMode: "bios",
+ BootMode: "uefi",
},
- /*
- "alpine-3.22.2-arm64":
- // Alpine Linux (cloud init) arm64 (Requires UEFI boot, TODO)
- {
- Name: "alpine-3.22.2-arm64",
- Description: "Alpine Linux 3.22.2 arm64 cloud image",
- Distro: "alpine",
- Version: "3.22.2",
- Arch: "arm64",
- Platform: "Linux/UNIX",
- CreatedAt: time.Date(2025, 10, 6, 0, 0, 0, 0, time.UTC),
- URL: "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/gcp_alpine-3.22.2-aarch64-uefi-cloudinit-metal-r0.raw.tar.gz",
- Checksum: "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/gcp_alpine-3.22.2-aarch64-uefi-cloudinit-metal-r0.raw.tar.gz.sha512",
- ChecksumType: "sha512",
- BootMode: "uefi",
- },
- */
-
+ "alpine-3.22.4-arm64": {
+ Name: "alpine-3.22.4-arm64",
+ Description: "Alpine Linux 3.22.4 arm64 cloud image",
+ Distro: "alpine",
+ Version: "3.22.4",
+ Arch: "arm64",
+ Platform: "Linux/UNIX",
+ CreatedAt: time.Date(2026, 4, 17, 0, 0, 0, 0, time.UTC),
+ URL: "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/generic_alpine-3.22.4-aarch64-uefi-cloudinit-r0.qcow2",
+ Checksum: "https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/cloud/generic_alpine-3.22.4-aarch64-uefi-cloudinit-r0.qcow2.sha512",
+ ChecksumType: "sha512",
+ BootMode: "uefi",
+ },
}
// AMI / image extraction utils
diff --git a/tests/e2e/run-pseudo-multinode-e2e.sh b/tests/e2e/run-pseudo-multinode-e2e.sh
index 5ba4e3e15..0e683d34f 100755
--- a/tests/e2e/run-pseudo-multinode-e2e.sh
+++ b/tests/e2e/run-pseudo-multinode-e2e.sh
@@ -295,6 +295,7 @@ IMPORT_LOG=$(./bin/spx admin images import \
--arch "$ARCH" \
--distro ubuntu \
--version "$UBUNTU_VER" \
+ --boot-mode uefi \
--config "$HOME/node1/config/spinifex.toml" \
--spinifex-dir "$HOME/node1/" \
--force 2>/dev/null)