from solverforge_legacy.solver.score import ( ConstraintFactory, ConstraintCollectors, Joiners, HardSoftScore, constraint_provider, ) from .domain import Server, VM # Constraint names CPU_CAPACITY = "cpuCapacity" MEMORY_CAPACITY = "memoryCapacity" STORAGE_CAPACITY = "storageCapacity" ANTI_AFFINITY = "antiAffinity" AFFINITY = "affinity" MINIMIZE_SERVERS_USED = "minimizeServersUsed" BALANCE_UTILIZATION = "balanceUtilization" PRIORITIZE_PLACEMENT = "prioritizePlacement" @constraint_provider def define_constraints(factory: ConstraintFactory): return [ # Hard constraints cpu_capacity(factory), memory_capacity(factory), storage_capacity(factory), anti_affinity(factory), # Soft constraints affinity(factory), minimize_servers_used(factory), balance_utilization(factory), prioritize_placement(factory), ] ############################################## # Hard constraints ############################################## def cpu_capacity(factory: ConstraintFactory): """ Hard constraint: Server CPU capacity cannot be exceeded. Groups VMs by server and penalizes if total CPU exceeds capacity. """ return ( factory.for_each(VM) .filter(lambda vm: vm.server is not None) .group_by(lambda vm: vm.server, ConstraintCollectors.sum(lambda vm: vm.cpu_cores)) .filter(lambda server, total_cpu: total_cpu > server.cpu_cores) .penalize( HardSoftScore.ONE_HARD, lambda server, total_cpu: total_cpu - server.cpu_cores, ) .as_constraint(CPU_CAPACITY) ) def memory_capacity(factory: ConstraintFactory): """ Hard constraint: Server memory capacity cannot be exceeded. Groups VMs by server and penalizes if total memory exceeds capacity. """ return ( factory.for_each(VM) .filter(lambda vm: vm.server is not None) .group_by(lambda vm: vm.server, ConstraintCollectors.sum(lambda vm: vm.memory_gb)) .filter(lambda server, total_memory: total_memory > server.memory_gb) .penalize( HardSoftScore.ONE_HARD, lambda server, total_memory: total_memory - server.memory_gb, ) .as_constraint(MEMORY_CAPACITY) ) def storage_capacity(factory: ConstraintFactory): """ Hard constraint: Server storage capacity cannot be exceeded. Groups VMs by server and penalizes if total storage exceeds capacity. """ return ( factory.for_each(VM) .filter(lambda vm: vm.server is not None) .group_by(lambda vm: vm.server, ConstraintCollectors.sum(lambda vm: vm.storage_gb)) .filter(lambda server, total_storage: total_storage > server.storage_gb) .penalize( HardSoftScore.ONE_HARD, lambda server, total_storage: total_storage - server.storage_gb, ) .as_constraint(STORAGE_CAPACITY) ) def anti_affinity(factory: ConstraintFactory): """ Hard constraint: VMs in the same anti-affinity group must be on different servers. This is commonly used for database replicas, redundant services, etc. Penalizes each pair of VMs that violate the constraint. """ return ( factory.for_each_unique_pair( VM, Joiners.equal(lambda vm: vm.anti_affinity_group), Joiners.equal(lambda vm: vm.server), ) .filter(lambda vm1, vm2: vm1.anti_affinity_group is not None) .filter(lambda vm1, vm2: vm1.server is not None) .penalize(HardSoftScore.ONE_HARD) .as_constraint(ANTI_AFFINITY) ) ############################################## # Soft constraints ############################################## def affinity(factory: ConstraintFactory): """ Soft constraint: VMs in the same affinity group should be on the same server. This is commonly used for tightly coupled services that benefit from low-latency communication. Penalizes each pair of VMs on different servers. """ return ( factory.for_each_unique_pair( VM, Joiners.equal(lambda vm: vm.affinity_group), ) .filter(lambda vm1, vm2: vm1.affinity_group is not None) .filter(lambda vm1, vm2: vm1.server is not None and vm2.server is not None) .filter(lambda vm1, vm2: vm1.server != vm2.server) .penalize(HardSoftScore.ONE_SOFT, lambda vm1, vm2: 100) .as_constraint(AFFINITY) ) def minimize_servers_used(factory: ConstraintFactory): """ Soft constraint: Minimize the number of servers in use. Consolidating VMs onto fewer servers reduces power consumption, cooling costs, and management overhead. Each active server incurs a cost. Weight is lower than prioritize_placement to ensure VMs get assigned before optimizing for server consolidation. """ return ( factory.for_each(VM) .filter(lambda vm: vm.server is not None) .group_by(lambda vm: vm.server, ConstraintCollectors.count()) .penalize(HardSoftScore.ONE_SOFT, lambda server, count: 100) .as_constraint(MINIMIZE_SERVERS_USED) ) def balance_utilization(factory: ConstraintFactory): """ Soft constraint: Balance utilization across active servers. Avoids hotspots by penalizing servers with high utilization. Uses a squared penalty to favor balanced distribution over consolidation when both are possible. """ return ( factory.for_each(VM) .filter(lambda vm: vm.server is not None) .group_by(lambda vm: vm.server, ConstraintCollectors.sum(lambda vm: vm.cpu_cores)) .penalize( HardSoftScore.ONE_SOFT, lambda server, total_cpu: int((total_cpu / server.cpu_cores) ** 2 * 10) if server.cpu_cores > 0 else 0, ) .as_constraint(BALANCE_UTILIZATION) ) def prioritize_placement(factory: ConstraintFactory): """ Soft constraint: Higher-priority VMs should be placed. Penalizes unassigned VMs weighted by their priority. Higher priority VMs incur a larger penalty when unassigned, encouraging the solver to place them first. Base penalty of 10000 ensures VMs are always placed before optimizing other soft constraints. Priority adds 0-5000 additional penalty. """ return ( factory.for_each(VM) .filter(lambda vm: vm.server is None) .penalize(HardSoftScore.ONE_SOFT, lambda vm: 10000 + vm.priority * 1000) .as_constraint(PRIORITIZE_PLACEMENT) )