fix: refactor instance type detection (#41)

This commit is contained in:
Julian Sommer
2026-02-17 15:54:45 +11:00
committed by GitHub
parent 7128a22fdb
commit 83b1435c99
7 changed files with 972 additions and 750 deletions
+3 -363
View File
@@ -16,7 +16,6 @@ import (
"path/filepath"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
"sync"
@@ -27,7 +26,6 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/gofiber/fiber/v2"
cpuid "github.com/klauspost/cpuid/v2"
"github.com/mulgadc/hive/hive/awserrors"
"github.com/mulgadc/hive/hive/config"
handlers_ec2_account "github.com/mulgadc/hive/hive/handlers/ec2/account"
@@ -38,6 +36,7 @@ import (
handlers_ec2_snapshot "github.com/mulgadc/hive/hive/handlers/ec2/snapshot"
handlers_ec2_tags "github.com/mulgadc/hive/hive/handlers/ec2/tags"
handlers_ec2_volume "github.com/mulgadc/hive/hive/handlers/ec2/volume"
"github.com/mulgadc/hive/hive/instancetypes"
"github.com/mulgadc/hive/hive/objectstore"
"github.com/mulgadc/hive/hive/qmp"
"github.com/mulgadc/hive/hive/utils"
@@ -127,183 +126,6 @@ type Daemon struct {
mu sync.Mutex
}
// cpuGeneration represents a specific CPU microarchitecture generation
// and the AWS instance families it maps to.
type cpuGeneration struct {
name string // e.g. "Intel Ice Lake", "AMD Genoa"
families []string // e.g. ["t3", "c6i", "m6i", "r6i"]
}
var (
// Intel generations
genIntelBroadwell = cpuGeneration{"Intel Broadwell", []string{"t2", "c4", "m4", "r4"}}
genIntelSkylake = cpuGeneration{"Intel Skylake/Cascade Lake", []string{"t3", "c5", "m5", "r5"}}
genIntelIceLake = cpuGeneration{"Intel Ice Lake", []string{"t3", "c6i", "m6i", "r6i"}}
genIntelSapphireRapids = cpuGeneration{"Intel Sapphire Rapids", []string{"t3", "c7i", "m7i", "r7i"}}
genIntelGraniteRapids = cpuGeneration{"Intel Granite Rapids", []string{"t3", "c8i", "m8i", "r8i"}}
// AMD generations
genAMDZen = cpuGeneration{"AMD Zen/Zen2 (Naples/Rome)", []string{"t3a", "c5a", "m5a", "r5a"}}
genAMDZen3 = cpuGeneration{"AMD Zen 3 (Milan)", []string{"t3a", "c6a", "m6a", "r6a"}}
genAMDZen4 = cpuGeneration{"AMD Zen 4 (Genoa)", []string{"t3a", "c7a", "m7a", "r7a"}}
genAMDZen5 = cpuGeneration{"AMD Zen 5 (Turin)", []string{"t3a", "c8a", "m8a", "r8a"}}
// ARM generations
genARMNeoverseN1 = cpuGeneration{"ARM Neoverse N1 (Graviton2)", []string{"t4g", "c6g", "m6g", "r6g"}}
genARMNeoverseV1 = cpuGeneration{"ARM Neoverse V1 (Graviton3)", []string{"t4g", "c7g", "m7g", "r7g"}}
genARMNeoverseV2 = cpuGeneration{"ARM Neoverse V2 (Graviton4)", []string{"t4g", "c8g", "m8g", "r8g"}}
// Unknown/fallback — expose only burstable family
genUnknownIntel = cpuGeneration{"Unknown Intel", []string{"t3"}}
genUnknownAMD = cpuGeneration{"Unknown AMD", []string{"t3a"}}
genUnknownARM = cpuGeneration{"Unknown ARM", []string{"t4g"}}
genUnknown = cpuGeneration{"Unknown", []string{"t3"}}
)
// detectCPUGeneration detects the CPU microarchitecture generation using CPUID.
func detectCPUGeneration() cpuGeneration {
switch cpuid.CPU.VendorID {
case cpuid.Intel:
return detectIntelGeneration(cpuid.CPU.Family, cpuid.CPU.Model)
case cpuid.AMD:
return detectAMDGeneration(cpuid.CPU.Family, cpuid.CPU.Model)
default:
if runtime.GOARCH == "arm64" {
return detectARMGeneration()
}
slog.Warn("CPUID vendor not recognized, falling back to brand string detection",
"vendorID", cpuid.CPU.VendorID, "brand", cpuid.CPU.BrandName)
}
return detectGenerationFromBrand(cpuid.CPU.BrandName, runtime.GOARCH)
}
// detectIntelGeneration maps Intel CPUID Family 6 model numbers to generations.
func detectIntelGeneration(family, model int) cpuGeneration {
if family != 6 {
slog.Warn("Unrecognized Intel CPU family, exposing t3 only", "family", family, "model", model)
return genUnknownIntel
}
switch model {
case 79, 86: // Broadwell server (BDX, BDX-DE)
return genIntelBroadwell
case 85: // Skylake-SP / Cascade Lake-SP
return genIntelSkylake
case 106, 108: // Ice Lake server (ICX, ICX-D)
return genIntelIceLake
case 143, 207: // Sapphire Rapids (SPR, EMR)
return genIntelSapphireRapids
case 173, 174: // Granite Rapids (GNR, GNR-D)
return genIntelGraniteRapids
// Consumer/desktop mapped to nearest server generation
case 151, 154: // Alder Lake
return genIntelIceLake
case 183, 191: // Raptor Lake
return genIntelSapphireRapids
case 197, 198: // Arrow Lake
return genIntelGraniteRapids
}
slog.Warn("Unrecognized Intel CPU model, exposing t3 only", "family", family, "model", model, "brand", cpuid.CPU.BrandName)
return genUnknownIntel
}
// detectAMDGeneration maps AMD CPUID family/model to generations.
func detectAMDGeneration(family, model int) cpuGeneration {
switch family {
case 23: // Zen, Zen+, Zen 2 (Naples, Rome, Matisse, etc.)
return genAMDZen
case 25: // Zen 3 and Zen 4 share family 25; model ranges distinguish them
// Zen 3 models: 0x00-0x0F (Milan/Vermeer), 0x20-0x5F (Rembrandt/Barcelo)
// Zen 4 models: 0x10-0x1F (Genoa), 0x60+ (Raphael/Phoenix)
isZen3 := model < 0x10 || (model >= 0x20 && model < 0x60)
if isZen3 {
return genAMDZen3
}
return genAMDZen4
case 26: // Zen 5 (Turin, Granite Ridge)
return genAMDZen5
}
slog.Warn("Unrecognized AMD CPU family, exposing t3a only", "family", family, "model", model, "brand", cpuid.CPU.BrandName)
return genUnknownAMD
}
// detectARMGeneration detects ARM CPU generation using brand string and feature flags.
func detectARMGeneration() cpuGeneration {
brand := strings.ToLower(cpuid.CPU.BrandName)
// Check for specific Graviton versions
if strings.Contains(brand, "graviton4") || strings.Contains(brand, "neoverse-v2") {
return genARMNeoverseV2
}
if strings.Contains(brand, "graviton3") || strings.Contains(brand, "neoverse-v1") {
return genARMNeoverseV1
}
if strings.Contains(brand, "graviton2") || strings.Contains(brand, "neoverse-n1") {
return genARMNeoverseN1
}
// SVE indicates Neoverse V1+ but cannot distinguish V1 from V2
if cpuid.CPU.Has(cpuid.SVE) {
slog.Warn("ARM generation detected via SVE heuristic, defaulting to Neoverse V1", "brand", cpuid.CPU.BrandName)
return genARMNeoverseV1
}
// Default ARM to Neoverse N1 (most common)
slog.Warn("Could not identify ARM generation, defaulting to Neoverse N1", "brand", cpuid.CPU.BrandName)
return genARMNeoverseN1
}
// detectGenerationFromBrand is a fallback for VMs/hypervisors where CPUID may be virtualized.
func detectGenerationFromBrand(brand, arch string) cpuGeneration {
if arch == "arm64" {
return detectARMGeneration()
}
brandLower := strings.ToLower(brand)
// Intel patterns
if strings.Contains(brandLower, "xeon") || strings.Contains(brandLower, "intel") {
switch {
case strings.Contains(brandLower, "granite"):
return genIntelGraniteRapids
case strings.Contains(brandLower, "sapphire"):
return genIntelSapphireRapids
case strings.Contains(brandLower, "ice lake") || strings.Contains(brandLower, "icelake"):
return genIntelIceLake
case strings.Contains(brandLower, "cascade") || strings.Contains(brandLower, "skylake"):
return genIntelSkylake
case strings.Contains(brandLower, "broadwell"):
return genIntelBroadwell
default:
// Generic Intel — default to Skylake (most common in VMs)
slog.Warn("Intel CPU detected via brand string but generation unknown, defaulting to Skylake", "brand", brand)
return genIntelSkylake
}
}
// AMD patterns
if strings.Contains(brandLower, "epyc") || strings.Contains(brandLower, "amd") || strings.Contains(brandLower, "ryzen") {
switch {
case strings.Contains(brandLower, "turin"):
return genAMDZen5
case strings.Contains(brandLower, "genoa") || strings.Contains(brandLower, "9004") || strings.Contains(brandLower, "raphael"):
return genAMDZen4
case strings.Contains(brandLower, "milan") || strings.Contains(brandLower, "7003") || strings.Contains(brandLower, "vermeer"):
return genAMDZen3
default:
// Generic AMD — default to Zen/Zen2
slog.Warn("AMD CPU detected via brand string but generation unknown, defaulting to Zen/Zen2", "brand", brand)
return genAMDZen
}
}
slog.Warn("Unrecognized CPU, exposing t3 only", "brand", brand, "arch", arch)
return genUnknown
}
// getSystemMemory returns the total system memory in GB
func getSystemMemory() (float64, error) {
switch runtime.GOOS {
@@ -347,181 +169,6 @@ func getSystemMemory() (float64, error) {
}
}
type instanceSize struct {
suffix string
vcpus int
memoryGB float64
}
type instanceFamilyDef struct {
name string
sizes []instanceSize
currentGen bool
}
// Size tables for each instance category
var burstableSizes = []instanceSize{
{"nano", 2, 0.5},
{"micro", 2, 1},
{"small", 2, 2},
{"medium", 2, 4},
{"large", 2, 8},
{"xlarge", 4, 16},
{"2xlarge", 8, 32},
}
var gpSizes = []instanceSize{
{"large", 2, 8},
{"xlarge", 4, 16},
{"2xlarge", 8, 32},
{"4xlarge", 16, 64},
{"8xlarge", 32, 128},
{"12xlarge", 48, 192},
{"16xlarge", 64, 256},
{"24xlarge", 96, 384},
}
// gpSizesSmall is gpSizes without 12xlarge and 24xlarge (older/ARM families).
var gpSizesSmall = slices.Clone(gpSizes[:6])
var computeSizes = []instanceSize{
{"large", 2, 4},
{"xlarge", 4, 8},
{"2xlarge", 8, 16},
{"4xlarge", 16, 32},
{"8xlarge", 32, 64},
{"12xlarge", 48, 96},
{"16xlarge", 64, 128},
{"24xlarge", 96, 192},
}
// computeSizesSmall is computeSizes without 12xlarge and 24xlarge (older/ARM families).
var computeSizesSmall = slices.Clone(computeSizes[:6])
var memorySizes = []instanceSize{
{"large", 2, 16},
{"xlarge", 4, 32},
{"2xlarge", 8, 64},
{"4xlarge", 16, 128},
{"8xlarge", 32, 256},
{"12xlarge", 48, 384},
{"16xlarge", 64, 512},
{"24xlarge", 96, 768},
}
// memorySizesSmall is memorySizes without 12xlarge and 24xlarge (older/ARM families).
var memorySizesSmall = slices.Clone(memorySizes[:6])
// instanceFamilyDefs defines all supported instance families with their vendor and sizes.
//
// We support the core families across burstable, general purpose, compute optimized,
// and memory optimized categories. The following AWS family categories are intentionally
// excluded because they require specialized hardware not available on standard bare-metal hosts:
//
// - Local disk variants (d/n suffixes): c5d, c5ad, c5n, m5d, m5ad, m5n, m5dn, m5zn, r5d, r5ad,
// r5n, r5dn, r5b, c6gd, c6gn, c6id, c6in, m6gd, m6id, m6idn, m6in, r6gd, r6id, r6idn, r6in,
// c7gd, c7gn, c7i-flex, m7gd, m7i-flex, r7gd, r7iz, c8gd, c8gn, c8i-flex, m8gd, m8i-flex,
// r8gd, r8gn, r8gb, r8i-flex — require NVMe instance storage or enhanced networking
// - GPU/accelerator: g2-g6, g6e, g6f, gr6, gr6f, p2-p6, inf1-inf2, trn1-trn2, dl1, dl2q — require
// GPU, Inferentia, Trainium, or other accelerator hardware. note inf1 and trn1 are not supported since its aws only hardware.
// - Storage optimized: d2, d3, d3en, h1, i2-i8g, i7ie, i8ge, im4gn, is4gen — require dense HDD/NVMe
// - FPGA: f1, f2 — require FPGA hardware
// - High memory: u-*, u7i-*, x1, x1e, x2gd, x2idn, x2iedn, x2iezn, x8g — require TB-scale memory
// - High frequency: z1d — specialized high clock-speed instances
// - (unsupported) Dedicated host: mac*, hpc* — require macOS/Apple hardware or HPC interconnects
// - (unsupported) Video: vt1 — requires video transcoding hardware
// - Legacy (pre-gen4): a1, c1, c3, cc1, cc2, cg1, cr1, hi1, hs1, m1, m2, m3, r3, t1
var instanceFamilyDefs = []instanceFamilyDef{
// Burstable
{name: "t2", sizes: burstableSizes, currentGen: false},
{name: "t3", sizes: burstableSizes, currentGen: true},
{name: "t3a", sizes: burstableSizes, currentGen: true},
{name: "t4g", sizes: burstableSizes, currentGen: true},
// General Purpose (1:4 vCPU:memory)
{name: "m4", sizes: gpSizesSmall, currentGen: false},
{name: "m5", sizes: gpSizes, currentGen: true},
{name: "m5a", sizes: gpSizes, currentGen: true},
{name: "m6i", sizes: gpSizes, currentGen: true},
{name: "m6a", sizes: gpSizes, currentGen: true},
{name: "m6g", sizes: gpSizesSmall, currentGen: true},
{name: "m7i", sizes: gpSizes, currentGen: true},
{name: "m7a", sizes: gpSizes, currentGen: true},
{name: "m7g", sizes: gpSizesSmall, currentGen: true},
{name: "m8i", sizes: gpSizes, currentGen: true},
{name: "m8a", sizes: gpSizes, currentGen: true},
{name: "m8g", sizes: gpSizesSmall, currentGen: true},
// Compute Optimized (1:2 vCPU:memory)
{name: "c4", sizes: computeSizesSmall, currentGen: false},
{name: "c5", sizes: computeSizes, currentGen: true},
{name: "c5a", sizes: computeSizes, currentGen: true},
{name: "c6i", sizes: computeSizes, currentGen: true},
{name: "c6a", sizes: computeSizes, currentGen: true},
{name: "c6g", sizes: computeSizesSmall, currentGen: true},
{name: "c7i", sizes: computeSizes, currentGen: true},
{name: "c7a", sizes: computeSizes, currentGen: true},
{name: "c7g", sizes: computeSizesSmall, currentGen: true},
{name: "c8i", sizes: computeSizes, currentGen: true},
{name: "c8a", sizes: computeSizes, currentGen: true},
{name: "c8g", sizes: computeSizesSmall, currentGen: true},
// Memory Optimized (1:8 vCPU:memory)
{name: "r4", sizes: memorySizesSmall, currentGen: false},
{name: "r5", sizes: memorySizes, currentGen: true},
{name: "r5a", sizes: memorySizes, currentGen: true},
{name: "r6i", sizes: memorySizes, currentGen: true},
{name: "r6a", sizes: memorySizes, currentGen: true},
{name: "r6g", sizes: memorySizesSmall, currentGen: true},
{name: "r7i", sizes: memorySizes, currentGen: true},
{name: "r7a", sizes: memorySizes, currentGen: true},
{name: "r7g", sizes: memorySizesSmall, currentGen: true},
{name: "r8i", sizes: memorySizes, currentGen: true},
{name: "r8a", sizes: memorySizes, currentGen: true},
{name: "r8g", sizes: memorySizesSmall, currentGen: true},
}
// generateInstanceTypes creates the instance type map for the given CPU generation.
// It generates all instance families matching the generation's family list across
// burstable, general purpose, compute optimized, and memory optimized categories.
func generateInstanceTypes(gen cpuGeneration, arch string) map[string]*ec2.InstanceTypeInfo {
// Build a set of allowed families for fast lookup
allowed := make(map[string]bool, len(gen.families))
for _, f := range gen.families {
allowed[f] = true
}
instanceTypes := make(map[string]*ec2.InstanceTypeInfo)
for _, def := range instanceFamilyDefs {
if !allowed[def.name] {
continue
}
burstable := strings.HasPrefix(def.name, "t")
for _, size := range def.sizes {
name := fmt.Sprintf("%s.%s", def.name, size.suffix)
instanceTypes[name] = &ec2.InstanceTypeInfo{
InstanceType: aws.String(name),
VCpuInfo: &ec2.VCpuInfo{
DefaultVCpus: aws.Int64(int64(size.vcpus)),
},
MemoryInfo: &ec2.MemoryInfo{
SizeInMiB: aws.Int64(int64(size.memoryGB * 1024)),
},
ProcessorInfo: &ec2.ProcessorInfo{
SupportedArchitectures: []*string{aws.String(arch)},
},
CurrentGeneration: aws.Bool(def.currentGen),
BurstablePerformanceSupported: aws.Bool(burstable),
Hypervisor: aws.String("kvm"),
SupportedVirtualizationTypes: []*string{aws.String("hvm")},
SupportedRootDeviceTypes: []*string{aws.String("ebs")},
}
}
}
return instanceTypes
}
// NewResourceManager creates a new resource manager with system capabilities
func NewResourceManager() *ResourceManager {
// Get system CPU cores
@@ -541,18 +188,11 @@ func NewResourceManager() *ResourceManager {
}
// Detect CPU generation and generate matching instance types
gen := detectCPUGeneration()
instanceTypes := generateInstanceTypes(gen, arch)
if len(instanceTypes) == 0 {
slog.Error("No instance types generated, daemon will be unable to run VMs",
"generation", gen.name, "arch", arch)
}
instanceTypes := instancetypes.DetectAndGenerate(instancetypes.HostCPU{}, arch)
slog.Info("System resources detected",
"vCPUs", numCPU, "memGB", totalMemGB,
"generation", gen.name, "families", gen.families,
"instanceTypes", len(instanceTypes), "os", runtime.GOOS)
"instanceTypes", len(instanceTypes))
return &ResourceManager{
availableVCPU: numCPU,
-387
View File
@@ -470,14 +470,6 @@ func TestGetInstanceTypeInfos(t *testing.T) {
}
}
// TestCPUDetection tests that detectCPUGeneration returns a valid generation on the current host
func TestCPUDetection(t *testing.T) {
gen := detectCPUGeneration()
assert.NotEmpty(t, gen.name, "Generation name should not be empty")
assert.NotEmpty(t, gen.families, "Generation families should not be empty")
t.Logf("Detected CPU generation: %s, families: %v", gen.name, gen.families)
}
// TestGetAvailableInstanceTypeInfos_ResourceFiltering tests that instance types are filtered by available resources
func TestGetAvailableInstanceTypeInfos_ResourceFiltering(t *testing.T) {
rm := NewResourceManager()
@@ -3188,385 +3180,6 @@ func TestSaveClusterConfig_ErrorOnInvalidPath(t *testing.T) {
assert.Contains(t, err.Error(), "failed to write config")
}
// --- generateInstanceTypes ---
func hasFamily(types map[string]*ec2.InstanceTypeInfo, prefix string) bool {
for name := range types {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}
func countFamily(types map[string]*ec2.InstanceTypeInfo, prefix string) int {
count := 0
for name := range types {
if strings.HasPrefix(name, prefix) {
count++
}
}
return count
}
// --- Generation-specific instance type tests ---
func TestGenerateInstanceTypes_IntelIceLake(t *testing.T) {
types := generateInstanceTypes(genIntelIceLake, "x86_64")
// t3(7) + c6i(8) + m6i(8) + r6i(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3.", "c6i.", "m6i.", "r6i."} {
assert.True(t, hasFamily(types, prefix), "expected Ice Lake types to include %s family", prefix)
}
// Verify other generation families are NOT present
for name := range types {
assert.False(t, strings.HasPrefix(name, "t3a."), "Ice Lake should not have t3a: %s", name)
assert.False(t, strings.HasPrefix(name, "c5."), "Ice Lake should not have c5: %s", name)
assert.False(t, strings.HasPrefix(name, "c7i."), "Ice Lake should not have c7i: %s", name)
}
}
func TestGenerateInstanceTypes_IntelBroadwell(t *testing.T) {
types := generateInstanceTypes(genIntelBroadwell, "x86_64")
// t2(7) + c4(6) + m4(6) + r4(6) = 25
assert.Len(t, types, 25)
for _, prefix := range []string{"t2.", "c4.", "m4.", "r4."} {
assert.True(t, hasFamily(types, prefix), "expected Broadwell types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_IntelSkylake(t *testing.T) {
types := generateInstanceTypes(genIntelSkylake, "x86_64")
// t3(7) + c5(8) + m5(8) + r5(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3.", "c5.", "m5.", "r5."} {
assert.True(t, hasFamily(types, prefix), "expected Skylake types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_IntelSapphireRapids(t *testing.T) {
types := generateInstanceTypes(genIntelSapphireRapids, "x86_64")
// t3(7) + c7i(8) + m7i(8) + r7i(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3.", "c7i.", "m7i.", "r7i."} {
assert.True(t, hasFamily(types, prefix), "expected Sapphire Rapids types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_IntelGraniteRapids(t *testing.T) {
types := generateInstanceTypes(genIntelGraniteRapids, "x86_64")
// t3(7) + c8i(8) + m8i(8) + r8i(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3.", "c8i.", "m8i.", "r8i."} {
assert.True(t, hasFamily(types, prefix), "expected Granite Rapids types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_AMDZen(t *testing.T) {
types := generateInstanceTypes(genAMDZen, "x86_64")
// t3a(7) + c5a(8) + m5a(8) + r5a(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3a.", "c5a.", "m5a.", "r5a."} {
assert.True(t, hasFamily(types, prefix), "expected Zen types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_AMDZen4(t *testing.T) {
types := generateInstanceTypes(genAMDZen4, "x86_64")
// t3a(7) + c7a(8) + m7a(8) + r7a(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3a.", "c7a.", "m7a.", "r7a."} {
assert.True(t, hasFamily(types, prefix), "expected Zen 4 types to include %s family", prefix)
}
// Verify older AMD families are NOT present
for name := range types {
assert.False(t, strings.HasPrefix(name, "c5a."), "Zen4 should not have c5a: %s", name)
assert.False(t, strings.HasPrefix(name, "c6a."), "Zen4 should not have c6a: %s", name)
}
}
func TestGenerateInstanceTypes_ARMN1(t *testing.T) {
types := generateInstanceTypes(genARMNeoverseN1, "arm64")
// t4g(7) + c6g(6) + m6g(6) + r6g(6) = 25
assert.Len(t, types, 25)
for _, prefix := range []string{"t4g.", "c6g.", "m6g.", "r6g."} {
assert.True(t, hasFamily(types, prefix), "expected N1 types to include %s family", prefix)
}
// Verify Intel/AMD families are NOT present
for name := range types {
assert.False(t, strings.HasPrefix(name, "t3."), "ARM should not have t3: %s", name)
assert.False(t, strings.HasPrefix(name, "t3a."), "ARM should not have t3a: %s", name)
}
}
func TestGenerateInstanceTypes_ARMV2(t *testing.T) {
types := generateInstanceTypes(genARMNeoverseV2, "arm64")
// t4g(7) + c8g(6) + m8g(6) + r8g(6) = 25
assert.Len(t, types, 25)
for _, prefix := range []string{"t4g.", "c8g.", "m8g.", "r8g."} {
assert.True(t, hasFamily(types, prefix), "expected V2 types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_UnknownFallback(t *testing.T) {
types := generateInstanceTypes(genUnknownIntel, "x86_64")
// Unknown Intel: t3 only = 7 types
assert.Len(t, types, 7)
assert.True(t, hasFamily(types, "t3."), "unknown Intel should have t3")
types = generateInstanceTypes(genUnknownAMD, "x86_64")
assert.Len(t, types, 7)
assert.True(t, hasFamily(types, "t3a."), "unknown AMD should have t3a")
types = generateInstanceTypes(genUnknownARM, "arm64")
assert.Len(t, types, 7)
assert.True(t, hasFamily(types, "t4g."), "unknown ARM should have t4g")
types = generateInstanceTypes(genUnknown, "x86_64")
assert.Len(t, types, 7)
assert.True(t, hasFamily(types, "t3."), "completely unknown should have t3")
}
func TestGenerateInstanceTypes_VerifyBurstableSizes(t *testing.T) {
types := generateInstanceTypes(genIntelSkylake, "x86_64")
expected := map[string]struct {
vcpus int64
memMB int64
}{
"t3.nano": {2, 512},
"t3.micro": {2, 1024},
"t3.small": {2, 2048},
"t3.medium": {2, 4096},
"t3.large": {2, 8192},
"t3.xlarge": {4, 16384},
"t3.2xlarge": {8, 32768},
}
for name, exp := range expected {
it, ok := types[name]
require.True(t, ok, "missing instance type %s", name)
assert.Equal(t, exp.vcpus, *it.VCpuInfo.DefaultVCpus, "%s vCPUs", name)
assert.Equal(t, exp.memMB, *it.MemoryInfo.SizeInMiB, "%s memory", name)
}
}
func TestGenerateInstanceTypes_ComputeRatio(t *testing.T) {
// Skylake for c5
skylakeTypes := generateInstanceTypes(genIntelSkylake, "x86_64")
expectedSkylake := map[string]struct {
vcpus int64
memMB int64
}{
"c5.large": {2, 4096},
"c5.xlarge": {4, 8192},
"c5.2xlarge": {8, 16384},
}
for name, exp := range expectedSkylake {
it, ok := skylakeTypes[name]
require.True(t, ok, "missing instance type %s", name)
assert.Equal(t, exp.vcpus, *it.VCpuInfo.DefaultVCpus, "%s vCPUs", name)
assert.Equal(t, exp.memMB, *it.MemoryInfo.SizeInMiB, "%s memory", name)
}
// Sapphire Rapids for c7i
sapphireTypes := generateInstanceTypes(genIntelSapphireRapids, "x86_64")
it, ok := sapphireTypes["c7i.4xlarge"]
require.True(t, ok, "missing instance type c7i.4xlarge")
assert.Equal(t, int64(16), *it.VCpuInfo.DefaultVCpus, "c7i.4xlarge vCPUs")
assert.Equal(t, int64(32768), *it.MemoryInfo.SizeInMiB, "c7i.4xlarge memory")
}
func TestGenerateInstanceTypes_MemoryRatio(t *testing.T) {
// Skylake for r5
skylakeTypes := generateInstanceTypes(genIntelSkylake, "x86_64")
expectedSkylake := map[string]struct {
vcpus int64
memMB int64
}{
"r5.large": {2, 16384},
"r5.xlarge": {4, 32768},
"r5.2xlarge": {8, 65536},
}
for name, exp := range expectedSkylake {
it, ok := skylakeTypes[name]
require.True(t, ok, "missing instance type %s", name)
assert.Equal(t, exp.vcpus, *it.VCpuInfo.DefaultVCpus, "%s vCPUs", name)
assert.Equal(t, exp.memMB, *it.MemoryInfo.SizeInMiB, "%s memory", name)
}
// Sapphire Rapids for r7i
sapphireTypes := generateInstanceTypes(genIntelSapphireRapids, "x86_64")
it, ok := sapphireTypes["r7i.4xlarge"]
require.True(t, ok, "missing instance type r7i.4xlarge")
assert.Equal(t, int64(16), *it.VCpuInfo.DefaultVCpus, "r7i.4xlarge vCPUs")
assert.Equal(t, int64(131072), *it.MemoryInfo.SizeInMiB, "r7i.4xlarge memory")
}
func TestGenerateInstanceTypes_NoSmallSizesForNonBurstable(t *testing.T) {
types := generateInstanceTypes(genIntelSkylake, "x86_64")
// Non-burstable families should not have nano/micro/small/medium sizes
for name := range types {
if strings.HasPrefix(name, "t") {
continue // skip all burstable families
}
for _, small := range []string{".nano", ".micro", ".small", ".medium"} {
assert.False(t, strings.HasSuffix(name, small),
"non-burstable type %s should not have %s size", name, small)
}
}
}
func TestGenerateInstanceTypes_OlderFamiliesHaveSmallerSizeRange(t *testing.T) {
// Broadwell has m4 = 6 sizes
broadwellTypes := generateInstanceTypes(genIntelBroadwell, "x86_64")
assert.Equal(t, 6, countFamily(broadwellTypes, "m4."), "m4 should have 6 sizes (large → 16xlarge)")
// Skylake has m5 = 8 sizes
skylakeTypes := generateInstanceTypes(genIntelSkylake, "x86_64")
assert.Equal(t, 8, countFamily(skylakeTypes, "m5."), "m5 should have 8 sizes (large → 24xlarge)")
}
func TestGenerateInstanceTypes_BurstableFlag(t *testing.T) {
// Test Broadwell (has prev-gen families)
broadwellTypes := generateInstanceTypes(genIntelBroadwell, "x86_64")
prevGen := map[string]bool{"t2": true, "m4": true, "c4": true, "r4": true}
for name, info := range broadwellTypes {
isBurstable := strings.HasPrefix(name, "t")
family := strings.SplitN(name, ".", 2)[0]
assert.Equal(t, isBurstable, *info.BurstablePerformanceSupported,
"%s burstable flag mismatch", name)
assert.Equal(t, !prevGen[family], *info.CurrentGeneration,
"%s current generation flag mismatch", name)
}
// Test current-gen (Sapphire Rapids) — all families should be currentGen=true
sapphireTypes := generateInstanceTypes(genIntelSapphireRapids, "x86_64")
for name, info := range sapphireTypes {
isBurstable := strings.HasPrefix(name, "t")
assert.Equal(t, isBurstable, *info.BurstablePerformanceSupported,
"%s burstable flag mismatch", name)
assert.True(t, *info.CurrentGeneration,
"%s should be current generation", name)
}
}
// --- CPU generation detection tests ---
func TestDetectIntelGeneration(t *testing.T) {
tests := []struct {
name string
family int
model int
expected cpuGeneration
}{
{"Broadwell BDX", 6, 79, genIntelBroadwell},
{"Broadwell BDX-DE", 6, 86, genIntelBroadwell},
{"Skylake-SP", 6, 85, genIntelSkylake},
{"Ice Lake ICX", 6, 106, genIntelIceLake},
{"Ice Lake ICX-D", 6, 108, genIntelIceLake},
{"Sapphire Rapids SPR", 6, 143, genIntelSapphireRapids},
{"Emerald Rapids EMR", 6, 207, genIntelSapphireRapids},
{"Granite Rapids GNR", 6, 173, genIntelGraniteRapids},
{"Granite Rapids GNR-D", 6, 174, genIntelGraniteRapids},
// Consumer mappings
{"Alder Lake", 6, 151, genIntelIceLake},
{"Alder Lake P", 6, 154, genIntelIceLake},
{"Raptor Lake", 6, 183, genIntelSapphireRapids},
{"Raptor Lake P", 6, 191, genIntelSapphireRapids},
{"Arrow Lake", 6, 197, genIntelGraniteRapids},
{"Arrow Lake S", 6, 198, genIntelGraniteRapids},
// Unknown
{"Unknown family", 15, 0, genUnknownIntel},
{"Unknown model", 6, 255, genUnknownIntel},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gen := detectIntelGeneration(tt.family, tt.model)
assert.Equal(t, tt.expected.name, gen.name)
assert.Equal(t, tt.expected.families, gen.families)
})
}
}
func TestDetectAMDGeneration(t *testing.T) {
tests := []struct {
name string
family int
model int
expected cpuGeneration
}{
{"Naples/Rome family 23", 23, 1, genAMDZen},
{"Zen3 Milan model 0x01", 25, 0x01, genAMDZen3},
{"Zen3 Vermeer model 0x21", 25, 0x21, genAMDZen3},
{"Zen4 Genoa model 0x11", 25, 0x11, genAMDZen4},
{"Zen4 Raphael model 0x61", 25, 0x61, genAMDZen4},
// Boundary tests for family 25 Zen3/Zen4 split
{"Zen3 boundary 0x0F", 25, 0x0F, genAMDZen3},
{"Zen4 boundary 0x10", 25, 0x10, genAMDZen4},
{"Zen4 boundary 0x1F", 25, 0x1F, genAMDZen4},
{"Zen3 boundary 0x20", 25, 0x20, genAMDZen3},
{"Zen3 boundary 0x5F", 25, 0x5F, genAMDZen3},
{"Zen4 boundary 0x60", 25, 0x60, genAMDZen4},
{"Zen4 max model", 25, 0xFF, genAMDZen4},
{"Zen5 Turin family 26", 26, 0, genAMDZen5},
{"Unknown AMD family", 20, 0, genUnknownAMD},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gen := detectAMDGeneration(tt.family, tt.model)
assert.Equal(t, tt.expected.name, gen.name)
assert.Equal(t, tt.expected.families, gen.families)
})
}
}
func TestDetectGenerationFromBrand(t *testing.T) {
tests := []struct {
name string
brand string
arch string
expected cpuGeneration
}{
{"Intel Skylake brand", "Intel(R) Xeon(R) Platinum 8175M (Skylake)", "x86_64", genIntelSkylake},
{"Intel Ice Lake brand", "Intel(R) Xeon(R) Platinum 8375C (Ice Lake)", "x86_64", genIntelIceLake},
{"Intel Sapphire brand", "Intel(R) Xeon(R) w9-3495X (Sapphire Rapids)", "x86_64", genIntelSapphireRapids},
{"Intel Granite brand", "Intel(R) Xeon(R) 6980P (Granite Rapids)", "x86_64", genIntelGraniteRapids},
{"Intel Broadwell brand", "Intel(R) Xeon(R) E5-2686 v4 (Broadwell)", "x86_64", genIntelBroadwell},
{"Generic Intel Xeon", "Intel(R) Xeon(R) CPU E5-2686 v4", "x86_64", genIntelSkylake}, // defaults to Skylake
{"AMD Milan brand", "AMD EPYC 7003 Milan", "x86_64", genAMDZen3},
{"AMD Genoa brand", "AMD EPYC 9004 Series", "x86_64", genAMDZen4},
{"Generic AMD EPYC", "AMD EPYC 7551", "x86_64", genAMDZen},
{"Completely unknown", "Some Random CPU", "x86_64", genUnknown},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gen := detectGenerationFromBrand(tt.brand, tt.arch)
assert.Equal(t, tt.expected.name, gen.name)
assert.Equal(t, tt.expected.families, gen.families)
})
}
}
// --- instanceTypeVCPUs / instanceTypeMemoryMiB nil safety ---
func TestInstanceTypeVCPUs_NilSafety(t *testing.T) {
+171
View File
@@ -0,0 +1,171 @@
package instancetypes
import "slices"
// cpuGeneration represents a specific CPU microarchitecture generation
// and the AWS instance families it maps to.
type cpuGeneration struct {
name string // e.g. "Intel Ice Lake", "AMD Genoa"
families []string // e.g. ["t3", "c6i", "m6i", "r6i"]
}
var (
// Intel generations
genIntelBroadwell = cpuGeneration{"Intel Broadwell", []string{"t2", "c4", "m4", "r4"}}
genIntelSkylake = cpuGeneration{"Intel Skylake/Cascade Lake", []string{"t3", "c5", "m5", "r5"}}
genIntelIceLake = cpuGeneration{"Intel Ice Lake", []string{"t3", "c6i", "m6i", "r6i"}}
genIntelSapphireRapids = cpuGeneration{"Intel Sapphire Rapids", []string{"t3", "c7i", "m7i", "r7i"}}
genIntelGraniteRapids = cpuGeneration{"Intel Granite Rapids", []string{"t3", "c8i", "m8i", "r8i"}}
// AMD generations
genAMDZen = cpuGeneration{"AMD Zen/Zen2 (Naples/Rome)", []string{"t3a", "c5a", "m5a", "r5a"}}
genAMDZen3 = cpuGeneration{"AMD Zen 3 (Milan)", []string{"t3a", "c6a", "m6a", "r6a"}}
genAMDZen4 = cpuGeneration{"AMD Zen 4 (Genoa)", []string{"t3a", "c7a", "m7a", "r7a"}}
genAMDZen5 = cpuGeneration{"AMD Zen 5 (Turin)", []string{"t3a", "c8a", "m8a", "r8a"}}
// ARM generations
genARMNeoverseN1 = cpuGeneration{"ARM Neoverse N1 (Graviton2)", []string{"t4g", "c6g", "m6g", "r6g"}}
genARMNeoverseV1 = cpuGeneration{"ARM Neoverse V1 (Graviton3)", []string{"t4g", "c7g", "m7g", "r7g"}}
genARMNeoverseV2 = cpuGeneration{"ARM Neoverse V2 (Graviton4)", []string{"t4g", "c8g", "m8g", "r8g"}}
// Unknown/fallback — expose only burstable family
genUnknownIntel = cpuGeneration{"Unknown Intel", []string{"t3"}}
genUnknownAMD = cpuGeneration{"Unknown AMD", []string{"t3a"}}
genUnknownARM = cpuGeneration{"Unknown ARM", []string{"t4g"}}
genUnknown = cpuGeneration{"Unknown", []string{"t3"}}
)
type instanceSize struct {
suffix string
vcpus int
memoryGB float64
}
type instanceFamilyDef struct {
name string
sizes []instanceSize
currentGen bool
}
// Size tables for each instance category
var burstableSizes = []instanceSize{
{"nano", 2, 0.5},
{"micro", 2, 1},
{"small", 2, 2},
{"medium", 2, 4},
{"large", 2, 8},
{"xlarge", 4, 16},
{"2xlarge", 8, 32},
}
var gpSizes = []instanceSize{
{"large", 2, 8},
{"xlarge", 4, 16},
{"2xlarge", 8, 32},
{"4xlarge", 16, 64},
{"8xlarge", 32, 128},
{"12xlarge", 48, 192},
{"16xlarge", 64, 256},
{"24xlarge", 96, 384},
}
// gpSizesSmall is gpSizes without 12xlarge and 24xlarge (older/ARM families).
var gpSizesSmall = slices.Clone(gpSizes[:6])
var computeSizes = []instanceSize{
{"large", 2, 4},
{"xlarge", 4, 8},
{"2xlarge", 8, 16},
{"4xlarge", 16, 32},
{"8xlarge", 32, 64},
{"12xlarge", 48, 96},
{"16xlarge", 64, 128},
{"24xlarge", 96, 192},
}
// computeSizesSmall is computeSizes without 12xlarge and 24xlarge (older/ARM families).
var computeSizesSmall = slices.Clone(computeSizes[:6])
var memorySizes = []instanceSize{
{"large", 2, 16},
{"xlarge", 4, 32},
{"2xlarge", 8, 64},
{"4xlarge", 16, 128},
{"8xlarge", 32, 256},
{"12xlarge", 48, 384},
{"16xlarge", 64, 512},
{"24xlarge", 96, 768},
}
// memorySizesSmall is memorySizes without 12xlarge and 24xlarge (older/ARM families).
var memorySizesSmall = slices.Clone(memorySizes[:6])
// instanceFamilyDefs defines all supported instance families with their vendor and sizes.
//
// We support the core families across burstable, general purpose, compute optimized,
// and memory optimized categories. The following AWS family categories are intentionally
// excluded because they require specialized hardware not available on standard bare-metal hosts:
//
// - Local disk variants (d/n suffixes): c5d, c5ad, c5n, m5d, m5ad, m5n, m5dn, m5zn, r5d, r5ad,
// r5n, r5dn, r5b, c6gd, c6gn, c6id, c6in, m6gd, m6id, m6idn, m6in, r6gd, r6id, r6idn, r6in,
// c7gd, c7gn, c7i-flex, m7gd, m7i-flex, r7gd, r7iz, c8gd, c8gn, c8i-flex, m8gd, m8i-flex,
// r8gd, r8gn, r8gb, r8i-flex — require NVMe instance storage or enhanced networking
// - GPU/accelerator: g2-g6, g6e, g6f, gr6, gr6f, p2-p6, inf1-inf2, trn1-trn2, dl1, dl2q — require
// GPU, Inferentia, Trainium, or other accelerator hardware. note inf1 and trn1 are not supported since its aws only hardware.
// - Storage optimized: d2, d3, d3en, h1, i2-i8g, i7ie, i8ge, im4gn, is4gen — require dense HDD/NVMe
// - FPGA: f1, f2 — require FPGA hardware
// - High memory: u-*, u7i-*, x1, x1e, x2gd, x2idn, x2iedn, x2iezn, x8g — require TB-scale memory
// - High frequency: z1d — specialized high clock-speed instances
// - (unsupported) Dedicated host: mac*, hpc* — require macOS/Apple hardware or HPC interconnects
// - (unsupported) Video: vt1 — requires video transcoding hardware
// - Legacy (pre-gen4): a1, c1, c3, cc1, cc2, cg1, cr1, hi1, hs1, m1, m2, m3, r3, t1
var instanceFamilyDefs = []instanceFamilyDef{
// Burstable
{name: "t2", sizes: burstableSizes, currentGen: false},
{name: "t3", sizes: burstableSizes, currentGen: true},
{name: "t3a", sizes: burstableSizes, currentGen: true},
{name: "t4g", sizes: burstableSizes, currentGen: true},
// General Purpose (1:4 vCPU:memory)
{name: "m4", sizes: gpSizesSmall, currentGen: false},
{name: "m5", sizes: gpSizes, currentGen: true},
{name: "m5a", sizes: gpSizes, currentGen: true},
{name: "m6i", sizes: gpSizes, currentGen: true},
{name: "m6a", sizes: gpSizes, currentGen: true},
{name: "m6g", sizes: gpSizesSmall, currentGen: true},
{name: "m7i", sizes: gpSizes, currentGen: true},
{name: "m7a", sizes: gpSizes, currentGen: true},
{name: "m7g", sizes: gpSizesSmall, currentGen: true},
{name: "m8i", sizes: gpSizes, currentGen: true},
{name: "m8a", sizes: gpSizes, currentGen: true},
{name: "m8g", sizes: gpSizesSmall, currentGen: true},
// Compute Optimized (1:2 vCPU:memory)
{name: "c4", sizes: computeSizesSmall, currentGen: false},
{name: "c5", sizes: computeSizes, currentGen: true},
{name: "c5a", sizes: computeSizes, currentGen: true},
{name: "c6i", sizes: computeSizes, currentGen: true},
{name: "c6a", sizes: computeSizes, currentGen: true},
{name: "c6g", sizes: computeSizesSmall, currentGen: true},
{name: "c7i", sizes: computeSizes, currentGen: true},
{name: "c7a", sizes: computeSizes, currentGen: true},
{name: "c7g", sizes: computeSizesSmall, currentGen: true},
{name: "c8i", sizes: computeSizes, currentGen: true},
{name: "c8a", sizes: computeSizes, currentGen: true},
{name: "c8g", sizes: computeSizesSmall, currentGen: true},
// Memory Optimized (1:8 vCPU:memory)
{name: "r4", sizes: memorySizesSmall, currentGen: false},
{name: "r5", sizes: memorySizes, currentGen: true},
{name: "r5a", sizes: memorySizes, currentGen: true},
{name: "r6i", sizes: memorySizes, currentGen: true},
{name: "r6a", sizes: memorySizes, currentGen: true},
{name: "r6g", sizes: memorySizesSmall, currentGen: true},
{name: "r7i", sizes: memorySizes, currentGen: true},
{name: "r7a", sizes: memorySizes, currentGen: true},
{name: "r7g", sizes: memorySizesSmall, currentGen: true},
{name: "r8i", sizes: memorySizes, currentGen: true},
{name: "r8a", sizes: memorySizes, currentGen: true},
{name: "r8g", sizes: memorySizesSmall, currentGen: true},
}
+171
View File
@@ -0,0 +1,171 @@
package instancetypes
import (
"log/slog"
"strings"
cpuid "github.com/klauspost/cpuid/v2"
)
// CPUInfo abstracts CPU identification for testability.
type CPUInfo interface {
VendorID() cpuid.Vendor
Family() int
Model() int
BrandName() string
HasFeature(cpuid.FeatureID) bool
}
// HostCPU implements CPUInfo by reading the host's cpuid.CPU global.
type HostCPU struct{}
func (HostCPU) VendorID() cpuid.Vendor { return cpuid.CPU.VendorID }
func (HostCPU) Family() int { return cpuid.CPU.Family }
func (HostCPU) Model() int { return cpuid.CPU.Model }
func (HostCPU) BrandName() string { return cpuid.CPU.BrandName }
func (HostCPU) HasFeature(f cpuid.FeatureID) bool { return cpuid.CPU.Has(f) }
// detectCPUGeneration detects the CPU microarchitecture generation using CPUID.
func detectCPUGeneration(cpu CPUInfo, arch string) cpuGeneration {
switch cpu.VendorID() {
case cpuid.Intel:
return detectIntelGeneration(cpu.Family(), cpu.Model())
case cpuid.AMD:
return detectAMDGeneration(cpu.Family(), cpu.Model())
default:
if arch == "arm64" {
return detectARMGeneration(cpu)
}
slog.Warn("CPUID vendor not recognized, falling back to brand string detection",
"vendorID", cpu.VendorID(), "brand", cpu.BrandName())
}
return detectGenerationFromBrand(cpu, arch)
}
// detectIntelGeneration maps Intel CPUID Family 6 model numbers to generations.
func detectIntelGeneration(family, model int) cpuGeneration {
if family != 6 {
slog.Warn("Unrecognized Intel CPU family, exposing t3 only", "family", family, "model", model)
return genUnknownIntel
}
switch model {
case 79, 86: // Broadwell server (BDX, BDX-DE)
return genIntelBroadwell
case 85: // Skylake-SP / Cascade Lake-SP
return genIntelSkylake
case 106, 108: // Ice Lake server (ICX, ICX-D)
return genIntelIceLake
case 143, 207: // Sapphire Rapids (SPR, EMR)
return genIntelSapphireRapids
case 173, 174: // Granite Rapids (GNR, GNR-D)
return genIntelGraniteRapids
// Consumer/desktop mapped to nearest server generation
case 151, 154: // Alder Lake
return genIntelIceLake
case 183, 191: // Raptor Lake
return genIntelSapphireRapids
case 197, 198: // Arrow Lake
return genIntelGraniteRapids
}
slog.Warn("Unrecognized Intel CPU model, exposing t3 only", "family", family, "model", model)
return genUnknownIntel
}
// detectAMDGeneration maps AMD CPUID family/model to generations.
func detectAMDGeneration(family, model int) cpuGeneration {
switch family {
case 23: // Zen, Zen+, Zen 2 (Naples, Rome, Matisse, etc.)
return genAMDZen
case 25: // Zen 3 and Zen 4 share family 25; model ranges distinguish them
// Zen 3 models: 0x00-0x0F (Milan/Vermeer), 0x20-0x5F (Rembrandt/Barcelo)
// Zen 4 models: 0x10-0x1F (Genoa), 0x60+ (Raphael/Phoenix)
isZen3 := model < 0x10 || (model >= 0x20 && model < 0x60)
if isZen3 {
return genAMDZen3
}
return genAMDZen4
case 26: // Zen 5 (Turin, Granite Ridge)
return genAMDZen5
}
slog.Warn("Unrecognized AMD CPU family, exposing t3a only", "family", family, "model", model)
return genUnknownAMD
}
// detectARMGeneration detects ARM CPU generation using brand string and feature flags.
func detectARMGeneration(cpu CPUInfo) cpuGeneration {
brand := strings.ToLower(cpu.BrandName())
// Check for specific Graviton versions
if strings.Contains(brand, "graviton4") || strings.Contains(brand, "neoverse-v2") {
return genARMNeoverseV2
}
if strings.Contains(brand, "graviton3") || strings.Contains(brand, "neoverse-v1") {
return genARMNeoverseV1
}
if strings.Contains(brand, "graviton2") || strings.Contains(brand, "neoverse-n1") {
return genARMNeoverseN1
}
// SVE indicates Neoverse V1+ but cannot distinguish V1 from V2
if cpu.HasFeature(cpuid.SVE) {
slog.Warn("ARM generation detected via SVE heuristic, defaulting to Neoverse V1", "brand", cpu.BrandName())
return genARMNeoverseV1
}
// Unknown ARM — expose only burstable t4g (same as unknown Intel/AMD behavior)
slog.Warn("Could not identify ARM generation, exposing t4g only", "brand", cpu.BrandName())
return genUnknownARM
}
// detectGenerationFromBrand is a fallback for VMs/hypervisors where CPUID may be virtualized.
func detectGenerationFromBrand(cpu CPUInfo, arch string) cpuGeneration {
if arch == "arm64" {
return detectARMGeneration(cpu)
}
brand := cpu.BrandName()
brandLower := strings.ToLower(brand)
// Intel patterns
if strings.Contains(brandLower, "xeon") || strings.Contains(brandLower, "intel") {
switch {
case strings.Contains(brandLower, "granite"):
return genIntelGraniteRapids
case strings.Contains(brandLower, "sapphire"):
return genIntelSapphireRapids
case strings.Contains(brandLower, "ice lake") || strings.Contains(brandLower, "icelake"):
return genIntelIceLake
case strings.Contains(brandLower, "cascade") || strings.Contains(brandLower, "skylake"):
return genIntelSkylake
case strings.Contains(brandLower, "broadwell"):
return genIntelBroadwell
default:
// Generic Intel — default to Skylake (most common in VMs)
slog.Warn("Intel CPU detected via brand string but generation unknown, defaulting to Skylake", "brand", brand)
return genIntelSkylake
}
}
// AMD patterns
if strings.Contains(brandLower, "epyc") || strings.Contains(brandLower, "amd") || strings.Contains(brandLower, "ryzen") {
switch {
case strings.Contains(brandLower, "turin"):
return genAMDZen5
case strings.Contains(brandLower, "genoa") || strings.Contains(brandLower, "9004") || strings.Contains(brandLower, "raphael"):
return genAMDZen4
case strings.Contains(brandLower, "milan") || strings.Contains(brandLower, "7003") || strings.Contains(brandLower, "vermeer"):
return genAMDZen3
default:
// Generic AMD — default to Zen/Zen2
slog.Warn("AMD CPU detected via brand string but generation unknown, defaulting to Zen/Zen2", "brand", brand)
return genAMDZen
}
}
slog.Warn("Unrecognized CPU, exposing t3 only", "brand", brand, "arch", arch)
return genUnknown
}
+239
View File
@@ -0,0 +1,239 @@
package instancetypes
import (
"runtime"
"testing"
cpuid "github.com/klauspost/cpuid/v2"
"github.com/stretchr/testify/assert"
)
func TestDetectIntelGeneration(t *testing.T) {
tests := []struct {
name string
family int
model int
expected cpuGeneration
}{
{"Broadwell BDX", 6, 79, genIntelBroadwell},
{"Broadwell BDX-DE", 6, 86, genIntelBroadwell},
{"Skylake-SP", 6, 85, genIntelSkylake},
{"Ice Lake ICX", 6, 106, genIntelIceLake},
{"Ice Lake ICX-D", 6, 108, genIntelIceLake},
{"Sapphire Rapids SPR", 6, 143, genIntelSapphireRapids},
{"Emerald Rapids EMR", 6, 207, genIntelSapphireRapids},
{"Granite Rapids GNR", 6, 173, genIntelGraniteRapids},
{"Granite Rapids GNR-D", 6, 174, genIntelGraniteRapids},
// Consumer mappings
{"Alder Lake", 6, 151, genIntelIceLake},
{"Alder Lake P", 6, 154, genIntelIceLake},
{"Raptor Lake", 6, 183, genIntelSapphireRapids},
{"Raptor Lake P", 6, 191, genIntelSapphireRapids},
{"Arrow Lake", 6, 197, genIntelGraniteRapids},
{"Arrow Lake S", 6, 198, genIntelGraniteRapids},
// Unknown
{"Unknown family", 15, 0, genUnknownIntel},
{"Unknown model", 6, 255, genUnknownIntel},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gen := detectIntelGeneration(tt.family, tt.model)
assert.Equal(t, tt.expected.name, gen.name)
assert.Equal(t, tt.expected.families, gen.families)
})
}
}
func TestDetectAMDGeneration(t *testing.T) {
tests := []struct {
name string
family int
model int
expected cpuGeneration
}{
{"Naples/Rome family 23", 23, 1, genAMDZen},
{"Zen3 Milan model 0x01", 25, 0x01, genAMDZen3},
{"Zen3 Vermeer model 0x21", 25, 0x21, genAMDZen3},
{"Zen4 Genoa model 0x11", 25, 0x11, genAMDZen4},
{"Zen4 Raphael model 0x61", 25, 0x61, genAMDZen4},
// Boundary tests for family 25 Zen3/Zen4 split
{"Zen3 boundary 0x0F", 25, 0x0F, genAMDZen3},
{"Zen4 boundary 0x10", 25, 0x10, genAMDZen4},
{"Zen4 boundary 0x1F", 25, 0x1F, genAMDZen4},
{"Zen3 boundary 0x20", 25, 0x20, genAMDZen3},
{"Zen3 boundary 0x5F", 25, 0x5F, genAMDZen3},
{"Zen4 boundary 0x60", 25, 0x60, genAMDZen4},
{"Zen4 max model", 25, 0xFF, genAMDZen4},
{"Zen5 Turin family 26", 26, 0, genAMDZen5},
{"Unknown AMD family", 20, 0, genUnknownAMD},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gen := detectAMDGeneration(tt.family, tt.model)
assert.Equal(t, tt.expected.name, gen.name)
assert.Equal(t, tt.expected.families, gen.families)
})
}
}
func TestDetectGenerationFromBrand(t *testing.T) {
tests := []struct {
name string
brand string
arch string
expected cpuGeneration
}{
{"Intel Skylake brand", "Intel(R) Xeon(R) Platinum 8175M (Skylake)", "x86_64", genIntelSkylake},
{"Intel Ice Lake brand", "Intel(R) Xeon(R) Platinum 8375C (Ice Lake)", "x86_64", genIntelIceLake},
{"Intel Sapphire brand", "Intel(R) Xeon(R) w9-3495X (Sapphire Rapids)", "x86_64", genIntelSapphireRapids},
{"Intel Granite brand", "Intel(R) Xeon(R) 6980P (Granite Rapids)", "x86_64", genIntelGraniteRapids},
{"Intel Broadwell brand", "Intel(R) Xeon(R) E5-2686 v4 (Broadwell)", "x86_64", genIntelBroadwell},
{"Generic Intel Xeon", "Intel(R) Xeon(R) CPU E5-2686 v4", "x86_64", genIntelSkylake}, // defaults to Skylake
{"AMD Milan brand", "AMD EPYC 7003 Milan", "x86_64", genAMDZen3},
{"AMD Genoa brand", "AMD EPYC 9004 Series", "x86_64", genAMDZen4},
{"Generic AMD EPYC", "AMD EPYC 7551", "x86_64", genAMDZen},
{"Completely unknown", "Some Random CPU", "x86_64", genUnknown},
// ARM delegation path
{"ARM via brand fallback", "AWS Graviton4", "arm64", genARMNeoverseV2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := &mockCPU{brandName: tt.brand}
gen := detectGenerationFromBrand(mock, tt.arch)
assert.Equal(t, tt.expected.name, gen.name)
assert.Equal(t, tt.expected.families, gen.families)
})
}
}
func TestCPUDetection(t *testing.T) {
cpu := HostCPU{}
gen := detectCPUGeneration(cpu, hostArch())
assert.NotEmpty(t, gen.name, "Generation name should not be empty")
assert.NotEmpty(t, gen.families, "Generation families should not be empty")
t.Logf("Detected CPU generation: %s, families: %v", gen.name, gen.families)
}
// --- Tests using mock CPUInfo ---
type mockCPU struct {
vendorID cpuid.Vendor
family int
model int
brandName string
features map[cpuid.FeatureID]bool
}
func (m *mockCPU) VendorID() cpuid.Vendor { return m.vendorID }
func (m *mockCPU) Family() int { return m.family }
func (m *mockCPU) Model() int { return m.model }
func (m *mockCPU) BrandName() string { return m.brandName }
func (m *mockCPU) HasFeature(f cpuid.FeatureID) bool { return m.features[f] }
func TestDetectCPUGeneration_IntelViaInterface(t *testing.T) {
cpu := &mockCPU{
vendorID: cpuid.Intel,
family: 6,
model: 143, // Sapphire Rapids
}
gen := detectCPUGeneration(cpu, "x86_64")
assert.Equal(t, genIntelSapphireRapids.name, gen.name)
assert.Equal(t, genIntelSapphireRapids.families, gen.families)
}
func TestDetectCPUGeneration_AMDViaInterface(t *testing.T) {
cpu := &mockCPU{
vendorID: cpuid.AMD,
family: 25,
model: 0x11, // Genoa (Zen 4)
}
gen := detectCPUGeneration(cpu, "x86_64")
assert.Equal(t, genAMDZen4.name, gen.name)
assert.Equal(t, genAMDZen4.families, gen.families)
}
func TestDetectCPUGeneration_ARMViaInterface(t *testing.T) {
cpu := &mockCPU{
vendorID: 0, // unknown vendor
brandName: "AWS Graviton3",
features: map[cpuid.FeatureID]bool{cpuid.SVE: true},
}
gen := detectCPUGeneration(cpu, "arm64")
assert.Equal(t, genARMNeoverseV1.name, gen.name)
assert.Equal(t, genARMNeoverseV1.families, gen.families)
}
func TestDetectCPUGeneration_ARMUnknown(t *testing.T) {
cpu := &mockCPU{
vendorID: 0, // unknown vendor
brandName: "Some Unknown ARM Processor",
}
gen := detectCPUGeneration(cpu, "arm64")
assert.Equal(t, genUnknownARM.name, gen.name)
assert.Equal(t, genUnknownARM.families, gen.families)
}
func TestDetectCPUGeneration_FallbackToBrand(t *testing.T) {
cpu := &mockCPU{
vendorID: 0, // unknown vendor
brandName: "AMD EPYC 9004 Series",
}
gen := detectCPUGeneration(cpu, "x86_64")
assert.Equal(t, genAMDZen4.name, gen.name)
assert.Equal(t, genAMDZen4.families, gen.families)
}
// --- detectARMGeneration coverage for all 5 code paths ---
func TestDetectARMGeneration(t *testing.T) {
tests := []struct {
name string
brand string
features map[cpuid.FeatureID]bool
expected cpuGeneration
}{
{"Graviton4 brand", "AWS Graviton4", nil, genARMNeoverseV2},
{"Neoverse V2 brand", "ARM Neoverse-V2", nil, genARMNeoverseV2},
{"Graviton3 brand", "AWS Graviton3", nil, genARMNeoverseV1},
{"Neoverse V1 brand", "ARM Neoverse-V1", nil, genARMNeoverseV1},
{"Graviton2 brand", "AWS Graviton2", nil, genARMNeoverseN1},
{"Neoverse N1 brand", "ARM Neoverse-N1", nil, genARMNeoverseN1},
{"SVE only, no brand match", "Generic ARM Processor", map[cpuid.FeatureID]bool{cpuid.SVE: true}, genARMNeoverseV1},
{"Unknown ARM, no SVE", "Generic ARM Processor", nil, genUnknownARM},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cpu := &mockCPU{brandName: tt.brand, features: tt.features}
gen := detectARMGeneration(cpu)
assert.Equal(t, tt.expected.name, gen.name)
assert.Equal(t, tt.expected.families, gen.families)
})
}
}
// --- DetectAndGenerate (public API) ---
func TestDetectAndGenerate_Intel(t *testing.T) {
cpu := &mockCPU{vendorID: cpuid.Intel, family: 6, model: 143}
types := DetectAndGenerate(cpu, "x86_64")
assert.NotEmpty(t, types)
assert.True(t, hasFamily(types, "c7i."), "Sapphire Rapids should include c7i")
}
func TestDetectAndGenerate_NormalizesAmd64(t *testing.T) {
cpu := &mockCPU{vendorID: cpuid.Intel, family: 6, model: 106}
types := DetectAndGenerate(cpu, "amd64")
assert.NotEmpty(t, types)
assert.True(t, hasFamily(types, "c6i."), "amd64 should normalize to x86_64 and produce Ice Lake types")
}
func hostArch() string {
if runtime.GOARCH == "arm64" {
return "arm64"
}
return "x86_64"
}
+73
View File
@@ -0,0 +1,73 @@
package instancetypes
import (
"fmt"
"log/slog"
"runtime"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
)
// generateForGeneration creates the instance type map for the given CPU generation.
// It generates all instance families matching the generation's family list across
// burstable, general purpose, compute optimized, and memory optimized categories.
func generateForGeneration(gen cpuGeneration, arch string) map[string]*ec2.InstanceTypeInfo {
// Build a set of allowed families for fast lookup
allowed := make(map[string]bool, len(gen.families))
for _, f := range gen.families {
allowed[f] = true
}
instanceTypes := make(map[string]*ec2.InstanceTypeInfo)
for _, def := range instanceFamilyDefs {
if !allowed[def.name] {
continue
}
burstable := strings.HasPrefix(def.name, "t")
for _, size := range def.sizes {
name := fmt.Sprintf("%s.%s", def.name, size.suffix)
instanceTypes[name] = &ec2.InstanceTypeInfo{
InstanceType: aws.String(name),
VCpuInfo: &ec2.VCpuInfo{
DefaultVCpus: aws.Int64(int64(size.vcpus)),
},
MemoryInfo: &ec2.MemoryInfo{
SizeInMiB: aws.Int64(int64(size.memoryGB * 1024)),
},
ProcessorInfo: &ec2.ProcessorInfo{
SupportedArchitectures: []*string{aws.String(arch)},
},
CurrentGeneration: aws.Bool(def.currentGen),
BurstablePerformanceSupported: aws.Bool(burstable),
Hypervisor: aws.String("kvm"),
SupportedVirtualizationTypes: []*string{aws.String("hvm")},
SupportedRootDeviceTypes: []*string{aws.String("ebs")},
}
}
}
return instanceTypes
}
// DetectAndGenerate detects the host CPU generation and generates matching instance types.
func DetectAndGenerate(cpu CPUInfo, arch string) map[string]*ec2.InstanceTypeInfo {
// Normalize Go's "amd64" to the Linux/AWS convention "x86_64".
if arch == "amd64" {
arch = "x86_64"
}
gen := detectCPUGeneration(cpu, arch)
types := generateForGeneration(gen, arch)
if len(types) == 0 {
slog.Error("No instance types generated, daemon will be unable to run VMs",
"generation", gen.name, "arch", arch)
} else {
slog.Info("CPU generation detected",
"generation", gen.name, "families", gen.families,
"instanceTypes", len(types), "os", runtime.GOOS)
}
return types
}
+315
View File
@@ -0,0 +1,315 @@
package instancetypes
import (
"strings"
"testing"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func hasFamily(types map[string]*ec2.InstanceTypeInfo, prefix string) bool {
for name := range types {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}
func countFamily(types map[string]*ec2.InstanceTypeInfo, prefix string) int {
count := 0
for name := range types {
if strings.HasPrefix(name, prefix) {
count++
}
}
return count
}
func TestGenerateInstanceTypes_IntelIceLake(t *testing.T) {
types := generateForGeneration(genIntelIceLake, "x86_64")
// t3(7) + c6i(8) + m6i(8) + r6i(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3.", "c6i.", "m6i.", "r6i."} {
assert.True(t, hasFamily(types, prefix), "expected Ice Lake types to include %s family", prefix)
}
// Verify other generation families are NOT present
for name := range types {
assert.False(t, strings.HasPrefix(name, "t3a."), "Ice Lake should not have t3a: %s", name)
assert.False(t, strings.HasPrefix(name, "c5."), "Ice Lake should not have c5: %s", name)
assert.False(t, strings.HasPrefix(name, "c7i."), "Ice Lake should not have c7i: %s", name)
}
}
func TestGenerateInstanceTypes_IntelBroadwell(t *testing.T) {
types := generateForGeneration(genIntelBroadwell, "x86_64")
// t2(7) + c4(6) + m4(6) + r4(6) = 25
assert.Len(t, types, 25)
for _, prefix := range []string{"t2.", "c4.", "m4.", "r4."} {
assert.True(t, hasFamily(types, prefix), "expected Broadwell types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_IntelSkylake(t *testing.T) {
types := generateForGeneration(genIntelSkylake, "x86_64")
// t3(7) + c5(8) + m5(8) + r5(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3.", "c5.", "m5.", "r5."} {
assert.True(t, hasFamily(types, prefix), "expected Skylake types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_IntelSapphireRapids(t *testing.T) {
types := generateForGeneration(genIntelSapphireRapids, "x86_64")
// t3(7) + c7i(8) + m7i(8) + r7i(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3.", "c7i.", "m7i.", "r7i."} {
assert.True(t, hasFamily(types, prefix), "expected Sapphire Rapids types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_IntelGraniteRapids(t *testing.T) {
types := generateForGeneration(genIntelGraniteRapids, "x86_64")
// t3(7) + c8i(8) + m8i(8) + r8i(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3.", "c8i.", "m8i.", "r8i."} {
assert.True(t, hasFamily(types, prefix), "expected Granite Rapids types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_AMDZen(t *testing.T) {
types := generateForGeneration(genAMDZen, "x86_64")
// t3a(7) + c5a(8) + m5a(8) + r5a(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3a.", "c5a.", "m5a.", "r5a."} {
assert.True(t, hasFamily(types, prefix), "expected Zen types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_AMDZen4(t *testing.T) {
types := generateForGeneration(genAMDZen4, "x86_64")
// t3a(7) + c7a(8) + m7a(8) + r7a(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3a.", "c7a.", "m7a.", "r7a."} {
assert.True(t, hasFamily(types, prefix), "expected Zen 4 types to include %s family", prefix)
}
// Verify older AMD families are NOT present
for name := range types {
assert.False(t, strings.HasPrefix(name, "c5a."), "Zen4 should not have c5a: %s", name)
assert.False(t, strings.HasPrefix(name, "c6a."), "Zen4 should not have c6a: %s", name)
}
}
func TestGenerateInstanceTypes_AMDZen3(t *testing.T) {
types := generateForGeneration(genAMDZen3, "x86_64")
// t3a(7) + c6a(8) + m6a(8) + r6a(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3a.", "c6a.", "m6a.", "r6a."} {
assert.True(t, hasFamily(types, prefix), "expected Zen 3 types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_AMDZen5(t *testing.T) {
types := generateForGeneration(genAMDZen5, "x86_64")
// t3a(7) + c8a(8) + m8a(8) + r8a(8) = 31
assert.Len(t, types, 31)
for _, prefix := range []string{"t3a.", "c8a.", "m8a.", "r8a."} {
assert.True(t, hasFamily(types, prefix), "expected Zen 5 types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_ARMV1(t *testing.T) {
types := generateForGeneration(genARMNeoverseV1, "arm64")
// t4g(7) + c7g(6) + m7g(6) + r7g(6) = 25
assert.Len(t, types, 25)
for _, prefix := range []string{"t4g.", "c7g.", "m7g.", "r7g."} {
assert.True(t, hasFamily(types, prefix), "expected V1 types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_ARMN1(t *testing.T) {
types := generateForGeneration(genARMNeoverseN1, "arm64")
// t4g(7) + c6g(6) + m6g(6) + r6g(6) = 25
assert.Len(t, types, 25)
for _, prefix := range []string{"t4g.", "c6g.", "m6g.", "r6g."} {
assert.True(t, hasFamily(types, prefix), "expected N1 types to include %s family", prefix)
}
// Verify Intel/AMD families are NOT present
for name := range types {
assert.False(t, strings.HasPrefix(name, "t3."), "ARM should not have t3: %s", name)
assert.False(t, strings.HasPrefix(name, "t3a."), "ARM should not have t3a: %s", name)
}
}
func TestGenerateInstanceTypes_ARMV2(t *testing.T) {
types := generateForGeneration(genARMNeoverseV2, "arm64")
// t4g(7) + c8g(6) + m8g(6) + r8g(6) = 25
assert.Len(t, types, 25)
for _, prefix := range []string{"t4g.", "c8g.", "m8g.", "r8g."} {
assert.True(t, hasFamily(types, prefix), "expected V2 types to include %s family", prefix)
}
}
func TestGenerateInstanceTypes_UnknownFallback(t *testing.T) {
types := generateForGeneration(genUnknownIntel, "x86_64")
// Unknown Intel: t3 only = 7 types
assert.Len(t, types, 7)
assert.True(t, hasFamily(types, "t3."), "unknown Intel should have t3")
types = generateForGeneration(genUnknownAMD, "x86_64")
assert.Len(t, types, 7)
assert.True(t, hasFamily(types, "t3a."), "unknown AMD should have t3a")
types = generateForGeneration(genUnknownARM, "arm64")
assert.Len(t, types, 7)
assert.True(t, hasFamily(types, "t4g."), "unknown ARM should have t4g")
types = generateForGeneration(genUnknown, "x86_64")
assert.Len(t, types, 7)
assert.True(t, hasFamily(types, "t3."), "completely unknown should have t3")
}
func TestGenerateInstanceTypes_VerifyBurstableSizes(t *testing.T) {
types := generateForGeneration(genIntelSkylake, "x86_64")
expected := map[string]struct {
vcpus int64
memMB int64
}{
"t3.nano": {2, 512},
"t3.micro": {2, 1024},
"t3.small": {2, 2048},
"t3.medium": {2, 4096},
"t3.large": {2, 8192},
"t3.xlarge": {4, 16384},
"t3.2xlarge": {8, 32768},
}
for name, exp := range expected {
it, ok := types[name]
require.True(t, ok, "missing instance type %s", name)
assert.Equal(t, exp.vcpus, *it.VCpuInfo.DefaultVCpus, "%s vCPUs", name)
assert.Equal(t, exp.memMB, *it.MemoryInfo.SizeInMiB, "%s memory", name)
}
}
func TestGenerateInstanceTypes_ComputeRatio(t *testing.T) {
// Skylake for c5
skylakeTypes := generateForGeneration(genIntelSkylake, "x86_64")
expectedSkylake := map[string]struct {
vcpus int64
memMB int64
}{
"c5.large": {2, 4096},
"c5.xlarge": {4, 8192},
"c5.2xlarge": {8, 16384},
}
for name, exp := range expectedSkylake {
it, ok := skylakeTypes[name]
require.True(t, ok, "missing instance type %s", name)
assert.Equal(t, exp.vcpus, *it.VCpuInfo.DefaultVCpus, "%s vCPUs", name)
assert.Equal(t, exp.memMB, *it.MemoryInfo.SizeInMiB, "%s memory", name)
}
// Sapphire Rapids for c7i
sapphireTypes := generateForGeneration(genIntelSapphireRapids, "x86_64")
it, ok := sapphireTypes["c7i.4xlarge"]
require.True(t, ok, "missing instance type c7i.4xlarge")
assert.Equal(t, int64(16), *it.VCpuInfo.DefaultVCpus, "c7i.4xlarge vCPUs")
assert.Equal(t, int64(32768), *it.MemoryInfo.SizeInMiB, "c7i.4xlarge memory")
}
func TestGenerateInstanceTypes_MemoryRatio(t *testing.T) {
// Skylake for r5
skylakeTypes := generateForGeneration(genIntelSkylake, "x86_64")
expectedSkylake := map[string]struct {
vcpus int64
memMB int64
}{
"r5.large": {2, 16384},
"r5.xlarge": {4, 32768},
"r5.2xlarge": {8, 65536},
}
for name, exp := range expectedSkylake {
it, ok := skylakeTypes[name]
require.True(t, ok, "missing instance type %s", name)
assert.Equal(t, exp.vcpus, *it.VCpuInfo.DefaultVCpus, "%s vCPUs", name)
assert.Equal(t, exp.memMB, *it.MemoryInfo.SizeInMiB, "%s memory", name)
}
// Sapphire Rapids for r7i
sapphireTypes := generateForGeneration(genIntelSapphireRapids, "x86_64")
it, ok := sapphireTypes["r7i.4xlarge"]
require.True(t, ok, "missing instance type r7i.4xlarge")
assert.Equal(t, int64(16), *it.VCpuInfo.DefaultVCpus, "r7i.4xlarge vCPUs")
assert.Equal(t, int64(131072), *it.MemoryInfo.SizeInMiB, "r7i.4xlarge memory")
}
func TestGenerateInstanceTypes_NoSmallSizesForNonBurstable(t *testing.T) {
types := generateForGeneration(genIntelSkylake, "x86_64")
// Non-burstable families should not have nano/micro/small/medium sizes
for name := range types {
if strings.HasPrefix(name, "t") {
continue // skip all burstable families
}
for _, small := range []string{".nano", ".micro", ".small", ".medium"} {
assert.False(t, strings.HasSuffix(name, small),
"non-burstable type %s should not have %s size", name, small)
}
}
}
func TestGenerateInstanceTypes_OlderFamiliesHaveSmallerSizeRange(t *testing.T) {
// Broadwell has m4 = 6 sizes
broadwellTypes := generateForGeneration(genIntelBroadwell, "x86_64")
assert.Equal(t, 6, countFamily(broadwellTypes, "m4."), "m4 should have 6 sizes (large → 16xlarge)")
// Skylake has m5 = 8 sizes
skylakeTypes := generateForGeneration(genIntelSkylake, "x86_64")
assert.Equal(t, 8, countFamily(skylakeTypes, "m5."), "m5 should have 8 sizes (large → 24xlarge)")
}
func TestGenerateInstanceTypes_BurstableFlag(t *testing.T) {
// Test Broadwell (has prev-gen families)
broadwellTypes := generateForGeneration(genIntelBroadwell, "x86_64")
prevGen := map[string]bool{"t2": true, "m4": true, "c4": true, "r4": true}
for name, info := range broadwellTypes {
isBurstable := strings.HasPrefix(name, "t")
family := strings.SplitN(name, ".", 2)[0]
assert.Equal(t, isBurstable, *info.BurstablePerformanceSupported,
"%s burstable flag mismatch", name)
assert.Equal(t, !prevGen[family], *info.CurrentGeneration,
"%s current generation flag mismatch", name)
}
// Test current-gen (Sapphire Rapids) — all families should be currentGen=true
sapphireTypes := generateForGeneration(genIntelSapphireRapids, "x86_64")
for name, info := range sapphireTypes {
isBurstable := strings.HasPrefix(name, "t")
assert.Equal(t, isBurstable, *info.BurstablePerformanceSupported,
"%s burstable flag mismatch", name)
assert.True(t, *info.CurrentGeneration,
"%s should be current generation", name)
}
}