blackopsrepl's picture
.
e2dcb4d verified
from solverforge_legacy.solver import SolverStatus
from solverforge_legacy.solver.score import HardSoftScore
from solverforge_legacy.solver.domain import (
planning_entity,
planning_solution,
PlanningId,
PlanningScore,
PlanningVariable,
PlanningEntityCollectionProperty,
ProblemFactCollectionProperty,
ValueRangeProvider,
)
from typing import Annotated, Optional, List, Union
from dataclasses import dataclass, field
from .json_serialization import (
JsonDomainBase,
IdSerializer,
IdListSerializer,
VMListValidator,
ServerValidator,
)
from pydantic import Field
@dataclass
class Server:
"""
A physical server that can host virtual machines.
Servers have capacity limits for CPU cores, memory (GB), and storage (GB).
This is a problem fact - it doesn't change during solving.
"""
id: Annotated[str, PlanningId]
name: str
cpu_cores: int
memory_gb: int
storage_gb: int
rack: Optional[str] = None
def __str__(self):
return self.name
def __repr__(self):
return f"Server({self.id}, {self.name})"
def __hash__(self):
return hash(self.id)
def __eq__(self, other):
if not isinstance(other, Server):
return False
return self.id == other.id
@planning_entity
@dataclass
class VM:
"""
A virtual machine that needs to be placed on a server.
VMs have resource requirements (CPU, memory, storage) and optional
affinity/anti-affinity constraints for placement.
The server field is the planning variable that the solver optimizes.
"""
id: Annotated[str, PlanningId]
name: str
cpu_cores: int
memory_gb: int
storage_gb: int
priority: int = 1
affinity_group: Optional[str] = None
anti_affinity_group: Optional[str] = None
server: Annotated[Optional[Server], PlanningVariable] = None
def __str__(self):
return self.name
def __repr__(self):
return f"VM({self.id}, {self.name})"
@planning_solution
@dataclass
class VMPlacementPlan:
"""
The planning solution containing all servers and VMs.
The solver will assign VMs to servers while respecting capacity constraints,
affinity/anti-affinity rules, and optimizing for consolidation and balance.
"""
name: str
servers: Annotated[list[Server], ProblemFactCollectionProperty, ValueRangeProvider]
vms: Annotated[list[VM], PlanningEntityCollectionProperty]
score: Annotated[Optional[HardSoftScore], PlanningScore] = None
solver_status: SolverStatus = SolverStatus.NOT_SOLVING
def get_vms_on_server(self, server: Server) -> list:
"""Get all VMs assigned to a specific server."""
return [vm for vm in self.vms if vm.server == server]
def get_server_used_cpu(self, server: Server) -> int:
"""Get total CPU cores used on a server."""
return sum(vm.cpu_cores for vm in self.vms if vm.server == server)
def get_server_used_memory(self, server: Server) -> int:
"""Get total memory (GB) used on a server."""
return sum(vm.memory_gb for vm in self.vms if vm.server == server)
def get_server_used_storage(self, server: Server) -> int:
"""Get total storage (GB) used on a server."""
return sum(vm.storage_gb for vm in self.vms if vm.server == server)
@property
def total_servers(self) -> int:
return len(self.servers)
@property
def active_servers(self) -> int:
active_server_ids = set(vm.server.id for vm in self.vms if vm.server is not None)
return len(active_server_ids)
@property
def unassigned_vms(self) -> int:
return sum(1 for vm in self.vms if vm.server is None)
@property
def total_cpu_utilization(self) -> float:
total_capacity = sum(s.cpu_cores for s in self.servers)
total_used = sum(vm.cpu_cores for vm in self.vms if vm.server is not None)
if total_capacity == 0:
return 0.0
return total_used / total_capacity
@property
def total_memory_utilization(self) -> float:
total_capacity = sum(s.memory_gb for s in self.servers)
total_used = sum(vm.memory_gb for vm in self.vms if vm.server is not None)
if total_capacity == 0:
return 0.0
return total_used / total_capacity
@property
def total_storage_utilization(self) -> float:
total_capacity = sum(s.storage_gb for s in self.servers)
total_used = sum(vm.storage_gb for vm in self.vms if vm.server is not None)
if total_capacity == 0:
return 0.0
return total_used / total_capacity
def __str__(self):
return f"VMPlacementPlan(name={self.name}, servers={len(self.servers)}, vms={len(self.vms)})"
# Pydantic REST models for API (used for deserialization and context)
class VMModel(JsonDomainBase):
id: str
name: str
cpu_cores: int = Field(..., alias="cpuCores")
memory_gb: int = Field(..., alias="memoryGb")
storage_gb: int = Field(..., alias="storageGb")
priority: int = 1
affinity_group: Optional[str] = Field(None, alias="affinityGroup")
anti_affinity_group: Optional[str] = Field(None, alias="antiAffinityGroup")
server: Annotated[
Union[str, "ServerModel", None],
IdSerializer,
ServerValidator,
] = None
class ServerModel(JsonDomainBase):
id: str
name: str
cpu_cores: int = Field(..., alias="cpuCores")
memory_gb: int = Field(..., alias="memoryGb")
storage_gb: int = Field(..., alias="storageGb")
rack: Optional[str] = None
vms: Annotated[
List[Union[str, VMModel]],
IdListSerializer,
VMListValidator,
] = Field(default_factory=list)
used_cpu: int = Field(0, alias="usedCpu")
used_memory: int = Field(0, alias="usedMemory")
used_storage: int = Field(0, alias="usedStorage")
cpu_utilization: float = Field(0.0, alias="cpuUtilization")
memory_utilization: float = Field(0.0, alias="memoryUtilization")
storage_utilization: float = Field(0.0, alias="storageUtilization")
class VMPlacementPlanModel(JsonDomainBase):
name: str
servers: List[ServerModel]
vms: List[VMModel]
score: Optional[str] = None
solver_status: Optional[str] = Field(None, alias="solverStatus")
total_servers: int = Field(0, alias="totalServers")
active_servers: int = Field(0, alias="activeServers")
unassigned_vms: int = Field(0, alias="unassignedVms")
total_cpu_utilization: float = Field(0.0, alias="totalCpuUtilization")
total_memory_utilization: float = Field(0.0, alias="totalMemoryUtilization")
total_storage_utilization: float = Field(0.0, alias="totalStorageUtilization")