Dashboard report
- {% block content %} - {% endblock %} -diff --git a/Dockerfile b/Dockerfile index d1a83e3..c480e43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,37 @@ -FROM alpine:3 AS build - -RUN apk update && \ - apk add --no-cache --virtual .build-deps \ - ca-certificates gcc postgresql-dev linux-headers musl-dev \ - libffi-dev jpeg-dev zlib-dev \ - git bash build-base python3-dev \ - dos2unix - -RUN python3 -m venv /venv -ENV PATH "/venv/bin:$PATH" -COPY ./requirements.txt / -RUN pip install -r /requirements.txt - -COPY ./docker-entrypoint.sh /docker-entrypoint.sh -RUN dos2unix /docker-entrypoint.sh && \ - chmod +x /docker-entrypoint.sh - - -FROM alpine:3 - -ENV LANG C.UTF-8 -ENV LC_ALL C.UTF-8 - -ENV PYTHONUNBUFFERED 1 -ENV PATH "/venv/bin:$PATH" - -RUN apk add --no-cache --update python3 curl - -COPY --from=build /venv /venv -COPY --from=build /docker-entrypoint.sh /docker-entrypoint.sh - -WORKDIR /app -COPY . /app - -ENTRYPOINT ["/docker-entrypoint.sh"] +FROM alpine:3 AS build + +RUN apk update && \ + apk add --no-cache --virtual .build-deps \ + ca-certificates gcc postgresql-dev linux-headers musl-dev \ + libffi-dev jpeg-dev zlib-dev \ + git bash build-base python3-dev \ + dos2unix + +RUN python3 -m venv /venv +ENV PATH "/venv/bin:$PATH" +COPY ./requirements.txt / +RUN pip install -r /requirements.txt + +COPY ./docker-entrypoint.sh /docker-entrypoint.sh +RUN dos2unix /docker-entrypoint.sh && \ + chmod +x /docker-entrypoint.sh + + +FROM alpine:3 + +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 + +ENV PYTHONUNBUFFERED 1 +ENV PATH "/venv/bin:$PATH" + +RUN apk add --no-cache --update python3 curl + +COPY --from=build /venv /venv +COPY --from=build /docker-entrypoint.sh /docker-entrypoint.sh + +WORKDIR /app +COPY . /app + +ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"] \ No newline at end of file diff --git a/dashboard/urls.py b/dashboard/urls.py index da9adb3..cc35b0a 100644 --- a/dashboard/urls.py +++ b/dashboard/urls.py @@ -1,8 +1,8 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('', views.index, name='index'), - path('api/stats/', views.api_stats), - path('api/audits/', views.api_audits), +from django.urls import path +from . import views + +urlpatterns = [ + path('', views.index, name='index'), + path('api/stats/', views.api_stats), + path('api/audits/', views.api_audits), ] \ No newline at end of file diff --git a/dashboard/views.py b/dashboard/views.py index d215c3c..84f24d3 100644 --- a/dashboard/views.py +++ b/dashboard/views.py @@ -1,252 +1,252 @@ -import json -from concurrent.futures import ThreadPoolExecutor, as_completed - -from django.conf import settings -from django.core.cache import cache -from django.http import JsonResponse -from django.shortcuts import render -from dashboard.openstack_utils.connect import get_connection -from dashboard.openstack_utils.flavor import get_flavor_list -from dashboard.prometheus_utils.query import query_prometheus -from dashboard.openstack_utils.audits import get_audits -from dashboard.mock_data import get_mock_context - -# Prometheus queries run in parallel (query_key -> query string) -_PROMETHEUS_QUERIES = { - "hosts_total": "count(node_exporter_build_info{job='node_exporter_compute'})", - "pcpu_total": "sum(count(node_cpu_seconds_total{job='node_exporter_compute', mode='idle'}) without (cpu,mode))", - "pcpu_usage": "sum(node_load5{job='node_exporter_compute'})", - "vcpu_allocated": "sum(libvirt_domain_info_virtual_cpus)", - "vcpu_overcommit_max": "avg(openstack_placement_resource_allocation_ratio{resourcetype='VCPU'})", - "pram_total": "sum(node_memory_MemTotal_bytes{job='node_exporter_compute'})", - "pram_usage": "sum(node_memory_Active_bytes{job='node_exporter_compute'})", - "vram_allocated": "sum(libvirt_domain_info_maximum_memory_bytes)", - "vram_overcommit_max": "avg(avg_over_time(openstack_placement_resource_allocation_ratio{resourcetype='MEMORY_MB'}[5m]))", - "vm_count": "sum(libvirt_domain_state_code)", - "vm_active": "sum(libvirt_domain_state_code{stateDesc='the domain is running'})", -} - - -def _fetch_prometheus_metrics(): - """Run all Prometheus queries in parallel and return a dict of name -> value.""" - result = {} - with ThreadPoolExecutor(max_workers=len(_PROMETHEUS_QUERIES)) as executor: - future_to_key = { - executor.submit(query_prometheus, query=q): key - for key, q in _PROMETHEUS_QUERIES.items() - } - for future in as_completed(future_to_key): - key = future_to_key[future] - try: - raw = future.result() - if key in ("pcpu_usage", "vcpu_overcommit_max", "vram_overcommit_max"): - result[key] = float(raw) - else: - result[key] = int(raw) - except (ValueError, TypeError): - result[key] = 0 if key in ("pcpu_usage", "vcpu_overcommit_max", "vram_overcommit_max") else 0 - return result - - -def collect_context(): - connection = get_connection() - region_name = connection._compute_region - flavors = get_flavor_list(connection=connection) - audits = get_audits(connection=connection) - - metrics = _fetch_prometheus_metrics() - hosts_total = metrics.get("hosts_total") or 1 - pcpu_total = metrics.get("pcpu_total", 0) - pcpu_usage = metrics.get("pcpu_usage", 0) - vcpu_allocated = metrics.get("vcpu_allocated", 0) - vcpu_overcommit_max = metrics.get("vcpu_overcommit_max", 0) - pram_total = metrics.get("pram_total", 0) - pram_usage = metrics.get("pram_usage", 0) - vram_allocated = metrics.get("vram_allocated", 0) - vram_overcommit_max = metrics.get("vram_overcommit_max", 0) - vm_count = metrics.get("vm_count", 0) - vm_active = metrics.get("vm_active", 0) - - vcpu_total = pcpu_total * vcpu_overcommit_max - vram_total = pram_total * vram_overcommit_max - - context = { - # <--- Region data ---> - "region": { - "name": region_name, - "hosts_total": hosts_total, - }, - # <--- CPU data ---> - # pCPU data - "pcpu": { - "total": pcpu_total, - "usage": pcpu_usage, - "free": pcpu_total - pcpu_usage, - "used_percentage": (pcpu_usage / pcpu_total * 100) if pcpu_total else 0, - }, - # vCPU data - "vcpu": { - "total": vcpu_total, - "allocated": vcpu_allocated, - "free": vcpu_total - vcpu_allocated, - "allocated_percentage": (vcpu_allocated / vcpu_total * 100) if vcpu_total else 0, - "overcommit_ratio": (vcpu_allocated / pcpu_total) if pcpu_total else 0, - "overcommit_max": vcpu_overcommit_max, - }, - # <--- RAM data ---> - # pRAM data - "pram": { - "total": pram_total, - "usage": pram_usage, - "free": pram_total - pram_usage, - "used_percentage": (pram_usage / pram_total * 100) if pram_total else 0, - }, - # vRAM data - "vram": { - "total": vram_total, - "allocated": vram_allocated, - "free": vram_total - vram_allocated, - "allocated_percentage": (vram_allocated / vram_total * 100) if vram_total else 0, - "overcommit_ratio": (vram_allocated / pram_total) if pram_total else 0, - "overcommit_max": vram_overcommit_max, - }, - # <--- VM data ---> - "vm": { - "count": vm_count, - "active": vm_active, - "stopped": vm_count - vm_active, - "avg_cpu": vcpu_allocated / vm_count if vm_count else 0, - "avg_ram": vram_allocated / vm_count if vm_count else 0, - "density": vm_count / hosts_total if hosts_total else 0, - }, - "flavors": flavors, - "audits": audits, - } - # Serialize audit list fields for JavaScript so cached context is render-ready - for audit in context["audits"]: - audit["migrations"] = json.dumps(audit["migrations"]) - audit["host_labels"] = json.dumps(audit["host_labels"]) - audit["cpu_current"] = json.dumps(audit["cpu_current"]) - audit["cpu_projected"] = json.dumps(audit["cpu_projected"]) - return context - - -def collect_stats(): - """Build stats dict: region, pcpu, pram, vcpu, vram, vm, flavors (no audits).""" - connection = get_connection() - region_name = connection._compute_region - flavors = get_flavor_list(connection=connection) - metrics = _fetch_prometheus_metrics() - hosts_total = metrics.get("hosts_total") or 1 - pcpu_total = metrics.get("pcpu_total", 0) - pcpu_usage = metrics.get("pcpu_usage", 0) - vcpu_allocated = metrics.get("vcpu_allocated", 0) - vcpu_overcommit_max = metrics.get("vcpu_overcommit_max", 0) - pram_total = metrics.get("pram_total", 0) - pram_usage = metrics.get("pram_usage", 0) - vram_allocated = metrics.get("vram_allocated", 0) - vram_overcommit_max = metrics.get("vram_overcommit_max", 0) - vm_count = metrics.get("vm_count", 0) - vm_active = metrics.get("vm_active", 0) - vcpu_total = pcpu_total * vcpu_overcommit_max - vram_total = pram_total * vram_overcommit_max - return { - "region": {"name": region_name, "hosts_total": hosts_total}, - "pcpu": { - "total": pcpu_total, - "usage": pcpu_usage, - "free": pcpu_total - pcpu_usage, - "used_percentage": (pcpu_usage / pcpu_total * 100) if pcpu_total else 0, - }, - "vcpu": { - "total": vcpu_total, - "allocated": vcpu_allocated, - "free": vcpu_total - vcpu_allocated, - "allocated_percentage": (vcpu_allocated / vcpu_total * 100) if vcpu_total else 0, - "overcommit_ratio": (vcpu_allocated / pcpu_total) if pcpu_total else 0, - "overcommit_max": vcpu_overcommit_max, - }, - "pram": { - "total": pram_total, - "usage": pram_usage, - "free": pram_total - pram_usage, - "used_percentage": (pram_usage / pram_total * 100) if pram_total else 0, - }, - "vram": { - "total": vram_total, - "allocated": vram_allocated, - "free": vram_total - vram_allocated, - "allocated_percentage": (vram_allocated / vram_total * 100) if vram_total else 0, - "overcommit_ratio": (vram_allocated / pram_total) if pram_total else 0, - "overcommit_max": vram_overcommit_max, - }, - "vm": { - "count": vm_count, - "active": vm_active, - "stopped": vm_count - vm_active, - "avg_cpu": vcpu_allocated / vm_count if vm_count else 0, - "avg_ram": vram_allocated / vm_count if vm_count else 0, - "density": vm_count / hosts_total if hosts_total else 0, - }, - "flavors": flavors, - } - - -def collect_audits(): - """Build audits list with serialized fields for frontend.""" - connection = get_connection() - audits = get_audits(connection=connection) - for audit in audits: - audit["migrations"] = json.dumps(audit["migrations"]) - audit["host_labels"] = json.dumps(audit["host_labels"]) - audit["cpu_current"] = json.dumps(audit["cpu_current"]) - audit["cpu_projected"] = json.dumps(audit["cpu_projected"]) - return audits - - -def _skeleton_context(): - """Minimal context for skeleton-only index render.""" - empty_flavors = { - "first_common_flavor": {"name": "—", "count": 0}, - "second_common_flavor": None, - "third_common_flavor": None, - } - return { - "skeleton": True, - "region": {"name": "—", "hosts_total": 0}, - "pcpu": {"total": 0, "usage": 0, "free": 0, "used_percentage": 0}, - "pram": {"total": 0, "usage": 0, "free": 0, "used_percentage": 0}, - "vcpu": {"total": 0, "allocated": 0, "free": 0, "allocated_percentage": 0, "overcommit_ratio": 0, "overcommit_max": 0}, - "vram": {"total": 0, "allocated": 0, "free": 0, "allocated_percentage": 0, "overcommit_ratio": 0, "overcommit_max": 0}, - "vm": {"count": 0, "active": 0, "stopped": 0, "avg_cpu": 0, "avg_ram": 0, "density": 0}, - "flavors": empty_flavors, - "audits": [], - } - - -def index(request): - if getattr(settings, "USE_MOCK_DATA", False): - context = get_mock_context() - return render(request, "index.html", context) - context = _skeleton_context() - return render(request, "index.html", context) - - -def api_stats(request): - cache_key = "dashboard_stats" - cache_ttl = getattr(settings, "DASHBOARD_CACHE_TTL", 120) - data = cache.get(cache_key) - if data is None: - data = collect_stats() - cache.set(cache_key, data, timeout=cache_ttl) - return JsonResponse(data) - - -def api_audits(request): - cache_key = "dashboard_audits" - cache_ttl = getattr(settings, "DASHBOARD_CACHE_TTL", 120) - audits = cache.get(cache_key) - if audits is None: - audits = collect_audits() - cache.set(cache_key, audits, timeout=cache_ttl) +import json +from concurrent.futures import ThreadPoolExecutor, as_completed + +from django.conf import settings +from django.core.cache import cache +from django.http import JsonResponse +from django.shortcuts import render +from dashboard.openstack_utils.connect import get_connection +from dashboard.openstack_utils.flavor import get_flavor_list +from dashboard.prometheus_utils.query import query_prometheus +from dashboard.openstack_utils.audits import get_audits +from dashboard.mock_data import get_mock_context + +# Prometheus queries run in parallel (query_key -> query string) +_PROMETHEUS_QUERIES = { + "hosts_total": "count(node_exporter_build_info{job='node_exporter_compute'})", + "pcpu_total": "sum(count(node_cpu_seconds_total{job='node_exporter_compute', mode='idle'}) without (cpu,mode))", + "pcpu_usage": "sum(node_load5{job='node_exporter_compute'})", + "vcpu_allocated": "sum(libvirt_domain_info_virtual_cpus)", + "vcpu_overcommit_max": "avg(openstack_placement_resource_allocation_ratio{resourcetype='VCPU'})", + "pram_total": "sum(node_memory_MemTotal_bytes{job='node_exporter_compute'})", + "pram_usage": "sum(node_memory_Active_bytes{job='node_exporter_compute'})", + "vram_allocated": "sum(libvirt_domain_info_maximum_memory_bytes)", + "vram_overcommit_max": "avg(avg_over_time(openstack_placement_resource_allocation_ratio{resourcetype='MEMORY_MB'}[5m]))", + "vm_count": "sum(libvirt_domain_state_code)", + "vm_active": "sum(libvirt_domain_state_code{stateDesc='the domain is running'})", +} + + +def _fetch_prometheus_metrics(): + """Run all Prometheus queries in parallel and return a dict of name -> value.""" + result = {} + with ThreadPoolExecutor(max_workers=len(_PROMETHEUS_QUERIES)) as executor: + future_to_key = { + executor.submit(query_prometheus, query=q): key + for key, q in _PROMETHEUS_QUERIES.items() + } + for future in as_completed(future_to_key): + key = future_to_key[future] + try: + raw = future.result() + if key in ("pcpu_usage", "vcpu_overcommit_max", "vram_overcommit_max"): + result[key] = float(raw) + else: + result[key] = int(raw) + except (ValueError, TypeError): + result[key] = 0 if key in ("pcpu_usage", "vcpu_overcommit_max", "vram_overcommit_max") else 0 + return result + + +def collect_context(): + connection = get_connection() + region_name = connection._compute_region + flavors = get_flavor_list(connection=connection) + audits = get_audits(connection=connection) + + metrics = _fetch_prometheus_metrics() + hosts_total = metrics.get("hosts_total") or 1 + pcpu_total = metrics.get("pcpu_total", 0) + pcpu_usage = metrics.get("pcpu_usage", 0) + vcpu_allocated = metrics.get("vcpu_allocated", 0) + vcpu_overcommit_max = metrics.get("vcpu_overcommit_max", 0) + pram_total = metrics.get("pram_total", 0) + pram_usage = metrics.get("pram_usage", 0) + vram_allocated = metrics.get("vram_allocated", 0) + vram_overcommit_max = metrics.get("vram_overcommit_max", 0) + vm_count = metrics.get("vm_count", 0) + vm_active = metrics.get("vm_active", 0) + + vcpu_total = pcpu_total * vcpu_overcommit_max + vram_total = pram_total * vram_overcommit_max + + context = { + # <--- Region data ---> + "region": { + "name": region_name, + "hosts_total": hosts_total, + }, + # <--- CPU data ---> + # pCPU data + "pcpu": { + "total": pcpu_total, + "usage": pcpu_usage, + "free": pcpu_total - pcpu_usage, + "used_percentage": (pcpu_usage / pcpu_total * 100) if pcpu_total else 0, + }, + # vCPU data + "vcpu": { + "total": vcpu_total, + "allocated": vcpu_allocated, + "free": vcpu_total - vcpu_allocated, + "allocated_percentage": (vcpu_allocated / vcpu_total * 100) if vcpu_total else 0, + "overcommit_ratio": (vcpu_allocated / pcpu_total) if pcpu_total else 0, + "overcommit_max": vcpu_overcommit_max, + }, + # <--- RAM data ---> + # pRAM data + "pram": { + "total": pram_total, + "usage": pram_usage, + "free": pram_total - pram_usage, + "used_percentage": (pram_usage / pram_total * 100) if pram_total else 0, + }, + # vRAM data + "vram": { + "total": vram_total, + "allocated": vram_allocated, + "free": vram_total - vram_allocated, + "allocated_percentage": (vram_allocated / vram_total * 100) if vram_total else 0, + "overcommit_ratio": (vram_allocated / pram_total) if pram_total else 0, + "overcommit_max": vram_overcommit_max, + }, + # <--- VM data ---> + "vm": { + "count": vm_count, + "active": vm_active, + "stopped": vm_count - vm_active, + "avg_cpu": vcpu_allocated / vm_count if vm_count else 0, + "avg_ram": vram_allocated / vm_count if vm_count else 0, + "density": vm_count / hosts_total if hosts_total else 0, + }, + "flavors": flavors, + "audits": audits, + } + # Serialize audit list fields for JavaScript so cached context is render-ready + for audit in context["audits"]: + audit["migrations"] = json.dumps(audit["migrations"]) + audit["host_labels"] = json.dumps(audit["host_labels"]) + audit["cpu_current"] = json.dumps(audit["cpu_current"]) + audit["cpu_projected"] = json.dumps(audit["cpu_projected"]) + return context + + +def collect_stats(): + """Build stats dict: region, pcpu, pram, vcpu, vram, vm, flavors (no audits).""" + connection = get_connection() + region_name = connection._compute_region + flavors = get_flavor_list(connection=connection) + metrics = _fetch_prometheus_metrics() + hosts_total = metrics.get("hosts_total") or 1 + pcpu_total = metrics.get("pcpu_total", 0) + pcpu_usage = metrics.get("pcpu_usage", 0) + vcpu_allocated = metrics.get("vcpu_allocated", 0) + vcpu_overcommit_max = metrics.get("vcpu_overcommit_max", 0) + pram_total = metrics.get("pram_total", 0) + pram_usage = metrics.get("pram_usage", 0) + vram_allocated = metrics.get("vram_allocated", 0) + vram_overcommit_max = metrics.get("vram_overcommit_max", 0) + vm_count = metrics.get("vm_count", 0) + vm_active = metrics.get("vm_active", 0) + vcpu_total = pcpu_total * vcpu_overcommit_max + vram_total = pram_total * vram_overcommit_max + return { + "region": {"name": region_name, "hosts_total": hosts_total}, + "pcpu": { + "total": pcpu_total, + "usage": pcpu_usage, + "free": pcpu_total - pcpu_usage, + "used_percentage": (pcpu_usage / pcpu_total * 100) if pcpu_total else 0, + }, + "vcpu": { + "total": vcpu_total, + "allocated": vcpu_allocated, + "free": vcpu_total - vcpu_allocated, + "allocated_percentage": (vcpu_allocated / vcpu_total * 100) if vcpu_total else 0, + "overcommit_ratio": (vcpu_allocated / pcpu_total) if pcpu_total else 0, + "overcommit_max": vcpu_overcommit_max, + }, + "pram": { + "total": pram_total, + "usage": pram_usage, + "free": pram_total - pram_usage, + "used_percentage": (pram_usage / pram_total * 100) if pram_total else 0, + }, + "vram": { + "total": vram_total, + "allocated": vram_allocated, + "free": vram_total - vram_allocated, + "allocated_percentage": (vram_allocated / vram_total * 100) if vram_total else 0, + "overcommit_ratio": (vram_allocated / pram_total) if pram_total else 0, + "overcommit_max": vram_overcommit_max, + }, + "vm": { + "count": vm_count, + "active": vm_active, + "stopped": vm_count - vm_active, + "avg_cpu": vcpu_allocated / vm_count if vm_count else 0, + "avg_ram": vram_allocated / vm_count if vm_count else 0, + "density": vm_count / hosts_total if hosts_total else 0, + }, + "flavors": flavors, + } + + +def collect_audits(): + """Build audits list with serialized fields for frontend.""" + connection = get_connection() + audits = get_audits(connection=connection) + for audit in audits: + audit["migrations"] = json.dumps(audit["migrations"]) + audit["host_labels"] = json.dumps(audit["host_labels"]) + audit["cpu_current"] = json.dumps(audit["cpu_current"]) + audit["cpu_projected"] = json.dumps(audit["cpu_projected"]) + return audits + + +def _skeleton_context(): + """Minimal context for skeleton-only index render.""" + empty_flavors = { + "first_common_flavor": {"name": "—", "count": 0}, + "second_common_flavor": None, + "third_common_flavor": None, + } + return { + "skeleton": True, + "region": {"name": "—", "hosts_total": 0}, + "pcpu": {"total": 0, "usage": 0, "free": 0, "used_percentage": 0}, + "pram": {"total": 0, "usage": 0, "free": 0, "used_percentage": 0}, + "vcpu": {"total": 0, "allocated": 0, "free": 0, "allocated_percentage": 0, "overcommit_ratio": 0, "overcommit_max": 0}, + "vram": {"total": 0, "allocated": 0, "free": 0, "allocated_percentage": 0, "overcommit_ratio": 0, "overcommit_max": 0}, + "vm": {"count": 0, "active": 0, "stopped": 0, "avg_cpu": 0, "avg_ram": 0, "density": 0}, + "flavors": empty_flavors, + "audits": [], + } + + +def index(request): + if getattr(settings, "USE_MOCK_DATA", False): + context = get_mock_context() + return render(request, "index.html", context) + context = _skeleton_context() + return render(request, "index.html", context) + + +def api_stats(request): + cache_key = "dashboard_stats" + cache_ttl = getattr(settings, "DASHBOARD_CACHE_TTL", 120) + data = cache.get(cache_key) + if data is None: + data = collect_stats() + cache.set(cache_key, data, timeout=cache_ttl) + return JsonResponse(data) + + +def api_audits(request): + cache_key = "dashboard_audits" + cache_ttl = getattr(settings, "DASHBOARD_CACHE_TTL", 120) + audits = cache.get(cache_key) + if audits is None: + audits = collect_audits() + cache.set(cache_key, audits, timeout=cache_ttl) return JsonResponse({"audits": audits}) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0224db2..ec99f10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,33 +1,33 @@ -asgiref==3.11.0 -certifi==2025.11.12 -cffi==2.0.0 -charset-normalizer==3.4.4 -cryptography==46.0.3 -decorator==5.2.1 -Django==5.2.8 -dogpile.cache==1.5.0 -idna==3.11 -iso8601==2.1.0 -jmespath==1.0.1 -jsonpatch==1.33 -jsonpointer==3.0.0 -keystoneauth1==5.12.0 -numpy==2.3.5 -openstacksdk==4.8.0 -os-service-types==1.8.2 -pandas==2.3.3 -pbr==7.0.3 -platformdirs==4.5.0 -psutil==7.1.3 -pycparser==2.23 -python-dateutil==2.9.0.post0 -pytz==2025.2 -PyYAML==6.0.3 -requests==2.32.5 -requestsexceptions==1.4.0 -six==1.17.0 -sqlparse==0.5.4 -stevedore==5.6.0 -typing_extensions==4.15.0 -tzdata==2025.2 -urllib3==2.5.0 +asgiref==3.11.0 +certifi==2025.11.12 +cffi==2.0.0 +charset-normalizer==3.4.4 +cryptography==46.0.3 +decorator==5.2.1 +Django==5.2.8 +dogpile.cache==1.5.0 +idna==3.11 +iso8601==2.1.0 +jmespath==1.0.1 +jsonpatch==1.33 +jsonpointer==3.0.0 +keystoneauth1==5.12.0 +numpy==2.3.5 +openstacksdk==4.8.0 +os-service-types==1.8.2 +pandas==2.3.3 +pbr==7.0.3 +platformdirs==4.5.0 +psutil==7.1.3 +pycparser==2.23 +python-dateutil==2.9.0.post0 +pytz==2025.2 +PyYAML==6.0.3 +requests==2.32.5 +requestsexceptions==1.4.0 +six==1.17.0 +sqlparse==0.5.4 +stevedore==5.6.0 +typing_extensions==4.15.0 +tzdata==2025.2 +urllib3==2.5.0 diff --git a/templates/base.html b/templates/base.html index c5f1a08..a26fa86 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,76 +1,76 @@ -{% load static %} - - -
- - -Dashboard report
- {% block content %} - {% endblock %} -Dashboard report
+ {% block content %} + {% endblock %} +