Skip to content

Commit affa2f8

Browse files
authored
Support different disk types for size matching (#528)
1 parent da14bf8 commit affa2f8

File tree

17 files changed

+767
-368
lines changed

17 files changed

+767
-368
lines changed

cmd/metal-api/internal/datastore/machine.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ type MachineSearchQuery struct {
3939
NetworkASNs []int64 `json:"network_asns" optional:"true"`
4040

4141
// hardware
42-
HardwareMemory *int64 `json:"hardware_memory" optional:"true"`
43-
HardwareCPUCores *int64 `json:"hardware_cpu_cores" optional:"true"`
42+
HardwareMemory *int64 `json:"hardware_memory" optional:"true"`
4443

4544
// nics
4645
NicsMacAddresses []string `json:"nics_mac_addresses" optional:"true"`
@@ -211,12 +210,6 @@ func (p *MachineSearchQuery) generateTerm(rs *RethinkStore) *r.Term {
211210
})
212211
}
213212

214-
if p.HardwareCPUCores != nil {
215-
q = q.Filter(func(row r.Term) r.Term {
216-
return row.Field("hardware").Field("cpu_cores").Eq(*p.HardwareCPUCores)
217-
})
218-
}
219-
220213
for _, mac := range p.NicsMacAddresses {
221214
mac := mac
222215
q = q.Filter(func(row r.Term) r.Term {

cmd/metal-api/internal/datastore/machine_integration_test.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -479,21 +479,6 @@ func TestRethinkStore_SearchMachines(t *testing.T) {
479479
},
480480
wantErr: nil,
481481
},
482-
{
483-
name: "search by hardware cpus",
484-
q: &MachineSearchQuery{
485-
HardwareCPUCores: pointer.Pointer(int64(8)),
486-
},
487-
mock: []*metal.Machine{
488-
{Base: metal.Base{ID: "1"}, Hardware: metal.MachineHardware{CPUCores: 1}},
489-
{Base: metal.Base{ID: "2"}, Hardware: metal.MachineHardware{CPUCores: 2}},
490-
{Base: metal.Base{ID: "3"}, Hardware: metal.MachineHardware{CPUCores: 8}},
491-
},
492-
want: []*metal.Machine{
493-
tt.defaultBody(&metal.Machine{Base: metal.Base{ID: "3"}, Hardware: metal.MachineHardware{CPUCores: 8}}),
494-
},
495-
wantErr: nil,
496-
},
497482
{
498483
name: "search by nic mac address",
499484
q: &MachineSearchQuery{

cmd/metal-api/internal/grpc/boot-service.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ func (b *BootService) Register(ctx context.Context, req *v1.BootServiceRegisterR
144144

145145
machineHardware := metal.MachineHardware{
146146
Memory: req.Hardware.Memory,
147-
CPUCores: int(req.Hardware.CpuCores),
148147
Disks: disks,
149148
Nics: nics,
150149
MetalCPUs: cpus,

cmd/metal-api/internal/grpc/boot-service_test.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,15 @@ func TestBootService_Register(t *testing.T) {
118118
req := &v1.BootServiceRegisterRequest{
119119
Uuid: tt.uuid,
120120
Hardware: &v1.MachineHardware{
121-
Memory: uint64(tt.memory),
122-
CpuCores: uint32(tt.numcores),
121+
Memory: uint64(tt.memory),
122+
123+
Cpus: []*v1.MachineCPU{
124+
{
125+
Model: "Intel Xeon Silver",
126+
Cores: uint32(tt.numcores),
127+
Threads: uint32(tt.numcores),
128+
},
129+
},
123130
Nics: []*v1.MachineNic{
124131
{
125132
Mac: "aa", Neighbors: []*v1.MachineNic{{Mac: string(tt.neighbormac1)}},

cmd/metal-api/internal/metal/machine.go

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import (
55
"log/slog"
66
"net/netip"
77
"os"
8+
"path/filepath"
89
"slices"
910
"strings"
1011
"time"
1112

1213
"github.com/dustin/go-humanize"
1314
mn "github.com/metal-stack/metal-lib/pkg/net"
15+
"github.com/samber/lo"
1416
)
1517

1618
// A MState is an enum which indicates the state of a machine
@@ -458,7 +460,6 @@ func (n NetworkType) String() string {
458460
// MachineHardware stores the data which is collected by our system on the hardware when it registers itself.
459461
type MachineHardware struct {
460462
Memory uint64 `rethinkdb:"memory" json:"memory"`
461-
CPUCores int `rethinkdb:"cpu_cores" json:"cpu_cores"`
462463
Nics Nics `rethinkdb:"network_interfaces" json:"network_interfaces"`
463464
Disks []BlockDevice `rethinkdb:"block_devices" json:"block_devices"`
464465
MetalCPUs []MetalCPU `rethinkdb:"cpus" json:"cpus"`
@@ -489,13 +490,51 @@ const (
489490
MachineResurrectAfter time.Duration = time.Hour
490491
)
491492

492-
// DiskCapacity calculates the capacity of all disks.
493-
func (hw *MachineHardware) DiskCapacity() uint64 {
494-
var c uint64
495-
for _, d := range hw.Disks {
496-
c += d.Size
493+
func capacityOf[V any](identifier string, vs []V, countFn func(v V) (model string, count uint64)) (uint64, []V) {
494+
var (
495+
sum uint64
496+
matched []V
497+
)
498+
499+
if identifier == "" {
500+
identifier = "*"
501+
}
502+
503+
for _, v := range vs {
504+
model, count := countFn(v)
505+
506+
matches, err := filepath.Match(identifier, model)
507+
if err != nil {
508+
// illegal identifiers are already prevented by size validation
509+
continue
510+
}
511+
512+
if !matches {
513+
continue
514+
}
515+
516+
sum += count
517+
matched = append(matched, v)
497518
}
498-
return c
519+
520+
return sum, matched
521+
}
522+
523+
func exhaustiveMatch[V comparable](cs []Constraint, vs []V, countFn func(v V) (model string, count uint64)) bool {
524+
unmatched := slices.Clone(vs)
525+
526+
for _, c := range cs {
527+
capacity, matched := capacityOf(c.Identifier, vs, countFn)
528+
529+
match := c.inRange(capacity)
530+
if !match {
531+
continue
532+
}
533+
534+
unmatched, _ = lo.Difference(unmatched, matched)
535+
}
536+
537+
return len(unmatched) == 0
499538
}
500539

501540
func (hw *MachineHardware) GPUModels() map[string]uint64 {
@@ -513,7 +552,10 @@ func (hw *MachineHardware) GPUModels() map[string]uint64 {
513552

514553
// ReadableSpec returns a human readable string for the hardware.
515554
func (hw *MachineHardware) ReadableSpec() string {
516-
return fmt.Sprintf("Cores: %d, Memory: %s, Storage: %s GPUs:%s", hw.CPUCores, humanize.Bytes(hw.Memory), humanize.Bytes(hw.DiskCapacity()), hw.MetalGPUs)
555+
diskCapacity, _ := capacityOf("*", hw.Disks, countDisk)
556+
cpus, _ := capacityOf("*", hw.MetalCPUs, countCPU)
557+
gpus, _ := capacityOf("*", hw.MetalGPUs, countGPU)
558+
return fmt.Sprintf("CPUs: %d, Memory: %s, Storage: %s, GPUs: %d", cpus, humanize.Bytes(hw.Memory), humanize.Bytes(diskCapacity), gpus)
517559
}
518560

519561
// BlockDevice information.

cmd/metal-api/internal/metal/machine_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,14 @@ func TestMachine_HasMAC(t *testing.T) {
2424
SizeID: "1",
2525
Allocation: nil,
2626
Hardware: MachineHardware{
27-
Memory: 100,
28-
CPUCores: 1,
27+
Memory: 100,
28+
MetalCPUs: []MetalCPU{
29+
{
30+
Model: "Intel Xeon Silver",
31+
Cores: 1,
32+
Threads: 1,
33+
},
34+
},
2935
Nics: Nics{
3036
Nic{
3137
MacAddress: "11:11:11:11:11:11",

cmd/metal-api/internal/metal/size.go

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,28 @@ const (
3838
GPUConstraint ConstraintType = "gpu"
3939
)
4040

41-
// A Constraint describes the hardware constraints for a given size. At the moment we only
42-
// consider the cpu cores and the memory.
41+
var allConstraintTypes = []ConstraintType{CoreConstraint, MemoryConstraint, StorageConstraint, GPUConstraint}
42+
43+
// A Constraint describes the hardware constraints for a given size.
4344
type Constraint struct {
4445
Type ConstraintType `rethinkdb:"type" json:"type"`
4546
Min uint64 `rethinkdb:"min" json:"min"`
4647
Max uint64 `rethinkdb:"max" json:"max"`
4748
Identifier string `rethinkdb:"identifier" json:"identifier" description:"glob of the identifier of this type"`
4849
}
4950

51+
func countCPU(cpu MetalCPU) (model string, count uint64) {
52+
return cpu.Model, uint64(cpu.Cores)
53+
}
54+
55+
func countGPU(gpu MetalGPU) (model string, count uint64) {
56+
return gpu.Model, 1
57+
}
58+
59+
func countDisk(disk BlockDevice) (model string, count uint64) {
60+
return disk.Name, disk.Size
61+
}
62+
5063
// Sizes is a list of sizes.
5164
type Sizes []Size
5265

@@ -72,55 +85,85 @@ func UnknownSize() *Size {
7285
}
7386
}
7487

75-
// Matches returns true if the given machine hardware is inside the min/max values of the
88+
func (c *Constraint) inRange(value uint64) bool {
89+
return value >= c.Min && value <= c.Max
90+
}
91+
92+
// matches returns true if the given machine hardware is inside the min/max values of the
7693
// constraint.
77-
func (c *Constraint) Matches(hw MachineHardware) bool {
94+
func (c *Constraint) matches(hw MachineHardware) bool {
7895
res := false
7996
switch c.Type {
8097
case CoreConstraint:
81-
res = uint64(hw.CPUCores) >= c.Min && uint64(hw.CPUCores) <= c.Max
98+
cores, _ := capacityOf(c.Identifier, hw.MetalCPUs, countCPU)
99+
res = c.inRange(cores)
82100
case MemoryConstraint:
83-
res = hw.Memory >= c.Min && hw.Memory <= c.Max
101+
res = c.inRange(hw.Memory)
84102
case StorageConstraint:
85-
res = hw.DiskCapacity() >= c.Min && hw.DiskCapacity() <= c.Max
103+
capacity, _ := capacityOf(c.Identifier, hw.Disks, countDisk)
104+
res = c.inRange(capacity)
86105
case GPUConstraint:
87-
for model, count := range hw.GPUModels() {
88-
idMatches, err := filepath.Match(c.Identifier, model)
89-
if err != nil {
90-
return false
91-
}
92-
res = count >= c.Min && count <= c.Max && idMatches
93-
if res {
94-
break
95-
}
96-
}
97-
106+
count, _ := capacityOf(c.Identifier, hw.MetalGPUs, countGPU)
107+
res = c.inRange(count)
98108
}
99109
return res
100110
}
101111

112+
// matches returns true if all provided disks and later GPUs are covered with at least one constraint.
113+
// With this we ensure that hardware matches exhaustive against the constraints.
114+
func (hw *MachineHardware) matches(constraints []Constraint, constraintType ConstraintType) bool {
115+
filtered := lo.Filter(constraints, func(c Constraint, _ int) bool { return c.Type == constraintType })
116+
if len(filtered) == 0 {
117+
return true
118+
}
119+
120+
switch constraintType {
121+
case StorageConstraint:
122+
return exhaustiveMatch(filtered, hw.Disks, countDisk)
123+
case GPUConstraint:
124+
return exhaustiveMatch(filtered, hw.MetalGPUs, countGPU)
125+
case CoreConstraint:
126+
return exhaustiveMatch(filtered, hw.MetalCPUs, countCPU)
127+
case MemoryConstraint:
128+
// Noop because we do not have different Memory types
129+
return true
130+
default:
131+
return true
132+
}
133+
}
134+
102135
// FromHardware searches a Size for given hardware specs. It will search
103136
// for a size where the constraints matches the given hardware.
104137
func (sz Sizes) FromHardware(hardware MachineHardware) (*Size, error) {
105-
var found []Size
138+
var (
139+
matchedSizes []Size
140+
)
141+
106142
nextsize:
107143
for _, s := range sz {
108144
for _, c := range s.Constraints {
109-
match := c.Matches(hardware)
145+
match := c.matches(hardware)
146+
if !match {
147+
continue nextsize
148+
}
149+
}
150+
151+
for _, ct := range allConstraintTypes {
152+
match := hardware.matches(s.Constraints, ct)
110153
if !match {
111154
continue nextsize
112155
}
113156
}
114-
found = append(found, s)
157+
matchedSizes = append(matchedSizes, s)
115158
}
116159

117-
if len(found) == 0 {
160+
if len(matchedSizes) == 0 {
118161
return nil, NotFound("no size found for hardware (%s)", hardware.ReadableSpec())
119162
}
120-
if len(found) > 1 {
121-
return nil, fmt.Errorf("%d sizes found for hardware (%s)", len(found), hardware.ReadableSpec())
163+
if len(matchedSizes) > 1 {
164+
return nil, fmt.Errorf("%d sizes found for hardware (%s)", len(matchedSizes), hardware.ReadableSpec())
122165
}
123-
return &found[0], nil
166+
return &matchedSizes[0], nil
124167
}
125168

126169
func (s *Size) overlaps(so *Size) bool {
@@ -172,23 +215,35 @@ func (c *Constraint) overlaps(other Constraint) bool {
172215

173216
// Validate a size, returns error if a invalid size is passed
174217
func (s *Size) Validate(partitions PartitionMap, projects map[string]*mdmv1.Project) error {
175-
constraintTypes := map[ConstraintType]bool{}
218+
constraintTypes := map[ConstraintType]uint{}
176219
for _, c := range s.Constraints {
177220
if c.Max < c.Min {
178221
return fmt.Errorf("size:%q type:%q max:%d is smaller than min:%d", s.ID, c.Type, c.Max, c.Min)
179222
}
180223

181-
_, ok := constraintTypes[c.Type]
182-
if ok {
183-
return fmt.Errorf("size:%q type:%q min:%d max:%d has duplicate constraint type", s.ID, c.Type, c.Min, c.Max)
224+
// CPU and Memory Constraints are not allowed more than once
225+
constraintTypes[c.Type]++
226+
count := constraintTypes[c.Type]
227+
if c.Type == CoreConstraint || c.Type == MemoryConstraint {
228+
if count > 1 {
229+
return fmt.Errorf("size:%q type:%q min:%d max:%d has duplicate constraint type", s.ID, c.Type, c.Min, c.Max)
230+
}
184231
}
185232

186233
// Ensure GPU Constraints always have identifier specified
187234
if c.Type == GPUConstraint && c.Identifier == "" {
188235
return fmt.Errorf("size:%q type:%q min:%d max:%d is a gpu size but has no identifier specified", s.ID, c.Type, c.Min, c.Max)
189236
}
190237

191-
constraintTypes[c.Type] = true
238+
// Ensure Memory Constraints do not have a identifier specified
239+
if c.Type == MemoryConstraint && c.Identifier != "" {
240+
return fmt.Errorf("size:%q type:%q min:%d max:%d is a memory size but has a identifier specified", s.ID, c.Type, c.Min, c.Max)
241+
}
242+
243+
if _, err := filepath.Match(c.Identifier, ""); err != nil {
244+
return fmt.Errorf("size:%q type:%q min:%d max:%d identifier:%q identifier is malformed:%w", s.ID, c.Type, c.Min, c.Max, c.Identifier, err)
245+
}
246+
192247
}
193248

194249
if err := s.Reservations.Validate(partitions, projects); err != nil {

0 commit comments

Comments
 (0)