Spaces:
Sleeping
Sleeping
- Dockerfile +24 -0
- README.md +133 -6
- pyproject.toml +20 -0
- src/vm_placement/__init__.py +16 -0
- src/vm_placement/__pycache__/__init__.cpython-312.pyc +0 -0
- src/vm_placement/__pycache__/constraints.cpython-312.pyc +0 -0
- src/vm_placement/__pycache__/converters.cpython-312.pyc +0 -0
- src/vm_placement/__pycache__/demo_data.cpython-312.pyc +0 -0
- src/vm_placement/__pycache__/domain.cpython-312.pyc +0 -0
- src/vm_placement/__pycache__/json_serialization.cpython-312.pyc +0 -0
- src/vm_placement/__pycache__/rest_api.cpython-312.pyc +0 -0
- src/vm_placement/__pycache__/solver.cpython-312.pyc +0 -0
- src/vm_placement/constraints.py +200 -0
- src/vm_placement/converters.py +161 -0
- src/vm_placement/demo_data.py +449 -0
- src/vm_placement/domain.py +206 -0
- src/vm_placement/json_serialization.py +81 -0
- src/vm_placement/rest_api.py +150 -0
- src/vm_placement/solver.py +23 -0
- static/app.js +974 -0
- static/config.js +163 -0
- static/index.html +701 -0
- static/webjars/solverforge/css/solverforge-webui.css +68 -0
- static/webjars/solverforge/img/solverforge-favicon.svg +65 -0
- static/webjars/solverforge/img/solverforge-horizontal-white.svg +66 -0
- static/webjars/solverforge/img/solverforge-horizontal.svg +65 -0
- static/webjars/solverforge/img/solverforge-logo-stacked.svg +73 -0
- static/webjars/solverforge/js/solverforge-webui.js +142 -0
- tests/__pycache__/test_constraints.cpython-312-pytest-8.2.2.pyc +0 -0
- tests/test_constraints.py +347 -0
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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: apache-2.0
|
| 9 |
-
short_description:
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
)
|