mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
fix: refactor instance type detection (#41)
This commit is contained in:
+3
-363
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user