Stitching things up
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
*.sqlite3
|
||||||
|
static/
|
||||||
|
media/
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,3 +21,6 @@ watcher_visio/static/css/tailwindcss.exe
|
|||||||
# DaisyUI
|
# DaisyUI
|
||||||
static/css/output.css
|
static/css/output.css
|
||||||
static/css/tailwindcss
|
static/css/tailwindcss
|
||||||
|
|
||||||
|
# NodeJS
|
||||||
|
node_modules
|
||||||
15
Dockerfile
15
Dockerfile
@@ -1,12 +1,4 @@
|
|||||||
FROM node:25-alpine AS node-build-stage
|
FROM alpine:3 AS build
|
||||||
|
|
||||||
COPY ./watcher_visio/static ./
|
|
||||||
RUN npx tailwindcss \
|
|
||||||
-i input.css \
|
|
||||||
-o ./tailwind.css --minify
|
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3 AS venv-build-stage
|
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache --virtual .build-deps \
|
apk add --no-cache --virtual .build-deps \
|
||||||
@@ -16,7 +8,7 @@ RUN apk update && \
|
|||||||
|
|
||||||
RUN python3 -m venv /venv
|
RUN python3 -m venv /venv
|
||||||
ENV PATH "/venv/bin:$PATH"
|
ENV PATH "/venv/bin:$PATH"
|
||||||
COPY ./watcher_visio/requirements.txt /
|
COPY ./requirements.txt /
|
||||||
RUN pip install -r /requirements.txt
|
RUN pip install -r /requirements.txt
|
||||||
|
|
||||||
|
|
||||||
@@ -33,7 +25,8 @@ RUN apk add --no-cache --update python3
|
|||||||
COPY --from=build /venv /venv
|
COPY --from=build /venv /venv
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
COPY --from=build /app /app
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY ./ /
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||||
CMD [ "python", "manage.py", "runserver", "0.0.0.0:8000" ]
|
CMD [ "python", "manage.py", "runserver", "0.0.0.0:8000" ]
|
||||||
7
dashboard/openstack_utils/connect.py
Normal file
7
dashboard/openstack_utils/connect.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import openstack
|
||||||
|
|
||||||
|
from watcher_visio.settings import OPENSTACK_CLOUD, OPENSTACK_REGION_NAME
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
connection = openstack.connect(cloud=OPENSTACK_CLOUD, region_name=OPENSTACK_REGION_NAME)
|
||||||
|
return connection
|
||||||
16
dashboard/prometheus_utils/query.py
Normal file
16
dashboard/prometheus_utils/query.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
from watcher_visio.settings import PROMETHEUS_URL
|
||||||
|
|
||||||
|
def query_prometheus(query):
|
||||||
|
url = f"{PROMETHEUS_URL}/api/v1/query"
|
||||||
|
params = {
|
||||||
|
"query": query,
|
||||||
|
}
|
||||||
|
response = requests.get(url=url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()["data"]["result"]
|
||||||
|
if len(result) > 1:
|
||||||
|
return result[0]["value"][1]
|
||||||
|
else:
|
||||||
|
return result[0]["values"]
|
||||||
@@ -1,20 +1,109 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from dashboard.openstack_utils.connect import get_connection
|
||||||
|
from dashboard.prometheus_utils.query import query_prometheus
|
||||||
|
|
||||||
_BASE = {
|
_BASE = {
|
||||||
"region_name": "ct3k1ldt"
|
"region_name": "ct3k1ldt"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def collect_context():
|
||||||
|
connection = get_connection()
|
||||||
|
hosts_total = int(
|
||||||
|
query_prometheus(
|
||||||
|
query="count(node_exporter_build_info{job='node_exporter_compute'})"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pcpu_total = int(
|
||||||
|
query_prometheus(
|
||||||
|
query="sum(count(node_cpu_seconds_total{job='node_exporter_compute', mode='idle'}) without (cpu,mode))"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pcpu_usage = float(
|
||||||
|
query_prometheus(
|
||||||
|
query=""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
vcpu_total = int (
|
||||||
|
query_prometheus(
|
||||||
|
query="(sum(count(node_cpu_seconds_total{job='node_exporter_compute', mode='idle'}) without (cpu,mode)))*(avg(openstack_placement_resource_allocation_ratio{resourcetype='VCPU'}))"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
vcpu_allocated = int(
|
||||||
|
query_prometheus(
|
||||||
|
query="sum(libvirt_domain_info_virtual_cpus)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
vcpu_overcommit_ratio = float(
|
||||||
|
query_prometheus(
|
||||||
|
query="sum(libvirt_domain_info_virtual_cpus)/(sum(count(node_cpu_seconds_total{job='node_exporter_compute', mode='idle'}) without (cpu,mode)))"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
vcpu_overcommit_max = float(
|
||||||
|
query_prometheus(
|
||||||
|
query="avg(openstack_placement_resource_allocation_ratio{resourcetype='VCPU'})"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
vm_count = int (
|
||||||
|
query_prometheus(
|
||||||
|
query="sum(libvirt_domain_state_code)"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
vm_active = int (
|
||||||
|
query_prometheus(
|
||||||
|
query="sum(libvirt_domain_state_code{stateDesc='the domain is running'})"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"region_name": "",
|
||||||
|
# <--- Hosts data --->
|
||||||
|
"hosts_total": hosts_total,
|
||||||
|
# <--- CPU data --->
|
||||||
|
# pCPU data
|
||||||
|
"pcpu_total": pcpu_total,
|
||||||
|
"pcpu_usage": pcpu_usage,
|
||||||
|
"pcpu_free": pcpu_total - pcpu_usage,
|
||||||
|
# vCPU data
|
||||||
|
"vcpu_total": vcpu_total,
|
||||||
|
"vcpu_allocated": vcpu_allocated,
|
||||||
|
"vcpu_free": vcpu_total - vcpu_allocated,
|
||||||
|
"vcpu_overcommit_ratio": vcpu_overcommit_ratio,
|
||||||
|
"vcpu_overcommit_max": vcpu_overcommit_max,
|
||||||
|
# <--- RAM data --->
|
||||||
|
# pRAM data
|
||||||
|
|
||||||
|
# vRAM data
|
||||||
|
|
||||||
|
# <--- VM data --->
|
||||||
|
"vm_count": vm_count,
|
||||||
|
"vm_active": vm_active,
|
||||||
|
"vm_stopped": vm_count - vm_active,
|
||||||
|
"vm_error": "",
|
||||||
|
"avg_cpu_per_vm": vcpu_allocated / vm_count,
|
||||||
|
"avg_ram_per_vm": "",
|
||||||
|
"vm_density": vm_count / hosts_total,
|
||||||
|
}
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
context = {**_BASE,
|
context = {**_BASE,
|
||||||
# CPU and RAM utilization data
|
# CPU and RAM utilization data
|
||||||
'cpu_used': 42,
|
'cpu_total': 160,
|
||||||
'cpu_free': 58,
|
'cpu_used': 45,
|
||||||
|
'cpu_free': 66,
|
||||||
'cpu_used_percentage': 42.0,
|
'cpu_used_percentage': 42.0,
|
||||||
'ram_used': 128,
|
'ram_used': 128,
|
||||||
'ram_free': 256,
|
'ram_free': 256,
|
||||||
'ram_used_percentage': 33.3,
|
'ram_used_percentage': 33.3,
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
|
||||||
# Instance summary data
|
# Instance summary data
|
||||||
'vm_count': 47,
|
'vm_count': 47,
|
||||||
'vm_active': 42,
|
'vm_active': 42,
|
||||||
@@ -31,14 +120,6 @@ def index(request):
|
|||||||
'count': 8
|
'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
|
# Quick stats
|
||||||
'avg_cpu_per_vm': 2.0,
|
'avg_cpu_per_vm': 2.0,
|
||||||
'avg_ram_per_vm': 8.2,
|
'avg_ram_per_vm': 8.2,
|
||||||
@@ -51,7 +132,7 @@ def index(request):
|
|||||||
'name': 'Weekly Optimization',
|
'name': 'Weekly Optimization',
|
||||||
'created_at': '2024-01-15',
|
'created_at': '2024-01-15',
|
||||||
'cpu_weight': 1.2,
|
'cpu_weight': 1.2,
|
||||||
'ram_weight': 0.8,
|
'ram_weight': 0.6,
|
||||||
'scope': 'Full Cluster',
|
'scope': 'Full Cluster',
|
||||||
'strategy': 'Load Balancing',
|
'strategy': 'Load Balancing',
|
||||||
'goal': 'Optimize CPU distribution across all hosts',
|
'goal': 'Optimize CPU distribution across all hosts',
|
||||||
@@ -4,4 +4,5 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./watcher_visio:/app
|
- ./:/app
|
||||||
|
restart: unless-stopped
|
||||||
0
docker-entrypoint.sh
Normal file → Executable file
0
docker-entrypoint.sh
Normal file → Executable file
1368
package-lock.json
generated
Normal file
1368
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "watcher-visio",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "npx @tailwindcss/cli -i ./static/css/main.css -o ./static/css/output.css --minify",
|
||||||
|
"dev": "npx @tailwindcss/cli -i ./static/css/main.css -o ./static/css/output.css --watch"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.arnike.ru/Arnike/watcher-visio.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
"autoprefixer": "^10.4.22",
|
||||||
|
"daisyui": "^5.5.5",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^4.1.17"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/cli": "^4.1.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,6 @@ pytz==2025.2
|
|||||||
PyYAML==6.0.3
|
PyYAML==6.0.3
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
requestsexceptions==1.4.0
|
requestsexceptions==1.4.0
|
||||||
setuptools==80.9.0
|
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
sqlparse==0.5.4
|
sqlparse==0.5.4
|
||||||
stevedore==5.6.0
|
stevedore==5.6.0
|
||||||
3
static/css/main.css
Normal file
3
static/css/main.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui";
|
||||||
|
@source "../../templates";
|
||||||
0
static/js/chartjs-plugin-datalabels.min.js
vendored
Normal file
0
static/js/chartjs-plugin-datalabels.min.js
vendored
Normal file
26
static/js/utils.js
Normal file
26
static/js/utils.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Color utilities
|
||||||
|
const getCSSVar = (varName) => {
|
||||||
|
return getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
|
||||||
|
}
|
||||||
|
function getColorWithOpacity(className) {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = className;
|
||||||
|
element.style.opacity = '1'; // Force opacity
|
||||||
|
element.textContent = '.';
|
||||||
|
|
||||||
|
document.body.appendChild(element);
|
||||||
|
const computedColor = window.getComputedStyle(element).color;
|
||||||
|
document.body.removeChild(element);
|
||||||
|
|
||||||
|
return computedColor;
|
||||||
|
}
|
||||||
|
// 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 };
|
||||||
|
}
|
||||||
14
tailwind.config.js
Normal file
14
tailwind.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./templates/**/*.html",
|
||||||
|
"./static/src/**/*.js",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require("daisyui")],
|
||||||
|
daisyui: {
|
||||||
|
themes: ["light", "dark"],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
<link rel="stylesheet" href="{% static 'css/output.css' %}">
|
<link rel="stylesheet" href="{% static 'css/output.css' %}">
|
||||||
{% block imports %}
|
{% block imports %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block css %}
|
||||||
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Navbar -->
|
<!-- Navbar -->
|
||||||
@@ -45,6 +47,8 @@
|
|||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
const checkbox = document.querySelector('.theme-controller');
|
const checkbox = document.querySelector('.theme-controller');
|
||||||
checkbox.checked = (theme === 'dark');
|
checkbox.checked = (theme === 'dark');
|
||||||
|
|
||||||
|
document.dispatchEvent(new Event("themechange"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load saved theme from localStorage
|
// Load saved theme from localStorage
|
||||||
580
templates/index.html
Normal file
580
templates/index.html
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static mathfilters %}
|
||||||
|
|
||||||
|
{% block imports %}
|
||||||
|
<script src="{% static 'js/utils.js' %}"></script>
|
||||||
|
<script src="{% static 'js/chart.js' %}"></script>
|
||||||
|
<script src="{% static 'js/chartjs-plugin-datalabels.min.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- MAIN DASHBOARD -->
|
||||||
|
<div class="p-4 space-y-4">
|
||||||
|
<!-- QUICK STATS ROW -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
|
<!-- CPU Utilization -->
|
||||||
|
<div class="card bg-base-100 shadow-sm hover:shadow transition-shadow">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-base-content/70">CPU Utilization</h3>
|
||||||
|
<div class="text-xs text-base-content/40 mt-0.5">{{ cpu_used }} / {{ cpu_used|add:cpu_free }} vCPU</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold text-primary">{{ cpu_used_percentage|floatformat:1 }}%</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="text-base-content/60">Used</span>
|
||||||
|
<span class="font-medium">{{ cpu_used }} vCPU</span>
|
||||||
|
</div>
|
||||||
|
<progress class="progress progress-primary w-full" value="{{ cpu_used_percentage }}" max="100"></progress>
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="text-base-content/60">Free</span>
|
||||||
|
<span class="font-medium">{{ cpu_free }} vCPU</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RAM Utilization -->
|
||||||
|
<div class="card bg-base-100 shadow-sm hover:shadow transition-shadow">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-base-content/70">RAM Utilization</h3>
|
||||||
|
<div class="text-xs text-base-content/40 mt-0.5">{{ ram_used }} / {{ ram_used|add:ram_free }} GB</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold text-secondary">{{ ram_used_percentage|floatformat:1 }}%</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="text-base-content/60">Used</span>
|
||||||
|
<span class="font-medium">{{ ram_used }} GB</span>
|
||||||
|
</div>
|
||||||
|
<progress class="progress progress-secondary w-full" value="{{ ram_used_percentage }}" max="100"></progress>
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="text-base-content/60">Free</span>
|
||||||
|
<span class="font-medium">{{ ram_free }} GB</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Instance Summary -->
|
||||||
|
<div class="card bg-base-100 shadow-sm hover:shadow transition-shadow">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-base-content/70">Instances</h3>
|
||||||
|
<div class="text-xs text-base-content/40 mt-0.5">{{ vm_active }} active / {{ vm_stopped }} stopped</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold text-accent">{{ vm_count }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex justify-between items-center text-xs">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-success"></div>
|
||||||
|
<span>Most Used Flavor</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium">{{ common_flavor }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center text-xs">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-info"></div>
|
||||||
|
<span>Avg. vCPU/VM</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium">{{ avg_cpu_per_vm|floatformat:1 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center text-xs">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-warning"></div>
|
||||||
|
<span>Density</span>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium">{{ vm_density|floatformat:1 }}/host</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DETAILED OVERVIEW -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<!-- Resource Allocation -->
|
||||||
|
<div class="card bg-base-100 shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h3 class="text-sm font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
|
||||||
|
Resource Allocation
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- CPU Allocation -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<span class="text-base-content/70">CPU Allocation</span>
|
||||||
|
<span class="font-medium">{{ cpu_allocated }} / {{ cpu_total }} vCPU</span>
|
||||||
|
</div>
|
||||||
|
{% with pct=cpu_allocated|div:cpu_total|mul:100 %}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<progress class="progress progress-primary flex-1" value="{{ pct }}" max="100"></progress>
|
||||||
|
<span class="text-xs font-medium w-12 text-right">{{ pct|floatformat:1 }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs mt-1">
|
||||||
|
<span class="text-base-content/50">OC: x{{ cpu_overcommit_ratio }}</span>
|
||||||
|
<span class="text-base-content/50">{{ pct|floatformat:1 }}% allocated</span>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RAM Allocation -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<span class="text-base-content/70">RAM Allocation</span>
|
||||||
|
<span class="font-medium">{{ ram_allocated }} / {{ ram_total }} GB</span>
|
||||||
|
</div>
|
||||||
|
{% with pct=ram_allocated|div:ram_total|mul:100 %}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<progress class="progress progress-secondary flex-1" value="{{ pct }}" max="100"></progress>
|
||||||
|
<span class="text-xs font-medium w-12 text-right">{{ pct|floatformat:1 }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs mt-1">
|
||||||
|
<span class="text-base-content/50">OC: x{{ ram_overcommit_ratio }}</span>
|
||||||
|
<span class="text-base-content/50">{{ pct|floatformat:1 }}% allocated</span>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flavor Distribution -->
|
||||||
|
<div class="card bg-base-100 shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h3 class="text-sm font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"/>
|
||||||
|
</svg>
|
||||||
|
Top Flavors
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<!-- Most Common -->
|
||||||
|
<div class="bg-base-200/50 rounded-lg p-3">
|
||||||
|
<div class="flex justify-between items-center mb-1">
|
||||||
|
<span class="text-sm font-medium">{{ common_flavor }}</span>
|
||||||
|
<span class="text-xs badge badge-primary">{{ common_flavor_count }} instances</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="text-base-content/60">Share</span>
|
||||||
|
<span class="font-medium">{{ common_flavor_count|div:vm_count|mul:100|floatformat:0 }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Other Flavors -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
{% if second_common_flavor %}
|
||||||
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-base-content/30"></div>
|
||||||
|
<span>{{ 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 justify-between items-center text-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-base-content/30"></div>
|
||||||
|
<span>{{ third_common_flavor.name }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs badge badge-outline">{{ third_common_flavor.count }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AUDIT CONTROL -->
|
||||||
|
<div class="card bg-base-100 shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold">Audit Analysis</h3>
|
||||||
|
<div class="text-xs text-base-content/50 mt-0.5">Select an audit to analyze resource distribution</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-base-content/50">{{ audits|length }} available</span>
|
||||||
|
<div class="dropdown dropdown-end">
|
||||||
|
<label tabindex="0" class="btn btn-xs btn-ghost">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<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>
|
||||||
|
Info
|
||||||
|
</label>
|
||||||
|
<div tabindex="0" class="dropdown-content z-[1] card card-compact w-64 p-2 shadow bg-base-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-xs space-y-1">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-base-content/60">Strategy:</span>
|
||||||
|
<span id="previewStrategy">Balanced</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-base-content/60">Scope:</span>
|
||||||
|
<span id="previewScope">Full Cluster</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-base-content/60">CPU Weight:</span>
|
||||||
|
<span id="previewCpu">1.0</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-base-content/60">RAM Weight:</span>
|
||||||
|
<span id="previewRam">1.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row gap-3">
|
||||||
|
<select id="auditSelector" class="select select-bordered select-sm flex-1">
|
||||||
|
{% 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" }})
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button onclick="loadSelectedAudit()" class="btn btn-primary btn-sm">
|
||||||
|
Load Analysis
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ANALYSIS VISUALIZATION -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<!-- Current State -->
|
||||||
|
<div class="card bg-base-100 shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h3 class="text-sm font-semibold mb-4">Current CPU Distribution</h3>
|
||||||
|
<div class="h-48">
|
||||||
|
<canvas id="cpuHostChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center gap-3 mt-3">
|
||||||
|
<div class="flex items-center gap-1 text-xs">
|
||||||
|
<div class="w-3 h-0.5 bg-success"></div>
|
||||||
|
<span class="text-success">Mean: <span id="currentCpuMean">0</span>%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Projected State -->
|
||||||
|
<div class="card bg-base-100 shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h3 class="text-sm font-semibold mb-4">Projected CPU Distribution</h3>
|
||||||
|
<div class="h-48">
|
||||||
|
<canvas id="cpuProjectedChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center gap-4 mt-3">
|
||||||
|
<div class="flex items-center gap-1 text-xs">
|
||||||
|
<div class="w-3 h-0.5 bg-success"></div>
|
||||||
|
<span class="text-success">Mean: <span id="projectedCpuMean">0</span>%</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 text-xs">
|
||||||
|
<div class="w-3 h-0.5 bg-error/60"></div>
|
||||||
|
<span class="text-error/60">±1σ: <span id="projectedCpuStd">0</span>%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MIGRATION ACTIONS -->
|
||||||
|
<div class="card bg-base-100 shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-sm font-semibold">Migration Actions</h3>
|
||||||
|
<div class="badge badge-neutral badge-sm" id="migrationCount">Select audit</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-medium">Instance</th>
|
||||||
|
<th class="text-xs font-medium">Source → Destination</th>
|
||||||
|
<th class="text-xs font-medium">Flavor</th>
|
||||||
|
<th class="text-xs font-medium">Impact</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="migrationTableBody" class="text-sm">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center py-6 text-base-content/40">
|
||||||
|
No audit selected. Load an audit to view migration recommendations.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script>
|
||||||
|
// Update audit preview
|
||||||
|
document.getElementById('auditSelector').addEventListener('change', function(e) {
|
||||||
|
const option = this.options[this.selectedIndex];
|
||||||
|
document.getElementById('previewCpu').textContent = option.dataset.cpu || '1.0';
|
||||||
|
document.getElementById('previewRam').textContent = option.dataset.ram || '1.0';
|
||||||
|
document.getElementById('previewScope').textContent = option.dataset.scope || 'Full Cluster';
|
||||||
|
document.getElementById('previewStrategy').textContent = option.dataset.strategy || 'Balanced';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Audit data
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Load selected audit
|
||||||
|
function loadSelectedAudit() {
|
||||||
|
const auditId = document.getElementById('auditSelector').value;
|
||||||
|
updateMigrationTable(auditId);
|
||||||
|
updateCPUCharts(auditId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update migration table
|
||||||
|
function updateMigrationTable(auditId) {
|
||||||
|
const tbody = document.getElementById('migrationTableBody');
|
||||||
|
const migrationCount = document.getElementById('migrationCount');
|
||||||
|
const data = auditData[auditId];
|
||||||
|
|
||||||
|
if (!data || !data.migrations || data.migrations.length === 0) {
|
||||||
|
tbody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center py-6 text-base-content/40">
|
||||||
|
No migration actions recommended
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
migrationCount.textContent = '0 actions';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
data.migrations.forEach(migration => {
|
||||||
|
const impact = migration.impact || 'Low';
|
||||||
|
const impactClass = {
|
||||||
|
'Low': 'badge-success',
|
||||||
|
'Medium': 'badge-warning',
|
||||||
|
'High': 'badge-error'
|
||||||
|
}[impact] || 'badge-neutral';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr>
|
||||||
|
<td class="font-medium">
|
||||||
|
<div>${migration.instanceName}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="badge badge-outline badge-xs">${migration.source}</span>
|
||||||
|
<svg class="w-3 h-3 text-base-content/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||||
|
</svg>
|
||||||
|
<span class="badge badge-primary badge-outline badge-xs">${migration.destination}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code class="text-xs bg-base-200 px-2 py-1 rounded">${migration.flavor}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge ${impactClass} badge-xs">${impact}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
migrationCount.textContent = `${data.migrations.length} action${data.migrations.length !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update CPU charts
|
||||||
|
function updateCPUCharts(auditId) {
|
||||||
|
const data = auditData[auditId];
|
||||||
|
if (!data || !data.hostData) return;
|
||||||
|
|
||||||
|
const currentCtx = document.getElementById('cpuHostChart').getContext('2d');
|
||||||
|
const projectedCtx = 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);
|
||||||
|
|
||||||
|
// Destroy existing charts
|
||||||
|
if (cpuHostChart) cpuHostChart.destroy();
|
||||||
|
if (cpuProjectedChart) cpuProjectedChart.destroy();
|
||||||
|
|
||||||
|
// Chart colors
|
||||||
|
const colors = {
|
||||||
|
primary: getCSSVar('--color-primary'),
|
||||||
|
secondary: getCSSVar('--color-secondary'),
|
||||||
|
accent: getCSSVar('--color-accent'),
|
||||||
|
neutral: getCSSVar('-color-neutral'),
|
||||||
|
info: getCSSVar('--color-info'),
|
||||||
|
success: getCSSVar('--color-success'),
|
||||||
|
warning: getCSSVar('--color-warning'),
|
||||||
|
error: getCSSVar('--color-error')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create current CPU chart
|
||||||
|
cpuHostChart = new Chart(currentCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: data.hostData.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'CPU %',
|
||||||
|
data: data.hostData.current,
|
||||||
|
backgroundColor: colors.info + '40',
|
||||||
|
borderColor: colors.info,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 3
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => `${ctx.parsed.y}% CPU`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
grid: { drawBorder: false },
|
||||||
|
ticks: {
|
||||||
|
callback: value => value + '%'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create projected CPU chart
|
||||||
|
cpuProjectedChart = new Chart(projectedCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: data.hostData.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Projected CPU %',
|
||||||
|
data: data.hostData.projected,
|
||||||
|
backgroundColor: colors.warning + '40',
|
||||||
|
borderColor: colors.warning,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 3
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => `${ctx.parsed.y}% CPU`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
grid: { drawBorder: false },
|
||||||
|
ticks: {
|
||||||
|
callback: value => value + '%'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
function calculateStats(data) {
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const initialAudit = "{{ audits.0.id|default:'' }}";
|
||||||
|
if (initialAudit && auditData[initialAudit]) {
|
||||||
|
document.getElementById('auditSelector').dispatchEvent(new Event('change'));
|
||||||
|
loadSelectedAudit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<style>
|
||||||
|
.progress {
|
||||||
|
@apply h-1.5;
|
||||||
|
}
|
||||||
|
.table td, .table th {
|
||||||
|
@apply px-4 py-2;
|
||||||
|
}
|
||||||
|
.badge-xs {
|
||||||
|
@apply px-1.5 py-0.5 text-xs;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
0
watcher_visio/__init__.py
Normal file
0
watcher_visio/__init__.py
Normal file
@@ -41,8 +41,17 @@ INSTALLED_APPS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Prometheus settings (environment override recommended)
|
# Prometheus settings (environment override recommended)
|
||||||
PROMETHEUS_URL = "http://localhost:9090" # replace with your Prometheus HTTP endpoint
|
PROMETHEUS_URL = "http://localhost:9090"
|
||||||
PROMETHEUS_DEFAULT_METRIC = "libvirt_domain_info_memory_usage_bytes"
|
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 = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'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
@@ -1,3 +0,0 @@
|
|||||||
@import "tailwindcss" source(none);
|
|
||||||
@plugin "./daisyui.js";
|
|
||||||
@source "../../templates";
|
|
||||||
@@ -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...
|
|
||||||
@@ -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...
|
|
||||||
@@ -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...
|
|
||||||
@@ -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 %}
|
|
||||||
Reference in New Issue
Block a user