blackopsrepl commited on
Commit
e2dcb4d
·
verified ·
1 Parent(s): e271174
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.12 base image
2
+ FROM python:3.12
3
+
4
+ # Install JDK 21 (required for solverforge-legacy)
5
+ RUN apt-get update && \
6
+ apt-get install -y wget gnupg2 && \
7
+ wget -O- https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor > /usr/share/keyrings/adoptium-archive-keyring.gpg && \
8
+ echo "deb [signed-by=/usr/share/keyrings/adoptium-archive-keyring.gpg] https://packages.adoptium.net/artifactory/deb bookworm main" > /etc/apt/sources.list.d/adoptium.list && \
9
+ apt-get update && \
10
+ apt-get install -y temurin-21-jdk && \
11
+ apt-get clean && \
12
+ rm -rf /var/lib/apt/lists/*
13
+
14
+ # Copy application files
15
+ COPY . .
16
+
17
+ # Install the application
18
+ RUN pip install --no-cache-dir -e .
19
+
20
+ # Expose port 8080
21
+ EXPOSE 8080
22
+
23
+ # Run the application
24
+ CMD ["run-app"]
README.md CHANGED
@@ -1,12 +1,139 @@
1
  ---
2
- title: Vm Placement
3
- emoji: 🌍
4
- colorFrom: purple
5
- colorTo: purple
6
  sdk: docker
 
7
  pinned: false
8
  license: apache-2.0
9
- short_description: Virtual Machine Placement Optimization with SolverForge
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Virtual Machine Placement (Python)
3
+ emoji: 👀
4
+ colorFrom: gray
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 8080
8
  pinned: false
9
  license: apache-2.0
10
+ short_description: SolverForge Quickstart for the Vehicle Routing problem
11
  ---
12
 
13
+
14
+ # VM Placement Quickstart
15
+
16
+ A SolverForge quickstart demonstrating **constraint-based virtual machine placement optimization**.
17
+
18
+ ## The Problem
19
+
20
+ You manage a datacenter with physical servers organized in racks, and must place virtual machines (VMs) onto those servers. Each server has limited CPU cores, memory, and storage capacity.
21
+
22
+ **The challenge**: Place all VMs while:
23
+ - Never exceeding any server's CPU, memory, or storage capacity
24
+ - Keeping database replicas on separate servers (anti-affinity)
25
+ - Placing related services together when possible (affinity)
26
+ - Minimizing the number of active servers (consolidation)
27
+ - Balancing load across active servers
28
+
29
+ ## Why Constraint Solving?
30
+
31
+ With constraints, you describe *what* a valid placement looks like, not *how* to compute one. Adding a new business rule (e.g., "GPU workloads need GPU servers") is a single constraint function—not a rewrite of your algorithm.
32
+
33
+ ## Quick Start
34
+
35
+ ```bash
36
+ # 1. Create and activate virtual environment
37
+ python -m venv .venv
38
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
39
+
40
+ # 2. Install dependencies
41
+ pip install -e .
42
+
43
+ # 3. Run the application
44
+ run-app
45
+
46
+ # 4. Open http://localhost:8080 in your browser
47
+ ```
48
+
49
+ **Requirement:** JDK 17+ must be installed (solverforge-legacy uses JPype to bridge Python and Java).
50
+
51
+ ## Running Tests
52
+
53
+ ```bash
54
+ # Run all tests
55
+ pytest
56
+
57
+ # Run with verbose output
58
+ pytest -v
59
+
60
+ # Run specific test file
61
+ pytest tests/test_constraints.py
62
+ ```
63
+
64
+ ## Project Structure
65
+
66
+ ```
67
+ vm-placement-fast/
68
+ ├── src/vm_placement/
69
+ │ ├── domain.py # Server, VM, and VMPlacementPlan
70
+ │ ├── constraints.py # Hard and soft placement constraints
71
+ │ ├── solver.py # SolverForge configuration
72
+ │ ├── demo_data.py # Sample infrastructure and VMs
73
+ │ ├── rest_api.py # FastAPI endpoints
74
+ │ └── converters.py # Domain ↔ REST model conversion
75
+ ├── tests/
76
+ │ └── test_constraints.py # Unit tests for each constraint
77
+ ├── static/
78
+ │ ├── index.html # Web UI with rack visualization
79
+ │ ├── app.js # Frontend logic
80
+ │ └── config.js # Advanced settings sliders
81
+ └── pyproject.toml # Dependencies
82
+ ```
83
+
84
+ ## Constraints
85
+
86
+ ### Hard Constraints (must be satisfied)
87
+
88
+ 1. **CPU Capacity**: Server CPU cannot be exceeded by assigned VMs
89
+ 2. **Memory Capacity**: Server memory cannot be exceeded by assigned VMs
90
+ 3. **Storage Capacity**: Server storage cannot be exceeded by assigned VMs
91
+ 4. **Anti-Affinity**: VMs in the same anti-affinity group (e.g., database replicas) must be on different servers
92
+
93
+ ### Soft Constraints (optimize for)
94
+
95
+ 5. **Affinity**: VMs in the same affinity group (e.g., web tier) should be on the same server
96
+ 6. **Minimize Servers Used**: Consolidate VMs onto fewer servers to reduce costs
97
+ 7. **Balance Utilization**: Distribute load evenly across active servers
98
+ 8. **Prioritize Placement**: Higher-priority VMs should be placed first
99
+
100
+ ## API Endpoints
101
+
102
+ | Method | Endpoint | Description |
103
+ |--------|----------|-------------|
104
+ | GET | `/demo-data` | List available datasets |
105
+ | GET | `/demo-data/{id}` | Load demo data |
106
+ | POST | `/demo-data/generate` | Generate custom infrastructure |
107
+ | POST | `/placements` | Submit for optimization |
108
+ | GET | `/placements/{id}` | Get current solution |
109
+ | GET | `/placements/{id}/status` | Get solving status |
110
+ | DELETE | `/placements/{id}` | Stop solving |
111
+ | PUT | `/placements/analyze` | Analyze placement score |
112
+
113
+ API documentation available at http://localhost:8080/q/swagger-ui
114
+
115
+ ## Advanced Settings
116
+
117
+ The web UI includes configurable sliders for:
118
+
119
+ - **Racks**: Number of server racks (1-8)
120
+ - **Servers per Rack**: Servers in each rack (2-10)
121
+ - **VMs**: Number of VMs to place (5-200)
122
+ - **Solver Time**: How long to optimize (5s-2min)
123
+
124
+ Click "Generate New Data" to create custom scenarios.
125
+
126
+ ## VM Placement Concepts
127
+
128
+ | Term | Definition |
129
+ |------|------------|
130
+ | **Server** | Physical machine with CPU, memory, and storage capacity |
131
+ | **VM** | Virtual machine requiring resources from a server |
132
+ | **Rack** | Physical grouping of servers in a datacenter |
133
+ | **Affinity** | VMs that should run on the same server |
134
+ | **Anti-Affinity** | VMs that must run on different servers |
135
+ | **Consolidation** | Using fewer servers to reduce power/cooling costs |
136
+
137
+ ## Learn More
138
+
139
+ - [SolverForge Documentation](https://solverforge.org/docs)
pyproject.toml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+
6
+ [project]
7
+ name = "vm_placement"
8
+ version = "1.0.0"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ 'solverforge-legacy == 1.24.1',
12
+ 'fastapi == 0.111.0',
13
+ 'pydantic == 2.7.3',
14
+ 'uvicorn == 0.30.1',
15
+ 'pytest == 8.2.2',
16
+ ]
17
+
18
+
19
+ [project.scripts]
20
+ run-app = "vm_placement:main"
src/vm_placement/__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uvicorn
2
+
3
+ from .rest_api import app as app
4
+
5
+
6
+ def main():
7
+ config = uvicorn.Config("vm_placement:app",
8
+ port=8080,
9
+ log_level="info",
10
+ use_colors=True)
11
+ server = uvicorn.Server(config)
12
+ server.run()
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()
src/vm_placement/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (697 Bytes). View file
 
src/vm_placement/__pycache__/constraints.cpython-312.pyc ADDED
Binary file (12.4 kB). View file
 
src/vm_placement/__pycache__/converters.cpython-312.pyc ADDED
Binary file (7.86 kB). View file
 
src/vm_placement/__pycache__/demo_data.cpython-312.pyc ADDED
Binary file (14.3 kB). View file
 
src/vm_placement/__pycache__/domain.cpython-312.pyc ADDED
Binary file (13.4 kB). View file
 
src/vm_placement/__pycache__/json_serialization.cpython-312.pyc ADDED
Binary file (3.65 kB). View file
 
src/vm_placement/__pycache__/rest_api.cpython-312.pyc ADDED
Binary file (8.66 kB). View file
 
src/vm_placement/__pycache__/solver.cpython-312.pyc ADDED
Binary file (1 kB). View file
 
src/vm_placement/constraints.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from solverforge_legacy.solver.score import (
2
+ ConstraintFactory,
3
+ ConstraintCollectors,
4
+ Joiners,
5
+ HardSoftScore,
6
+ constraint_provider,
7
+ )
8
+
9
+ from .domain import Server, VM
10
+
11
+ # Constraint names
12
+ CPU_CAPACITY = "cpuCapacity"
13
+ MEMORY_CAPACITY = "memoryCapacity"
14
+ STORAGE_CAPACITY = "storageCapacity"
15
+ ANTI_AFFINITY = "antiAffinity"
16
+ AFFINITY = "affinity"
17
+ MINIMIZE_SERVERS_USED = "minimizeServersUsed"
18
+ BALANCE_UTILIZATION = "balanceUtilization"
19
+ PRIORITIZE_PLACEMENT = "prioritizePlacement"
20
+
21
+
22
+ @constraint_provider
23
+ def define_constraints(factory: ConstraintFactory):
24
+ return [
25
+ # Hard constraints
26
+ cpu_capacity(factory),
27
+ memory_capacity(factory),
28
+ storage_capacity(factory),
29
+ anti_affinity(factory),
30
+ # Soft constraints
31
+ affinity(factory),
32
+ minimize_servers_used(factory),
33
+ balance_utilization(factory),
34
+ prioritize_placement(factory),
35
+ ]
36
+
37
+
38
+ ##############################################
39
+ # Hard constraints
40
+ ##############################################
41
+
42
+
43
+ def cpu_capacity(factory: ConstraintFactory):
44
+ """
45
+ Hard constraint: Server CPU capacity cannot be exceeded.
46
+
47
+ Groups VMs by server and penalizes if total CPU exceeds capacity.
48
+ """
49
+ return (
50
+ factory.for_each(VM)
51
+ .filter(lambda vm: vm.server is not None)
52
+ .group_by(lambda vm: vm.server, ConstraintCollectors.sum(lambda vm: vm.cpu_cores))
53
+ .filter(lambda server, total_cpu: total_cpu > server.cpu_cores)
54
+ .penalize(
55
+ HardSoftScore.ONE_HARD,
56
+ lambda server, total_cpu: total_cpu - server.cpu_cores,
57
+ )
58
+ .as_constraint(CPU_CAPACITY)
59
+ )
60
+
61
+
62
+ def memory_capacity(factory: ConstraintFactory):
63
+ """
64
+ Hard constraint: Server memory capacity cannot be exceeded.
65
+
66
+ Groups VMs by server and penalizes if total memory exceeds capacity.
67
+ """
68
+ return (
69
+ factory.for_each(VM)
70
+ .filter(lambda vm: vm.server is not None)
71
+ .group_by(lambda vm: vm.server, ConstraintCollectors.sum(lambda vm: vm.memory_gb))
72
+ .filter(lambda server, total_memory: total_memory > server.memory_gb)
73
+ .penalize(
74
+ HardSoftScore.ONE_HARD,
75
+ lambda server, total_memory: total_memory - server.memory_gb,
76
+ )
77
+ .as_constraint(MEMORY_CAPACITY)
78
+ )
79
+
80
+
81
+ def storage_capacity(factory: ConstraintFactory):
82
+ """
83
+ Hard constraint: Server storage capacity cannot be exceeded.
84
+
85
+ Groups VMs by server and penalizes if total storage exceeds capacity.
86
+ """
87
+ return (
88
+ factory.for_each(VM)
89
+ .filter(lambda vm: vm.server is not None)
90
+ .group_by(lambda vm: vm.server, ConstraintCollectors.sum(lambda vm: vm.storage_gb))
91
+ .filter(lambda server, total_storage: total_storage > server.storage_gb)
92
+ .penalize(
93
+ HardSoftScore.ONE_HARD,
94
+ lambda server, total_storage: total_storage - server.storage_gb,
95
+ )
96
+ .as_constraint(STORAGE_CAPACITY)
97
+ )
98
+
99
+
100
+ def anti_affinity(factory: ConstraintFactory):
101
+ """
102
+ Hard constraint: VMs in the same anti-affinity group must be on different servers.
103
+
104
+ This is commonly used for database replicas, redundant services, etc.
105
+ Penalizes each pair of VMs that violate the constraint.
106
+ """
107
+ return (
108
+ factory.for_each_unique_pair(
109
+ VM,
110
+ Joiners.equal(lambda vm: vm.anti_affinity_group),
111
+ Joiners.equal(lambda vm: vm.server),
112
+ )
113
+ .filter(lambda vm1, vm2: vm1.anti_affinity_group is not None)
114
+ .filter(lambda vm1, vm2: vm1.server is not None)
115
+ .penalize(HardSoftScore.ONE_HARD)
116
+ .as_constraint(ANTI_AFFINITY)
117
+ )
118
+
119
+
120
+ ##############################################
121
+ # Soft constraints
122
+ ##############################################
123
+
124
+
125
+ def affinity(factory: ConstraintFactory):
126
+ """
127
+ Soft constraint: VMs in the same affinity group should be on the same server.
128
+
129
+ This is commonly used for tightly coupled services that benefit from
130
+ low-latency communication. Penalizes each pair of VMs on different servers.
131
+ """
132
+ return (
133
+ factory.for_each_unique_pair(
134
+ VM,
135
+ Joiners.equal(lambda vm: vm.affinity_group),
136
+ )
137
+ .filter(lambda vm1, vm2: vm1.affinity_group is not None)
138
+ .filter(lambda vm1, vm2: vm1.server is not None and vm2.server is not None)
139
+ .filter(lambda vm1, vm2: vm1.server != vm2.server)
140
+ .penalize(HardSoftScore.ONE_SOFT, lambda vm1, vm2: 100)
141
+ .as_constraint(AFFINITY)
142
+ )
143
+
144
+
145
+ def minimize_servers_used(factory: ConstraintFactory):
146
+ """
147
+ Soft constraint: Minimize the number of servers in use.
148
+
149
+ Consolidating VMs onto fewer servers reduces power consumption,
150
+ cooling costs, and management overhead. Each active server incurs a cost.
151
+
152
+ Weight is lower than prioritize_placement to ensure VMs get assigned
153
+ before optimizing for server consolidation.
154
+ """
155
+ return (
156
+ factory.for_each(VM)
157
+ .filter(lambda vm: vm.server is not None)
158
+ .group_by(lambda vm: vm.server, ConstraintCollectors.count())
159
+ .penalize(HardSoftScore.ONE_SOFT, lambda server, count: 100)
160
+ .as_constraint(MINIMIZE_SERVERS_USED)
161
+ )
162
+
163
+
164
+ def balance_utilization(factory: ConstraintFactory):
165
+ """
166
+ Soft constraint: Balance utilization across active servers.
167
+
168
+ Avoids hotspots by penalizing servers with high utilization.
169
+ Uses a squared penalty to favor balanced distribution over consolidation
170
+ when both are possible.
171
+ """
172
+ return (
173
+ factory.for_each(VM)
174
+ .filter(lambda vm: vm.server is not None)
175
+ .group_by(lambda vm: vm.server, ConstraintCollectors.sum(lambda vm: vm.cpu_cores))
176
+ .penalize(
177
+ HardSoftScore.ONE_SOFT,
178
+ lambda server, total_cpu: int((total_cpu / server.cpu_cores) ** 2 * 10) if server.cpu_cores > 0 else 0,
179
+ )
180
+ .as_constraint(BALANCE_UTILIZATION)
181
+ )
182
+
183
+
184
+ def prioritize_placement(factory: ConstraintFactory):
185
+ """
186
+ Soft constraint: Higher-priority VMs should be placed.
187
+
188
+ Penalizes unassigned VMs weighted by their priority. Higher priority VMs
189
+ incur a larger penalty when unassigned, encouraging the solver to place
190
+ them first.
191
+
192
+ Base penalty of 10000 ensures VMs are always placed before optimizing
193
+ other soft constraints. Priority adds 0-5000 additional penalty.
194
+ """
195
+ return (
196
+ factory.for_each(VM)
197
+ .filter(lambda vm: vm.server is None)
198
+ .penalize(HardSoftScore.ONE_SOFT, lambda vm: 10000 + vm.priority * 1000)
199
+ .as_constraint(PRIORITIZE_PLACEMENT)
200
+ )
src/vm_placement/converters.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from . import domain
2
+
3
+
4
+ # Conversion functions from domain to API models
5
+ def vm_to_model(vm: domain.VM) -> domain.VMModel:
6
+ # Handle both Server objects and string IDs
7
+ server_id = None
8
+ if vm.server:
9
+ server_id = vm.server if isinstance(vm.server, str) else vm.server.id
10
+ return domain.VMModel(
11
+ id=vm.id,
12
+ name=vm.name,
13
+ cpu_cores=vm.cpu_cores,
14
+ memory_gb=vm.memory_gb,
15
+ storage_gb=vm.storage_gb,
16
+ priority=vm.priority,
17
+ affinity_group=vm.affinity_group,
18
+ anti_affinity_group=vm.anti_affinity_group,
19
+ server=server_id,
20
+ )
21
+
22
+
23
+ def server_to_model(server: domain.Server, plan: domain.VMPlacementPlan) -> domain.ServerModel:
24
+ """Convert a Server to ServerModel, computing VM assignments from the plan."""
25
+ # Get VMs assigned to this server
26
+ vms_on_server = [vm for vm in plan.vms if vm.server == server]
27
+ vm_ids = [vm.id for vm in vms_on_server]
28
+
29
+ # Compute utilization
30
+ used_cpu = sum(vm.cpu_cores for vm in vms_on_server)
31
+ used_memory = sum(vm.memory_gb for vm in vms_on_server)
32
+ used_storage = sum(vm.storage_gb for vm in vms_on_server)
33
+
34
+ cpu_utilization = used_cpu / server.cpu_cores if server.cpu_cores > 0 else 0.0
35
+ memory_utilization = used_memory / server.memory_gb if server.memory_gb > 0 else 0.0
36
+ storage_utilization = used_storage / server.storage_gb if server.storage_gb > 0 else 0.0
37
+
38
+ return domain.ServerModel(
39
+ id=server.id,
40
+ name=server.name,
41
+ cpu_cores=server.cpu_cores,
42
+ memory_gb=server.memory_gb,
43
+ storage_gb=server.storage_gb,
44
+ rack=server.rack,
45
+ vms=vm_ids,
46
+ used_cpu=used_cpu,
47
+ used_memory=used_memory,
48
+ used_storage=used_storage,
49
+ cpu_utilization=cpu_utilization,
50
+ memory_utilization=memory_utilization,
51
+ storage_utilization=storage_utilization,
52
+ )
53
+
54
+
55
+ def plan_to_model(plan: domain.VMPlacementPlan) -> domain.VMPlacementPlanModel:
56
+ return domain.VMPlacementPlanModel(
57
+ name=plan.name,
58
+ servers=[server_to_model(s, plan) for s in plan.servers],
59
+ vms=[vm_to_model(vm) for vm in plan.vms],
60
+ score=str(plan.score) if plan.score else None,
61
+ solver_status=plan.solver_status.name if plan.solver_status else None,
62
+ total_servers=plan.total_servers,
63
+ active_servers=plan.active_servers,
64
+ unassigned_vms=plan.unassigned_vms,
65
+ total_cpu_utilization=plan.total_cpu_utilization,
66
+ total_memory_utilization=plan.total_memory_utilization,
67
+ total_storage_utilization=plan.total_storage_utilization,
68
+ )
69
+
70
+
71
+ # Conversion functions from API models to domain
72
+ def model_to_vm(model: domain.VMModel, server_lookup: dict) -> domain.VM:
73
+ server = None
74
+ if model.server:
75
+ if isinstance(model.server, str):
76
+ server = server_lookup.get(model.server)
77
+ else:
78
+ server = server_lookup.get(model.server.id)
79
+
80
+ return domain.VM(
81
+ id=model.id,
82
+ name=model.name,
83
+ cpu_cores=model.cpu_cores,
84
+ memory_gb=model.memory_gb,
85
+ storage_gb=model.storage_gb,
86
+ priority=model.priority,
87
+ affinity_group=model.affinity_group,
88
+ anti_affinity_group=model.anti_affinity_group,
89
+ server=server,
90
+ )
91
+
92
+
93
+ def model_to_server(model: domain.ServerModel) -> domain.Server:
94
+ """Convert ServerModel to Server (Server no longer has vms list)."""
95
+ return domain.Server(
96
+ id=model.id,
97
+ name=model.name,
98
+ cpu_cores=model.cpu_cores,
99
+ memory_gb=model.memory_gb,
100
+ storage_gb=model.storage_gb,
101
+ rack=model.rack,
102
+ )
103
+
104
+
105
+ def model_to_plan(model: domain.VMPlacementPlanModel) -> domain.VMPlacementPlan:
106
+ # Convert servers first
107
+ servers = []
108
+ for server_model in model.servers:
109
+ server = domain.Server(
110
+ id=server_model.id,
111
+ name=server_model.name,
112
+ cpu_cores=server_model.cpu_cores,
113
+ memory_gb=server_model.memory_gb,
114
+ storage_gb=server_model.storage_gb,
115
+ rack=server_model.rack,
116
+ )
117
+ servers.append(server)
118
+
119
+ # Create server lookup
120
+ server_lookup = {s.id: s for s in servers}
121
+
122
+ # Convert VMs with server references
123
+ vms = []
124
+ for vm_model in model.vms:
125
+ # Get server reference from VM's server field
126
+ server = None
127
+ if vm_model.server:
128
+ server_id = vm_model.server if isinstance(vm_model.server, str) else vm_model.server.id
129
+ server = server_lookup.get(server_id)
130
+
131
+ vm = domain.VM(
132
+ id=vm_model.id,
133
+ name=vm_model.name,
134
+ cpu_cores=vm_model.cpu_cores,
135
+ memory_gb=vm_model.memory_gb,
136
+ storage_gb=vm_model.storage_gb,
137
+ priority=vm_model.priority,
138
+ affinity_group=vm_model.affinity_group,
139
+ anti_affinity_group=vm_model.anti_affinity_group,
140
+ server=server,
141
+ )
142
+ vms.append(vm)
143
+
144
+ # Handle score
145
+ score = None
146
+ if model.score:
147
+ from solverforge_legacy.solver.score import HardSoftScore
148
+ score = HardSoftScore.parse(model.score)
149
+
150
+ # Handle solver status
151
+ solver_status = domain.SolverStatus.NOT_SOLVING
152
+ if model.solver_status:
153
+ solver_status = domain.SolverStatus[model.solver_status]
154
+
155
+ return domain.VMPlacementPlan(
156
+ name=model.name,
157
+ servers=servers,
158
+ vms=vms,
159
+ score=score,
160
+ solver_status=solver_status,
161
+ )
src/vm_placement/demo_data.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+ import random
3
+ from .domain import Server, VM, VMPlacementPlan
4
+
5
+
6
+ class DemoData(Enum):
7
+ SMALL = "SMALL"
8
+ MEDIUM = "MEDIUM"
9
+ LARGE = "LARGE"
10
+
11
+
12
+ def generate_custom_data(
13
+ rack_count: int = 3,
14
+ servers_per_rack: int = 4,
15
+ vm_count: int = 20
16
+ ) -> VMPlacementPlan:
17
+ """
18
+ Generate custom demo data with configurable infrastructure and workload.
19
+
20
+ Args:
21
+ rack_count: Number of racks (1-8)
22
+ servers_per_rack: Number of servers per rack (2-10)
23
+ vm_count: Number of VMs to place (5-200)
24
+ """
25
+ rack_count = max(1, min(8, rack_count))
26
+ servers_per_rack = max(2, min(10, servers_per_rack))
27
+ vm_count = max(5, min(200, vm_count))
28
+
29
+ servers = []
30
+ server_id = 1
31
+
32
+ # Generate rack names
33
+ rack_names = [f"rack-{chr(ord('a') + i)}" for i in range(rack_count)]
34
+
35
+ # Server templates: (cpu_cores, memory_gb, storage_gb, name_prefix)
36
+ server_templates = [
37
+ (48, 192, 2000, "large"),
38
+ (32, 128, 1000, "large"),
39
+ (24, 96, 1000, "medium"),
40
+ (16, 64, 512, "medium"),
41
+ (12, 48, 500, "small"),
42
+ (8, 32, 256, "small"),
43
+ ]
44
+
45
+ for rack_idx, rack_name in enumerate(rack_names):
46
+ for server_idx in range(servers_per_rack):
47
+ # Alternate between server sizes for variety
48
+ template_idx = (rack_idx + server_idx) % len(server_templates)
49
+ cpu, mem, storage, size = server_templates[template_idx]
50
+
51
+ servers.append(Server(
52
+ id=f"s{server_id}",
53
+ name=f"srv-{size}-{server_id:02d}",
54
+ cpu_cores=cpu,
55
+ memory_gb=mem,
56
+ storage_gb=storage,
57
+ rack=rack_name
58
+ ))
59
+ server_id += 1
60
+
61
+ vms = []
62
+ vm_id = 1
63
+
64
+ # VM templates: (cpu_cores, memory_gb, storage_gb, name_prefix, priority, group_type)
65
+ # group_type: None, "affinity", "anti-affinity"
66
+ vm_templates = [
67
+ # Critical database VMs - anti-affinity for HA
68
+ (8, 32, 200, "db", 5, "anti-affinity"),
69
+ # API servers
70
+ (4, 8, 50, "api", 4, None),
71
+ # Web tier - affinity for locality
72
+ (2, 4, 20, "web", 3, "affinity"),
73
+ # Cache tier - affinity
74
+ (4, 16, 30, "cache", 3, "affinity"),
75
+ # Workers
76
+ (2, 4, 40, "worker", 2, None),
77
+ # Dev/test - low priority
78
+ (2, 4, 30, "dev", 1, None),
79
+ ]
80
+
81
+ # Distribute VMs across templates
82
+ template_weights = [0.08, 0.15, 0.20, 0.10, 0.32, 0.15] # Proportions
83
+
84
+ # Track affinity/anti-affinity group counts
85
+ affinity_groups = {}
86
+ anti_affinity_groups = {}
87
+
88
+ for i in range(vm_count):
89
+ # Pick template based on weighted distribution
90
+ rand_val = random.random()
91
+ cumulative = 0
92
+ template_idx = 0
93
+ for idx, weight in enumerate(template_weights):
94
+ cumulative += weight
95
+ if rand_val <= cumulative:
96
+ template_idx = idx
97
+ break
98
+
99
+ cpu, mem, storage, prefix, priority, group_type = vm_templates[template_idx]
100
+
101
+ # Determine group assignment
102
+ affinity_group = None
103
+ anti_affinity_group = None
104
+
105
+ if group_type == "affinity":
106
+ group_name = f"{prefix}-tier"
107
+ affinity_groups[group_name] = affinity_groups.get(group_name, 0) + 1
108
+ affinity_group = group_name
109
+ elif group_type == "anti-affinity":
110
+ # Create multiple anti-affinity clusters (max 3-5 VMs per cluster)
111
+ cluster_num = anti_affinity_groups.get(prefix, 0) // 4 + 1
112
+ group_name = f"{prefix}-cluster-{cluster_num}"
113
+ anti_affinity_groups[prefix] = anti_affinity_groups.get(prefix, 0) + 1
114
+ anti_affinity_group = group_name
115
+
116
+ # Count VMs with this prefix
117
+ count = sum(1 for v in vms if v.name.startswith(prefix)) + 1
118
+
119
+ vms.append(VM(
120
+ id=f"vm{vm_id}",
121
+ name=f"{prefix}-{count:02d}",
122
+ cpu_cores=cpu,
123
+ memory_gb=mem,
124
+ storage_gb=storage,
125
+ priority=priority,
126
+ affinity_group=affinity_group,
127
+ anti_affinity_group=anti_affinity_group
128
+ ))
129
+ vm_id += 1
130
+
131
+ total_servers = rack_count * servers_per_rack
132
+ return VMPlacementPlan(
133
+ name=f"CUSTOM ({total_servers} servers, {vm_count} VMs)",
134
+ servers=servers,
135
+ vms=vms
136
+ )
137
+
138
+
139
+ def generate_demo_data(demo: DemoData) -> VMPlacementPlan:
140
+ """Generate demo data for the specified demo type."""
141
+ if demo == DemoData.SMALL:
142
+ return _generate_small()
143
+ elif demo == DemoData.MEDIUM:
144
+ return _generate_medium()
145
+ elif demo == DemoData.LARGE:
146
+ return _generate_large()
147
+ else:
148
+ raise ValueError(f"Unknown demo: {demo}")
149
+
150
+
151
+ def _generate_small() -> VMPlacementPlan:
152
+ """
153
+ Small demo: 5 servers, 20 VMs
154
+ - 2 large servers (32 cores, 128GB, 1TB)
155
+ - 3 medium servers (16 cores, 64GB, 512GB)
156
+ - Mix of small, medium, and large VMs
157
+ - Includes affinity and anti-affinity groups
158
+ """
159
+ servers = [
160
+ # Large servers
161
+ Server(id="s1", name="server-large-01", cpu_cores=32, memory_gb=128, storage_gb=1000, rack="rack-a"),
162
+ Server(id="s2", name="server-large-02", cpu_cores=32, memory_gb=128, storage_gb=1000, rack="rack-b"),
163
+ # Medium servers
164
+ Server(id="s3", name="server-medium-01", cpu_cores=16, memory_gb=64, storage_gb=512, rack="rack-a"),
165
+ Server(id="s4", name="server-medium-02", cpu_cores=16, memory_gb=64, storage_gb=512, rack="rack-b"),
166
+ Server(id="s5", name="server-medium-03", cpu_cores=16, memory_gb=64, storage_gb=512, rack="rack-a"),
167
+ ]
168
+
169
+ vms = [
170
+ # Web tier (affinity group - should be together)
171
+ VM(id="vm1", name="web-01", cpu_cores=2, memory_gb=4, storage_gb=20, priority=3, affinity_group="web-tier"),
172
+ VM(id="vm2", name="web-02", cpu_cores=2, memory_gb=4, storage_gb=20, priority=3, affinity_group="web-tier"),
173
+ VM(id="vm3", name="web-03", cpu_cores=2, memory_gb=4, storage_gb=20, priority=3, affinity_group="web-tier"),
174
+
175
+ # Database replicas (anti-affinity - must be on different servers)
176
+ VM(id="vm4", name="db-primary", cpu_cores=8, memory_gb=32, storage_gb=200, priority=5, anti_affinity_group="db-cluster"),
177
+ VM(id="vm5", name="db-replica-1", cpu_cores=8, memory_gb=32, storage_gb=200, priority=5, anti_affinity_group="db-cluster"),
178
+ VM(id="vm6", name="db-replica-2", cpu_cores=8, memory_gb=32, storage_gb=200, priority=4, anti_affinity_group="db-cluster"),
179
+
180
+ # API servers
181
+ VM(id="vm7", name="api-01", cpu_cores=4, memory_gb=8, storage_gb=50, priority=4),
182
+ VM(id="vm8", name="api-02", cpu_cores=4, memory_gb=8, storage_gb=50, priority=4),
183
+
184
+ # Cache servers (affinity - benefit from being together)
185
+ VM(id="vm9", name="cache-01", cpu_cores=4, memory_gb=16, storage_gb=30, priority=3, affinity_group="cache-tier"),
186
+ VM(id="vm10", name="cache-02", cpu_cores=4, memory_gb=16, storage_gb=30, priority=3, affinity_group="cache-tier"),
187
+
188
+ # Worker nodes
189
+ VM(id="vm11", name="worker-01", cpu_cores=2, memory_gb=4, storage_gb=40, priority=2),
190
+ VM(id="vm12", name="worker-02", cpu_cores=2, memory_gb=4, storage_gb=40, priority=2),
191
+ VM(id="vm13", name="worker-03", cpu_cores=2, memory_gb=4, storage_gb=40, priority=2),
192
+ VM(id="vm14", name="worker-04", cpu_cores=2, memory_gb=4, storage_gb=40, priority=2),
193
+
194
+ # Monitoring
195
+ VM(id="vm15", name="monitoring", cpu_cores=4, memory_gb=8, storage_gb=100, priority=3),
196
+ VM(id="vm16", name="logging", cpu_cores=4, memory_gb=8, storage_gb=150, priority=3),
197
+
198
+ # Dev/test VMs (lower priority)
199
+ VM(id="vm17", name="dev-01", cpu_cores=2, memory_gb=4, storage_gb=30, priority=1),
200
+ VM(id="vm18", name="dev-02", cpu_cores=2, memory_gb=4, storage_gb=30, priority=1),
201
+ VM(id="vm19", name="test-01", cpu_cores=2, memory_gb=4, storage_gb=30, priority=1),
202
+ VM(id="vm20", name="test-02", cpu_cores=2, memory_gb=4, storage_gb=30, priority=1),
203
+ ]
204
+
205
+ return VMPlacementPlan(name="SMALL", servers=servers, vms=vms)
206
+
207
+
208
+ def _generate_medium() -> VMPlacementPlan:
209
+ """
210
+ Medium demo: 10 servers, 50 VMs
211
+ - Realistic small cluster scenario
212
+ - Multiple affinity and anti-affinity groups
213
+ """
214
+ servers = [
215
+ # Large servers (rack-a)
216
+ Server(id="s1", name="server-large-01", cpu_cores=32, memory_gb=128, storage_gb=1000, rack="rack-a"),
217
+ Server(id="s2", name="server-large-02", cpu_cores=32, memory_gb=128, storage_gb=1000, rack="rack-a"),
218
+ Server(id="s3", name="server-large-03", cpu_cores=32, memory_gb=128, storage_gb=1000, rack="rack-b"),
219
+ Server(id="s4", name="server-large-04", cpu_cores=32, memory_gb=128, storage_gb=1000, rack="rack-b"),
220
+ # Medium servers
221
+ Server(id="s5", name="server-medium-01", cpu_cores=16, memory_gb=64, storage_gb=512, rack="rack-a"),
222
+ Server(id="s6", name="server-medium-02", cpu_cores=16, memory_gb=64, storage_gb=512, rack="rack-a"),
223
+ Server(id="s7", name="server-medium-03", cpu_cores=16, memory_gb=64, storage_gb=512, rack="rack-b"),
224
+ Server(id="s8", name="server-medium-04", cpu_cores=16, memory_gb=64, storage_gb=512, rack="rack-b"),
225
+ # Small servers
226
+ Server(id="s9", name="server-small-01", cpu_cores=8, memory_gb=32, storage_gb=256, rack="rack-a"),
227
+ Server(id="s10", name="server-small-02", cpu_cores=8, memory_gb=32, storage_gb=256, rack="rack-b"),
228
+ ]
229
+
230
+ vms = []
231
+ vm_id = 1
232
+
233
+ # Web tier (6 VMs, affinity)
234
+ for i in range(6):
235
+ vms.append(VM(
236
+ id=f"vm{vm_id}", name=f"web-{i+1:02d}",
237
+ cpu_cores=2, memory_gb=4, storage_gb=20,
238
+ priority=3, affinity_group="web-tier"
239
+ ))
240
+ vm_id += 1
241
+
242
+ # Database cluster (3 VMs, anti-affinity)
243
+ for i, name in enumerate(["db-primary", "db-replica-1", "db-replica-2"]):
244
+ vms.append(VM(
245
+ id=f"vm{vm_id}", name=name,
246
+ cpu_cores=8, memory_gb=32, storage_gb=200,
247
+ priority=5, anti_affinity_group="db-cluster"
248
+ ))
249
+ vm_id += 1
250
+
251
+ # API servers (8 VMs)
252
+ for i in range(8):
253
+ vms.append(VM(
254
+ id=f"vm{vm_id}", name=f"api-{i+1:02d}",
255
+ cpu_cores=4, memory_gb=8, storage_gb=50,
256
+ priority=4
257
+ ))
258
+ vm_id += 1
259
+
260
+ # Cache tier (4 VMs, affinity)
261
+ for i in range(4):
262
+ vms.append(VM(
263
+ id=f"vm{vm_id}", name=f"cache-{i+1:02d}",
264
+ cpu_cores=4, memory_gb=16, storage_gb=30,
265
+ priority=3, affinity_group="cache-tier"
266
+ ))
267
+ vm_id += 1
268
+
269
+ # Worker nodes (12 VMs)
270
+ for i in range(12):
271
+ vms.append(VM(
272
+ id=f"vm{vm_id}", name=f"worker-{i+1:02d}",
273
+ cpu_cores=2, memory_gb=4, storage_gb=40,
274
+ priority=2
275
+ ))
276
+ vm_id += 1
277
+
278
+ # Analytics cluster (3 VMs, anti-affinity for HA)
279
+ for i in range(3):
280
+ vms.append(VM(
281
+ id=f"vm{vm_id}", name=f"analytics-{i+1:02d}",
282
+ cpu_cores=8, memory_gb=24, storage_gb=150,
283
+ priority=3, anti_affinity_group="analytics-cluster"
284
+ ))
285
+ vm_id += 1
286
+
287
+ # Monitoring & logging (4 VMs)
288
+ for name in ["prometheus", "grafana", "elasticsearch", "kibana"]:
289
+ vms.append(VM(
290
+ id=f"vm{vm_id}", name=name,
291
+ cpu_cores=4, memory_gb=8, storage_gb=100,
292
+ priority=3
293
+ ))
294
+ vm_id += 1
295
+
296
+ # CI/CD (2 VMs)
297
+ for name in ["jenkins", "gitlab-runner"]:
298
+ vms.append(VM(
299
+ id=f"vm{vm_id}", name=name,
300
+ cpu_cores=4, memory_gb=8, storage_gb=80,
301
+ priority=2
302
+ ))
303
+ vm_id += 1
304
+
305
+ # Dev/test VMs (6 VMs, lower priority)
306
+ for i in range(6):
307
+ vms.append(VM(
308
+ id=f"vm{vm_id}", name=f"dev-{i+1:02d}",
309
+ cpu_cores=2, memory_gb=4, storage_gb=30,
310
+ priority=1
311
+ ))
312
+ vm_id += 1
313
+
314
+ return VMPlacementPlan(name="MEDIUM", servers=servers, vms=vms)
315
+
316
+
317
+ def _generate_large() -> VMPlacementPlan:
318
+ """
319
+ Large demo: 20 servers, 100 VMs
320
+ - Stress test scenario
321
+ - Multiple racks for anti-affinity
322
+ """
323
+ servers = []
324
+ server_id = 1
325
+ racks = ["rack-a", "rack-b", "rack-c", "rack-d"]
326
+
327
+ # Large servers (8)
328
+ for i in range(8):
329
+ servers.append(Server(
330
+ id=f"s{server_id}", name=f"server-large-{i+1:02d}",
331
+ cpu_cores=48, memory_gb=192, storage_gb=2000,
332
+ rack=racks[i % len(racks)]
333
+ ))
334
+ server_id += 1
335
+
336
+ # Medium servers (8)
337
+ for i in range(8):
338
+ servers.append(Server(
339
+ id=f"s{server_id}", name=f"server-medium-{i+1:02d}",
340
+ cpu_cores=24, memory_gb=96, storage_gb=1000,
341
+ rack=racks[i % len(racks)]
342
+ ))
343
+ server_id += 1
344
+
345
+ # Small servers (4)
346
+ for i in range(4):
347
+ servers.append(Server(
348
+ id=f"s{server_id}", name=f"server-small-{i+1:02d}",
349
+ cpu_cores=12, memory_gb=48, storage_gb=500,
350
+ rack=racks[i % len(racks)]
351
+ ))
352
+ server_id += 1
353
+
354
+ vms = []
355
+ vm_id = 1
356
+
357
+ # Web tier (12 VMs, affinity)
358
+ for i in range(12):
359
+ vms.append(VM(
360
+ id=f"vm{vm_id}", name=f"web-{i+1:02d}",
361
+ cpu_cores=2, memory_gb=4, storage_gb=20,
362
+ priority=3, affinity_group="web-tier"
363
+ ))
364
+ vm_id += 1
365
+
366
+ # Database clusters (2 clusters, 3 VMs each, anti-affinity)
367
+ for cluster in range(2):
368
+ for i in range(3):
369
+ role = "primary" if i == 0 else f"replica-{i}"
370
+ vms.append(VM(
371
+ id=f"vm{vm_id}", name=f"db-cluster{cluster+1}-{role}",
372
+ cpu_cores=8, memory_gb=32, storage_gb=300,
373
+ priority=5, anti_affinity_group=f"db-cluster-{cluster+1}"
374
+ ))
375
+ vm_id += 1
376
+
377
+ # API servers (16 VMs)
378
+ for i in range(16):
379
+ vms.append(VM(
380
+ id=f"vm{vm_id}", name=f"api-{i+1:02d}",
381
+ cpu_cores=4, memory_gb=8, storage_gb=50,
382
+ priority=4
383
+ ))
384
+ vm_id += 1
385
+
386
+ # Cache tier (8 VMs, affinity)
387
+ for i in range(8):
388
+ vms.append(VM(
389
+ id=f"vm{vm_id}", name=f"cache-{i+1:02d}",
390
+ cpu_cores=4, memory_gb=24, storage_gb=30,
391
+ priority=3, affinity_group="cache-tier"
392
+ ))
393
+ vm_id += 1
394
+
395
+ # Worker nodes (24 VMs)
396
+ for i in range(24):
397
+ vms.append(VM(
398
+ id=f"vm{vm_id}", name=f"worker-{i+1:02d}",
399
+ cpu_cores=2, memory_gb=4, storage_gb=40,
400
+ priority=2
401
+ ))
402
+ vm_id += 1
403
+
404
+ # Message queue cluster (3 VMs, anti-affinity)
405
+ for i in range(3):
406
+ vms.append(VM(
407
+ id=f"vm{vm_id}", name=f"kafka-{i+1:02d}",
408
+ cpu_cores=4, memory_gb=16, storage_gb=200,
409
+ priority=4, anti_affinity_group="kafka-cluster"
410
+ ))
411
+ vm_id += 1
412
+
413
+ # Search cluster (5 VMs, anti-affinity)
414
+ for i in range(5):
415
+ vms.append(VM(
416
+ id=f"vm{vm_id}", name=f"elasticsearch-{i+1:02d}",
417
+ cpu_cores=6, memory_gb=24, storage_gb=250,
418
+ priority=3, anti_affinity_group="search-cluster"
419
+ ))
420
+ vm_id += 1
421
+
422
+ # Monitoring stack (6 VMs)
423
+ for name in ["prometheus", "grafana", "alertmanager", "thanos-query", "thanos-store", "thanos-compact"]:
424
+ vms.append(VM(
425
+ id=f"vm{vm_id}", name=name,
426
+ cpu_cores=4, memory_gb=8, storage_gb=100,
427
+ priority=3
428
+ ))
429
+ vm_id += 1
430
+
431
+ # CI/CD (4 VMs)
432
+ for name in ["jenkins-master", "jenkins-agent-1", "jenkins-agent-2", "artifact-repo"]:
433
+ vms.append(VM(
434
+ id=f"vm{vm_id}", name=name,
435
+ cpu_cores=4, memory_gb=8, storage_gb=120,
436
+ priority=2
437
+ ))
438
+ vm_id += 1
439
+
440
+ # Dev/test VMs (10 VMs, lower priority)
441
+ for i in range(10):
442
+ vms.append(VM(
443
+ id=f"vm{vm_id}", name=f"dev-{i+1:02d}",
444
+ cpu_cores=2, memory_gb=4, storage_gb=30,
445
+ priority=1
446
+ ))
447
+ vm_id += 1
448
+
449
+ return VMPlacementPlan(name="LARGE", servers=servers, vms=vms)
src/vm_placement/domain.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from solverforge_legacy.solver import SolverStatus
2
+ from solverforge_legacy.solver.score import HardSoftScore
3
+ from solverforge_legacy.solver.domain import (
4
+ planning_entity,
5
+ planning_solution,
6
+ PlanningId,
7
+ PlanningScore,
8
+ PlanningVariable,
9
+ PlanningEntityCollectionProperty,
10
+ ProblemFactCollectionProperty,
11
+ ValueRangeProvider,
12
+ )
13
+
14
+ from typing import Annotated, Optional, List, Union
15
+ from dataclasses import dataclass, field
16
+ from .json_serialization import (
17
+ JsonDomainBase,
18
+ IdSerializer,
19
+ IdListSerializer,
20
+ VMListValidator,
21
+ ServerValidator,
22
+ )
23
+ from pydantic import Field
24
+
25
+
26
+ @dataclass
27
+ class Server:
28
+ """
29
+ A physical server that can host virtual machines.
30
+
31
+ Servers have capacity limits for CPU cores, memory (GB), and storage (GB).
32
+ This is a problem fact - it doesn't change during solving.
33
+ """
34
+
35
+ id: Annotated[str, PlanningId]
36
+ name: str
37
+ cpu_cores: int
38
+ memory_gb: int
39
+ storage_gb: int
40
+ rack: Optional[str] = None
41
+
42
+ def __str__(self):
43
+ return self.name
44
+
45
+ def __repr__(self):
46
+ return f"Server({self.id}, {self.name})"
47
+
48
+ def __hash__(self):
49
+ return hash(self.id)
50
+
51
+ def __eq__(self, other):
52
+ if not isinstance(other, Server):
53
+ return False
54
+ return self.id == other.id
55
+
56
+
57
+ @planning_entity
58
+ @dataclass
59
+ class VM:
60
+ """
61
+ A virtual machine that needs to be placed on a server.
62
+
63
+ VMs have resource requirements (CPU, memory, storage) and optional
64
+ affinity/anti-affinity constraints for placement.
65
+ The server field is the planning variable that the solver optimizes.
66
+ """
67
+
68
+ id: Annotated[str, PlanningId]
69
+ name: str
70
+ cpu_cores: int
71
+ memory_gb: int
72
+ storage_gb: int
73
+ priority: int = 1
74
+ affinity_group: Optional[str] = None
75
+ anti_affinity_group: Optional[str] = None
76
+ server: Annotated[Optional[Server], PlanningVariable] = None
77
+
78
+ def __str__(self):
79
+ return self.name
80
+
81
+ def __repr__(self):
82
+ return f"VM({self.id}, {self.name})"
83
+
84
+
85
+ @planning_solution
86
+ @dataclass
87
+ class VMPlacementPlan:
88
+ """
89
+ The planning solution containing all servers and VMs.
90
+
91
+ The solver will assign VMs to servers while respecting capacity constraints,
92
+ affinity/anti-affinity rules, and optimizing for consolidation and balance.
93
+ """
94
+
95
+ name: str
96
+ servers: Annotated[list[Server], ProblemFactCollectionProperty, ValueRangeProvider]
97
+ vms: Annotated[list[VM], PlanningEntityCollectionProperty]
98
+ score: Annotated[Optional[HardSoftScore], PlanningScore] = None
99
+ solver_status: SolverStatus = SolverStatus.NOT_SOLVING
100
+
101
+ def get_vms_on_server(self, server: Server) -> list:
102
+ """Get all VMs assigned to a specific server."""
103
+ return [vm for vm in self.vms if vm.server == server]
104
+
105
+ def get_server_used_cpu(self, server: Server) -> int:
106
+ """Get total CPU cores used on a server."""
107
+ return sum(vm.cpu_cores for vm in self.vms if vm.server == server)
108
+
109
+ def get_server_used_memory(self, server: Server) -> int:
110
+ """Get total memory (GB) used on a server."""
111
+ return sum(vm.memory_gb for vm in self.vms if vm.server == server)
112
+
113
+ def get_server_used_storage(self, server: Server) -> int:
114
+ """Get total storage (GB) used on a server."""
115
+ return sum(vm.storage_gb for vm in self.vms if vm.server == server)
116
+
117
+ @property
118
+ def total_servers(self) -> int:
119
+ return len(self.servers)
120
+
121
+ @property
122
+ def active_servers(self) -> int:
123
+ active_server_ids = set(vm.server.id for vm in self.vms if vm.server is not None)
124
+ return len(active_server_ids)
125
+
126
+ @property
127
+ def unassigned_vms(self) -> int:
128
+ return sum(1 for vm in self.vms if vm.server is None)
129
+
130
+ @property
131
+ def total_cpu_utilization(self) -> float:
132
+ total_capacity = sum(s.cpu_cores for s in self.servers)
133
+ total_used = sum(vm.cpu_cores for vm in self.vms if vm.server is not None)
134
+ if total_capacity == 0:
135
+ return 0.0
136
+ return total_used / total_capacity
137
+
138
+ @property
139
+ def total_memory_utilization(self) -> float:
140
+ total_capacity = sum(s.memory_gb for s in self.servers)
141
+ total_used = sum(vm.memory_gb for vm in self.vms if vm.server is not None)
142
+ if total_capacity == 0:
143
+ return 0.0
144
+ return total_used / total_capacity
145
+
146
+ @property
147
+ def total_storage_utilization(self) -> float:
148
+ total_capacity = sum(s.storage_gb for s in self.servers)
149
+ total_used = sum(vm.storage_gb for vm in self.vms if vm.server is not None)
150
+ if total_capacity == 0:
151
+ return 0.0
152
+ return total_used / total_capacity
153
+
154
+ def __str__(self):
155
+ return f"VMPlacementPlan(name={self.name}, servers={len(self.servers)}, vms={len(self.vms)})"
156
+
157
+
158
+ # Pydantic REST models for API (used for deserialization and context)
159
+ class VMModel(JsonDomainBase):
160
+ id: str
161
+ name: str
162
+ cpu_cores: int = Field(..., alias="cpuCores")
163
+ memory_gb: int = Field(..., alias="memoryGb")
164
+ storage_gb: int = Field(..., alias="storageGb")
165
+ priority: int = 1
166
+ affinity_group: Optional[str] = Field(None, alias="affinityGroup")
167
+ anti_affinity_group: Optional[str] = Field(None, alias="antiAffinityGroup")
168
+ server: Annotated[
169
+ Union[str, "ServerModel", None],
170
+ IdSerializer,
171
+ ServerValidator,
172
+ ] = None
173
+
174
+
175
+ class ServerModel(JsonDomainBase):
176
+ id: str
177
+ name: str
178
+ cpu_cores: int = Field(..., alias="cpuCores")
179
+ memory_gb: int = Field(..., alias="memoryGb")
180
+ storage_gb: int = Field(..., alias="storageGb")
181
+ rack: Optional[str] = None
182
+ vms: Annotated[
183
+ List[Union[str, VMModel]],
184
+ IdListSerializer,
185
+ VMListValidator,
186
+ ] = Field(default_factory=list)
187
+ used_cpu: int = Field(0, alias="usedCpu")
188
+ used_memory: int = Field(0, alias="usedMemory")
189
+ used_storage: int = Field(0, alias="usedStorage")
190
+ cpu_utilization: float = Field(0.0, alias="cpuUtilization")
191
+ memory_utilization: float = Field(0.0, alias="memoryUtilization")
192
+ storage_utilization: float = Field(0.0, alias="storageUtilization")
193
+
194
+
195
+ class VMPlacementPlanModel(JsonDomainBase):
196
+ name: str
197
+ servers: List[ServerModel]
198
+ vms: List[VMModel]
199
+ score: Optional[str] = None
200
+ solver_status: Optional[str] = Field(None, alias="solverStatus")
201
+ total_servers: int = Field(0, alias="totalServers")
202
+ active_servers: int = Field(0, alias="activeServers")
203
+ unassigned_vms: int = Field(0, alias="unassignedVms")
204
+ total_cpu_utilization: float = Field(0.0, alias="totalCpuUtilization")
205
+ total_memory_utilization: float = Field(0.0, alias="totalMemoryUtilization")
206
+ total_storage_utilization: float = Field(0.0, alias="totalStorageUtilization")
src/vm_placement/json_serialization.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from solverforge_legacy.solver.score import HardSoftScore
2
+
3
+ from typing import Any
4
+ from pydantic import (
5
+ BaseModel,
6
+ ConfigDict,
7
+ PlainSerializer,
8
+ BeforeValidator,
9
+ ValidationInfo,
10
+ )
11
+ from pydantic.alias_generators import to_camel
12
+
13
+
14
+ class JsonDomainBase(BaseModel):
15
+ model_config = ConfigDict(
16
+ alias_generator=to_camel,
17
+ populate_by_name=True,
18
+ from_attributes=True,
19
+ )
20
+
21
+
22
+ def make_id_item_validator(key: str):
23
+ def validator(v: Any, info: ValidationInfo) -> Any:
24
+ if v is None:
25
+ return None
26
+
27
+ if not isinstance(v, str) or not info.context:
28
+ return v
29
+
30
+ return info.context.get(key)[v]
31
+
32
+ return BeforeValidator(validator)
33
+
34
+
35
+ def make_id_list_item_validator(key: str):
36
+ def validator(v: Any, info: ValidationInfo) -> Any:
37
+ if v is None:
38
+ return None
39
+
40
+ if isinstance(v, (list, tuple)):
41
+ out = []
42
+ for item in v:
43
+ if not isinstance(item, str) or not info.context:
44
+ return v
45
+ out.append(info.context.get(key)[item])
46
+ return out
47
+
48
+ return v
49
+
50
+ return BeforeValidator(validator)
51
+
52
+
53
+ ScoreSerializer = PlainSerializer(
54
+ lambda score: str(score) if score is not None else None, return_type=str | None
55
+ )
56
+ IdSerializer = PlainSerializer(
57
+ lambda item: item if isinstance(item, str) else (item.id if item is not None else None),
58
+ return_type=str | None
59
+ )
60
+ IdListSerializer = PlainSerializer(
61
+ lambda items: [item if isinstance(item, str) else item.id for item in items],
62
+ return_type=list
63
+ )
64
+
65
+ VMListValidator = make_id_list_item_validator("vms")
66
+ VMValidator = make_id_item_validator("vms")
67
+ ServerValidator = make_id_item_validator("servers")
68
+
69
+
70
+ def validate_score(v: Any, info: ValidationInfo) -> Any:
71
+ if isinstance(v, HardSoftScore) or v is None:
72
+ return v
73
+ if isinstance(v, str):
74
+ # Handle "None" string or empty string as null
75
+ if v in ("None", "null", ""):
76
+ return None
77
+ return HardSoftScore.parse(v)
78
+ raise ValueError('"score" should be a string')
79
+
80
+
81
+ ScoreValidator = BeforeValidator(validate_score)
src/vm_placement/rest_api.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.staticfiles import StaticFiles
3
+ from uuid import uuid4
4
+ from typing import Dict, List
5
+ from dataclasses import asdict
6
+ from enum import Enum
7
+ import logging
8
+
9
+ from .domain import VMPlacementPlan, VMPlacementPlanModel
10
+ from .converters import plan_to_model, model_to_plan
11
+ from .demo_data import generate_demo_data, generate_custom_data, DemoData
12
+ from .solver import solver_manager, solution_manager
13
+ from pydantic import BaseModel, Field
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ app = FastAPI(docs_url='/q/swagger-ui')
18
+
19
+ data_sets: Dict[str, VMPlacementPlan] = {}
20
+
21
+
22
+ class ConstraintMatchDTO(BaseModel):
23
+ name: str
24
+ score: str
25
+ justification: str
26
+
27
+
28
+ class ConstraintAnalysisDTO(BaseModel):
29
+ name: str
30
+ weight: str
31
+ score: str
32
+ matches: List[ConstraintMatchDTO]
33
+
34
+
35
+ @app.get("/demo-data")
36
+ async def get_demo_data():
37
+ """Get available demo data sets."""
38
+ return [demo.name for demo in DemoData]
39
+
40
+
41
+ @app.get("/demo-data/{demo_name}", response_model=VMPlacementPlanModel)
42
+ async def get_demo_data_by_name(demo_name: str) -> VMPlacementPlanModel:
43
+ """Get a specific demo data set."""
44
+ try:
45
+ demo_data = DemoData[demo_name]
46
+ domain_plan = generate_demo_data(demo_data)
47
+ return plan_to_model(domain_plan)
48
+ except KeyError:
49
+ raise HTTPException(status_code=404, detail=f"Demo data '{demo_name}' not found")
50
+
51
+
52
+ class CustomDataRequest(BaseModel):
53
+ rack_count: int = Field(default=3, ge=1, le=8, description="Number of racks")
54
+ servers_per_rack: int = Field(default=4, ge=2, le=10, description="Servers per rack")
55
+ vm_count: int = Field(default=20, ge=5, le=200, description="Number of VMs")
56
+
57
+
58
+ @app.post("/demo-data/generate", response_model=VMPlacementPlanModel)
59
+ async def generate_custom_demo_data(request: CustomDataRequest) -> VMPlacementPlanModel:
60
+ """Generate custom demo data with configurable infrastructure and workload."""
61
+ domain_plan = generate_custom_data(
62
+ rack_count=request.rack_count,
63
+ servers_per_rack=request.servers_per_rack,
64
+ vm_count=request.vm_count
65
+ )
66
+ return plan_to_model(domain_plan)
67
+
68
+
69
+ @app.get("/placements/{problem_id}", response_model=VMPlacementPlanModel, response_model_exclude_none=True)
70
+ async def get_placement(problem_id: str) -> VMPlacementPlanModel:
71
+ """Get the current VM placement solution for a given job ID."""
72
+ placement = data_sets.get(problem_id)
73
+ if not placement:
74
+ raise HTTPException(status_code=404, detail="Placement plan not found")
75
+ placement.solver_status = solver_manager.get_solver_status(problem_id)
76
+ return plan_to_model(placement)
77
+
78
+
79
+ @app.post("/placements")
80
+ async def solve_placement(plan_model: VMPlacementPlanModel) -> str:
81
+ """Start solving a VM placement problem. Returns a job ID."""
82
+ job_id = str(uuid4())
83
+ domain_plan = model_to_plan(plan_model)
84
+ data_sets[job_id] = domain_plan
85
+ solver_manager.solve_and_listen(
86
+ job_id,
87
+ domain_plan,
88
+ lambda solution: data_sets.update({job_id: solution})
89
+ )
90
+ return job_id
91
+
92
+
93
+ @app.put("/placements/analyze")
94
+ async def analyze_placement(plan_model: VMPlacementPlanModel) -> dict:
95
+ """Analyze constraints for a given VM placement solution."""
96
+ domain_plan = model_to_plan(plan_model)
97
+ analysis = solution_manager.analyze(domain_plan)
98
+ constraints = []
99
+ for constraint in getattr(analysis, 'constraint_analyses', []) or []:
100
+ matches = [
101
+ ConstraintMatchDTO(
102
+ name=str(getattr(getattr(match, 'constraint_ref', None), 'constraint_name', "")),
103
+ score=str(getattr(match, 'score', "0hard/0soft")),
104
+ justification=str(getattr(match, 'justification', ""))
105
+ )
106
+ for match in getattr(constraint, 'matches', []) or []
107
+ ]
108
+ constraints.append(ConstraintAnalysisDTO(
109
+ name=str(getattr(constraint, 'constraint_name', "")),
110
+ weight=str(getattr(constraint, 'weight', "0hard/0soft")),
111
+ score=str(getattr(constraint, 'score', "0hard/0soft")),
112
+ matches=matches
113
+ ))
114
+ return {"constraints": [c.model_dump() for c in constraints]}
115
+
116
+
117
+ @app.get("/placements")
118
+ async def list_placements() -> List[str]:
119
+ """List the job IDs of all submitted placement problems."""
120
+ return list(data_sets.keys())
121
+
122
+
123
+ @app.get("/placements/{problem_id}/status")
124
+ async def get_placement_status(problem_id: str) -> dict:
125
+ """Get the placement status and score for a given job ID."""
126
+ placement = data_sets.get(problem_id)
127
+ if not placement:
128
+ raise HTTPException(status_code=404, detail="Placement plan not found")
129
+ solver_status = solver_manager.get_solver_status(problem_id)
130
+ return {
131
+ "name": placement.name,
132
+ "score": str(placement.score) if placement.score else None,
133
+ "solverStatus": solver_status.name if solver_status else None,
134
+ "activeServers": placement.active_servers,
135
+ "unassignedVms": placement.unassigned_vms,
136
+ }
137
+
138
+
139
+ @app.delete("/placements/{problem_id}", response_model=VMPlacementPlanModel)
140
+ async def stop_solving(problem_id: str) -> VMPlacementPlanModel:
141
+ """Terminate solving for a given job ID. Returns the best solution so far."""
142
+ solver_manager.terminate_early(problem_id)
143
+ placement = data_sets.get(problem_id)
144
+ if not placement:
145
+ raise HTTPException(status_code=404, detail="Placement plan not found")
146
+ placement.solver_status = solver_manager.get_solver_status(problem_id)
147
+ return plan_to_model(placement)
148
+
149
+
150
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")
src/vm_placement/solver.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from solverforge_legacy.solver import SolverManager, SolutionManager
2
+ from solverforge_legacy.solver.config import (
3
+ SolverConfig,
4
+ ScoreDirectorFactoryConfig,
5
+ TerminationConfig,
6
+ Duration,
7
+ )
8
+
9
+ from .domain import VM, VMPlacementPlan
10
+ from .constraints import define_constraints
11
+
12
+
13
+ solver_config = SolverConfig(
14
+ solution_class=VMPlacementPlan,
15
+ entity_class_list=[VM],
16
+ score_director_factory_config=ScoreDirectorFactoryConfig(
17
+ constraint_provider_function=define_constraints
18
+ ),
19
+ termination_config=TerminationConfig(spent_limit=Duration(seconds=30)),
20
+ )
21
+
22
+ solver_manager = SolverManager.create(solver_config)
23
+ solution_manager = SolutionManager.create(solver_manager)
static/app.js ADDED
@@ -0,0 +1,974 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // VM Placement Optimizer - Frontend Application with Real-Time Animations
2
+
3
+ let autoRefreshIntervalId = null;
4
+ let demoDataId = null;
5
+ let placementId = null;
6
+ let loadedPlacement = null;
7
+
8
+ // Animation state tracking
9
+ let vmPositionCache = {}; // { vmId: serverId | null }
10
+ let isFirstRender = true;
11
+ let previousScore = null;
12
+ let serverLookup = {}; // For quick server access during animations
13
+
14
+ $(document).ready(function () {
15
+ let initialized = false;
16
+
17
+ function safeInitialize() {
18
+ if (!initialized) {
19
+ initialized = true;
20
+ initializeApp();
21
+ }
22
+ }
23
+
24
+ $(window).on('load', safeInitialize);
25
+ setTimeout(safeInitialize, 100);
26
+ });
27
+
28
+ function initializeApp() {
29
+ replaceQuickstartSolverForgeAutoHeaderFooter();
30
+
31
+ $("#solveButton").click(function () {
32
+ solve();
33
+ });
34
+ $("#stopSolvingButton").click(function () {
35
+ stopSolving();
36
+ });
37
+ $("#analyzeButton").click(function () {
38
+ analyze();
39
+ });
40
+
41
+ // View toggle
42
+ $('input[name="viewToggle"]').change(function() {
43
+ if ($('#rackView').is(':checked')) {
44
+ $('#rackViewContainer').show();
45
+ $('#cardViewContainer').hide();
46
+ } else {
47
+ $('#rackViewContainer').hide();
48
+ $('#cardViewContainer').show();
49
+ }
50
+ });
51
+
52
+ setupAjax();
53
+ fetchDemoData();
54
+ }
55
+
56
+ function setupAjax() {
57
+ $.ajaxSetup({
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ 'Accept': 'application/json,text/plain',
61
+ }
62
+ });
63
+ jQuery.each(["put", "delete"], function (i, method) {
64
+ jQuery[method] = function (url, data, callback, type) {
65
+ if (jQuery.isFunction(data)) {
66
+ type = type || callback;
67
+ callback = data;
68
+ data = undefined;
69
+ }
70
+ return jQuery.ajax({
71
+ url: url,
72
+ type: method,
73
+ dataType: type,
74
+ data: data,
75
+ success: callback
76
+ });
77
+ };
78
+ });
79
+ }
80
+
81
+ function fetchDemoData() {
82
+ $.get("/demo-data", function (data) {
83
+ data.forEach(item => {
84
+ $("#testDataButton").append($('<a id="' + item + 'TestData" class="dropdown-item" href="#">' + item + '</a>'));
85
+ $("#" + item + "TestData").click(function () {
86
+ switchDataDropDownItemActive(item);
87
+ placementId = null;
88
+ demoDataId = item;
89
+ // Reset animation state for new dataset
90
+ isFirstRender = true;
91
+ vmPositionCache = {};
92
+ previousScore = null;
93
+ refreshPlacement();
94
+ });
95
+ });
96
+ if (data.length > 0) {
97
+ demoDataId = data[0];
98
+ switchDataDropDownItemActive(demoDataId);
99
+ refreshPlacement();
100
+ }
101
+ }).fail(function (xhr, ajaxOptions, thrownError) {
102
+ let $demo = $("#demo");
103
+ $demo.empty();
104
+ $demo.html("<h1><p align=\"center\">No test data available</p></h1>");
105
+ });
106
+ }
107
+
108
+ function switchDataDropDownItemActive(newItem) {
109
+ const activeCssClass = "active";
110
+ $("#testDataButton > a." + activeCssClass).removeClass(activeCssClass);
111
+ $("#" + newItem + "TestData").addClass(activeCssClass);
112
+ }
113
+
114
+ function refreshPlacement() {
115
+ let path = "/placements/" + placementId;
116
+ if (placementId === null) {
117
+ if (demoDataId === null) {
118
+ showSimpleError("Please select a test data set.");
119
+ return;
120
+ }
121
+ path = "/demo-data/" + demoDataId;
122
+ }
123
+ $.getJSON(path, function (placement) {
124
+ loadedPlacement = placement;
125
+ renderPlacement(placement);
126
+ }).fail(function (xhr, ajaxOptions, thrownError) {
127
+ showError("Getting the placement has failed.", xhr);
128
+ refreshSolvingButtons(false);
129
+ });
130
+ }
131
+
132
+ function renderPlacement(placement) {
133
+ if (!placement) {
134
+ console.error('No placement data provided');
135
+ return;
136
+ }
137
+
138
+ const isSolving = placement.solverStatus != null && placement.solverStatus !== "NOT_SOLVING";
139
+ refreshSolvingButtons(isSolving);
140
+
141
+ // Update score with animation
142
+ const newScore = placement.score == null ? "?" : placement.score;
143
+ const scoreEl = $("#score");
144
+ if (previousScore !== newScore) {
145
+ scoreEl.text("Score: " + newScore);
146
+ if (previousScore !== null && newScore !== "?") {
147
+ scoreEl.addClass("updated");
148
+ setTimeout(() => scoreEl.removeClass("updated"), 300);
149
+ }
150
+ previousScore = newScore;
151
+ }
152
+
153
+ // Update summary cards with change detection
154
+ updateSummaryCard("#totalServers", placement.servers ? placement.servers.length : 0);
155
+ updateSummaryCard("#activeServers", placement.activeServers || 0);
156
+ updateSummaryCard("#totalVms", placement.vms ? placement.vms.length : 0);
157
+ updateSummaryCard("#unassignedVms", placement.unassignedVms || 0);
158
+ updateSummaryCard("#cpuUtil", Math.round((placement.totalCpuUtilization || 0) * 100) + "%");
159
+ updateSummaryCard("#memUtil", Math.round((placement.totalMemoryUtilization || 0) * 100) + "%");
160
+
161
+ // Update unassigned card styling
162
+ const unassignedCard = $("#unassignedCard");
163
+ const unassignedVms = placement.unassignedVms || 0;
164
+ unassignedCard.removeClass("warning danger");
165
+ if (unassignedVms > 0) {
166
+ unassignedCard.addClass(unassignedVms > 5 ? "danger" : "warning");
167
+ }
168
+
169
+ if (!placement.servers || !placement.vms) {
170
+ return;
171
+ }
172
+
173
+ // Create VM and server lookups
174
+ const vmById = {};
175
+ placement.vms.forEach(vm => vmById[vm.id] = vm);
176
+
177
+ serverLookup = {};
178
+ placement.servers.forEach(s => serverLookup[s.id] = s);
179
+
180
+ if (isFirstRender) {
181
+ // Full render on first load
182
+ renderRackView(placement, vmById);
183
+ renderCardView(placement, vmById);
184
+ renderUnassignedVMs(placement.vms);
185
+ vmPositionCache = buildPositionCache(placement);
186
+ isFirstRender = false;
187
+ return;
188
+ }
189
+
190
+ // Incremental animated update
191
+ const newPositions = buildPositionCache(placement);
192
+ const changes = diffPositions(vmPositionCache, newPositions);
193
+
194
+ if (changes.length > 0) {
195
+ console.log(`Detected ${changes.length} VM position changes:`, changes.slice(0, 5));
196
+ animateVmChanges(changes, placement, vmById);
197
+ }
198
+
199
+ // Always update utilization bars smoothly
200
+ updateUtilizationBars(placement);
201
+
202
+ // Update unassigned list
203
+ updateUnassignedList(placement.vms, vmById);
204
+
205
+ vmPositionCache = newPositions;
206
+ }
207
+
208
+ function updateSummaryCard(selector, newValue) {
209
+ const el = $(selector);
210
+ const oldValue = el.text();
211
+ if (oldValue !== String(newValue)) {
212
+ el.text(newValue);
213
+ el.addClass("changed");
214
+ setTimeout(() => el.removeClass("changed"), 300);
215
+ }
216
+ }
217
+
218
+ function buildPositionCache(placement) {
219
+ const cache = {};
220
+ placement.vms.forEach(vm => {
221
+ cache[vm.id] = vm.server || null;
222
+ });
223
+ // Debug: log sample of cache
224
+ const sample = Object.entries(cache).slice(0, 3);
225
+ console.log('Position cache sample:', sample);
226
+ return cache;
227
+ }
228
+
229
+ function diffPositions(oldCache, newCache) {
230
+ const changes = [];
231
+ for (const vmId in newCache) {
232
+ const oldServer = oldCache[vmId];
233
+ const newServer = newCache[vmId];
234
+ if (oldServer !== newServer) {
235
+ changes.push({ vmId, from: oldServer, to: newServer });
236
+ }
237
+ }
238
+ return changes;
239
+ }
240
+
241
+ function animateVmChanges(changes, placement, vmById) {
242
+ changes.forEach(({ vmId, from, to }) => {
243
+ const vm = vmById[vmId];
244
+ if (!vm) return;
245
+
246
+ // Get source and target elements
247
+ let sourceEl, targetEl;
248
+ let sourceChipEl = null; // The actual chip element to hide during animation
249
+
250
+ if (from) {
251
+ sourceEl = $(`#server-blade-${from} .vm-chips`);
252
+ sourceChipEl = $(`#server-blade-${from} #vm-chip-${vmId}`);
253
+ // Highlight sending server
254
+ $(`#server-blade-${from}`).addClass('sending');
255
+ setTimeout(() => $(`#server-blade-${from}`).removeClass('sending'), 200);
256
+ } else {
257
+ // VM is coming from unassigned list
258
+ sourceEl = $(`#unassigned-${vmId}`);
259
+ sourceChipEl = sourceEl;
260
+ }
261
+
262
+ if (to) {
263
+ targetEl = $(`#server-blade-${to} .vm-chips`);
264
+ // Highlight receiving server
265
+ $(`#server-blade-${to}`).addClass('receiving');
266
+ setTimeout(() => $(`#server-blade-${to}`).removeClass('receiving'), 300);
267
+ } else {
268
+ targetEl = $('#unassignedList');
269
+ }
270
+
271
+ // If elements not found, just update DOM directly
272
+ if (!sourceEl.length || !targetEl.length) {
273
+ console.log(`Animation fallback: sourceEl=${sourceEl.length}, targetEl=${targetEl.length} for VM ${vmId}`);
274
+ updateVmPosition(vmId, from, to, vm);
275
+ return;
276
+ }
277
+
278
+ // Create flying clone
279
+ const flyingChip = createVmChip(vm);
280
+ flyingChip.addClass('flying-vm');
281
+
282
+ // Get positions
283
+ const sourceRect = sourceEl[0].getBoundingClientRect();
284
+ const targetRect = targetEl[0].getBoundingClientRect();
285
+
286
+ // Position at source
287
+ flyingChip.css({
288
+ left: sourceRect.left + 'px',
289
+ top: sourceRect.top + 'px',
290
+ transform: 'translate(0, 0)'
291
+ });
292
+
293
+ $('body').append(flyingChip);
294
+
295
+ // Hide original element during animation
296
+ if (sourceChipEl && sourceChipEl.length) {
297
+ sourceChipEl.css('opacity', '0');
298
+ }
299
+
300
+ // Trigger fly animation (use double RAF for reliable animation start)
301
+ requestAnimationFrame(() => {
302
+ requestAnimationFrame(() => {
303
+ flyingChip.addClass('animate');
304
+ flyingChip.css({
305
+ transform: `translate(${targetRect.left - sourceRect.left}px, ${targetRect.top - sourceRect.top}px)`
306
+ });
307
+ });
308
+ });
309
+
310
+ // Clean up and update DOM after animation
311
+ setTimeout(() => {
312
+ flyingChip.remove();
313
+ updateVmPosition(vmId, from, to, vm);
314
+ }, 220); // Slightly longer than animation duration to ensure completion
315
+ });
316
+ }
317
+
318
+ function updateVmPosition(vmId, from, to, vm) {
319
+ // Remove from source
320
+ if (from) {
321
+ $(`#server-blade-${from} #vm-chip-${vmId}`).remove();
322
+ } else {
323
+ $(`#unassigned-${vmId}`).remove();
324
+ }
325
+
326
+ // Add to target
327
+ if (to) {
328
+ let vmChipsContainer = $(`#server-blade-${to} .vm-chips`);
329
+ if (!vmChipsContainer.length) {
330
+ // Create vm-chips container if it doesn't exist
331
+ vmChipsContainer = $('<div class="vm-chips"></div>');
332
+ $(`#server-blade-${to}`).append(vmChipsContainer);
333
+ }
334
+ const newChip = createVmChip(vm);
335
+ newChip.attr('id', `vm-chip-${vm.id}`);
336
+ vmChipsContainer.append(newChip);
337
+
338
+ // Update server blade state (empty/not empty)
339
+ $(`#server-blade-${to}`).removeClass('empty');
340
+ }
341
+
342
+ // Update source server empty state
343
+ if (from) {
344
+ const sourceChips = $(`#server-blade-${from} .vm-chips`).children();
345
+ if (sourceChips.length === 0) {
346
+ $(`#server-blade-${from}`).addClass('empty');
347
+ }
348
+ }
349
+ }
350
+
351
+ function updateUtilizationBars(placement) {
352
+ placement.servers.forEach(server => {
353
+ const cpuBar = $(`#util-${server.id}-cpu`);
354
+ const memBar = $(`#util-${server.id}-mem`);
355
+ const stoBar = $(`#util-${server.id}-sto`);
356
+
357
+ const updates = [
358
+ { bar: cpuBar, util: server.cpuUtilization || 0 },
359
+ { bar: memBar, util: server.memoryUtilization || 0 },
360
+ { bar: stoBar, util: server.storageUtilization || 0 }
361
+ ];
362
+
363
+ updates.forEach(({ bar, util }) => {
364
+ if (bar.length) {
365
+ const newPct = Math.min(util * 100, 100);
366
+ const oldPct = parseFloat(bar[0].style.width) || 0;
367
+
368
+ bar.css('width', newPct + '%');
369
+ bar.removeClass('low medium high over').addClass(getUtilClass(util));
370
+
371
+ if (Math.abs(oldPct - newPct) > 1) {
372
+ bar.addClass('changed');
373
+ setTimeout(() => bar.removeClass('changed'), 300);
374
+ }
375
+ }
376
+ });
377
+
378
+ // Update server blade empty/overcommitted state
379
+ const blade = $(`#server-blade-${server.id}`);
380
+ const hasVms = server.vms && server.vms.length > 0;
381
+ const isOvercommitted = (server.cpuUtilization || 0) > 1 ||
382
+ (server.memoryUtilization || 0) > 1 ||
383
+ (server.storageUtilization || 0) > 1;
384
+
385
+ blade.toggleClass('empty', !hasVms);
386
+ blade.toggleClass('overcommitted', isOvercommitted);
387
+ });
388
+ }
389
+
390
+ function updateUnassignedList(vms, vmById) {
391
+ const container = $("#unassignedList");
392
+ const unassigned = vms.filter(vm => !vm.server);
393
+
394
+ if (unassigned.length === 0) {
395
+ if (!container.find('.all-assigned').length) {
396
+ container.empty();
397
+ container.append(`
398
+ <div class="all-assigned">
399
+ <i class="fas fa-check-circle"></i>
400
+ <strong>All VMs assigned!</strong>
401
+ </div>
402
+ `);
403
+ }
404
+ return;
405
+ }
406
+
407
+ // Remove "all assigned" message if present
408
+ container.find('.all-assigned').remove();
409
+
410
+ // Update existing or add new unassigned VMs
411
+ unassigned.sort((a, b) => (b.priority || 1) - (a.priority || 1));
412
+
413
+ unassigned.forEach(vm => {
414
+ if (!$(`#unassigned-${vm.id}`).length) {
415
+ const vmDiv = createUnassignedVmElement(vm);
416
+ container.append(vmDiv);
417
+ }
418
+ });
419
+ }
420
+
421
+ function createUnassignedVmElement(vm) {
422
+ return $(`
423
+ <div id="unassigned-${vm.id}" class="unassigned-vm">
424
+ <div class="name">
425
+ ${vm.name}
426
+ ${vm.affinityGroup ? `<span class="constraint-marker affinity">${vm.affinityGroup}</span>` : ''}
427
+ ${vm.antiAffinityGroup ? `<span class="constraint-marker anti-affinity">${vm.antiAffinityGroup}</span>` : ''}
428
+ </div>
429
+ <div class="details">
430
+ <i class="fas fa-microchip"></i> ${vm.cpuCores}c
431
+ <i class="fas fa-memory ms-2"></i> ${vm.memoryGb}GB
432
+ <i class="fas fa-hdd ms-2"></i> ${vm.storageGb}GB
433
+ <span class="ms-2 badge bg-${getPriorityBadgeClass(vm.priority)}">P${vm.priority || 1}</span>
434
+ </div>
435
+ </div>
436
+ `);
437
+ }
438
+
439
+ function renderRackView(placement, vmById) {
440
+ const container = $("#rackViewContainer");
441
+ container.empty();
442
+
443
+ // Group servers by rack
444
+ const rackGroups = {};
445
+ placement.servers.forEach(server => {
446
+ const rack = server.rack || "Unracked";
447
+ if (!rackGroups[rack]) {
448
+ rackGroups[rack] = [];
449
+ }
450
+ rackGroups[rack].push(server);
451
+ });
452
+
453
+ // Sort racks alphabetically
454
+ const sortedRacks = Object.keys(rackGroups).sort();
455
+
456
+ sortedRacks.forEach(rackName => {
457
+ const servers = rackGroups[rackName];
458
+ servers.sort((a, b) => a.name.localeCompare(b.name));
459
+
460
+ const rackDiv = $('<div class="rack"></div>');
461
+ const rackHeader = $(`
462
+ <div class="rack-header">
463
+ <i class="fas fa-server"></i>
464
+ <span>${rackName}</span>
465
+ <span class="ms-auto badge bg-secondary">${servers.length} servers</span>
466
+ </div>
467
+ `);
468
+ rackDiv.append(rackHeader);
469
+
470
+ servers.forEach(server => {
471
+ const blade = createServerBlade(server, vmById);
472
+ rackDiv.append(blade);
473
+ });
474
+
475
+ container.append(rackDiv);
476
+ });
477
+ }
478
+
479
+ function createServerBlade(server, vmById) {
480
+ const cpuUtil = server.cpuUtilization || 0;
481
+ const memUtil = server.memoryUtilization || 0;
482
+ const storageUtil = server.storageUtilization || 0;
483
+ const isEmpty = !server.vms || server.vms.length === 0;
484
+ const isOvercommitted = cpuUtil > 1 || memUtil > 1 || storageUtil > 1;
485
+
486
+ let bladeClass = "server-blade";
487
+ if (isEmpty) bladeClass += " empty";
488
+ if (isOvercommitted) bladeClass += " overcommitted";
489
+
490
+ const blade = $(`<div id="server-blade-${server.id}" class="${bladeClass}"></div>`);
491
+
492
+ // Header
493
+ const header = $(`
494
+ <div class="server-blade-header">
495
+ <span class="server-name">${server.name}</span>
496
+ <span class="server-specs">${server.cpuCores}c / ${server.memoryGb}GB / ${server.storageGb}GB</span>
497
+ </div>
498
+ `);
499
+ blade.append(header);
500
+
501
+ // Utilization bars with IDs for animation
502
+ const utilBars = $('<div class="utilization-mini"></div>');
503
+ utilBars.append(createMiniUtilBar(cpuUtil, "CPU", `util-${server.id}-cpu`));
504
+ utilBars.append(createMiniUtilBar(memUtil, "MEM", `util-${server.id}-mem`));
505
+ utilBars.append(createMiniUtilBar(storageUtil, "STO", `util-${server.id}-sto`));
506
+ blade.append(utilBars);
507
+
508
+ // VM chips with IDs for animation
509
+ const vmChips = $('<div class="vm-chips"></div>');
510
+ if (server.vms && server.vms.length > 0) {
511
+ server.vms.forEach(vmId => {
512
+ const vm = vmById[vmId];
513
+ if (vm) {
514
+ const chip = createVmChip(vm);
515
+ chip.attr('id', `vm-chip-${vm.id}`);
516
+ vmChips.append(chip);
517
+ }
518
+ });
519
+ }
520
+ blade.append(vmChips);
521
+
522
+ // Click handler for details - pass server ID to get fresh data
523
+ blade.click(function(e) {
524
+ if (!$(e.target).hasClass('vm-chip')) {
525
+ showServerDetails(server.id);
526
+ }
527
+ });
528
+
529
+ return blade;
530
+ }
531
+
532
+ function createMiniUtilBar(value, label, id) {
533
+ const percentage = Math.min(value * 100, 100);
534
+ const utilClass = getUtilClass(value);
535
+
536
+ return $(`
537
+ <div class="util-mini-bar" title="${label}: ${Math.round(value * 100)}%">
538
+ <div id="${id}" class="util-mini-fill ${utilClass}" style="width: ${percentage}%"></div>
539
+ </div>
540
+ `);
541
+ }
542
+
543
+ function createVmChip(vm) {
544
+ const priority = vm.priority || 1;
545
+ let chipClass = `vm-chip priority-${priority}`;
546
+ if (vm.affinityGroup) chipClass += " affinity";
547
+ if (vm.antiAffinityGroup) chipClass += " anti-affinity";
548
+
549
+ let tooltip = vm.name;
550
+ tooltip += `\nCPU: ${vm.cpuCores} cores`;
551
+ tooltip += `\nMemory: ${vm.memoryGb} GB`;
552
+ tooltip += `\nStorage: ${vm.storageGb} GB`;
553
+ tooltip += `\nPriority: ${priority}`;
554
+ if (vm.affinityGroup) tooltip += `\nAffinity: ${vm.affinityGroup}`;
555
+ if (vm.antiAffinityGroup) tooltip += `\nAnti-Affinity: ${vm.antiAffinityGroup}`;
556
+
557
+ return $(`<span class="${chipClass}" title="${tooltip}">${vm.name}</span>`);
558
+ }
559
+
560
+ function renderCardView(placement, vmById) {
561
+ const container = $("#cardViewContainer");
562
+ container.empty();
563
+
564
+ const sortedServers = [...placement.servers].sort((a, b) => {
565
+ const aVms = a.vms ? a.vms.length : 0;
566
+ const bVms = b.vms ? b.vms.length : 0;
567
+ if (bVms !== aVms) return bVms - aVms;
568
+ return a.name.localeCompare(b.name);
569
+ });
570
+
571
+ sortedServers.forEach(server => {
572
+ const card = createServerCard(server, vmById);
573
+ container.append(card);
574
+ });
575
+ }
576
+
577
+ function createServerCard(server, vmById) {
578
+ const cpuUtil = server.cpuUtilization || 0;
579
+ const memUtil = server.memoryUtilization || 0;
580
+ const storageUtil = server.storageUtilization || 0;
581
+ const isEmpty = !server.vms || server.vms.length === 0;
582
+
583
+ const cardWrapper = $('<div class="col-md-6 col-lg-4"></div>');
584
+ const card = $(`
585
+ <div class="card h-100 ${isEmpty ? 'opacity-50' : ''}">
586
+ <div class="card-header d-flex justify-content-between align-items-center">
587
+ <strong>${server.name}</strong>
588
+ ${server.rack ? `<span class="badge bg-secondary">${server.rack}</span>` : ''}
589
+ </div>
590
+ <div class="card-body">
591
+ <div class="mb-3">
592
+ ${createUtilRow("CPU", cpuUtil, server.cpuCores + " cores")}
593
+ ${createUtilRow("Memory", memUtil, server.memoryGb + " GB")}
594
+ ${createUtilRow("Storage", storageUtil, server.storageGb + " GB")}
595
+ </div>
596
+ <div class="vm-chips">
597
+ ${(server.vms || []).map(vmId => {
598
+ const vm = vmById[vmId];
599
+ if (vm) {
600
+ const priority = vm.priority || 1;
601
+ let chipClass = `vm-chip priority-${priority}`;
602
+ if (vm.affinityGroup) chipClass += " affinity";
603
+ if (vm.antiAffinityGroup) chipClass += " anti-affinity";
604
+ return `<span class="${chipClass}" title="${vm.name}">${vm.name}</span>`;
605
+ }
606
+ return '';
607
+ }).join('')}
608
+ </div>
609
+ </div>
610
+ </div>
611
+ `);
612
+ cardWrapper.append(card);
613
+ return cardWrapper;
614
+ }
615
+
616
+ function createUtilRow(label, value, capacity) {
617
+ const percentage = Math.min(value * 100, 100);
618
+ const displayPercentage = Math.round(value * 100);
619
+ const utilClass = getUtilClass(value);
620
+
621
+ const bgClass = {
622
+ 'low': 'bg-success',
623
+ 'medium': 'bg-warning',
624
+ 'high': 'bg-danger',
625
+ 'over': 'bg-danger'
626
+ }[utilClass];
627
+
628
+ return `
629
+ <div class="mb-2">
630
+ <div class="d-flex justify-content-between small">
631
+ <span>${label}</span>
632
+ <span class="text-muted">${capacity} (${displayPercentage}%)</span>
633
+ </div>
634
+ <div class="progress" style="height: 6px;">
635
+ <div class="progress-bar ${bgClass}" style="width: ${percentage}%"></div>
636
+ </div>
637
+ </div>
638
+ `;
639
+ }
640
+
641
+ function renderUnassignedVMs(vms) {
642
+ const container = $("#unassignedList");
643
+ container.empty();
644
+
645
+ const unassigned = vms.filter(vm => !vm.server);
646
+
647
+ if (unassigned.length === 0) {
648
+ container.append(`
649
+ <div class="all-assigned">
650
+ <i class="fas fa-check-circle"></i>
651
+ <strong>All VMs assigned!</strong>
652
+ </div>
653
+ `);
654
+ return;
655
+ }
656
+
657
+ unassigned.sort((a, b) => (b.priority || 1) - (a.priority || 1));
658
+
659
+ unassigned.forEach(vm => {
660
+ const vmDiv = createUnassignedVmElement(vm);
661
+ container.append(vmDiv);
662
+ });
663
+ }
664
+
665
+ function getPriorityBadgeClass(priority) {
666
+ switch(priority) {
667
+ case 5: return "danger";
668
+ case 4: return "primary";
669
+ case 3: return "success";
670
+ case 2: return "secondary";
671
+ default: return "light text-dark";
672
+ }
673
+ }
674
+
675
+ function showServerDetails(serverId) {
676
+ // Always get fresh data from loadedPlacement
677
+ if (!loadedPlacement || !loadedPlacement.servers) {
678
+ console.error('No placement data available');
679
+ return;
680
+ }
681
+
682
+ const server = loadedPlacement.servers.find(s => s.id === serverId);
683
+ if (!server) {
684
+ console.error('Server not found:', serverId);
685
+ return;
686
+ }
687
+
688
+ // Build VM lookup from current placement
689
+ const vmById = {};
690
+ if (loadedPlacement.vms) {
691
+ loadedPlacement.vms.forEach(vm => vmById[vm.id] = vm);
692
+ }
693
+
694
+ const modal = new bootstrap.Modal("#serverDetailModal");
695
+ const content = $("#serverDetailModalContent");
696
+ $("#serverDetailModalLabel").html(`<i class="fas fa-server me-2"></i>${server.name}`);
697
+
698
+ const cpuUtil = server.cpuUtilization || 0;
699
+ const memUtil = server.memoryUtilization || 0;
700
+ const storageUtil = server.storageUtilization || 0;
701
+
702
+ content.html(`
703
+ <div class="mb-3">
704
+ <h6>Specifications</h6>
705
+ <table class="table table-sm">
706
+ <tr><td>Rack</td><td>${server.rack || 'Unracked'}</td></tr>
707
+ <tr><td>CPU Cores</td><td>${server.cpuCores}</td></tr>
708
+ <tr><td>Memory</td><td>${server.memoryGb} GB</td></tr>
709
+ <tr><td>Storage</td><td>${server.storageGb} GB</td></tr>
710
+ </table>
711
+ </div>
712
+ <div class="mb-3">
713
+ <h6>Utilization</h6>
714
+ ${createUtilRow("CPU", cpuUtil, `${server.usedCpu || 0}/${server.cpuCores} cores`)}
715
+ ${createUtilRow("Memory", memUtil, `${server.usedMemory || 0}/${server.memoryGb} GB`)}
716
+ ${createUtilRow("Storage", storageUtil, `${server.usedStorage || 0}/${server.storageGb} GB`)}
717
+ </div>
718
+ <div>
719
+ <h6>Assigned VMs (${server.vms ? server.vms.length : 0})</h6>
720
+ ${server.vms && server.vms.length > 0 ? `
721
+ <table class="table table-sm">
722
+ <thead>
723
+ <tr>
724
+ <th>Name</th>
725
+ <th>CPU</th>
726
+ <th>Memory</th>
727
+ <th>Storage</th>
728
+ <th>Priority</th>
729
+ </tr>
730
+ </thead>
731
+ <tbody>
732
+ ${server.vms.map(vmId => {
733
+ const vm = vmById[vmId];
734
+ if (vm) {
735
+ return `
736
+ <tr>
737
+ <td>${vm.name}</td>
738
+ <td>${vm.cpuCores}</td>
739
+ <td>${vm.memoryGb} GB</td>
740
+ <td>${vm.storageGb} GB</td>
741
+ <td><span class="badge bg-${getPriorityBadgeClass(vm.priority)}">P${vm.priority || 1}</span></td>
742
+ </tr>
743
+ `;
744
+ }
745
+ return '';
746
+ }).join('')}
747
+ </tbody>
748
+ </table>
749
+ ` : '<p class="text-muted">No VMs assigned</p>'}
750
+ </div>
751
+ `);
752
+
753
+ modal.show();
754
+ }
755
+
756
+ function getUtilClass(value) {
757
+ if (value > 1) return 'over';
758
+ if (value > 0.8) return 'high';
759
+ if (value > 0.5) return 'medium';
760
+ return 'low';
761
+ }
762
+
763
+ function solve() {
764
+ if (!loadedPlacement) {
765
+ showSimpleError("No placement data loaded. Please wait for the data to load or refresh the page.");
766
+ return;
767
+ }
768
+
769
+ // Reset animation state for solving - capture current positions before solving modifies them
770
+ vmPositionCache = buildPositionCache(loadedPlacement);
771
+ console.log('Solve started. Initial position cache size:', Object.keys(vmPositionCache).length);
772
+
773
+ $.post("/placements", JSON.stringify(loadedPlacement), function (data) {
774
+ placementId = data;
775
+ refreshSolvingButtons(true);
776
+ }).fail(function (xhr, ajaxOptions, thrownError) {
777
+ showError("Start solving failed.", xhr);
778
+ refreshSolvingButtons(false);
779
+ }, "text");
780
+ }
781
+
782
+ function stopSolving() {
783
+ $.delete(`/placements/${placementId}`, function () {
784
+ refreshSolvingButtons(false);
785
+ // Do a final full render to ensure accuracy
786
+ isFirstRender = true;
787
+ refreshPlacement();
788
+ }).fail(function (xhr, ajaxOptions, thrownError) {
789
+ showError("Stop solving failed.", xhr);
790
+ });
791
+ }
792
+
793
+ function analyze() {
794
+ const modal = new bootstrap.Modal("#scoreAnalysisModal");
795
+ modal.show();
796
+
797
+ const scoreAnalysisModalContent = $("#scoreAnalysisModalContent");
798
+ scoreAnalysisModalContent.empty();
799
+
800
+ if (!loadedPlacement || loadedPlacement.score == null) {
801
+ scoreAnalysisModalContent.text("No score to analyze yet, please first press the 'Solve' button.");
802
+ return;
803
+ }
804
+
805
+ $('#scoreAnalysisScoreLabel').text(`(${loadedPlacement.score})`);
806
+
807
+ $.put("/placements/analyze", JSON.stringify(loadedPlacement), function (scoreAnalysis) {
808
+ let constraints = scoreAnalysis.constraints;
809
+
810
+ constraints.sort((a, b) => {
811
+ let aComponents = getScoreComponents(a.score);
812
+ let bComponents = getScoreComponents(b.score);
813
+ if (aComponents.hard < 0 && bComponents.hard > 0) return -1;
814
+ if (aComponents.hard > 0 && bComponents.soft < 0) return 1;
815
+ if (Math.abs(aComponents.hard) > Math.abs(bComponents.hard)) {
816
+ return -1;
817
+ } else {
818
+ if (Math.abs(aComponents.soft) > Math.abs(bComponents.soft)) {
819
+ return -1;
820
+ }
821
+ return Math.abs(bComponents.soft) - Math.abs(aComponents.soft);
822
+ }
823
+ });
824
+
825
+ constraints = constraints.map((e) => {
826
+ let components = getScoreComponents(e.weight);
827
+ e.type = components.hard !== 0 ? 'hard' : 'soft';
828
+ e.weight = components[e.type];
829
+ let scores = getScoreComponents(e.score);
830
+ e.implicitScore = scores.hard !== 0 ? scores.hard : scores.soft;
831
+ return e;
832
+ });
833
+
834
+ scoreAnalysisModalContent.empty();
835
+
836
+ const analysisTable = $(`<table class="table"/>`).css({textAlign: 'center'});
837
+ const analysisTHead = $(`<thead/>`).append($(`<tr/>`)
838
+ .append($(`<th></th>`))
839
+ .append($(`<th>Constraint</th>`).css({textAlign: 'left'}))
840
+ .append($(`<th>Type</th>`))
841
+ .append($(`<th># Matches</th>`))
842
+ .append($(`<th>Weight</th>`))
843
+ .append($(`<th>Score</th>`)));
844
+ analysisTable.append(analysisTHead);
845
+
846
+ const analysisTBody = $(`<tbody/>`);
847
+ $.each(constraints, (index, constraintAnalysis) => {
848
+ let icon = constraintAnalysis.type === "hard" && constraintAnalysis.implicitScore < 0
849
+ ? '<span class="fas fa-exclamation-triangle text-danger"></span>'
850
+ : '';
851
+ if (!icon) {
852
+ icon = constraintAnalysis.matches.length === 0
853
+ ? '<span class="fas fa-check-circle text-success"></span>'
854
+ : '';
855
+ }
856
+
857
+ let row = $(`<tr/>`);
858
+ row.append($(`<td/>`).html(icon))
859
+ .append($(`<td/>`).text(constraintAnalysis.name).css({textAlign: 'left'}))
860
+ .append($(`<td/>`).html(`<span class="badge bg-${constraintAnalysis.type === 'hard' ? 'danger' : 'warning'}">${constraintAnalysis.type}</span>`))
861
+ .append($(`<td/>`).html(`<b>${constraintAnalysis.matches.length}</b>`))
862
+ .append($(`<td/>`).text(constraintAnalysis.weight))
863
+ .append($(`<td/>`).text(constraintAnalysis.implicitScore));
864
+ analysisTBody.append(row);
865
+ });
866
+ analysisTable.append(analysisTBody);
867
+ scoreAnalysisModalContent.append(analysisTable);
868
+ }).fail(function (xhr, ajaxOptions, thrownError) {
869
+ showError("Analyze failed.", xhr);
870
+ }, "text");
871
+ }
872
+
873
+ function getScoreComponents(score) {
874
+ let components = {hard: 0, soft: 0};
875
+ if (!score) return components;
876
+
877
+ $.each([...score.matchAll(/(-?\d*\.?\d+)(hard|soft)/g)], (i, parts) => {
878
+ components[parts[2]] = parseFloat(parts[1]);
879
+ });
880
+
881
+ return components;
882
+ }
883
+
884
+ function refreshSolvingButtons(solving) {
885
+ if (solving) {
886
+ $("#solveButton").hide();
887
+ $("#stopSolvingButton").show();
888
+ $("#solvingSpinner").addClass("active");
889
+ // Add solving pulse animation to summary card values
890
+ $(".summary-card .value").addClass("solving-pulse");
891
+ if (autoRefreshIntervalId == null) {
892
+ // 250ms polling for smooth real-time animations
893
+ autoRefreshIntervalId = setInterval(refreshPlacement, 250);
894
+ }
895
+ } else {
896
+ $("#solveButton").show();
897
+ $("#stopSolvingButton").hide();
898
+ $("#solvingSpinner").removeClass("active");
899
+ // Remove solving pulse animation
900
+ $(".summary-card .value").removeClass("solving-pulse");
901
+ if (autoRefreshIntervalId != null) {
902
+ clearInterval(autoRefreshIntervalId);
903
+ autoRefreshIntervalId = null;
904
+ }
905
+ }
906
+ }
907
+
908
+ function replaceQuickstartSolverForgeAutoHeaderFooter() {
909
+ const solverforgeHeader = $("header#solverforge-auto-header");
910
+ if (solverforgeHeader != null) {
911
+ solverforgeHeader.css("background-color", "#ffffff");
912
+ solverforgeHeader.append(
913
+ $(`<div class="container-fluid">
914
+ <nav class="navbar sticky-top navbar-expand-lg shadow-sm mb-3" style="background-color: #ffffff;">
915
+ <a class="navbar-brand" href="https://www.solverforge.org">
916
+ <img src="/webjars/solverforge/img/solverforge-horizontal.svg" alt="SolverForge logo" width="400">
917
+ </a>
918
+ <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
919
+ <span class="navbar-toggler-icon"></span>
920
+ </button>
921
+ <div class="collapse navbar-collapse" id="navbarNav">
922
+ <ul class="nav nav-pills">
923
+ <li class="nav-item active" id="navUIItem">
924
+ <button class="nav-link active" id="navUI" data-bs-toggle="pill" data-bs-target="#demo" type="button" style="color: #1f2937;">Demo UI</button>
925
+ </li>
926
+ <li class="nav-item" id="navRestItem">
927
+ <button class="nav-link" id="navRest" data-bs-toggle="pill" data-bs-target="#rest" type="button" style="color: #1f2937;">Guide</button>
928
+ </li>
929
+ <li class="nav-item" id="navOpenApiItem">
930
+ <button class="nav-link" id="navOpenApi" data-bs-toggle="pill" data-bs-target="#openapi" type="button" style="color: #1f2937;">REST API</button>
931
+ </li>
932
+ </ul>
933
+ </div>
934
+ <div class="ms-auto">
935
+ <div class="dropdown">
936
+ <button class="btn dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" style="background-color: #10b981; color: #ffffff; border-color: #10b981;">
937
+ Data
938
+ </button>
939
+ <div id="testDataButton" class="dropdown-menu" aria-labelledby="dropdownMenuButton"></div>
940
+ </div>
941
+ </div>
942
+ </nav>
943
+ </div>`));
944
+ }
945
+
946
+ const solverforgeFooter = $("footer#solverforge-auto-footer");
947
+ if (solverforgeFooter != null) {
948
+ solverforgeFooter.append(
949
+ $(`<footer class="bg-black text-white-50">
950
+ <div class="container">
951
+ <div class="hstack gap-3 p-4">
952
+ <div class="ms-auto"><a class="text-white" href="https://www.solverforge.org">SolverForge</a></div>
953
+ <div class="vr"></div>
954
+ <div><a class="text-white" href="https://www.solverforge.org/docs">Documentation</a></div>
955
+ <div class="vr"></div>
956
+ <div><a class="text-white" href="https://github.com/SolverForge/solverforge-legacy">Code</a></div>
957
+ <div class="vr"></div>
958
+ <div class="me-auto"><a class="text-white" href="mailto:info@solverforge.org">Support</a></div>
959
+ </div>
960
+ </div>
961
+ </footer>`));
962
+ }
963
+ }
964
+
965
+ function copyTextToClipboard(elementId) {
966
+ const element = document.getElementById(elementId);
967
+ if (element) {
968
+ navigator.clipboard.writeText(element.textContent).then(() => {
969
+ // Optional: show feedback
970
+ }).catch(err => {
971
+ console.error('Failed to copy text: ', err);
972
+ });
973
+ }
974
+ }
static/config.js ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * VM Placement Configuration Module
3
+ *
4
+ * Provides configurable infrastructure and workload settings for the VM placement
5
+ * quickstart, including sliders for racks, servers, VMs, and solver time.
6
+ */
7
+
8
+ // =============================================================================
9
+ // Configuration State
10
+ // =============================================================================
11
+
12
+ let currentConfig = {
13
+ rackCount: 3,
14
+ serversPerRack: 4,
15
+ vmCount: 20,
16
+ solverTime: 30
17
+ };
18
+
19
+ // =============================================================================
20
+ // Initialization
21
+ // =============================================================================
22
+
23
+ /**
24
+ * Initialize configuration UI and event handlers.
25
+ * Called automatically when the script loads.
26
+ */
27
+ function initConfig() {
28
+ // Rack count slider
29
+ $("#rackCountSlider").on("input", function() {
30
+ currentConfig.rackCount = parseInt(this.value);
31
+ $("#rackCountValue").text(this.value);
32
+ updateConfigSummary();
33
+ });
34
+
35
+ // Servers per rack slider
36
+ $("#serversPerRackSlider").on("input", function() {
37
+ currentConfig.serversPerRack = parseInt(this.value);
38
+ $("#serversPerRackValue").text(this.value);
39
+ updateConfigSummary();
40
+ });
41
+
42
+ // VM count slider
43
+ $("#vmCountSlider").on("input", function() {
44
+ currentConfig.vmCount = parseInt(this.value);
45
+ $("#vmCountValue").text(this.value);
46
+ updateConfigSummary();
47
+ });
48
+
49
+ // Solver time slider
50
+ $("#solverTimeSlider").on("input", function() {
51
+ currentConfig.solverTime = parseInt(this.value);
52
+ $("#solverTimeValue").text(formatSolverTime(this.value));
53
+ });
54
+
55
+ // Generate button
56
+ $("#generateDataBtn").click(function() {
57
+ generateCustomData();
58
+ });
59
+
60
+ // Initialize summary
61
+ updateConfigSummary();
62
+ }
63
+
64
+ // =============================================================================
65
+ // UI Updates
66
+ // =============================================================================
67
+
68
+ function updateConfigSummary() {
69
+ const totalServers = currentConfig.rackCount * currentConfig.serversPerRack;
70
+ $("#configSummary").text(
71
+ `${totalServers} servers across ${currentConfig.rackCount} rack${currentConfig.rackCount > 1 ? 's' : ''}, ` +
72
+ `${currentConfig.vmCount} VMs to place`
73
+ );
74
+ }
75
+
76
+ function formatSolverTime(seconds) {
77
+ if (seconds >= 60) {
78
+ const mins = Math.floor(seconds / 60);
79
+ const secs = seconds % 60;
80
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
81
+ }
82
+ return `${seconds}s`;
83
+ }
84
+
85
+ // =============================================================================
86
+ // Data Generation
87
+ // =============================================================================
88
+
89
+ function generateCustomData() {
90
+ const btn = $("#generateDataBtn");
91
+ const originalHtml = btn.html();
92
+
93
+ // Show loading state
94
+ btn.prop("disabled", true);
95
+ btn.html('<i class="fas fa-spinner fa-spin me-1"></i> Generating...');
96
+
97
+ $.ajax({
98
+ url: "/demo-data/generate",
99
+ type: "POST",
100
+ data: JSON.stringify({
101
+ rack_count: currentConfig.rackCount,
102
+ servers_per_rack: currentConfig.serversPerRack,
103
+ vm_count: currentConfig.vmCount
104
+ }),
105
+ contentType: "application/json"
106
+ })
107
+ .done(function(placement) {
108
+ // Reset animation state for new data
109
+ isFirstRender = true;
110
+ vmPositionCache = {};
111
+ previousScore = null;
112
+ placementId = null;
113
+
114
+ // Store and render new data
115
+ loadedPlacement = placement;
116
+ renderPlacement(placement);
117
+
118
+ // Flash success
119
+ btn.html('<i class="fas fa-check me-1"></i> Generated!');
120
+ btn.removeClass("btn-primary").addClass("btn-success");
121
+
122
+ setTimeout(() => {
123
+ btn.html(originalHtml);
124
+ btn.removeClass("btn-success").addClass("btn-primary");
125
+ btn.prop("disabled", false);
126
+ }, 1500);
127
+ })
128
+ .fail(function(xhr) {
129
+ showError("Failed to generate custom data", xhr);
130
+ btn.html(originalHtml);
131
+ btn.prop("disabled", false);
132
+ });
133
+ }
134
+
135
+ // =============================================================================
136
+ // Solver Time Integration
137
+ // =============================================================================
138
+
139
+ /**
140
+ * Get the current solver time setting.
141
+ * Can be used by app.js to configure solver termination.
142
+ */
143
+ function getSolverTime() {
144
+ return currentConfig.solverTime;
145
+ }
146
+
147
+ /**
148
+ * Get the current configuration.
149
+ */
150
+ function getCurrentConfig() {
151
+ return { ...currentConfig };
152
+ }
153
+
154
+ // Export for use in app.js
155
+ window.currentConfig = currentConfig;
156
+ window.getSolverTime = getSolverTime;
157
+ window.getCurrentConfig = getCurrentConfig;
158
+ window.initConfig = initConfig;
159
+
160
+ // Initialize when DOM is ready
161
+ $(document).ready(function() {
162
+ initConfig();
163
+ });
static/index.html ADDED
@@ -0,0 +1,701 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7
+ <title>VM Placement - SolverForge for Python</title>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
10
+ <link rel="stylesheet" href="/webjars/solverforge/css/solverforge-webui.css">
11
+ <link rel="icon" href="/webjars/solverforge/img/solverforge-favicon.svg" type="image/svg+xml">
12
+ <style>
13
+ /* Solving spinner */
14
+ #solvingSpinner {
15
+ display: none;
16
+ width: 1.25rem;
17
+ height: 1.25rem;
18
+ border: 2px solid #10b981;
19
+ border-top-color: transparent;
20
+ border-radius: 50%;
21
+ animation: spin 0.75s linear infinite;
22
+ vertical-align: middle;
23
+ }
24
+ #solvingSpinner.active {
25
+ display: inline-block;
26
+ }
27
+ @keyframes spin {
28
+ to { transform: rotate(360deg); }
29
+ }
30
+
31
+ /* Rack visualization */
32
+ .rack-container {
33
+ display: flex;
34
+ flex-wrap: wrap;
35
+ gap: 1.5rem;
36
+ padding: 1rem;
37
+ }
38
+ .rack {
39
+ background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
40
+ border-radius: 8px;
41
+ padding: 1rem;
42
+ min-width: 280px;
43
+ flex: 1;
44
+ max-width: 400px;
45
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
46
+ }
47
+ .rack-header {
48
+ color: #10b981;
49
+ font-weight: bold;
50
+ font-size: 0.9rem;
51
+ text-transform: uppercase;
52
+ letter-spacing: 1px;
53
+ margin-bottom: 0.75rem;
54
+ padding-bottom: 0.5rem;
55
+ border-bottom: 2px solid #10b981;
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 0.5rem;
59
+ }
60
+ .rack-header i {
61
+ font-size: 1rem;
62
+ }
63
+ .server-blade {
64
+ background: linear-gradient(90deg, #2d3748 0%, #1a202c 100%);
65
+ border: 1px solid #4a5568;
66
+ border-radius: 4px;
67
+ padding: 0.75rem;
68
+ margin-bottom: 0.5rem;
69
+ transition: all 0.3s ease;
70
+ cursor: pointer;
71
+ }
72
+ .server-blade:hover {
73
+ border-color: #10b981;
74
+ box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
75
+ transform: translateX(4px);
76
+ }
77
+ .server-blade.empty {
78
+ opacity: 0.5;
79
+ }
80
+ .server-blade.overcommitted {
81
+ border-color: #ef4444;
82
+ box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
83
+ }
84
+ .server-blade-header {
85
+ display: flex;
86
+ justify-content: space-between;
87
+ align-items: center;
88
+ margin-bottom: 0.5rem;
89
+ }
90
+ .server-name {
91
+ color: #e2e8f0;
92
+ font-weight: 600;
93
+ font-size: 0.85rem;
94
+ }
95
+ .server-specs {
96
+ color: #718096;
97
+ font-size: 0.7rem;
98
+ }
99
+ .utilization-mini {
100
+ display: flex;
101
+ gap: 0.25rem;
102
+ margin-bottom: 0.5rem;
103
+ }
104
+ .util-mini-bar {
105
+ flex: 1;
106
+ height: 6px;
107
+ background: #4a5568;
108
+ border-radius: 3px;
109
+ overflow: hidden;
110
+ position: relative;
111
+ }
112
+ .util-mini-fill {
113
+ height: 100%;
114
+ border-radius: 3px;
115
+ transition: width 0.5s ease, background 0.3s ease;
116
+ }
117
+ .util-mini-fill.low { background: linear-gradient(90deg, #10b981, #34d399); }
118
+ .util-mini-fill.medium { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
119
+ .util-mini-fill.high { background: linear-gradient(90deg, #ef4444, #f87171); }
120
+ .util-mini-fill.over { background: linear-gradient(90deg, #991b1b, #dc2626); }
121
+ .util-mini-label {
122
+ position: absolute;
123
+ right: 2px;
124
+ top: -1px;
125
+ font-size: 0.5rem;
126
+ color: #fff;
127
+ text-shadow: 0 0 2px rgba(0,0,0,0.8);
128
+ }
129
+ .vm-chips {
130
+ display: flex;
131
+ flex-wrap: wrap;
132
+ gap: 4px;
133
+ }
134
+ .vm-chip {
135
+ font-size: 0.65rem;
136
+ padding: 2px 6px;
137
+ border-radius: 3px;
138
+ color: white;
139
+ font-weight: 500;
140
+ transition: transform 0.2s ease;
141
+ cursor: default;
142
+ }
143
+ .vm-chip:hover {
144
+ transform: scale(1.1);
145
+ z-index: 10;
146
+ }
147
+ .vm-chip.priority-5 { background: linear-gradient(135deg, #7c3aed, #a855f7); }
148
+ .vm-chip.priority-4 { background: linear-gradient(135deg, #2563eb, #3b82f6); }
149
+ .vm-chip.priority-3 { background: linear-gradient(135deg, #059669, #10b981); }
150
+ .vm-chip.priority-2 { background: linear-gradient(135deg, #4b5563, #6b7280); }
151
+ .vm-chip.priority-1 { background: linear-gradient(135deg, #9ca3af, #d1d5db); color: #1f2937; }
152
+ .vm-chip.affinity {
153
+ box-shadow: 0 0 0 2px #f59e0b;
154
+ }
155
+ .vm-chip.anti-affinity {
156
+ box-shadow: 0 0 0 2px #ef4444;
157
+ }
158
+
159
+ /* Summary cards */
160
+ .summary-card {
161
+ background: white;
162
+ border-radius: 12px;
163
+ padding: 1.25rem;
164
+ text-align: center;
165
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
166
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
167
+ }
168
+ .summary-card:hover {
169
+ transform: translateY(-2px);
170
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
171
+ }
172
+ .summary-card .value {
173
+ font-size: 2rem;
174
+ font-weight: 700;
175
+ color: #1f2937;
176
+ line-height: 1.2;
177
+ }
178
+ .summary-card .label {
179
+ font-size: 0.8rem;
180
+ color: #6b7280;
181
+ text-transform: uppercase;
182
+ letter-spacing: 0.5px;
183
+ }
184
+ .summary-card.highlight .value {
185
+ color: #10b981;
186
+ }
187
+ .summary-card.warning .value {
188
+ color: #f59e0b;
189
+ }
190
+ .summary-card.danger .value {
191
+ color: #ef4444;
192
+ }
193
+
194
+ /* Unassigned VMs panel */
195
+ .unassigned-panel {
196
+ background: white;
197
+ border-radius: 12px;
198
+ padding: 1rem;
199
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
200
+ max-height: 400px;
201
+ overflow-y: auto;
202
+ }
203
+ .unassigned-vm {
204
+ background: linear-gradient(135deg, #fef3c7, #fde68a);
205
+ border-left: 4px solid #f59e0b;
206
+ padding: 0.75rem;
207
+ margin-bottom: 0.5rem;
208
+ border-radius: 0 8px 8px 0;
209
+ transition: transform 0.2s ease;
210
+ }
211
+ .unassigned-vm:hover {
212
+ transform: translateX(4px);
213
+ }
214
+ .unassigned-vm .name {
215
+ font-weight: 600;
216
+ color: #92400e;
217
+ }
218
+ .unassigned-vm .details {
219
+ font-size: 0.75rem;
220
+ color: #78716c;
221
+ margin-top: 0.25rem;
222
+ }
223
+ .all-assigned {
224
+ background: linear-gradient(135deg, #d1fae5, #a7f3d0);
225
+ border-left: 4px solid #10b981;
226
+ padding: 1rem;
227
+ border-radius: 0 8px 8px 0;
228
+ color: #065f46;
229
+ text-align: center;
230
+ }
231
+ .all-assigned i {
232
+ font-size: 1.5rem;
233
+ margin-bottom: 0.5rem;
234
+ display: block;
235
+ }
236
+
237
+ /* Legend */
238
+ .legend {
239
+ background: #f9fafb;
240
+ border-radius: 8px;
241
+ padding: 1rem;
242
+ margin-top: 1rem;
243
+ }
244
+ .legend h6 {
245
+ font-size: 0.8rem;
246
+ color: #6b7280;
247
+ text-transform: uppercase;
248
+ letter-spacing: 0.5px;
249
+ margin-bottom: 0.75rem;
250
+ }
251
+ .legend-item {
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 0.5rem;
255
+ font-size: 0.75rem;
256
+ margin-bottom: 0.25rem;
257
+ }
258
+ .legend-color {
259
+ width: 16px;
260
+ height: 16px;
261
+ border-radius: 4px;
262
+ }
263
+
264
+ /* Constraint markers */
265
+ .constraint-marker {
266
+ font-size: 0.6rem;
267
+ padding: 1px 4px;
268
+ border-radius: 2px;
269
+ margin-left: 4px;
270
+ }
271
+ .constraint-marker.affinity {
272
+ background: #fef3c7;
273
+ color: #92400e;
274
+ }
275
+ .constraint-marker.anti-affinity {
276
+ background: #fee2e2;
277
+ color: #991b1b;
278
+ }
279
+
280
+ /* View toggle */
281
+ .view-toggle {
282
+ margin-bottom: 1rem;
283
+ }
284
+ .view-toggle .btn-check:checked + .btn {
285
+ background-color: #10b981;
286
+ border-color: #10b981;
287
+ }
288
+
289
+ /* Responsive adjustments */
290
+ @media (max-width: 768px) {
291
+ .rack {
292
+ max-width: 100%;
293
+ }
294
+ }
295
+
296
+ /* Flying VM animation */
297
+ .flying-vm {
298
+ position: fixed;
299
+ z-index: 9999;
300
+ pointer-events: none;
301
+ transition: none;
302
+ }
303
+ .flying-vm.animate {
304
+ transition: transform 200ms ease-in-out, opacity 200ms ease;
305
+ }
306
+
307
+ /* Utilization bar pulse on change */
308
+ @keyframes utilPulse {
309
+ 0%, 100% { box-shadow: none; }
310
+ 50% { box-shadow: 0 0 8px 2px rgba(16, 185, 129, 0.6); }
311
+ }
312
+ .util-mini-fill.changed {
313
+ animation: utilPulse 300ms ease;
314
+ }
315
+
316
+ /* Server highlight when receiving VM */
317
+ @keyframes serverReceive {
318
+ 0%, 100% { box-shadow: inset 0 0 0 0 rgba(16, 185, 129, 0); }
319
+ 50% { box-shadow: inset 0 0 20px 5px rgba(16, 185, 129, 0.3); }
320
+ }
321
+ .server-blade.receiving {
322
+ animation: serverReceive 300ms ease;
323
+ }
324
+
325
+ /* Server highlight when losing VM */
326
+ @keyframes serverSend {
327
+ 0%, 100% { box-shadow: inset 0 0 0 0 rgba(239, 68, 68, 0); }
328
+ 50% { box-shadow: inset 0 0 15px 3px rgba(239, 68, 68, 0.2); }
329
+ }
330
+ .server-blade.sending {
331
+ animation: serverSend 200ms ease;
332
+ }
333
+
334
+ /* Score update flash */
335
+ @keyframes scoreFlash {
336
+ 0%, 100% { background: transparent; }
337
+ 50% { background: rgba(16, 185, 129, 0.2); }
338
+ }
339
+ .score.updated {
340
+ animation: scoreFlash 300ms ease;
341
+ border-radius: 4px;
342
+ padding: 2px 8px;
343
+ }
344
+
345
+ /* Summary card value change */
346
+ @keyframes valueChange {
347
+ 0%, 100% { transform: scale(1); }
348
+ 50% { transform: scale(1.1); }
349
+ }
350
+ .summary-card .value.changed {
351
+ animation: valueChange 300ms ease;
352
+ }
353
+
354
+ /* Progress animation during solving */
355
+ @keyframes pulse {
356
+ 0%, 100% { opacity: 1; }
357
+ 50% { opacity: 0.6; }
358
+ }
359
+ .solving-pulse {
360
+ animation: pulse 1.5s ease-in-out infinite;
361
+ }
362
+
363
+ /* Advanced settings panel */
364
+ .settings-card {
365
+ background: white;
366
+ border-radius: 12px;
367
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
368
+ }
369
+ .settings-card .card-body {
370
+ padding: 1.25rem;
371
+ }
372
+ .form-range::-webkit-slider-thumb {
373
+ background: #10b981;
374
+ }
375
+ .form-range::-moz-range-thumb {
376
+ background: #10b981;
377
+ }
378
+ .config-value {
379
+ font-weight: 600;
380
+ color: #10b981;
381
+ }
382
+ .preset-description {
383
+ font-size: 0.85rem;
384
+ color: #64748b;
385
+ }
386
+ </style>
387
+ </head>
388
+ <body>
389
+
390
+ <header id="solverforge-auto-header">
391
+ <!-- Filled in by app.js -->
392
+ </header>
393
+
394
+ <div class="tab-content">
395
+ <div id="demo" class="tab-pane fade show active container-fluid">
396
+ <div class="sticky-top d-flex justify-content-center align-items-center" aria-live="polite" aria-atomic="true">
397
+ <div id="notificationPanel" style="position: absolute; top: .5rem;"></div>
398
+ </div>
399
+
400
+ <h1>VM Placement Optimizer</h1>
401
+ <p>Optimize virtual machine placement across servers to maximize resource utilization while respecting constraints.</p>
402
+
403
+ <div class="mb-4">
404
+ <button id="solveButton" type="button" class="btn btn-success">
405
+ <i class="fas fa-play"></i> Solve
406
+ </button>
407
+ <button id="stopSolvingButton" type="button" class="btn btn-danger" style="display: none;">
408
+ <i class="fas fa-stop"></i> Stop solving
409
+ </button>
410
+ <span id="solvingSpinner" class="ms-2"></span>
411
+ <span id="score" class="score ms-2 align-middle fw-bold">Score: ?</span>
412
+ <button id="analyzeButton" type="button" class="ms-2 btn btn-secondary">
413
+ <i class="fas fa-question"></i>
414
+ </button>
415
+
416
+ <div class="float-end">
417
+ <button class="btn btn-outline-secondary btn-sm me-2" type="button"
418
+ data-bs-toggle="collapse" data-bs-target="#advancedSettings"
419
+ aria-expanded="false" aria-controls="advancedSettings">
420
+ <i class="fas fa-cog"></i> Advanced
421
+ </button>
422
+ <div class="btn-group view-toggle me-2" role="group" aria-label="View toggle">
423
+ <input type="radio" class="btn-check" name="viewToggle" id="rackView" autocomplete="off" checked>
424
+ <label class="btn btn-outline-secondary btn-sm" for="rackView">
425
+ <i class="fas fa-server"></i> Rack View
426
+ </label>
427
+ <input type="radio" class="btn-check" name="viewToggle" id="cardView" autocomplete="off">
428
+ <label class="btn btn-outline-secondary btn-sm" for="cardView">
429
+ <i class="fas fa-th-large"></i> Card View
430
+ </label>
431
+ </div>
432
+ </div>
433
+ </div>
434
+
435
+ <!-- Advanced Settings Panel -->
436
+ <div class="collapse mb-4" id="advancedSettings">
437
+ <div class="settings-card">
438
+ <div class="card-body">
439
+ <div class="row g-4">
440
+ <!-- Infrastructure Settings -->
441
+ <div class="col-md-6">
442
+ <h6 class="text-muted mb-3"><i class="fas fa-server me-2"></i>Infrastructure</h6>
443
+ <div class="row g-3">
444
+ <div class="col-6">
445
+ <label class="form-label">
446
+ Racks: <span id="rackCountValue" class="config-value">3</span>
447
+ </label>
448
+ <input type="range" class="form-range" id="rackCountSlider"
449
+ min="1" max="8" step="1" value="3">
450
+ <div class="d-flex justify-content-between text-muted" style="font-size: 0.7rem;">
451
+ <span>1</span>
452
+ <span>8</span>
453
+ </div>
454
+ </div>
455
+ <div class="col-6">
456
+ <label class="form-label">
457
+ Servers/Rack: <span id="serversPerRackValue" class="config-value">4</span>
458
+ </label>
459
+ <input type="range" class="form-range" id="serversPerRackSlider"
460
+ min="2" max="10" step="1" value="4">
461
+ <div class="d-flex justify-content-between text-muted" style="font-size: 0.7rem;">
462
+ <span>2</span>
463
+ <span>10</span>
464
+ </div>
465
+ </div>
466
+ </div>
467
+ </div>
468
+
469
+ <!-- Workload Settings -->
470
+ <div class="col-md-6">
471
+ <h6 class="text-muted mb-3"><i class="fas fa-cubes me-2"></i>Workload</h6>
472
+ <div class="row g-3">
473
+ <div class="col-6">
474
+ <label class="form-label">
475
+ VMs: <span id="vmCountValue" class="config-value">20</span>
476
+ </label>
477
+ <input type="range" class="form-range" id="vmCountSlider"
478
+ min="5" max="200" step="5" value="20">
479
+ <div class="d-flex justify-content-between text-muted" style="font-size: 0.7rem;">
480
+ <span>5</span>
481
+ <span>200</span>
482
+ </div>
483
+ </div>
484
+ <div class="col-6">
485
+ <label class="form-label">
486
+ Solver Time: <span id="solverTimeValue" class="config-value">30s</span>
487
+ </label>
488
+ <input type="range" class="form-range" id="solverTimeSlider"
489
+ min="5" max="120" step="5" value="30">
490
+ <div class="d-flex justify-content-between text-muted" style="font-size: 0.7rem;">
491
+ <span>5s</span>
492
+ <span>2min</span>
493
+ </div>
494
+ </div>
495
+ </div>
496
+ </div>
497
+ </div>
498
+
499
+ <!-- Action buttons -->
500
+ <div class="mt-3 d-flex justify-content-between align-items-center">
501
+ <div class="preset-description">
502
+ <i class="fas fa-info-circle me-1"></i>
503
+ <span id="configSummary">12 servers across 3 racks, 20 VMs to place</span>
504
+ </div>
505
+ <button id="generateDataBtn" class="btn btn-primary btn-sm">
506
+ <i class="fas fa-sync-alt me-1"></i> Generate New Data
507
+ </button>
508
+ </div>
509
+ </div>
510
+ </div>
511
+ </div>
512
+
513
+ <!-- Summary Cards -->
514
+ <div class="row mb-4 g-3" id="summaryCards">
515
+ <div class="col-6 col-md-4 col-lg-2">
516
+ <div class="summary-card">
517
+ <div class="value" id="totalServers">-</div>
518
+ <div class="label">Total Servers</div>
519
+ </div>
520
+ </div>
521
+ <div class="col-6 col-md-4 col-lg-2">
522
+ <div class="summary-card highlight">
523
+ <div class="value" id="activeServers">-</div>
524
+ <div class="label">Active Servers</div>
525
+ </div>
526
+ </div>
527
+ <div class="col-6 col-md-4 col-lg-2">
528
+ <div class="summary-card">
529
+ <div class="value" id="totalVms">-</div>
530
+ <div class="label">Total VMs</div>
531
+ </div>
532
+ </div>
533
+ <div class="col-6 col-md-4 col-lg-2">
534
+ <div class="summary-card" id="unassignedCard">
535
+ <div class="value" id="unassignedVms">-</div>
536
+ <div class="label">Unassigned</div>
537
+ </div>
538
+ </div>
539
+ <div class="col-6 col-md-4 col-lg-2">
540
+ <div class="summary-card">
541
+ <div class="value" id="cpuUtil">-</div>
542
+ <div class="label">CPU Util</div>
543
+ </div>
544
+ </div>
545
+ <div class="col-6 col-md-4 col-lg-2">
546
+ <div class="summary-card">
547
+ <div class="value" id="memUtil">-</div>
548
+ <div class="label">Memory Util</div>
549
+ </div>
550
+ </div>
551
+ </div>
552
+
553
+ <!-- Main Content -->
554
+ <div class="row">
555
+ <!-- Rack/Card View -->
556
+ <div class="col-lg-9">
557
+ <div id="rackViewContainer" class="rack-container">
558
+ <p class="text-muted">Select a dataset to see server rack visualization</p>
559
+ </div>
560
+ <div id="cardViewContainer" class="row g-3" style="display: none;">
561
+ <p class="text-muted">Select a dataset to see server cards</p>
562
+ </div>
563
+ </div>
564
+
565
+ <!-- Sidebar -->
566
+ <div class="col-lg-3">
567
+ <div class="unassigned-panel mb-3">
568
+ <h5 class="mb-3"><i class="fas fa-exclamation-triangle text-warning me-2"></i>Unassigned VMs</h5>
569
+ <div id="unassignedList">
570
+ <p class="text-muted small">No data loaded</p>
571
+ </div>
572
+ </div>
573
+
574
+ <div class="legend">
575
+ <h6><i class="fas fa-palette me-1"></i>Priority Legend</h6>
576
+ <div class="legend-item">
577
+ <div class="legend-color" style="background: linear-gradient(135deg, #7c3aed, #a855f7);"></div>
578
+ <span>Critical (5)</span>
579
+ </div>
580
+ <div class="legend-item">
581
+ <div class="legend-color" style="background: linear-gradient(135deg, #2563eb, #3b82f6);"></div>
582
+ <span>High (4)</span>
583
+ </div>
584
+ <div class="legend-item">
585
+ <div class="legend-color" style="background: linear-gradient(135deg, #059669, #10b981);"></div>
586
+ <span>Medium (3)</span>
587
+ </div>
588
+ <div class="legend-item">
589
+ <div class="legend-color" style="background: linear-gradient(135deg, #4b5563, #6b7280);"></div>
590
+ <span>Low (2)</span>
591
+ </div>
592
+ <div class="legend-item">
593
+ <div class="legend-color" style="background: linear-gradient(135deg, #9ca3af, #d1d5db);"></div>
594
+ <span>Lowest (1)</span>
595
+ </div>
596
+ <hr class="my-2">
597
+ <h6><i class="fas fa-link me-1"></i>Constraints</h6>
598
+ <div class="legend-item">
599
+ <div class="legend-color" style="background: #fef3c7; box-shadow: 0 0 0 2px #f59e0b;"></div>
600
+ <span>Affinity Group</span>
601
+ </div>
602
+ <div class="legend-item">
603
+ <div class="legend-color" style="background: #fee2e2; box-shadow: 0 0 0 2px #ef4444;"></div>
604
+ <span>Anti-Affinity Group</span>
605
+ </div>
606
+ </div>
607
+ </div>
608
+ </div>
609
+ </div>
610
+
611
+ <div id="rest" class="tab-pane fade container-fluid">
612
+ <h1>REST API Guide</h1>
613
+
614
+ <h2>VM Placement solver integration via cURL</h2>
615
+
616
+ <h3>1. Download demo data</h3>
617
+ <pre>
618
+ <button class="btn btn-outline-dark btn-sm float-end" onclick="copyTextToClipboard('curl1')">Copy</button>
619
+ <code id="curl1">curl -X GET -H 'Accept:application/json' http://localhost:8080/demo-data/SMALL -o sample.json</code>
620
+ </pre>
621
+
622
+ <h3>2. Post the sample data for solving</h3>
623
+ <p>The POST operation returns a <code>jobId</code> that should be used in subsequent commands.</p>
624
+ <pre>
625
+ <button class="btn btn-outline-dark btn-sm float-end" onclick="copyTextToClipboard('curl2')">Copy</button>
626
+ <code id="curl2">curl -X POST -H 'Content-Type:application/json' http://localhost:8080/placements -d@sample.json</code>
627
+ </pre>
628
+
629
+ <h3>3. Get the current status and score</h3>
630
+ <pre>
631
+ <button class="btn btn-outline-dark btn-sm float-end" onclick="copyTextToClipboard('curl3')">Copy</button>
632
+ <code id="curl3">curl -X GET -H 'Accept:application/json' http://localhost:8080/placements/{jobId}/status</code>
633
+ </pre>
634
+
635
+ <h3>4. Get the complete solution</h3>
636
+ <pre>
637
+ <button class="btn btn-outline-dark btn-sm float-end" onclick="copyTextToClipboard('curl4')">Copy</button>
638
+ <code id="curl4">curl -X GET -H 'Accept:application/json' http://localhost:8080/placements/{jobId}</code>
639
+ </pre>
640
+
641
+ <h3>5. Terminate solving early</h3>
642
+ <pre>
643
+ <button class="btn btn-outline-dark btn-sm float-end" onclick="copyTextToClipboard('curl5')">Copy</button>
644
+ <code id="curl5">curl -X DELETE -H 'Accept:application/json' http://localhost:8080/placements/{jobId}</code>
645
+ </pre>
646
+ </div>
647
+
648
+ <div id="openapi" class="tab-pane fade container-fluid">
649
+ <h1>REST API Reference</h1>
650
+ <div class="ratio ratio-1x1">
651
+ <iframe src="/q/swagger-ui" style="overflow:hidden;" scrolling="no"></iframe>
652
+ </div>
653
+ </div>
654
+ </div>
655
+
656
+ <!-- Score Analysis Modal -->
657
+ <div class="modal fade" id="scoreAnalysisModal" tabindex="-1" aria-labelledby="scoreAnalysisModalLabel" aria-hidden="true">
658
+ <div class="modal-dialog modal-lg modal-dialog-scrollable">
659
+ <div class="modal-content">
660
+ <div class="modal-header">
661
+ <h5 class="modal-title" id="scoreAnalysisModalLabel">Score Analysis <span id="scoreAnalysisScoreLabel"></span></h5>
662
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
663
+ </div>
664
+ <div class="modal-body" id="scoreAnalysisModalContent">
665
+ <!-- Filled in by app.js -->
666
+ </div>
667
+ <div class="modal-footer">
668
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
669
+ </div>
670
+ </div>
671
+ </div>
672
+ </div>
673
+
674
+ <!-- Server Detail Modal -->
675
+ <div class="modal fade" id="serverDetailModal" tabindex="-1" aria-labelledby="serverDetailModalLabel" aria-hidden="true">
676
+ <div class="modal-dialog">
677
+ <div class="modal-content">
678
+ <div class="modal-header">
679
+ <h5 class="modal-title" id="serverDetailModalLabel"><i class="fas fa-server me-2"></i>Server Details</h5>
680
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
681
+ </div>
682
+ <div class="modal-body" id="serverDetailModalContent">
683
+ <!-- Filled in by app.js -->
684
+ </div>
685
+ <div class="modal-footer">
686
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
687
+ </div>
688
+ </div>
689
+ </div>
690
+ </div>
691
+
692
+ <footer id="solverforge-auto-footer"></footer>
693
+
694
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
695
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js"></script>
696
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js"></script>
697
+ <script src="/webjars/solverforge/js/solverforge-webui.js"></script>
698
+ <script src="/app.js"></script>
699
+ <script src="/config.js"></script>
700
+ </body>
701
+ </html>
static/webjars/solverforge/css/solverforge-webui.css ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* Keep in sync with .navbar height on a large screen. */
3
+ --ts-navbar-height: 109px;
4
+
5
+ --ts-green-1-rgb: #10b981;
6
+ --ts-green-2-rgb: #059669;
7
+ --ts-violet-1-rgb: #3E00FF;
8
+ --ts-violet-2-rgb: #3423A6;
9
+ --ts-violet-3-rgb: #2E1760;
10
+ --ts-violet-4-rgb: #200F4F;
11
+ --ts-violet-5-rgb: #000000; /* TODO FIXME */
12
+ --ts-violet-dark-1-rgb: #b6adfd;
13
+ --ts-violet-dark-2-rgb: #c1bbfd;
14
+ --ts-gray-rgb: #666666;
15
+ --ts-white-rgb: #FFFFFF;
16
+ --ts-light-rgb: #F2F2F2;
17
+ --ts-gray-border: #c5c5c5;
18
+
19
+ --tf-light-rgb-transparent: rgb(242,242,242,0.5); /* #F2F2F2 = rgb(242,242,242) */
20
+ --bs-body-bg: var(--ts-light-rgb); /* link to html bg */
21
+ --bs-link-color: var(--ts-violet-1-rgb);
22
+ --bs-link-hover-color: var(--ts-violet-2-rgb);
23
+
24
+ --bs-navbar-color: var(--ts-white-rgb);
25
+ --bs-navbar-hover-color: var(--ts-white-rgb);
26
+ --bs-nav-link-font-size: 18px;
27
+ --bs-nav-link-font-weight: 400;
28
+ --bs-nav-link-color: var(--ts-white-rgb);
29
+ --ts-nav-link-hover-border-color: var(--ts-violet-1-rgb);
30
+ }
31
+ .btn {
32
+ --bs-btn-border-radius: 1.5rem;
33
+ }
34
+ .btn-primary {
35
+ --bs-btn-bg: var(--ts-violet-1-rgb);
36
+ --bs-btn-border-color: var(--ts-violet-1-rgb);
37
+ --bs-btn-hover-bg: var(--ts-violet-2-rgb);
38
+ --bs-btn-hover-border-color: var(--ts-violet-2-rgb);
39
+ --bs-btn-active-bg: var(--ts-violet-2-rgb);
40
+ --bs-btn-active-border-bg: var(--ts-violet-2-rgb);
41
+ --bs-btn-disabled-bg: var(--ts-violet-1-rgb);
42
+ --bs-btn-disabled-border-color: var(--ts-violet-1-rgb);
43
+ }
44
+ .btn-outline-primary {
45
+ --bs-btn-color: var(--ts-violet-1-rgb);
46
+ --bs-btn-border-color: var(--ts-violet-1-rgb);
47
+ --bs-btn-hover-bg: var(--ts-violet-1-rgb);
48
+ --bs-btn-hover-border-color: var(--ts-violet-1-rgb);
49
+ --bs-btn-active-bg: var(--ts-violet-1-rgb);
50
+ --bs-btn-active-border-color: var(--ts-violet-1-rgb);
51
+ --bs-btn-disabled-color: var(--ts-violet-1-rgb);
52
+ --bs-btn-disabled-border-color: var(--ts-violet-1-rgb);
53
+ }
54
+ .navbar-dark {
55
+ --bs-link-color: var(--ts-violet-dark-1-rgb);
56
+ --bs-link-hover-color: var(--ts-violet-dark-2-rgb);
57
+ --bs-navbar-color: var(--ts-white-rgb);
58
+ --bs-navbar-hover-color: var(--ts-white-rgb);
59
+ }
60
+ .nav-pills {
61
+ --bs-nav-pills-link-active-bg: var(--ts-green-1-rgb);
62
+ }
63
+ .nav-pills .nav-link:hover {
64
+ color: var(--ts-green-1-rgb);
65
+ }
66
+ .nav-pills .nav-link.active:hover {
67
+ color: var(--ts-white-rgb);
68
+ }
static/webjars/solverforge/img/solverforge-favicon.svg ADDED
static/webjars/solverforge/img/solverforge-horizontal-white.svg ADDED
static/webjars/solverforge/img/solverforge-horizontal.svg ADDED
static/webjars/solverforge/img/solverforge-logo-stacked.svg ADDED
static/webjars/solverforge/js/solverforge-webui.js ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function replaceSolverForgeAutoHeaderFooter() {
2
+ const solverforgeHeader = $("header#solverforge-auto-header");
3
+ if (solverforgeHeader != null) {
4
+ solverforgeHeader.addClass("bg-black")
5
+ solverforgeHeader.append(
6
+ $(`<div class="container-fluid">
7
+ <nav class="navbar sticky-top navbar-expand-lg navbar-dark shadow mb-3">
8
+ <a class="navbar-brand" href="https://www.solverforge.org">
9
+ <img src="/solverforge/img/solverforge-horizontal-white.svg" alt="SolverForge logo" width="200">
10
+ </a>
11
+ </nav>
12
+ </div>`));
13
+ }
14
+ const solverforgeFooter = $("footer#solverforge-auto-footer");
15
+ if (solverforgeFooter != null) {
16
+ solverforgeFooter.append(
17
+ $(`<footer class="bg-black text-white-50">
18
+ <div class="container">
19
+ <div class="hstack gap-3 p-4">
20
+ <div class="ms-auto"><a class="text-white" href="https://www.solverforge.org">SolverForge</a></div>
21
+ <div class="vr"></div>
22
+ <div><a class="text-white" href="https://www.solverforge.org/docs">Documentation</a></div>
23
+ <div class="vr"></div>
24
+ <div><a class="text-white" href="https://github.com/SolverForge/solverforge-legacy">Code</a></div>
25
+ <div class="vr"></div>
26
+ <div class="me-auto"><a class="text-white" href="mailto:info@solverforge.org">Support</a></div>
27
+ </div>
28
+ </div>
29
+ <div id="applicationInfo" class="container text-center"></div>
30
+ </footer>`));
31
+
32
+ applicationInfo();
33
+ }
34
+
35
+ }
36
+
37
+ function showSimpleError(title) {
38
+ const notification = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="min-width: 50rem"/>`)
39
+ .append($(`<div class="toast-header bg-danger">
40
+ <strong class="me-auto text-dark">Error</strong>
41
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
42
+ </div>`))
43
+ .append($(`<div class="toast-body"/>`)
44
+ .append($(`<p/>`).text(title))
45
+ );
46
+ $("#notificationPanel").append(notification);
47
+ notification.toast({delay: 30000});
48
+ notification.toast('show');
49
+ }
50
+
51
+ function showError(title, xhr) {
52
+ var serverErrorMessage = !xhr.responseJSON ? `${xhr.status}: ${xhr.statusText}` : xhr.responseJSON.message;
53
+ var serverErrorCode = !xhr.responseJSON ? `unknown` : xhr.responseJSON.code;
54
+ var serverErrorId = !xhr.responseJSON ? `----` : xhr.responseJSON.id;
55
+ var serverErrorDetails = !xhr.responseJSON ? `no details provided` : xhr.responseJSON.details;
56
+
57
+ if (xhr.responseJSON && !serverErrorMessage) {
58
+ serverErrorMessage = JSON.stringify(xhr.responseJSON);
59
+ serverErrorCode = xhr.statusText + '(' + xhr.status + ')';
60
+ serverErrorId = `----`;
61
+ }
62
+
63
+ console.error(title + "\n" + serverErrorMessage + " : " + serverErrorDetails);
64
+ const notification = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="min-width: 50rem"/>`)
65
+ .append($(`<div class="toast-header bg-danger">
66
+ <strong class="me-auto text-dark">Error</strong>
67
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
68
+ </div>`))
69
+ .append($(`<div class="toast-body"/>`)
70
+ .append($(`<p/>`).text(title))
71
+ .append($(`<pre/>`)
72
+ .append($(`<code/>`).text(serverErrorMessage + "\n\nCode: " + serverErrorCode + "\nError id: " + serverErrorId))
73
+ )
74
+ );
75
+ $("#notificationPanel").append(notification);
76
+ notification.toast({delay: 30000});
77
+ notification.toast('show');
78
+ }
79
+
80
+ // ****************************************************************************
81
+ // Application info
82
+ // ****************************************************************************
83
+
84
+ function applicationInfo() {
85
+ $.getJSON("info", function (info) {
86
+ $("#applicationInfo").append("<small>" + info.application + " (version: " + info.version + ", built at: " + info.built + ")</small>");
87
+ }).fail(function (xhr, ajaxOptions, thrownError) {
88
+ console.warn("Unable to collect application information");
89
+ });
90
+ }
91
+
92
+ // ****************************************************************************
93
+ // TangoColorFactory
94
+ // ****************************************************************************
95
+
96
+ const SEQUENCE_1 = [0x8AE234, 0xFCE94F, 0x729FCF, 0xE9B96E, 0xAD7FA8];
97
+ const SEQUENCE_2 = [0x73D216, 0xEDD400, 0x3465A4, 0xC17D11, 0x75507B];
98
+
99
+ var colorMap = new Map;
100
+ var nextColorCount = 0;
101
+
102
+ function pickColor(object) {
103
+ let color = colorMap[object];
104
+ if (color !== undefined) {
105
+ return color;
106
+ }
107
+ color = nextColor();
108
+ colorMap[object] = color;
109
+ return color;
110
+ }
111
+
112
+ function nextColor() {
113
+ let color;
114
+ let colorIndex = nextColorCount % SEQUENCE_1.length;
115
+ let shadeIndex = Math.floor(nextColorCount / SEQUENCE_1.length);
116
+ if (shadeIndex === 0) {
117
+ color = SEQUENCE_1[colorIndex];
118
+ } else if (shadeIndex === 1) {
119
+ color = SEQUENCE_2[colorIndex];
120
+ } else {
121
+ shadeIndex -= 3;
122
+ let floorColor = SEQUENCE_2[colorIndex];
123
+ let ceilColor = SEQUENCE_1[colorIndex];
124
+ let base = Math.floor((shadeIndex / 2) + 1);
125
+ let divisor = 2;
126
+ while (base >= divisor) {
127
+ divisor *= 2;
128
+ }
129
+ base = (base * 2) - divisor + 1;
130
+ let shadePercentage = base / divisor;
131
+ color = buildPercentageColor(floorColor, ceilColor, shadePercentage);
132
+ }
133
+ nextColorCount++;
134
+ return "#" + color.toString(16);
135
+ }
136
+
137
+ function buildPercentageColor(floorColor, ceilColor, shadePercentage) {
138
+ let red = (floorColor & 0xFF0000) + Math.floor(shadePercentage * ((ceilColor & 0xFF0000) - (floorColor & 0xFF0000))) & 0xFF0000;
139
+ let green = (floorColor & 0x00FF00) + Math.floor(shadePercentage * ((ceilColor & 0x00FF00) - (floorColor & 0x00FF00))) & 0x00FF00;
140
+ let blue = (floorColor & 0x0000FF) + Math.floor(shadePercentage * ((ceilColor & 0x0000FF) - (floorColor & 0x0000FF))) & 0x0000FF;
141
+ return red | green | blue;
142
+ }
tests/__pycache__/test_constraints.cpython-312-pytest-8.2.2.pyc ADDED
Binary file (12 kB). View file
 
tests/test_constraints.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from solverforge_legacy.solver.test import ConstraintVerifier
2
+
3
+ from vm_placement.domain import Server, VM, VMPlacementPlan
4
+ from vm_placement.constraints import (
5
+ define_constraints,
6
+ cpu_capacity,
7
+ memory_capacity,
8
+ storage_capacity,
9
+ anti_affinity,
10
+ affinity,
11
+ minimize_servers_used,
12
+ balance_utilization,
13
+ prioritize_placement,
14
+ )
15
+
16
+ # VM is the only planning entity (Server is a problem fact)
17
+ constraint_verifier = ConstraintVerifier.build(
18
+ define_constraints, VMPlacementPlan, VM
19
+ )
20
+
21
+
22
+ def assign(server: Server, *vms: VM):
23
+ """Helper to assign VMs to a server."""
24
+ for vm in vms:
25
+ vm.server = server
26
+
27
+
28
+ ##############################################
29
+ # CPU Capacity Tests
30
+ ##############################################
31
+
32
+
33
+ def test_cpu_capacity_not_exceeded():
34
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
35
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=4, memory_gb=8, storage_gb=50)
36
+ vm2 = VM(id="vm2", name="VM2", cpu_cores=8, memory_gb=16, storage_gb=100)
37
+ assign(server, vm1, vm2)
38
+
39
+ (
40
+ constraint_verifier.verify_that(cpu_capacity)
41
+ .given(server, vm1, vm2)
42
+ .penalizes_by(0)
43
+ )
44
+
45
+
46
+ def test_cpu_capacity_exceeded():
47
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
48
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=12, memory_gb=8, storage_gb=50)
49
+ vm2 = VM(id="vm2", name="VM2", cpu_cores=8, memory_gb=16, storage_gb=100)
50
+ assign(server, vm1, vm2)
51
+
52
+ # 12 + 8 = 20 cores, capacity = 16, excess = 4
53
+ (
54
+ constraint_verifier.verify_that(cpu_capacity)
55
+ .given(server, vm1, vm2)
56
+ .penalizes_by(4)
57
+ )
58
+
59
+
60
+ ##############################################
61
+ # Memory Capacity Tests
62
+ ##############################################
63
+
64
+
65
+ def test_memory_capacity_not_exceeded():
66
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
67
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=4, memory_gb=32, storage_gb=50)
68
+ vm2 = VM(id="vm2", name="VM2", cpu_cores=4, memory_gb=16, storage_gb=100)
69
+ assign(server, vm1, vm2)
70
+
71
+ (
72
+ constraint_verifier.verify_that(memory_capacity)
73
+ .given(server, vm1, vm2)
74
+ .penalizes_by(0)
75
+ )
76
+
77
+
78
+ def test_memory_capacity_exceeded():
79
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
80
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=4, memory_gb=48, storage_gb=50)
81
+ vm2 = VM(id="vm2", name="VM2", cpu_cores=4, memory_gb=32, storage_gb=100)
82
+ assign(server, vm1, vm2)
83
+
84
+ # 48 + 32 = 80 GB, capacity = 64, excess = 16
85
+ (
86
+ constraint_verifier.verify_that(memory_capacity)
87
+ .given(server, vm1, vm2)
88
+ .penalizes_by(16)
89
+ )
90
+
91
+
92
+ ##############################################
93
+ # Storage Capacity Tests
94
+ ##############################################
95
+
96
+
97
+ def test_storage_capacity_not_exceeded():
98
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
99
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=4, memory_gb=8, storage_gb=200)
100
+ vm2 = VM(id="vm2", name="VM2", cpu_cores=4, memory_gb=16, storage_gb=250)
101
+ assign(server, vm1, vm2)
102
+
103
+ (
104
+ constraint_verifier.verify_that(storage_capacity)
105
+ .given(server, vm1, vm2)
106
+ .penalizes_by(0)
107
+ )
108
+
109
+
110
+ def test_storage_capacity_exceeded():
111
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
112
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=4, memory_gb=8, storage_gb=300)
113
+ vm2 = VM(id="vm2", name="VM2", cpu_cores=4, memory_gb=16, storage_gb=300)
114
+ assign(server, vm1, vm2)
115
+
116
+ # 300 + 300 = 600 GB, capacity = 500, excess = 100
117
+ (
118
+ constraint_verifier.verify_that(storage_capacity)
119
+ .given(server, vm1, vm2)
120
+ .penalizes_by(100)
121
+ )
122
+
123
+
124
+ ##############################################
125
+ # Anti-Affinity Tests
126
+ ##############################################
127
+
128
+
129
+ def test_anti_affinity_satisfied():
130
+ server1 = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
131
+ server2 = Server(id="s2", name="Server2", cpu_cores=16, memory_gb=64, storage_gb=500)
132
+ vm1 = VM(
133
+ id="vm1", name="DB-Primary", cpu_cores=4, memory_gb=8, storage_gb=50,
134
+ anti_affinity_group="db-replicas"
135
+ )
136
+ vm2 = VM(
137
+ id="vm2", name="DB-Replica", cpu_cores=4, memory_gb=8, storage_gb=50,
138
+ anti_affinity_group="db-replicas"
139
+ )
140
+ assign(server1, vm1)
141
+ assign(server2, vm2)
142
+
143
+ (
144
+ constraint_verifier.verify_that(anti_affinity)
145
+ .given(server1, server2, vm1, vm2)
146
+ .penalizes_by(0)
147
+ )
148
+
149
+
150
+ def test_anti_affinity_violated():
151
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
152
+ vm1 = VM(
153
+ id="vm1", name="DB-Primary", cpu_cores=4, memory_gb=8, storage_gb=50,
154
+ anti_affinity_group="db-replicas"
155
+ )
156
+ vm2 = VM(
157
+ id="vm2", name="DB-Replica", cpu_cores=4, memory_gb=8, storage_gb=50,
158
+ anti_affinity_group="db-replicas"
159
+ )
160
+ assign(server, vm1, vm2)
161
+
162
+ # Both VMs on same server with same anti-affinity group = 1 violation
163
+ (
164
+ constraint_verifier.verify_that(anti_affinity)
165
+ .given(server, vm1, vm2)
166
+ .penalizes_by(1)
167
+ )
168
+
169
+
170
+ def test_anti_affinity_no_group():
171
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
172
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=4, memory_gb=8, storage_gb=50)
173
+ vm2 = VM(id="vm2", name="VM2", cpu_cores=4, memory_gb=8, storage_gb=50)
174
+ assign(server, vm1, vm2)
175
+
176
+ # No anti-affinity group, so no penalty
177
+ (
178
+ constraint_verifier.verify_that(anti_affinity)
179
+ .given(server, vm1, vm2)
180
+ .penalizes_by(0)
181
+ )
182
+
183
+
184
+ ##############################################
185
+ # Affinity Tests
186
+ ##############################################
187
+
188
+
189
+ def test_affinity_satisfied():
190
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
191
+ vm1 = VM(
192
+ id="vm1", name="WebApp1", cpu_cores=2, memory_gb=4, storage_gb=20,
193
+ affinity_group="web-tier"
194
+ )
195
+ vm2 = VM(
196
+ id="vm2", name="WebApp2", cpu_cores=2, memory_gb=4, storage_gb=20,
197
+ affinity_group="web-tier"
198
+ )
199
+ assign(server, vm1, vm2)
200
+
201
+ (
202
+ constraint_verifier.verify_that(affinity)
203
+ .given(server, vm1, vm2)
204
+ .penalizes_by(0)
205
+ )
206
+
207
+
208
+ def test_affinity_violated():
209
+ server1 = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
210
+ server2 = Server(id="s2", name="Server2", cpu_cores=16, memory_gb=64, storage_gb=500)
211
+ vm1 = VM(
212
+ id="vm1", name="WebApp1", cpu_cores=2, memory_gb=4, storage_gb=20,
213
+ affinity_group="web-tier"
214
+ )
215
+ vm2 = VM(
216
+ id="vm2", name="WebApp2", cpu_cores=2, memory_gb=4, storage_gb=20,
217
+ affinity_group="web-tier"
218
+ )
219
+ assign(server1, vm1)
220
+ assign(server2, vm2)
221
+
222
+ # VMs on different servers with same affinity group = penalty of 100
223
+ (
224
+ constraint_verifier.verify_that(affinity)
225
+ .given(server1, server2, vm1, vm2)
226
+ .penalizes_by(100)
227
+ )
228
+
229
+
230
+ def test_affinity_no_group():
231
+ server1 = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
232
+ server2 = Server(id="s2", name="Server2", cpu_cores=16, memory_gb=64, storage_gb=500)
233
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=2, memory_gb=4, storage_gb=20)
234
+ vm2 = VM(id="vm2", name="VM2", cpu_cores=2, memory_gb=4, storage_gb=20)
235
+ assign(server1, vm1)
236
+ assign(server2, vm2)
237
+
238
+ # No affinity group, so no penalty
239
+ (
240
+ constraint_verifier.verify_that(affinity)
241
+ .given(server1, server2, vm1, vm2)
242
+ .penalizes_by(0)
243
+ )
244
+
245
+
246
+ ##############################################
247
+ # Minimize Servers Used Tests
248
+ ##############################################
249
+
250
+
251
+ def test_minimize_servers_no_active():
252
+ server1 = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
253
+ server2 = Server(id="s2", name="Server2", cpu_cores=16, memory_gb=64, storage_gb=500)
254
+
255
+ (
256
+ constraint_verifier.verify_that(minimize_servers_used)
257
+ .given(server1, server2)
258
+ .penalizes_by(0)
259
+ )
260
+
261
+
262
+ def test_minimize_servers_one_active():
263
+ server1 = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
264
+ server2 = Server(id="s2", name="Server2", cpu_cores=16, memory_gb=64, storage_gb=500)
265
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=2, memory_gb=4, storage_gb=20)
266
+ assign(server1, vm1)
267
+
268
+ # 1 active server = 100 penalty
269
+ (
270
+ constraint_verifier.verify_that(minimize_servers_used)
271
+ .given(server1, server2, vm1)
272
+ .penalizes_by(100)
273
+ )
274
+
275
+
276
+ def test_minimize_servers_two_active():
277
+ server1 = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
278
+ server2 = Server(id="s2", name="Server2", cpu_cores=16, memory_gb=64, storage_gb=500)
279
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=2, memory_gb=4, storage_gb=20)
280
+ vm2 = VM(id="vm2", name="VM2", cpu_cores=2, memory_gb=4, storage_gb=20)
281
+ assign(server1, vm1)
282
+ assign(server2, vm2)
283
+
284
+ # 2 active servers = 200 penalty
285
+ (
286
+ constraint_verifier.verify_that(minimize_servers_used)
287
+ .given(server1, server2, vm1, vm2)
288
+ .penalizes_by(200)
289
+ )
290
+
291
+
292
+ ##############################################
293
+ # Balance Utilization Tests
294
+ ##############################################
295
+
296
+
297
+ def test_balance_utilization_empty():
298
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
299
+
300
+ (
301
+ constraint_verifier.verify_that(balance_utilization)
302
+ .given(server)
303
+ .penalizes_by(0)
304
+ )
305
+
306
+
307
+ def test_balance_utilization_50_percent():
308
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
309
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=8, memory_gb=32, storage_gb=250)
310
+ assign(server, vm1)
311
+
312
+ # 50% utilization = 0.5, squared = 0.25, * 10 = 2.5, int = 2
313
+ (
314
+ constraint_verifier.verify_that(balance_utilization)
315
+ .given(server, vm1)
316
+ .penalizes_by(2)
317
+ )
318
+
319
+
320
+ def test_balance_utilization_100_percent():
321
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
322
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=16, memory_gb=64, storage_gb=500)
323
+ assign(server, vm1)
324
+
325
+ # 100% utilization = 1.0, squared = 1.0, * 10 = 10
326
+ (
327
+ constraint_verifier.verify_that(balance_utilization)
328
+ .given(server, vm1)
329
+ .penalizes_by(10)
330
+ )
331
+
332
+
333
+ ##############################################
334
+ # Prioritize Placement Tests
335
+ ##############################################
336
+
337
+
338
+ def test_prioritize_placement_assigned():
339
+ server = Server(id="s1", name="Server1", cpu_cores=16, memory_gb=64, storage_gb=500)
340
+ vm1 = VM(id="vm1", name="VM1", cpu_cores=4, memory_gb=8, storage_gb=50, priority=5)
341
+ assign(server, vm1)
342
+
343
+ (
344
+ constraint_verifier.verify_that(prioritize_placement)
345
+ .given(server, vm1)
346
+ .penalizes_by(0)
347
+ )