Stitching things up

This commit is contained in:
2025-12-03 12:04:48 +03:00
parent 79bad39bd9
commit 4e37cd1ef8
37 changed files with 2183 additions and 2154 deletions

View File

@@ -1,17 +0,0 @@
from django import template
register = template.Library()
@register.filter
def div(a, b):
try:
return float(a) / float(b)
except:
return 0
@register.filter
def mul(a, b):
try:
return float(a) * float(b)
except:
return 0

View File

@@ -1,6 +0,0 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]

View File

@@ -1,174 +0,0 @@
import json
from django.shortcuts import render
_BASE = {
"region_name": "ct3k1ldt"
}
def index(request):
context = {**_BASE,
# CPU and RAM utilization data
'cpu_used': 42,
'cpu_free': 58,
'cpu_used_percentage': 42.0,
'ram_used': 128,
'ram_free': 256,
'ram_used_percentage': 33.3,
# Instance summary data
'vm_count': 47,
'vm_active': 42,
'vm_stopped': 5,
'vm_error': 0,
'common_flavor': 'm1.medium',
'common_flavor_count': 18,
'second_common_flavor': {
'name': 'm1.small',
'count': 12
},
'third_common_flavor': {
'name': 'm1.large',
'count': 8
},
# Resource allocation data
'cpu_allocated': 94,
'cpu_total': 160,
'cpu_overcommit_ratio': 1.5,
'ram_allocated': 384,
'ram_total': 512,
'ram_overcommit_ratio': 1.2,
# Quick stats
'avg_cpu_per_vm': 2.0,
'avg_ram_per_vm': 8.2,
'vm_density': 9.4,
# Audit data
'audits': [
{
'id': 'audit_001',
'name': 'Weekly Optimization',
'created_at': '2024-01-15',
'cpu_weight': 1.2,
'ram_weight': 0.8,
'scope': 'Full Cluster',
'strategy': 'Load Balancing',
'goal': 'Optimize CPU distribution across all hosts',
'migrations': [
{
'instanceName': 'web-server-01',
'source': 'compute-02',
'destination': 'compute-05',
'flavor': 'm1.medium',
'impact': 'Low'
},
{
'instanceName': 'db-replica-03',
'source': 'compute-01',
'destination': 'compute-04',
'flavor': 'm1.large',
'impact': 'Medium'
},
{
'instanceName': 'api-gateway',
'source': 'compute-03',
'destination': 'compute-06',
'flavor': 'm1.small',
'impact': 'Low'
},
{
'instanceName': 'cache-node-02',
'source': 'compute-01',
'destination': 'compute-07',
'flavor': 'm1.small',
'impact': 'Low'
},
{
'instanceName': 'monitoring-server',
'source': 'compute-04',
'destination': 'compute-02',
'flavor': 'm1.medium',
'impact': 'Low'
}
],
'host_labels': ['compute-01', 'compute-02', 'compute-03', 'compute-04', 'compute-05', 'compute-06', 'compute-07'],
'cpu_current': [78, 65, 42, 89, 34, 56, 71],
'cpu_projected': [65, 58, 45, 72, 48, 61, 68]
},
{
'id': 'audit_002',
'name': 'Emergency Rebalance',
'created_at': '2024-01-14',
'cpu_weight': 1.0,
'ram_weight': 1.0,
'scope': 'Overloaded Hosts',
'strategy': 'Hotspot Reduction',
'goal': 'Reduce load on compute-01 and compute-04',
'migrations': [
{
'instanceName': 'app-server-02',
'source': 'compute-01',
'destination': 'compute-06',
'flavor': 'm1.medium',
'impact': 'Medium'
},
{
'instanceName': 'file-server-01',
'source': 'compute-04',
'destination': 'compute-07',
'flavor': 'm1.large',
'impact': 'High'
}
],
'host_labels': ['compute-01', 'compute-02', 'compute-03', 'compute-04', 'compute-05', 'compute-06', 'compute-07'],
'cpu_current': [92, 65, 42, 85, 34, 56, 71],
'cpu_projected': [72, 65, 42, 65, 34, 66, 81]
},
{
'id': 'audit_003',
'name': 'Pre-Maintenance Planning',
'created_at': '2024-01-10',
'cpu_weight': 0.8,
'ram_weight': 1.5,
'scope': 'Maintenance Zone',
'strategy': 'Evacuation',
'goal': 'Empty compute-03 for maintenance',
'migrations': [
{
'instanceName': 'test-vm-01',
'source': 'compute-03',
'destination': 'compute-02',
'flavor': 'm1.small',
'impact': 'Low'
},
{
'instanceName': 'dev-server',
'source': 'compute-03',
'destination': 'compute-05',
'flavor': 'm1.medium',
'impact': 'Low'
},
{
'instanceName': 'staging-db',
'source': 'compute-03',
'destination': 'compute-07',
'flavor': 'm1.large',
'impact': 'High'
}
],
'host_labels': ['compute-01', 'compute-02', 'compute-03', 'compute-04', 'compute-05', 'compute-06', 'compute-07'],
'cpu_current': [78, 65, 56, 89, 34, 56, 71],
'cpu_projected': [78, 75, 0, 89, 54, 56, 81]
}
]
}
# Serialize lists for JavaScript
for audit in context['audits']:
audit['migrations'] = json.dumps(audit['migrations'])
audit['host_labels'] = json.dumps(audit['host_labels'])
audit['cpu_current'] = json.dumps(audit['cpu_current'])
audit['cpu_projected'] = json.dumps(audit['cpu_projected'])
return render(request, 'index.html', context)

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'watcher_visio.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@@ -1,34 +0,0 @@
asgiref==3.11.0
certifi==2025.11.12
cffi==2.0.0
charset-normalizer==3.4.4
cryptography==46.0.3
decorator==5.2.1
Django==5.2.8
dogpile.cache==1.5.0
idna==3.11
iso8601==2.1.0
jmespath==1.0.1
jsonpatch==1.33
jsonpointer==3.0.0
keystoneauth1==5.12.0
numpy==2.3.5
openstacksdk==4.8.0
os-service-types==1.8.2
pandas==2.3.3
pbr==7.0.3
platformdirs==4.5.0
psutil==7.1.3
pycparser==2.23
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.3
requests==2.32.5
requestsexceptions==1.4.0
setuptools==80.9.0
six==1.17.0
sqlparse==0.5.4
stevedore==5.6.0
typing_extensions==4.15.0
tzdata==2025.2
urllib3==2.5.0

View File

@@ -41,8 +41,17 @@ INSTALLED_APPS = [
]
# Prometheus settings (environment override recommended)
PROMETHEUS_URL = "http://localhost:9090" # replace with your Prometheus HTTP endpoint
PROMETHEUS_DEFAULT_METRIC = "libvirt_domain_info_memory_usage_bytes"
PROMETHEUS_URL = "http://localhost:9090"
PROMETHEUS_DEFAULT_CPU_METRIC = "libvirt_domain_info_cpu_time_seconds_total"
PROMETHEUS_DEFAULT_RAM_METRIC = "libvirt_domain_info_memory_usage_bytes"
# Openstack cloud settings
OPENSTACK_REGION_NAME = "default"
OPENSTACK_CLOUD = "default"
# Openstack watcher endoint settings
WATCHER_ENDPOINT_NAME = "infra-optim"
WATCHER_INTERFACE_NAME = "public"
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
@import "tailwindcss" source(none);
@plugin "./daisyui.js";
@source "../../templates";

View File

@@ -1,9 +0,0 @@
// Audit-related JavaScript functions
function initializeAuditSelector() {
const selector = document.getElementById('auditSelector');
if (selector && selector.options.length > 0) {
selector.dispatchEvent(new Event('change'));
}
}
// Export other audit-related functions...

View File

@@ -1,55 +0,0 @@
// Chart configuration and initialization
const chartConfig = {
cutout: "60%",
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return `${context.label}: ${context.parsed} vCPU`;
}
}
}
},
animation: {
animateScale: true,
animateRotate: true
}
};
function initializeUtilizationCharts(cpuMean, ramMean) {
const cpuCtx = document.getElementById("cpuChart").getContext('2d');
const ramCtx = document.getElementById("ramChart").getContext('2d');
new Chart(cpuCtx, {
type: "doughnut",
data: {
labels: ["Used", "Free"],
datasets: [{
data: [{{ cpu_used }}, {{ cpu_free }}],
backgroundColor: ["#3b82f6", "#e5e7eb"],
borderWidth: 2,
borderColor: "#ffffff"
}]
},
options: chartConfig
});
new Chart(ramCtx, {
type: "doughnut",
data: {
labels: ["Used", "Free"],
datasets: [{
data: [{{ ram_used }}, {{ ram_free }}],
backgroundColor: ["#f97316", "#e5e7eb"],
borderWidth: 2,
borderColor: "#ffffff"
}]
},
options: chartConfig
});
}
// Export other chart-related functions...

View File

@@ -1,12 +0,0 @@
// Utility functions
function calculateStats(data) {
if (!data || data.length === 0) return { mean: 0, std: 0 };
const mean = data.reduce((a, b) => a + b, 0) / data.length;
const variance = data.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / data.length;
const std = Math.sqrt(variance);
return { mean, std };
}
// Export other utility functions...

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,64 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Watcher Visio{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/output.css' %}">
{% block imports %}
{% endblock %}
</head>
<body>
<!-- Navbar -->
<div class="navbar bg-base-100 shadow-lg">
<div class="navbar-start">
<a class="btn btn-ghost text-xl" href="{% url 'index' %}">Watcher Visio</a>
</div>
<div class="navbar-center hidden lg:flex">
</div>
<div class="navbar-end">
<div class="px-1 flex gap-3 pr-10">
<span class="badge badge-primary badge-lg">{{ region_name }}</span>
<label class="swap swap-rotate">
<input type="checkbox" class="theme-controller" value="dark" />
<svg class="swap-off fill-current w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"/>
</svg>
<svg class="swap-on fill-current w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"/>
</svg>
</label>
</div>
</div>
</div>
<!-- Main Content -->
<main class="container mx-auto px-4 py-8 min-h-screen">
{% block content %}
{% endblock %}
</main>
<script>
// Function to apply theme
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
const checkbox = document.querySelector('.theme-controller');
checkbox.checked = (theme === 'dark');
}
// Load saved theme from localStorage
const savedTheme = localStorage.getItem('theme') || 'light';
applyTheme(savedTheme);
// Listen for toggle changes
document.querySelector('.theme-controller').addEventListener('change', function() {
const newTheme = this.checked ? 'dark' : 'light';
applyTheme(newTheme);
localStorage.setItem('theme', newTheme);
});
</script>
{% block script %}
{% endblock %}
</body>
</html>

View File

@@ -1,866 +0,0 @@
{% extends 'base.html' %}
{% load static mathfilters %}
{% block title %}System Analytics - Django + DaisyUI{% endblock %}
{% block imports %}
<script src="{% static 'js/chart.js' %}"></script>
<script src="{% static 'js/chartjs-plugin-datalabels.min.js' %}"></script>
{% endblock %}
{% block content %}
<!-- MAIN GRID -->
<div class="p-4 grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- CPU Utilization Card -->
<div class="card bg-base-100 shadow hover:shadow-md transition-shadow">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-2">
<h2 class="card-title text-sm font-semibold">CPU Utilization</h2>
<div class="text-xs badge badge-ghost">{{ cpu_used }} / {{ cpu_used|add:cpu_free }} vCPU</div>
</div>
<div class="relative h-40 flex items-center justify-center">
<canvas id="cpuChart"></canvas>
<div class="absolute inset-0 flex items-center justify-center">
<div class="text-center">
<div class="text-2xl font-bold text-primary">{{ cpu_used_percentage|floatformat:1 }}%</div>
<div class="text-xs text-base-content/60">Used</div>
</div>
</div>
</div>
<div class="flex justify-between text-xs mt-2">
<span class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full bg-primary"></span>
Used: {{ cpu_used }} vCPU
</span>
<span class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full bg-base-300"></span>
Free: {{ cpu_free }} vCPU
</span>
</div>
</div>
</div>
<!-- RAM Utilization Card -->
<div class="card bg-base-100 shadow hover:shadow-md transition-shadow">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-2">
<h2 class="card-title text-sm font-semibold">RAM Utilization</h2>
<div class="text-xs badge badge-ghost">{{ ram_used }} / {{ ram_used|add:ram_free }} GB</div>
</div>
<div class="relative h-40 flex items-center justify-center">
<canvas id="ramChart"></canvas>
<div class="absolute inset-0 flex items-center justify-center">
<div class="text-center">
<div class="text-2xl font-bold text-secondary">{{ ram_used_percentage|floatformat:1 }}%</div>
<div class="text-xs text-base-content/60">Used</div>
</div>
</div>
</div>
<div class="flex justify-between text-xs mt-2">
<span class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full bg-secondary"></span>
Used: {{ ram_used }} GB
</span>
<span class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full bg-base-300"></span>
Free: {{ ram_free }} GB
</span>
</div>
</div>
</div>
<!-- Instance Summary Card - IMPROVED -->
<div class="card bg-base-100 shadow hover:shadow-md transition-shadow col-span-1 lg:col-span-2">
<div class="card-body p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="card-title text-lg font-semibold flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
Instance Summary
</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- VM Statistics -->
<div class="bg-base-200 rounded-xl p-4 space-y-4">
<div class="flex items-center justify-between">
<div>
<div class="text-sm font-medium text-base-content/70">Total Instances</div>
<div class="text-3xl font-bold text-primary mt-1">{{ vm_count }}</div>
</div>
<div class="text-primary opacity-80">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between items-center">
<span class="text-xs text-base-content/60">Active</span>
<span class="font-semibold">{{ vm_active|default:"N/A" }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-xs text-base-content/60">Stopped</span>
<span class="font-semibold">{{ vm_stopped|default:"N/A" }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-xs text-base-content/60">Error</span>
<span class="font-semibold">{{ vm_error|default:"0" }}</span>
</div>
</div>
</div>
<!-- Top Flavor -->
<div class="bg-base-200 rounded-xl p-4 space-y-4">
<div class="flex items-center justify-between">
<div>
<div class="text-sm font-medium text-base-content/70">Most Used Flavor</div>
<div class="text-2xl font-bold text-secondary mt-1">{{ common_flavor }}</div>
</div>
<div class="text-secondary opacity-80">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between bg-base-300/50 p-2 rounded-lg">
<div>
<div class="text-xs text-base-content/60">Instances</div>
<div class="font-bold">{{ common_flavor_count }}</div>
</div>
<div class="text-right">
<div class="text-xs text-base-content/60">Share</div>
<div class="font-bold">
{{ common_flavor_count|div:vm_count|mul:100|floatformat:0 }}%
</div>
</div>
</div>
{% if second_common_flavor %}
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-xs text-base-content/60">2nd:</span>
<span class="font-medium">{{ second_common_flavor.name }}</span>
</div>
<span class="text-xs badge badge-outline">{{ second_common_flavor.count }}</span>
</div>
{% endif %}
{% if third_common_flavor %}
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-xs text-base-content/60">3rd:</span>
<span class="font-medium">{{ third_common_flavor.name }}</span>
</div>
<span class="text-xs badge badge-outline">{{ third_common_flavor.count }}</span>
</div>
{% endif %}
</div>
</div>
<!-- Resource Allocation -->
<div class="bg-base-200 rounded-xl p-4 space-y-4">
<div class="flex items-center justify-between">
<div>
<div class="text-sm font-medium text-base-content/70">Resource Allocation</div>
<div class="text-xs text-base-content/60">Provisioned vs Total</div>
</div>
<div class="text-warning opacity-80">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<div class="space-y-3">
<!-- CPU Allocation -->
<div>
<div class="flex justify-between text-xs mb-1">
<div class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full bg-primary"></span>
<span>CPU</span>
</div>
<div class="font-semibold">
{{ cpu_allocated }} / {{ cpu_total }} vCPU
</div>
</div>
{% with pct=cpu_allocated|div:cpu_total|mul:100 %}
<div class="relative pt-1">
<div class="flex mb-2 items-center justify-between">
<div class="text-right">
<span class="text-xs font-semibold inline-block">
{{ pct|floatformat:1 }}%
</span>
</div>
</div>
<div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-base-300">
<div style="width: {{ pct }}%" class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-primary"></div>
</div>
<div class="flex justify-between text-xs">
<span>Allocated</span>
<span class="text-warning">OC: x{{ cpu_overcommit_ratio }}</span>
</div>
</div>
{% endwith %}
</div>
<!-- RAM Allocation -->
<div>
<div class="flex justify-between text-xs mb-1">
<div class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full bg-secondary"></span>
<span>RAM</span>
</div>
<div class="font-semibold">
{{ ram_allocated }} / {{ ram_total }} GB
</div>
</div>
{% with pct=ram_allocated|div:ram_total|mul:100 %}
<div class="relative pt-1">
<div class="flex mb-2 items-center justify-between">
<div class="text-right">
<span class="text-xs font-semibold inline-block">
{{ pct|floatformat:1 }}%
</span>
</div>
</div>
<div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-base-300">
<div style="width: {{ pct }}%" class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-secondary"></div>
</div>
<div class="flex justify-between text-xs">
<span>Allocated</span>
<span class="text-warning">OC: x{{ ram_overcommit_ratio }}</span>
</div>
</div>
{% endwith %}
</div>
</div>
</div>
</div>
<!-- Quick Stats Footer -->
<div class="mt-4 pt-4 border-t border-base-300">
<div class="flex flex-wrap gap-3">
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-success"></div>
<span class="text-xs">Avg. CPU per VM:</span>
<span class="text-xs font-semibold">{{ avg_cpu_per_vm|floatformat:1 }} vCPU</span>
</div>
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-info"></div>
<span class="text-xs">Avg. RAM per VM:</span>
<span class="text-xs font-semibold">{{ avg_ram_per_vm|floatformat:1 }} GB</span>
</div>
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full bg-warning"></div>
<span class="text-xs">Density:</span>
<span class="text-xs font-semibold">{{ vm_density|floatformat:1 }} VMs/host</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- AUDIT ANALYSIS SECTION -->
<div class="p-4">
<div class="card bg-base-100 shadow">
<div class="card-body p-4">
<!-- Selector Row -->
<div class="flex flex-col md:flex-row md:items-center gap-4">
<div class="flex-1">
<label class="label pb-1">
<span class="label-text font-medium">Available Audits</span>
<span class="label-text-alt text-xs">{{ audits|length }} available</span>
</label>
<select id="auditSelector" class="select select-bordered w-full">
{% for audit in audits %}
<option value="{{ audit.id }}"
data-cpu="{{ audit.cpu_weight }}"
data-ram="{{ audit.ram_weight }}"
data-scope="{{ audit.scope }}"
data-strategy="{{ audit.strategy }}"
data-goal="{{ audit.goal }}">
{{ audit.name }} ({{ audit.created_at|date:"m/d/y" }})
</option>
{% endfor %}
</select>
</div>
</div>
<!-- Full Parameters (Initially hidden, can be toggled) -->
<div id="auditDetails" class="mt-4 space-y-3 hidden">
<div class="divider text-xs">Audit Configuration</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<div class="text-sm font-medium mb-2">Resource Weights</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm">CPU Priority:</span>
<span class="font-bold text-primary" id="detailCpu">1.0</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">RAM Priority:</span>
<span class="font-bold text-secondary" id="detailRam">1.0</span>
</div>
</div>
</div>
<div>
<div class="text-sm font-medium mb-2">Configuration</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm">Strategy:</span>
<span class="font-medium" id="detailStrategy">Balanced</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">Scope:</span>
<span class="font-medium" id="detailScope">Full Cluster</span>
</div>
</div>
</div>
</div>
<div>
<div class="text-sm font-medium mb-2">Goal</div>
<div class="bg-base-200 rounded p-3 text-sm" id="detailGoal">
Optimize resource distribution
</div>
</div>
</div>
<!-- Simple Footer -->
<div class="flex justify-between items-center mt-4 pt-4 border-t border-base-300">
<div class="text-xs text-base-content/60" id="selectedAuditInfo">
Selected: {{ audits.0.name|default:"No audit selected" }}
</div>
<div class="flex gap-2">
<button class="btn btn-xs btn-ghost" onclick="toggleAuditDetails()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Details
</button>
<button class="btn btn-xs btn-primary" onclick="loadSelectedAudit()">
Load Audit
</button>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 space-y-4">
<!-- Charts Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="card bg-base-100 shadow">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold">Current CPU Utilization</h3>
<div class="badge badge-primary badge-outline">Live</div>
</div>
<div class="h-64">
<canvas id="cpuHostChart"></canvas>
</div>
<div class="text-center text-xs text-base-content/60 mt-2">
Real-time CPU usage across compute hosts
</div>
<!-- Mean baseline indicator -->
<div class="flex items-center justify-center gap-2 mt-2">
<div class="flex items-center gap-1">
<div class="w-3 h-0.5 bg-success"></div>
<span class="text-xs text-success">Mean: <span id="currentCpuMean">0</span>%</span>
</div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold">Projected CPU Utilization</h3>
<div class="badge badge-warning badge-outline">Projection</div>
</div>
<div class="h-64">
<canvas id="cpuProjectedChart"></canvas>
</div>
<div class="text-center text-xs text-base-content/60 mt-2">
Estimated CPU usage after migrations
</div>
<!-- Mean and std deviation indicators -->
<div class="flex items-center justify-center gap-4 mt-2">
<div class="flex items-center gap-1">
<div class="w-3 h-0.5 bg-success"></div>
<span class="text-xs text-success">Mean: <span id="projectedCpuMean">0</span>%</span>
</div>
<div class="flex items-center gap-1">
<div class="w-3 h-0.5 bg-error/60"></div>
<span class="text-xs text-error/60">±1σ: <span id="projectedCpuStd">0</span>%</span>
</div>
</div>
</div>
</div>
</div>
<!-- Migration Table -->
<div class="card bg-base-100 shadow">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="font-semibold">Migration Actions</h3>
<div class="badge badge-neutral" id="migrationCount">0 migrations</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra table-sm">
<thead>
<tr class="bg-base-200">
<th class="text-xs font-semibold">Instance</th>
<th class="text-xs font-semibold">Source Host</th>
<th class="text-xs font-semibold">Destination Host</th>
<th class="text-xs font-semibold">Flavor</th>
<th class="text-xs font-semibold">Impact</th>
</tr>
</thead>
<tbody id="migrationTableBody" class="text-sm">
<!-- Will be populated by JavaScript -->
<tr>
<td colspan="5" class="text-center py-8 text-base-content/40">
Select an audit to view migration actions
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script>
// Update preview when selection changes
document.getElementById('auditSelector').addEventListener('change', function(e) {
const option = this.options[this.selectedIndex];
// Update details section if visible
document.getElementById('detailCpu').textContent = option.dataset.cpu || '1.0';
document.getElementById('detailRam').textContent = option.dataset.ram || '1.0';
document.getElementById('detailScope').textContent = option.dataset.scope || 'Full Cluster';
document.getElementById('detailStrategy').textContent = option.dataset.strategy || 'Balanced';
document.getElementById('detailGoal').textContent = option.dataset.goal || 'Optimize resource distribution';
document.getElementById('selectedAuditInfo').textContent = `Selected: ${option.text}`;
});
function toggleAuditDetails() {
const details = document.getElementById('auditDetails');
details.classList.toggle('hidden');
}
function loadSelectedAudit() {
const auditId = document.getElementById('auditSelector').value;
// Your existing load audit logic here
updateMigrationTable(auditId);
updateCPUCharts(auditId);
// Show toast notification
const toast = document.createElement('div');
toast.className = 'toast toast-top toast-end';
toast.innerHTML = `
<div class="alert alert-success">
<span>Audit loaded successfully</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
const selector = document.getElementById('auditSelector');
if (selector.options.length > 0) {
selector.dispatchEvent(new Event('change'));
}
});
// Utility function to calculate mean and standard deviation
function calculateStats(data) {
if (!data || data.length === 0) return { mean: 0, std: 0 };
const mean = data.reduce((a, b) => a + b, 0) / data.length;
const variance = data.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / data.length;
const std = Math.sqrt(variance);
return { mean, std };
}
// Chart configuration
const chartConfig = {
cutout: "60%",
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return `${context.label}: ${context.parsed} vCPU`;
}
}
}
},
animation: {
animateScale: true,
animateRotate: true
}
};
// Initialize CPU and RAM donut charts with mean baselines
const cpuCtx = document.getElementById("cpuChart").getContext('2d');
const ramCtx = document.getElementById("ramChart").getContext('2d');
// Calculate CPU mean for baseline
const cpuMean = {{ cpu_used_percentage }};
const cpuChart = new Chart(cpuCtx, {
type: "doughnut",
data: {
labels: ["Used", "Free"],
datasets: [{
data: [{{ cpu_used }}, {{ cpu_free }}],
backgroundColor: ["#3b82f6", "#e5e7eb"],
borderWidth: 2,
borderColor: "#ffffff"
}]
},
options: {
...chartConfig,
plugins: {
...chartConfig.plugins,
annotation: {
annotations: {
meanLine: {
type: 'line',
yMin: cpuMean,
yMax: cpuMean,
borderColor: '#10b981',
borderWidth: 2,
borderDash: [5, 5],
label: {
enabled: true,
content: `Mean: ${cpuMean.toFixed(1)}%`,
position: 'end',
backgroundColor: '#10b981',
color: 'white',
font: {
size: 10
}
}
}
}
}
}
}
});
// Calculate RAM mean for baseline
const ramMean = {{ ram_used_percentage }};
const ramChart = new Chart(ramCtx, {
type: "doughnut",
data: {
labels: ["Used", "Free"],
datasets: [{
data: [{{ ram_used }}, {{ ram_free }}],
backgroundColor: ["#f97316", "#e5e7eb"],
borderWidth: 2,
borderColor: "#ffffff"
}]
},
options: {
...chartConfig,
plugins: {
...chartConfig.plugins,
annotation: {
annotations: {
meanLine: {
type: 'line',
yMin: ramMean,
yMax: ramMean,
borderColor: '#10b981',
borderWidth: 2,
borderDash: [5, 5],
label: {
enabled: true,
content: `Mean: ${ramMean.toFixed(1)}%`,
position: 'end',
backgroundColor: '#10b981',
color: 'white',
font: {
size: 10
}
}
}
}
}
}
}
});
// Audit data (in production, this should come from Django context)
const auditData = {
{% for audit in audits %}
"{{ audit.id }}": {
name: "{{ audit.name }}",
migrations: {{ audit.migrations|safe }},
hostData: {
labels: {{ audit.host_labels|safe }},
current: {{ audit.cpu_current|safe }},
projected: {{ audit.cpu_projected|safe }}
}
}{% if not forloop.last %},{% endif %}
{% endfor %}
};
// Chart instances
let cpuHostChart = null;
let cpuProjectedChart = null;
// Update migration table
function updateMigrationTable(auditId) {
const tbody = document.getElementById('migrationTableBody');
const migrationCount = document.getElementById('migrationCount');
const data = auditData[auditId];
if (!data || data.migrations.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="text-center py-8 text-base-content/40">
No migration actions for this audit
</td>
</tr>
`;
migrationCount.textContent = '0 migrations';
return;
}
let html = '';
data.migrations.forEach(migration => {
const impact = migration.impact || 'Low';
const impactClass = {
'Low': 'badge-success',
'Medium': 'badge-warning',
'High': 'badge-error'
}[impact];
html += `
<tr>
<td class="font-medium">${migration.instanceName}</td>
<td><span class="badge badge-outline">${migration.source}</span></td>
<td><span class="badge badge-primary badge-outline">${migration.destination}</span></td>
<td><code class="text-xs">${migration.flavor}</code></td>
<td><span class="badge ${impactClass}">${impact}</span></td>
</tr>
`;
});
tbody.innerHTML = html;
migrationCount.textContent = `${data.migrations.length} migration${data.migrations.length !== 1 ? 's' : ''}`;
}
// Update CPU charts
function updateCPUCharts(auditId) {
const data = auditData[auditId];
if (!data) return;
const hostCtx = document.getElementById('cpuHostChart').getContext('2d');
const projCtx = document.getElementById('cpuProjectedChart').getContext('2d');
// Calculate statistics
const currentStats = calculateStats(data.hostData.current);
const projectedStats = calculateStats(data.hostData.projected);
// Update stats displays
document.getElementById('currentCpuMean').textContent = currentStats.mean.toFixed(1);
document.getElementById('projectedCpuMean').textContent = projectedStats.mean.toFixed(1);
document.getElementById('projectedCpuStd').textContent = projectedStats.std.toFixed(1);
// Create mean and std deviation arrays for projected chart
const meanLine = Array(data.hostData.labels.length).fill(projectedStats.mean);
const stdPlusOne = Array(data.hostData.labels.length).fill(projectedStats.mean + projectedStats.std);
const stdMinusOne = Array(data.hostData.labels.length).fill(projectedStats.mean - projectedStats.std);
// Destroy existing charts
if (cpuHostChart) cpuHostChart.destroy();
if (cpuProjectedChart) cpuProjectedChart.destroy();
// Create current CPU chart with mean baseline
cpuHostChart = new Chart(hostCtx, {
type: 'bar',
data: {
labels: data.hostData.labels,
datasets: [{
label: 'CPU Utilization (%)',
data: data.hostData.current,
backgroundColor: '#3b82f6',
borderRadius: 4,
borderSkipped: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: 'Percentage (%)'
},
grid: {
drawBorder: false
}
},
x: {
grid: {
display: false
}
}
},
plugins: {
tooltip: {
callbacks: {
label: (ctx) => `${ctx.parsed.y}% CPU`
}
},
annotation: {
annotations: {
meanLine: {
type: 'line',
yMin: currentStats.mean,
yMax: currentStats.mean,
borderColor: '#10b981',
borderWidth: 2,
borderDash: [5, 5],
label: {
enabled: true,
content: `Mean: ${currentStats.mean.toFixed(1)}%`,
position: 'end',
backgroundColor: '#10b981',
color: 'white',
font: {
size: 10
}
}
}
}
}
}
}
});
// Create projected CPU chart with mean and std deviation baselines
cpuProjectedChart = new Chart(projCtx, {
type: 'bar',
data: {
labels: data.hostData.labels,
datasets: [
{
label: 'Projected CPU (%)',
data: data.hostData.projected,
backgroundColor: '#f97316',
borderRadius: 4,
borderSkipped: false
},
{
label: 'Mean',
data: meanLine,
type: 'line',
borderColor: '#10b981',
borderWidth: 2,
borderDash: [5, 5],
fill: false,
pointRadius: 0,
tension: 0
},
{
label: '+1σ',
data: stdPlusOne,
type: 'line',
borderColor: '#ef4444',
borderWidth: 1,
borderDash: [3, 3],
fill: false,
pointRadius: 0,
tension: 0
},
{
label: '-1σ',
data: stdMinusOne,
type: 'line',
borderColor: '#ef4444',
borderWidth: 1,
borderDash: [3, 3],
fill: false,
pointRadius: 0,
tension: 0
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: 'Percentage (%)'
},
grid: {
drawBorder: false
}
},
x: {
grid: {
display: false
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (ctx) => {
if (ctx.datasetIndex === 0) {
return `Projected: ${ctx.parsed.y}%`;
} else if (ctx.datasetIndex === 1) {
return `Mean: ${ctx.parsed.y.toFixed(1)}%`;
} else {
return `${ctx.datasetIndex === 2 ? '+1σ' : '-1σ'}: ${ctx.parsed.y.toFixed(1)}%`;
}
}
}
}
}
}
});
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
const initialAudit = "{{ audits.0.id }}";
updateMigrationTable(initialAudit);
updateCPUCharts(initialAudit);
});
</script>
{% endblock %}