From f8fff2aa345dbc667eb2daa9b038a0d637dfc212 Mon Sep 17 00:00:00 2001 From: Asclepius Date: Mon, 23 Jun 2025 15:53:35 +0200 Subject: [PATCH 1/6] Added kubernetes/prometheus/prometheus-targets-pvc to Tiltfileto ensure automatic creation of targets directory --- Tiltfile | 3 +++ kubernetes/prometheus/deployment.yaml | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/Tiltfile b/Tiltfile index 0231ea7..01695d2 100644 --- a/Tiltfile +++ b/Tiltfile @@ -8,7 +8,10 @@ k8s_yaml('kubernetes/django/createsuperuser.yaml') k8s_yaml('kubernetes/prometheus/configmap.yaml') k8s_yaml('kubernetes/prometheus/deployment.yaml') +k8s_yaml('kubernetes/prometheus/prometheus-targets-pvc.yaml') k8s_yaml('kubernetes/prometheus/service.yaml') + + k8s_yaml('kubernetes/grafana/deployment.yaml') k8s_yaml('kubernetes/grafana/service.yaml') diff --git a/kubernetes/prometheus/deployment.yaml b/kubernetes/prometheus/deployment.yaml index d7d73ed..43c3c1e 100644 --- a/kubernetes/prometheus/deployment.yaml +++ b/kubernetes/prometheus/deployment.yaml @@ -12,6 +12,18 @@ spec: labels: app: prometheus spec: + initContainers: + - name: init-targets + image: busybox + command: + - /bin/sh + - -c + - | + mkdir -p /etc/prometheus/targets && \ + echo '[]' > /etc/prometheus/targets/targets.json + volumeMounts: + - name: targets-volume + mountPath: /etc/prometheus/targets containers: - name: prometheus image: prom/prometheus -- GitLab From ff0152e3ebcbbf4520158616945ff21f4a610d28 Mon Sep 17 00:00:00 2001 From: Asclepius Date: Mon, 23 Jun 2025 19:33:01 +0200 Subject: [PATCH 2/6] Changed view_devices to display db rows as part of same organization as the logged in user --- api/migrations/0007_device_nodeexporterip.py | 18 +++++++ api/migrations/0008_device_organization.py | 19 +++++++ api/models.py | 20 ++++---- api/urls.py | 1 + api/views.py | 53 ++++++++++++++++++-- kubernetes/prometheus/deployment.yaml | 35 +++++++++++-- templates/devices.html | 44 ++++++---------- vmmanager/settings.py | 2 +- 8 files changed, 145 insertions(+), 47 deletions(-) create mode 100644 api/migrations/0007_device_nodeexporterip.py create mode 100644 api/migrations/0008_device_organization.py diff --git a/api/migrations/0007_device_nodeexporterip.py b/api/migrations/0007_device_nodeexporterip.py new file mode 100644 index 0000000..0b08e0d --- /dev/null +++ b/api/migrations/0007_device_nodeexporterip.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.2 on 2025-06-23 16:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0006_organization_remove_userprofile_team_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='nodeexporterip', + field=models.CharField(blank=True, max_length=100, null=True, unique=True), + ), + ] diff --git a/api/migrations/0008_device_organization.py b/api/migrations/0008_device_organization.py new file mode 100644 index 0000000..024ff2a --- /dev/null +++ b/api/migrations/0008_device_organization.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.2 on 2025-06-23 17:18 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0007_device_nodeexporterip'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='organization', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organization'), + ), + ] diff --git a/api/models.py b/api/models.py index dff58e5..d87f7af 100644 --- a/api/models.py +++ b/api/models.py @@ -1,6 +1,13 @@ from django.db import models from django.contrib.auth.models import User +class Organization(models.Model): + name = models.CharField(max_length=100, blank=True) + organization_created_time = models.DateTimeField(auto_now=False, auto_now_add=True) + + def __str__(self): + return f"{self.name}" + class Device(models.Model): name = models.CharField(max_length=100, null=True, blank=True) region = models.CharField(max_length=50, null=True, blank=True) @@ -16,17 +23,8 @@ class Device(models.Model): proxmox_vmid = models.CharField(max_length=100, unique=True, null=True, blank=True) time_purchased = models.DateTimeField(auto_now_add=True, null=True, blank=True) last_edited = models.DateTimeField(auto_now=True, null=True, blank=True) - - -from django.db import models -from django.contrib.auth.models import User - -class Organization(models.Model): - name = models.CharField(max_length=100, blank=True) - organization_created_time = models.DateTimeField(auto_now=False, auto_now_add=True) - - def __str__(self): - return f"{self.name}" + nodeexporterip = models.CharField(max_length=100, unique=True, null=True, blank=True) + organization = models.ForeignKey(Organization, on_delete=models.CASCADE, null=True, blank=True) class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) diff --git a/api/urls.py b/api/urls.py index 3fb21cb..c3f39e9 100644 --- a/api/urls.py +++ b/api/urls.py @@ -35,6 +35,7 @@ urlpatterns = [ path("signup", views.signup_user, name="signup_user"), path("login", views.login_user, name="login_user"), path("usersettings", views.usersettings, name="usersettings"), + path('prometheus/targets/', views.prometheus_targets), # Swagger UI: re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), diff --git a/api/views.py b/api/views.py index 178fd2a..3bffe66 100644 --- a/api/views.py +++ b/api/views.py @@ -15,6 +15,18 @@ from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required from .forms import UserSettingsForm + +from django.http import JsonResponse +from .models import Device + +def prometheus_targets(request): + targets = [f"{device.nodeexporterip}:9100" for device in Device.objects.exclude(nodeexporterip="")] + return JsonResponse([{ + "targets": targets, + "labels": {"job": "devices"} + }], safe=False) + + def signup_user(response): if response.method == "POST": form = SignUpForm(response.POST) @@ -66,14 +78,49 @@ def login_user(request): return render(request, "login.html", {"form": form}) +from django.contrib.auth.decorators import login_required +from django.shortcuts import render + +@login_required def view_devices(request): + user_org = getattr(request.user.userprofile, 'organization', None) + if not user_org: + devices = [] # Or maybe no devices if no org assigned + else: + devices = Device.objects.filter(organization=user_org) + try: - vms = get_all_vm_info() + proxmox_vms = get_all_vm_info() # Assume returns a list of VM dicts with 'vmid', 'status', 'config' except Exception as e: - vms = [] + proxmox_vms = [] print(f"Error retrieving VMs: {e}") - context = {'vms': vms} + # Create dict for quick lookup by proxmox_vmid (string keys) + proxmox_vms_dict = {str(vm['vmid']): vm for vm in proxmox_vms} + + # Enrich each device with Proxmox info or fallback message + devices_with_proxmox = [] + for device in devices: + proxmox_data = proxmox_vms_dict.get(str(device.proxmox_vmid)) + if proxmox_data: + status = proxmox_data.get('status', {}).get('status', 'Unknown') + memory = proxmox_data.get('config', {}).get('memory', 'Unknown') + cores = proxmox_data.get('config', {}).get('cores', 'Unknown') + else: + status = "has been deleted in proxmox" + memory = "-" + cores = "-" + + devices_with_proxmox.append({ + 'device': device, + 'status': status, + 'memory': memory, + 'cores': cores, + }) + + context = { + 'devices_with_proxmox': devices_with_proxmox, + } return render(request, 'devices.html', context) diff --git a/kubernetes/prometheus/deployment.yaml b/kubernetes/prometheus/deployment.yaml index 43c3c1e..12d458f 100644 --- a/kubernetes/prometheus/deployment.yaml +++ b/kubernetes/prometheus/deployment.yaml @@ -13,17 +13,19 @@ spec: app: prometheus spec: initContainers: - - name: init-targets + - name: fix-permissions image: busybox command: - - /bin/sh + - sh - -c - | - mkdir -p /etc/prometheus/targets && \ - echo '[]' > /etc/prometheus/targets/targets.json + chown -R 65534:65534 /etc/prometheus/targets + securityContext: + runAsUser: 0 volumeMounts: - name: targets-volume mountPath: /etc/prometheus/targets + containers: - name: prometheus image: prom/prometheus @@ -36,6 +38,31 @@ spec: mountPath: /etc/prometheus - name: targets-volume mountPath: /etc/prometheus/targets + + - name: sync-targets + image: curlimages/curl + command: + - /bin/sh + - -c + - | + while true; do + echo "Fetching targets..." + if curl -sfL http://django-service:8000/prometheus/targets -o /etc/prometheus/targets/targets.tmp; then + if [ -s /etc/prometheus/targets/targets.tmp ]; then + mv /etc/prometheus/targets/targets.tmp /etc/prometheus/targets/targets.json + echo "Successfully updated targets.json:" + cat /etc/prometheus/targets/targets.json + else + echo "Downloaded file is empty, skipping update." + fi + else + echo "Failed to fetch targets from Django" + fi + sleep 30 + done + volumeMounts: + - name: targets-volume + mountPath: /etc/prometheus/targets volumes: - name: config-volume configMap: diff --git a/templates/devices.html b/templates/devices.html index b3e9011..1d2dfae 100644 --- a/templates/devices.html +++ b/templates/devices.html @@ -52,34 +52,22 @@ - {% for vm in vms %} - - - {{ vm.config.name }} - - {{ vm.vmid }} - {{ vm.status.status }} - {{ vm.config.memory }} - {{ vm.config.cores }} - -
-
- - {% if vm.status.status == 'running' %}Running{% elif vm.status.status == 'paused' %}Paused{% else %}Stopped{% endif %} - -
- - -
- - - {% endfor %} + {% for item in devices_with_proxmox %} + + + {{ item.device.name }} + + {{ item.device.proxmox_vmid }} + {{ item.status }} + {{ item.memory }} + {{ item.cores }} + + + +{% empty %} +No devices found for your organization. +{% endfor %} + diff --git a/vmmanager/settings.py b/vmmanager/settings.py index 36126f0..f56a9fe 100644 --- a/vmmanager/settings.py +++ b/vmmanager/settings.py @@ -26,7 +26,7 @@ SECRET_KEY = 'django-insecure-n1hq6q0%no#_6)+v+l^d#it4!nfeoy$t4*^%^pmkgig_c*781n # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["vmmanager.local", "localhost"] +ALLOWED_HOSTS = ["vmmanager.local", "localhost", "django-service"] CRISPY_TEMPLATE_PACK="bootstrap4" -- GitLab From d52c5bd7ecad2ec6206a5101630dd4633953e605 Mon Sep 17 00:00:00 2001 From: Asclepius Date: Mon, 23 Jun 2025 21:38:44 +0200 Subject: [PATCH 3/6] CPU graph works with IP from Device.nodeexporterip --- api/urls.py | 5 +- api/views.py | 112 +++++++++++---------------- templates/device.html | 171 +++++------------------------------------- 3 files changed, 64 insertions(+), 224 deletions(-) diff --git a/api/urls.py b/api/urls.py index c3f39e9..d5f8411 100644 --- a/api/urls.py +++ b/api/urls.py @@ -30,6 +30,7 @@ urlpatterns = [ path('api/network-in/', views.network_in), path('api/network-out/', views.network_out), path('api/uptime/', views.uptime), + path('new_device', views.new_device), path("new_device/submit/", views.submit_new_device, name="submit_new_device"), path("signup", views.signup_user, name="signup_user"), @@ -47,6 +48,4 @@ urlpatterns = [ re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), -] - - +] \ No newline at end of file diff --git a/api/views.py b/api/views.py index 3bffe66..081cde4 100644 --- a/api/views.py +++ b/api/views.py @@ -14,8 +14,18 @@ from django.contrib import messages from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required from .forms import UserSettingsForm - - +from datetime import datetime, timedelta +import requests +from django.http import JsonResponse +from django.views.decorators.http import require_GET +from django.shortcuts import get_object_or_404 +from .models import Device +from django.shortcuts import redirect +from django.views.decorators.http import require_POST +from django.contrib import messages +from .models import Device +from .proxmox_utils import connect_to_proxmox, clone_vm_from_template +import logging from django.http import JsonResponse from .models import Device @@ -146,17 +156,24 @@ def new_device(request): 'os_options': os_options, }) +from django.shortcuts import get_object_or_404 + def view_device(request): device_id = request.GET.get('device_id') - node_name = request.GET.get('node_name') + + # Get Device object or 404 if not found + device = get_object_or_404(Device, proxmox_vmid=device_id) try: - vm = get_single_vm_info(device_id, node_name) + vm = get_single_vm_info(device.proxmox_vmid, "vbox") except Exception as e: vm = {"error": str(e)} print(f"Error retrieving VM info: {e}") - context = {'vm': vm} + context = { + 'vm': vm, + 'device': device, + } return render(request, 'device.html', context) def vms_get_all_info(request): @@ -179,24 +196,25 @@ def vm_get_all_info(request): ) return JsonResponse([vm_info], safe=False) -PROMETHEUS_URL = 'http://192.168.59.131:30090/api/v1/query_range' # your Prometheus server URL +PROMETHEUS_URL = 'http://192.168.59.158:30090//api/v1/query_range' -import requests -from django.http import JsonResponse -from django.views.decorators.http import require_GET +def query_prometheus_range(device_label, range_seconds): + # Create full PromQL query string with label + promql = f'avg(rate(node_cpu_seconds_total{{mode="idle",instance="{device_label}:9100"}}[{range_seconds}s]))' -# Utility function to query Prometheus with a range query (last 5 minutes, 15s step) -def query_prometheus_range(query): end = int(datetime.utcnow().timestamp()) - start = int((datetime.utcnow() - timedelta(minutes=5)).timestamp()) + start = end - range_seconds + params = { - "query": query, + "query": promql, "start": start, "end": end, "step": "15s" } + response = requests.get(PROMETHEUS_URL, params=params) - response.raise_for_status() + response.raise_for_status() # Will throw HTTPError if status != 200 + data = response.json() if data["status"] == "success" and data["data"]["result"]: return data["data"]["result"][0]["values"] @@ -204,65 +222,19 @@ def query_prometheus_range(query): @require_GET def cpu_usage(request): - # Example metric: node_cpu_seconds_total with mode="idle" -> we invert idle to get usage - # Prometheus query to get CPU idle ratio averaged over all CPUs - query = '1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]))' - values = query_prometheus_range(query) - return JsonResponse({"results": [{"values": values}]}) - -@require_GET -def ram_usage(request): - # Calculate RAM usage percentage: (total - free - buffers - cached) / total - query = '1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)' - values = query_prometheus_range(query) - return JsonResponse({"results": [{"values": values}]}) - -@require_GET -def disk_used(request): - # Disk used bytes, e.g. node_filesystem_size_bytes - node_filesystem_free_bytes for root mount point - query = 'node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_free_bytes{mountpoint="/"}' - values = query_prometheus_range(query) - # Convert bytes to GB - values_gb = [[ts, str(float(val)/1e9)] for ts, val in values] - return JsonResponse({"results": [{"values": values_gb}]}) - -@require_GET -def disk_free(request): - query = 'node_filesystem_free_bytes{mountpoint="/"}' - values = query_prometheus_range(query) - # Convert bytes to GB - values_gb = [[ts, str(float(val)/1e9)] for ts, val in values] - return JsonResponse({"results": [{"values": values_gb}]}) - -@require_GET -def network_in(request): - # Rate of bytes received on interface (e.g., eth0) - query = 'rate(node_network_receive_bytes_total{device="eth0"}[5m])' - values = query_prometheus_range(query) - return JsonResponse({"results": [{"values": values}]}) - -@require_GET -def network_out(request): - # Rate of bytes transmitted on interface - query = 'rate(node_network_transmit_bytes_total{device="eth0"}[5m])' - values = query_prometheus_range(query) - return JsonResponse({"results": [{"values": values}]}) - -@require_GET -def uptime(request): - # node_time_seconds - node_boot_time_seconds gives uptime in seconds - query = 'node_time_seconds - node_boot_time_seconds' - values = query_prometheus_range(query) - return JsonResponse({"results": [{"values": values}]}) + device_id = request.GET.get("device_id") + range_seconds = int(request.GET.get("range", 300)) + try: + device = Device.objects.get(proxmox_vmid=device_id) + values = query_prometheus_range(device.nodeexporterip, range_seconds) + return JsonResponse({"results": [{"values": values}]}) + except Device.DoesNotExist: + return JsonResponse({"error": "Device not found"}, status=404) + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) -from django.shortcuts import redirect -from django.views.decorators.http import require_POST -from django.contrib import messages -from .models import Device -from .proxmox_utils import connect_to_proxmox, clone_vm_from_template -import logging @require_POST def submit_new_device(request): diff --git a/templates/device.html b/templates/device.html index be79c04..f88e546 100644 --- a/templates/device.html +++ b/templates/device.html @@ -2,7 +2,6 @@ {% block content %} - @@ -32,6 +31,16 @@

Name: {{ vm.config.name }}

+
+

Device Info (DB)

+

Name: {{ device.name }}

+

IP: {{ device.ip }}

+

Organization: {{ device.organization.name }}

+

Description: {{ device.description }}

+

Location: {{ device.location }}

+

Node Exporter IP: {{ device.nodeexporterip }}

+
+

Configuration

@@ -89,13 +98,6 @@ - - - -
- -

-
{% else %}

No device information available.

@@ -113,15 +115,18 @@ case '1h': return 3600; case '12h': return 43200; case '24h': return 86400; - case '30d': return 2592000; - case '90d': return 7776000; default: return 300; } } + // Pass deviceId to fetchPrometheusData + const deviceId = "{{ device.proxmox_vmid }}"; // or use device.id depending on your model + + console.log("The device ID is:") + console.log(deviceId) + async function fetchPrometheusData(apiUrl, rangeSeconds) { - // Append range param to API call, e.g. ?range=300 - const url = `${apiUrl}?range=${rangeSeconds}`; + const url = `${apiUrl}?range=${rangeSeconds}&device_id=${deviceId}`; try { const response = await fetch(url); if (!response.ok) throw new Error(`Failed fetching ${url}`); @@ -140,7 +145,7 @@ return { labels, dataPoints }; } - let cpuChart, ramChart, diskChart, uptimeChart; + let cpuChart; async function renderCpuChart(rangeSeconds) { const values = await fetchPrometheusData('/api/cpu-usage/', rangeSeconds); @@ -172,151 +177,14 @@ }); } - async function renderRamChart(rangeSeconds) { - const values = await fetchPrometheusData('/api/ram-usage/', rangeSeconds); - const { labels, dataPoints } = processValues(values); - - if (ramChart) ramChart.destroy(); - - const ctx = document.getElementById('ramChart').getContext('2d'); - ramChart = new Chart(ctx, { - type: 'line', - data: { - labels, - datasets: [{ - label: 'RAM Usage (%)', - data: dataPoints, - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - fill: true, - tension: 0.2 - }] - }, - options: { - scales: { - y: { beginAtZero: true, max: 100 } - }, - plugins: { legend: { labels: { font: { size: 12 } } } } - } - }); - } - - async function renderDiskChart(rangeSeconds) { - const valuesUsed = await fetchPrometheusData('/api/disk-used/', rangeSeconds); - const valuesFree = await fetchPrometheusData('/api/disk-free/', rangeSeconds); - if (valuesUsed.length === 0 || valuesFree.length === 0) return; - - const labels = valuesUsed.map(v => new Date(v[0] * 1000).toLocaleTimeString()); - const dataUsed = valuesUsed.map(v => parseFloat(v[1])); - const dataFree = valuesFree.map(v => parseFloat(v[1])); - - if (diskChart) diskChart.destroy(); - - const ctx = document.getElementById('diskChart').getContext('2d'); - diskChart = new Chart(ctx, { - type: 'line', - data: { - labels, - datasets: [ - { - label: 'Disk Used (GB)', - data: dataUsed, - borderColor: 'rgba(255, 159, 64, 1)', - backgroundColor: 'rgba(255, 159, 64, 0.2)', - fill: true, - tension: 0.2 - }, - { - label: 'Disk Free (GB)', - data: dataFree, - borderColor: 'rgba(54, 162, 235, 1)', - backgroundColor: 'rgba(54, 162, 235, 0.2)', - fill: true, - tension: 0.2 - } - ] - }, - options: { - scales: { - y: { beginAtZero: true } - }, - plugins: { legend: { labels: { font: { size: 12 } } } } - } - }); - } - - // Convert seconds uptime to human readable string - function formatUptime(seconds) { - if (seconds < 60) return `${seconds.toFixed(0)} seconds`; - if (seconds < 3600) return `${(seconds / 60).toFixed(1)} minutes`; - if (seconds < 86400) return `${(seconds / 3600).toFixed(2)} hours`; - return `${(seconds / 86400).toFixed(2)} days`; - } - - async function renderUptimeChart() { - // For uptime we will show last datapoint only, as uptime is cumulative. - // Fetch longer range for better context, say 30 days: - const values = await fetchPrometheusData('/api/uptime/', 2592000); // 30 days - if (values.length === 0) return; - - // Use just timestamps and uptime seconds: - const labels = values.map(v => new Date(v[0] * 1000).toLocaleDateString()); - const dataPoints = values.map(v => parseFloat(v[1])); - - if (uptimeChart) uptimeChart.destroy(); - - const ctx = document.getElementById('uptimeChart').getContext('2d'); - uptimeChart = new Chart(ctx, { - type: 'bar', - data: { - labels, - datasets: [{ - label: 'Uptime (seconds)', - data: dataPoints, - backgroundColor: 'rgba(0, 123, 255, 0.7)' - }] - }, - options: { - scales: { - y: { - beginAtZero: true, - ticks: { - callback: function(value) { - return formatUptime(value); - } - } - } - }, - plugins: { - legend: { labels: { font: { size: 12 } } }, - tooltip: { - callbacks: { - label: ctx => formatUptime(ctx.parsed.y) - } - } - } - } - }); - - // Show last uptime below chart - const lastUptime = dataPoints[dataPoints.length - 1]; - document.getElementById('uptimeReadable').textContent = `Latest uptime: ${formatUptime(lastUptime)}`; - } - async function updateCharts() { const selectedRange = timeRangeSelect.value; const seconds = timeRangeToSeconds(selectedRange); - - await Promise.all([ - renderCpuChart(seconds), - renderRamChart(seconds), - renderDiskChart(seconds) - ]); + await renderCpuChart(seconds); } // Initial load updateCharts(); - renderUptimeChart(); // Update on dropdown change timeRangeSelect.addEventListener('change', updateCharts); @@ -331,4 +199,5 @@ + {% endblock content %} \ No newline at end of file -- GitLab From a3da84745318f593233a10996d10eb841957f166 Mon Sep 17 00:00:00 2001 From: Asclepius Date: Tue, 24 Jun 2025 00:01:46 +0200 Subject: [PATCH 4/6] Added ramChart to monitor devices --- Makefile | 2 +- api/urls.py | 5 -- api/views.py | 24 ++++++--- .../prometheus/prometheus-targets-pvc.yaml | 2 +- templates/device.html | 52 ++++++++++++++----- 5 files changed, 59 insertions(+), 26 deletions(-) diff --git a/Makefile b/Makefile index dfc5d1c..372f07a 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ build-image: docker-env ## Build Docker image inside Minikube Docker start-minikube: ## Start Minikube VM with custom resources minikube stop || true - minikube start --driver=virtualbox --cpus=7 --memory=16000 --disk-size=60g + minikube start --driver=virtualbox --cpus=7 --memory=16000 --disk-size=90g reset-k8s: ## Delete all Kubernetes resources -files=$$(find kubernetes/ -name '*.yaml' -o -name '*.yml'); \ diff --git a/api/urls.py b/api/urls.py index d5f8411..f09ce1a 100644 --- a/api/urls.py +++ b/api/urls.py @@ -25,11 +25,6 @@ urlpatterns = [ path("view_devices", views.view_devices, name="view_devices"), path('api/cpu-usage/', views.cpu_usage), path('api/ram-usage/', views.ram_usage), - path('api/disk-used/', views.disk_used), - path('api/disk-free/', views.disk_free), - path('api/network-in/', views.network_in), - path('api/network-out/', views.network_out), - path('api/uptime/', views.uptime), path('new_device', views.new_device), path("new_device/submit/", views.submit_new_device, name="submit_new_device"), diff --git a/api/views.py b/api/views.py index 081cde4..8437856 100644 --- a/api/views.py +++ b/api/views.py @@ -196,12 +196,9 @@ def vm_get_all_info(request): ) return JsonResponse([vm_info], safe=False) -PROMETHEUS_URL = 'http://192.168.59.158:30090//api/v1/query_range' - -def query_prometheus_range(device_label, range_seconds): - # Create full PromQL query string with label - promql = f'avg(rate(node_cpu_seconds_total{{mode="idle",instance="{device_label}:9100"}}[{range_seconds}s]))' +PROMETHEUS_URL = 'http://192.168.59.160:30090//api/v1/query_range' +def query_prometheus_range(promql, range_seconds): end = int(datetime.utcnow().timestamp()) start = end - range_seconds @@ -227,13 +224,28 @@ def cpu_usage(request): try: device = Device.objects.get(proxmox_vmid=device_id) - values = query_prometheus_range(device.nodeexporterip, range_seconds) + promql = f'avg(rate(node_cpu_seconds_total{{mode="idle",instance="{device.nodeexporterip}:9100"}}[{range_seconds}s]))' + values = query_prometheus_range(promql, range_seconds) return JsonResponse({"results": [{"values": values}]}) except Device.DoesNotExist: return JsonResponse({"error": "Device not found"}, status=404) except Exception as e: return JsonResponse({"error": str(e)}, status=500) +@require_GET +def ram_usage(request): + device_id = request.GET.get("device_id") + range_seconds = int(request.GET.get("range", 300)) + + try: + device = Device.objects.get(proxmox_vmid=device_id) + promql = f'(avg_over_time(node_memory_MemTotal_bytes{{instance="{device.nodeexporterip}:9100"}}[{range_seconds}s]) - avg_over_time(node_memory_MemAvailable_bytes{{instance="{device.nodeexporterip}:9100"}}[{range_seconds}s])) / 1073741824' + values = query_prometheus_range(promql, range_seconds) + return JsonResponse({"results": [{"values": values}]}) + except Device.DoesNotExist: + return JsonResponse({"error": "Device not found"}, status=404) + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) @require_POST diff --git a/kubernetes/prometheus/prometheus-targets-pvc.yaml b/kubernetes/prometheus/prometheus-targets-pvc.yaml index 72180a8..bee55f8 100644 --- a/kubernetes/prometheus/prometheus-targets-pvc.yaml +++ b/kubernetes/prometheus/prometheus-targets-pvc.yaml @@ -7,4 +7,4 @@ spec: - ReadWriteOnce resources: requests: - storage: 1Gi \ No newline at end of file + storage: 10Gi \ No newline at end of file diff --git a/templates/device.html b/templates/device.html index f88e546..ad35e50 100644 --- a/templates/device.html +++ b/templates/device.html @@ -98,6 +98,7 @@ + {% else %}

No device information available.

@@ -105,6 +106,7 @@ + + {% endblock content %} \ No newline at end of file diff --git a/vmmanager/settings.py b/vmmanager/settings.py index f56a9fe..933ab2e 100644 --- a/vmmanager/settings.py +++ b/vmmanager/settings.py @@ -136,6 +136,10 @@ USE_TZ = True # https://docs.djangoproject.com/en/5.2/howto/static-files/ STATIC_URL = 'static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATICFILES_DIRS = ( + os.path.join(BASE_DIR, 'static_assets'), +) # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -- GitLab From 6c1d1fbe6c5f7a838df297935ef0e2e2651c3ad1 Mon Sep 17 00:00:00 2001 From: Asclepius Date: Tue, 24 Jun 2025 21:23:37 +0200 Subject: [PATCH 6/6] Changed device layout to include tabs --- api/views.py | 2 +- templates/device.html | 184 +++++++++++++++++++++++++----------------- 2 files changed, 109 insertions(+), 77 deletions(-) diff --git a/api/views.py b/api/views.py index 59714bb..08960cf 100644 --- a/api/views.py +++ b/api/views.py @@ -196,7 +196,7 @@ def vm_get_all_info(request): ) return JsonResponse([vm_info], safe=False) -PROMETHEUS_URL = 'http://192.168.59.160:30090//api/v1/query_range' +PROMETHEUS_URL = 'http://192.168.59.162:30090//api/v1/query_range' def query_prometheus_range(promql, range_seconds): end = int(datetime.utcnow().timestamp()) diff --git a/templates/device.html b/templates/device.html index 48509ba..87a927a 100644 --- a/templates/device.html +++ b/templates/device.html @@ -5,91 +5,113 @@ {% load static %} - + - Device Details + {{ device.name }} -
- -
- - ← BACK - +
+ +
+
+ + ← BACK + +

+ {{ device.name }} + + +
+ {% if vm.status.status == "running" %} + +
+ The device is running +
+ {% elif vm.status.status == "stopped" %} + +
+ The device is stopped +
+ {% else %} + +
+ The status is unknown +
+ {% endif %} +
+

+
+
+ + +
+
-

Device Details

+ +
+
+ +
+

General

+

Region: {{ device.region }}

+

SLA (availability): {{ device.availability_sla }}%

- {% if vm and vm != "Could not get VM" %} - -
-

Basic Information

-

Node: {{ vm.node }}

-

VMID: {{ vm.vmid }}

-

Name: {{ vm.config.name }}

-
+

OS: {{ device.operating_system }}

+

CPU: {{ device.cpu_cores }}

+

RAM: {{ device.ram_gb }}GB

+

ID: {{ device.proxmox_vmid }}

-
-

Device Info (DB)

-

Name: {{ device.name }}

-

IP: {{ device.ip }}

-

Organization: {{ device.organization.name }}

-

Description: {{ device.description }}

-

Location: {{ device.location }}

-

Node Exporter IP: {{ device.nodeexporterip }}

-
+

Purchased: {{ device.time_purchased }}

- -
-

Configuration

-

OS Type: {{ vm.config.ostype }}

-

CPU: {{ vm.config.cpu }}

-

Cores: {{ vm.config.cores }}

-

Memory: {{ vm.config.memory }} MB

-

Sockets: {{ vm.config.sockets }}

-

Boot Order: {{ vm.config.boot }}

-
- -
-

Status

-

Status: {{ vm.status.status }}

-

Uptime: {{ vm.status.uptime }}

-

CPUs: {{ vm.status.cpus }}

-

Max Memory: {{ vm.status.maxmem|filesizeformat }}

-

Max Disk: {{ vm.status.maxdisk|filesizeformat }}

-
+
+ + +
+

Notes

+
    +
  • {{ device.comment }}
  • +
+
+ + +
+

Activities

+
    +
  • Started VM
  • +
  • Updated Software
  • +
  • Snapshot created
  • +
  • Resource alert
  • +
+
+ - -
-

Network and Storage

-

Net0: {{ vm.config.net0 }}

-

SCSI0: {{ vm.config.scsi0 }}

-

Disk Read: {{ vm.status.diskread }}

-

Disk Write: {{ vm.status.diskwrite }}

-
- -
-

Snapshots

-
    - {% for snap in vm.snapshots %} -
  • {{ snap.name }}: {{ snap.description }}
  • - {% endfor %} -
+
+ + + - + +
+ + + + + + - - - - {% endblock content %} \ No newline at end of file -- GitLab