Skip to content

Commit

Permalink
refactor Matcher and FlavorSpec
Browse files Browse the repository at this point in the history
Scoring logic should live in the FlavorSpec and provide an easy way to
obtain score for a given Machine.

This reduces Matcher functionality to simply interacting with a
collection of flavors and matching them against Machine.
  • Loading branch information
skrobul committed Aug 29, 2024
1 parent 11eea2b commit ce7af74
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 209 deletions.
56 changes: 56 additions & 0 deletions python/ironic-understack/ironic_understack/flavor_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import yaml

from ironic_understack.machine import Machine


@dataclass
class FlavorSpec:
Expand Down Expand Up @@ -42,3 +44,57 @@ def from_directory(directory: str = "/etc/flavors/") -> list["FlavorSpec"]:
except Exception as e:
print(f"Error processing file {filename}: {e}")
return flavor_specs

def score_machine(self, machine: Machine):
# Scoring Rules:
#
# 1. 100% match gets highest priority, no further evaluation needed
# 2. If the machine has less memory size than specified in the flavor,
# it cannot be used - the score should be 0.
# 3. If the machine has smaller disk size than specified in the flavor,
# it cannot be used - the score should be 0.
# 4. Machine must match the flavor on one of the CPU models exactly.
# 5. If the machine has exact amount memory as specified in flavor, but
# more disk space it is less desirable than the machine that matches
# exactly on both disk and memory.
# 6. If the machine has exact amount of disk as specified in flavor,
# but more memory space it is less desirable than the machine that
# matches exactly on both disk and memory.


# Rule 1: 100% match gets the highest priority
if (
machine.memory_gb == self.memory_gb and
machine.disk_gb in self.drives and
machine.cpu in self.cpu_models
):
return 100

# Rule 2: If machine has less memory than specified in the flavor, it cannot be used
if machine.memory_gb < self.memory_gb:
return 0

# Rule 3: If machine has smaller disk than specified in the flavor, it cannot be used
if any(machine.disk_gb < drive for drive in self.drives):
return 0

# Rule 4: Machine must match the flavor on one of the CPU models exactly
if machine.cpu not in self.cpu_models:
return 0

# Rule 5 and 6: Rank based on exact matches or excess capacity
score = 0

# Exact memory match gives preference
if machine.memory_gb == self.memory_gb:
score += 10
elif machine.memory_gb > self.memory_gb:
score += 5 # Less desirable but still usable

# Exact disk match gives preference
if machine.disk_gb in self.drives:
score += 10
elif all(machine.disk_gb > drive for drive in self.drives):
score += 5 # Less desirable but still usable

return score
56 changes: 1 addition & 55 deletions python/ironic-understack/ironic_understack/matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,67 +6,13 @@ class Matcher:
def __init__(self, flavors: list[FlavorSpec]):
self.flavors = flavors

def score_machine_to_flavor(self, machine: Machine, flavor: FlavorSpec) -> int:
# Scoring Rules:
#
# 1. 100% match gets highest priority, no further evaluation needed
# 2. If the machine has less memory size than specified in the flavor,
# it cannot be used - the score should be 0.
# 3. If the machine has smaller disk size than specified in the flavor,
# it cannot be used - the score should be 0.
# 4. Machine must match the flavor on one of the CPU models exactly.
# 5. If the machine has exact amount memory as specified in flavor, but
# more disk space it is less desirable than the machine that matches
# exactly on both disk and memory.
# 6. If the machine has exact amount of disk as specified in flavor,
# but more memory space it is less desirable than the machine that
# matches exactly on both disk and memory.


# Rule 1: 100% match gets the highest priority
if (
machine.memory_gb == flavor.memory_gb and
machine.disk_gb in flavor.drives and
machine.cpu in flavor.cpu_models
):
return 100

# Rule 2: If machine has less memory than specified in the flavor, it cannot be used
if machine.memory_gb < flavor.memory_gb:
return 0

# Rule 3: If machine has smaller disk than specified in the flavor, it cannot be used
if any(machine.disk_gb < drive for drive in flavor.drives):
return 0

# Rule 4: Machine must match the flavor on one of the CPU models exactly
if machine.cpu not in flavor.cpu_models:
return 0

# Rule 5 and 6: Rank based on exact matches or excess capacity
score = 0

# Exact memory match gives preference
if machine.memory_gb == flavor.memory_gb:
score += 10
elif machine.memory_gb > flavor.memory_gb:
score += 5 # Less desirable but still usable

# Exact disk match gives preference
if machine.disk_gb in flavor.drives:
score += 10
elif all(machine.disk_gb > drive for drive in flavor.drives):
score += 5 # Less desirable but still usable

return score

def match(self, machine: Machine) -> list[FlavorSpec]:
"""
Find list of all flavors that the machine is eligible for.
"""
results = []
for flavor in self.flavors:
score = self.score_machine_to_flavor(machine, flavor)
score = flavor.score_machine(machine)
if score > 0:
results.append(flavor)
return results
Expand Down
125 changes: 125 additions & 0 deletions python/ironic-understack/ironic_understack/tests/test_flavor_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
from ironic_understack.flavor_spec import FlavorSpec
from ironic_understack.machine import Machine


@pytest.fixture
Expand Down Expand Up @@ -91,3 +92,127 @@ def test_from_directory_with_real_files(yaml_directory):
def test_empty_directory(tmp_path):
specs = FlavorSpec.from_directory(str(tmp_path))
assert len(specs) == 0

@pytest.fixture
def machines():
return [
# 1024 GB, exact CPU, medium
Machine(
memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=1000
),
# 800 GB, non-matching CPU
Machine(memory_mb=800000, cpu="Intel Xeon E5-2676 v3", disk_gb=500),
# 200 GB, exact CPU, medium
Machine(memory_mb=200000, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=1500),
# 300 GB, non-matching CPU
Machine(memory_mb=300000, cpu="Intel Xeon E5-2676 v3", disk_gb=500),
# 409 GB, exact CPU, large
Machine(memory_mb=409600, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=2000),
]


@pytest.fixture
def flavors():
return [
FlavorSpec(
name="small",
memory_gb=100,
cpu_cores=13,
cpu_models=["AMD EPYC 9254 245-Core Processor", "Pentium 60"],
drives=[500, 500],
devices=[],
),
FlavorSpec(
name="medium",
memory_gb=200,
cpu_cores=15,
cpu_models=["AMD EPYC 9254 245-Core Processor", "Intel 80386DX"],
drives=[1500, 1500],
devices=[],
),
FlavorSpec(
name="large",
memory_gb=400,
cpu_cores=27,
cpu_models=["AMD EPYC 9254 245-Core Processor"],
drives=[1800, 1800],
devices=[],
),
]

def test_exact_match(flavors):
machine = Machine(memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=500)
assert flavors[0].score_machine(machine) == 100
assert flavors[1].score_machine(machine) == 0


def test_memory_too_small(flavors):
machine = Machine(memory_mb=51200, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=500)
assert all(flavor.score_machine(machine) for flavor in flavors) == 0


def test_disk_too_small(flavors):
machine = Machine(memory_mb=204800, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=100)
assert all(flavor.score_machine(machine) for flavor in flavors) == 0


def test_cpu_model_not_matching(flavors):
machine = Machine(memory_mb=102400, cpu="Non-Existent CPU Model", disk_gb=500)
assert all(flavor.score_machine(machine) for flavor in flavors) == 0


def test_memory_match_but_more_disk(flavors):
machine = Machine(memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=1000)
assert flavors[0].score_machine(machine) > 0


def test_disk_match_but_more_memory(flavors):
machine = Machine(memory_mb=204800, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=500)

assert flavors[0].score_machine(machine) > 0
assert flavors[1].score_machine(machine) == 0
assert flavors[2].score_machine(machine) == 0

# Edge cases
def test_memory_slightly_less(flavors):
# Machine with slightly less memory than required by the smallest flavor
machine = Machine(memory_mb=102300, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=500)
# Should not match because memory is slightly less
assert all(flavor.score_machine(machine) for flavor in flavors) == 0


def test_disk_slightly_less(flavors):
# Machine with slightly less disk space than required by the smallest flavor
machine = Machine(memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=499)
# Should not match because disk space is slightly less
assert all(flavor.score_machine(machine) for flavor in flavors) == 0


def test_memory_exact_disk_slightly_more(flavors):
# Machine with exact memory but slightly more disk space than required
machine = Machine(memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=501)
assert flavors[0].score_machine(machine) > 0
assert flavors[1].score_machine(machine) == 0
assert flavors[2].score_machine(machine) == 0


def test_disk_exact_memory_slightly_more(flavors):
# Machine with exact disk space but slightly more memory than required
machine = Machine(memory_mb=102500, cpu="AMD EPYC 9254 245-Core Processor", disk_gb=500)
assert flavors[0].score_machine(machine) > 0
assert flavors[1].score_machine(machine) == 0
assert flavors[2].score_machine(machine) == 0


def test_cpu_model_not_exact_but_memory_and_disk_match(flavors):
# Machine with exact memory and disk space but CPU model is close but not exact
machine = Machine(memory_mb=102400, cpu="AMD EPYC 9254 245-Core Processor v2", disk_gb=500)
# Should not match because CPU model is not exactly listed
assert all(flavor.score_machine(machine) for flavor in flavors) == 0


def test_large_flavor_memory_slightly_less_disk_exact(flavors):
# Machine with slightly less memory than required for the medium flavor, exact disk space
machine = Machine(memory_mb=204600, cpu="Intel 80386DX", disk_gb=1800)
# Should not match because memory is slightly less than required
assert all(flavor.score_machine(machine) for flavor in flavors) == 0
19 changes: 19 additions & 0 deletions python/ironic-understack/ironic_understack/tests/test_machine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest
from ironic_understack.machine import Machine

def test_memory_gb_property():
# Test a machine with exactly 1 GB of memory
machine = Machine(memory_mb=1024, cpu="x86", disk_gb=50)
assert machine.memory_gb == 1

# Test a machine with 2 GB of memory
machine = Machine(memory_mb=2048, cpu="x86", disk_gb=50)
assert machine.memory_gb == 2

# Test a machine with non-exact GB memory (should floor the value)
machine = Machine(memory_mb=3072, cpu="x86", disk_gb=50)
assert machine.memory_gb == 3

# Test a machine with less than 1 GB of memory
machine = Machine(memory_mb=512, cpu="x86", disk_gb=50)
assert machine.memory_gb == 0
Loading

0 comments on commit ce7af74

Please sign in to comment.