File size: 6,731 Bytes
e2dcb4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
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")