Klaster OKD 4.19 (OpenShift) – Projekt monitorujący serwer PVE 9 i klaster OKD morsGUI, aplikacja mors-gui-web

Wstęp

Poniższy opis jest częścią większej całości https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-monitorowanie-serwera-pve-9-i-klastra-okd-projekt-mors/

Wymaga przygotowania klastra OKD 4.19 oraz serwera baz danych PostgreSQL

Prawidłowa kolejność przygotowania projektu MORS na klastrze OKD 4.19

Opis aplikacji mors-gui-web

Aplikacja służy do wyświetlania wykresów na podstawie danych umieszczonych w bazie danych PostgreSQL: dba_mors.
Dane są generowane przez 2 aplikacje

i wczytywane za pomocą API w aplikacji

Kod aplikacji mors-gui-api czyli zawartość index.php ze względu na swoją długość znajduje się na samym końcu tego wpisu.

Struktura aplikacji mors-gui-web

/repo/pakiety/morsgui/1.0.0/mors-gui-web
drwxr-xr-x 4 bastuser bastuser    46 Feb 13 17:49 ..
-rw-r--r-- 1 bastuser bastuser   565 Feb 13 17:51 cm-mors-gui-web.yaml
-rw-r--r-- 1 bastuser bastuser   380 Feb 13 18:00 Dockerfile
-rw-r--r-- 1 bastuser bastuser 23030 Feb 13 18:18 index.php

Przygotowanie configmapy dla aplikacji mors-gui-web

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-mors-gui-web
  namespace: morsgui-prod
data:
  # UI / wykresy
  PAGE_TITLE: "morsusl-gui • mors-gui-web"
  DEFAULT_RANGE_HOURS: "2"     # domyślne okno czasu po wejściu na stronę
  MAX_RANGE_HOURS: "72"        # twardy limit na zakres (żeby nie zajechać DB i frontu)
  MAX_POINTS: "1500"           # downsampling: max punktów na serię
  REFRESH_SECONDS: "10"        # co ile sekund odświeżać (zapisy są co 5s; 10s jest sensowne)
  DEBUG: "0"                   # 1 pokaże trochę info na dole strony

Przygotowanie Dockerfile na potrzeby zbudowania obrazu aplikacji

FROM php:8.2-cli-alpine
# Runtime libs dla pdo_pgsql (libpq.so.5)
RUN apk add --no-cache libpq
# Build deps tylko na czas kompilacji rozszerzenia
RUN apk add --no-cache --virtual .build-deps postgresql-dev \
  && docker-php-ext-install pdo_pgsql \
  && apk del .build-deps
WORKDIR /app
COPY index.php /app/index.php
EXPOSE 8080
CMD ["php", "-S", "0.0.0.0:8080", "-t", "/app"]

Uruchomienie aplikacji mors-gui-web w projekcie mors-gui-prod

oc new-project morsgui-prod

# Build binarny z Dockerfile
oc new-build --strategy=docker --binary --name=mors-gui-web-build

cd /repo/pakiety/morsgui/1.0.0/mors-gui-web/
oc start-build mors-gui-web-build --from-dir=. --follow

# ConfigMap web (tuning UI)
oc apply -f cm-mors-gui-web.yaml

# Tworzy deployment+svc z imagestream
oc new-app mors-gui-web-build:latest --name=mors-gui-web

# Wstrzyknij ustawienia UI
oc set env deployment/mors-gui-web --from=configmap/cm-mors-gui-web

# Wstrzyknij dane DB z istniejącej configmapy (TO JEST KLUCZOWE)
oc set env deployment/mors-gui-web --from=configmap/cm-mors-gui-api

# Route (żeby wejść w przeglądarce)
oc -n morsgui-prod create route edge mors-gui-web --service=mors-gui-web --port=8080

# Restart
oc rollout restart deployment/mors-gui-web

# Logi
oc logs -f deployment/mors-gui-web

Po zmianach w kodzie aplikacji

cd /repo/pakiety/morsgui/1.0.0/mors-gui-web/
oc start-build mors-gui-web-build --from-dir=. --follow
oc -n morsgui-prod rollout restart deployment/mors-gui-web
oc -n morsgui-prod logs -f deployment/mors-gui-web

Kod aplikacji mors-gui-web

Kod aplikacji znajduje się w 1 pliku index.php

  • Ten plik robi 2 rzeczy:
    1. Serwuje HTML (Bootstrap + Chart.js)
    2. Udostępnia API JSON pod ?api=proxmox i ?api=okd
  • Ma:
    • 2 wykresy (jeden pod drugim)
    • legendę
    • checkboxy pod każdym wykresem (włącz/wyłącz serię)
    • zakres dat (datetime-local) + szybkie “ostatnie X godzin”
    • auto-refresh (domyślnie ON) z ConfigMap
    • downsampling do sensownej liczby punktów (żeby Chart.js nie umarł przy 1 dniu)
getTimestamp() - $start->getTimestamp();
    if ($diff > $maxSeconds) {
        $start = $end->sub(new DateInterval('PT' . $maxSeconds . 'S'));
    }
    return [$start, $end];
}

function pdo(): PDO {
    $host = env('DBA_SERVER');
    $port = env('DBA_PORT', '5432');
    $db   = env('DBA_NAME');
    $user = env('DBA_USER');
    $pass = env('DBA_PASSWORD', '');
    $sslmode = env('DBA_SSLMODE', 'prefer'); // disable|allow|prefer|require|verify-ca|verify-full

    if ($host === '' || $db === '' || $user === '') {
        jsonOut([
            'error' => 'Brak wymaganych zmiennych środowiskowych DBA_SERVER/DBA_NAME/DBA_USER (sprawdź cm-mors-gui-api i oc set env).'
        ], 500);
    }

    $dsn = "pgsql:host={$host};port={$port};dbname={$db};sslmode={$sslmode}";

    $pdo = new PDO($dsn, $user, $pass, [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);

    // Wymuś sensowne formaty dat/czasu (opcjonalnie)
    $pdo->exec("SET TIME ZONE 'UTC'");

    return $pdo;
}

function downsampleRows(array $rows, int $maxPoints): array {
    $n = count($rows);
    if ($maxPoints <= 0 || $n <= $maxPoints) return $rows;

    $step = (int)ceil($n / $maxPoints);
    $out = [];
    for ($i = 0; $i < $n; $i += $step) {
        $out[] = $rows[$i];
    }
    return $out;
}

function toIsoUtc(string $ts): string {
    // $ts może mieć mikrosekundy; i tak da się to sparsować
    try {
        $dt = new DateTimeImmutable($ts, new DateTimeZone('UTC'));
    } catch (Throwable) {
        $dt = new DateTimeImmutable('now', new DateTimeZone('UTC'));
    }
    return $dt->setTimezone(new DateTimeZone('UTC'))->format(DateTimeInterface::ATOM);
}

function apiProxmox(DateTimeImmutable $start, DateTimeImmutable $end, int $maxPoints): void {
    $pdo = pdo();
    $stmt = $pdo->prepare("
        SELECT created_at, cpu_usage, cpu_temp_c, sys_load, ram_usage_prc, swap_usage_prc,
               gpu_busy_prc, gpu_temp_c, nvme_temp_c
        FROM proxmox
        WHERE created_at BETWEEN :start AND :end
        ORDER BY created_at ASC
    ");
    $stmt->execute([
        ':start' => $start->format('Y-m-d H:i:sP'),
        ':end'   => $end->format('Y-m-d H:i:sP'),
    ]);

    $rows = $stmt->fetchAll();
    $rows = downsampleRows($rows, $maxPoints);

    $out = [];
    foreach ($rows as $r) {
        $load = [null, null, null];
        if (isset($r['sys_load']) && $r['sys_load'] !== null) {
            // oczekiwany format: "5.87,5.92,6.07"
            $parts = array_map('trim', explode(',', (string)$r['sys_load']));
            for ($i = 0; $i < 3; $i++) {
                if (isset($parts[$i]) && $parts[$i] !== '') {
                    $load[$i] = (float)$parts[$i];
                }
            }
        }

        $out[] = [
            't' => toIsoUtc((string)$r['created_at']),
            'cpu_usage'      => isset($r['cpu_usage']) ? (float)$r['cpu_usage'] : null,
            'cpu_temp_c'     => isset($r['cpu_temp_c']) ? (float)$r['cpu_temp_c'] : null,
            'sys_load_1'      => $load[0],
            'sys_load_5'      => $load[1],
            'sys_load_15'     => $load[2],
            'ram_usage_prc'  => isset($r['ram_usage_prc']) ? (float)$r['ram_usage_prc'] : null,
            'swap_usage_prc' => isset($r['swap_usage_prc']) ? (float)$r['swap_usage_prc'] : null,
            'gpu_busy_prc'   => isset($r['gpu_busy_prc']) ? (float)$r['gpu_busy_prc'] : null,
            'gpu_temp_c'     => isset($r['gpu_temp_c']) ? (float)$r['gpu_temp_c'] : null,
            'nvme_temp_c'    => isset($r['nvme_temp_c']) ? (float)$r['nvme_temp_c'] : null,
        ];
    }

    jsonOut([
        'meta' => [
            'table' => 'proxmox',
            'start' => $start->format(DateTimeInterface::ATOM),
            'end'   => $end->format(DateTimeInterface::ATOM),
            'points' => count($out),
        ],
        'rows' => $out,
    ]);
}

function apiOkd(DateTimeImmutable $start, DateTimeImmutable $end, int $maxPoints): void {
    $pdo = pdo();
    $stmt = $pdo->prepare("
        SELECT created_at,
               compute_1_cpu_prc, compute_2_cpu_prc, compute_3_cpu_prc,
               control_1_cpu_prc, control_2_cpu_prc, control_3_cpu_prc,
               compute_1_ram_prc, compute_2_ram_prc, compute_3_ram_prc,
               control_1_ram_prc, control_2_ram_prc, control_3_ram_prc
        FROM okd
        WHERE created_at BETWEEN :start AND :end
        ORDER BY created_at ASC
    ");
    $stmt->execute([
        ':start' => $start->format('Y-m-d H:i:sP'),
        ':end'   => $end->format('Y-m-d H:i:sP'),
    ]);

    $rows = $stmt->fetchAll();
    $rows = downsampleRows($rows, $maxPoints);

    $out = [];
    foreach ($rows as $r) {
        $out[] = [
            't' => toIsoUtc((string)$r['created_at']),
            'compute_1_cpu_prc' => isset($r['compute_1_cpu_prc']) ? (float)$r['compute_1_cpu_prc'] : null,
            'compute_2_cpu_prc' => isset($r['compute_2_cpu_prc']) ? (float)$r['compute_2_cpu_prc'] : null,
            'compute_3_cpu_prc' => isset($r['compute_3_cpu_prc']) ? (float)$r['compute_3_cpu_prc'] : null,
            'control_1_cpu_prc' => isset($r['control_1_cpu_prc']) ? (float)$r['control_1_cpu_prc'] : null,
            'control_2_cpu_prc' => isset($r['control_2_cpu_prc']) ? (float)$r['control_2_cpu_prc'] : null,
            'control_3_cpu_prc' => isset($r['control_3_cpu_prc']) ? (float)$r['control_3_cpu_prc'] : null,
            'compute_1_ram_prc' => isset($r['compute_1_ram_prc']) ? (float)$r['compute_1_ram_prc'] : null,
            'compute_2_ram_prc' => isset($r['compute_2_ram_prc']) ? (float)$r['compute_2_ram_prc'] : null,
            'compute_3_ram_prc' => isset($r['compute_3_ram_prc']) ? (float)$r['compute_3_ram_prc'] : null,
            'control_1_ram_prc' => isset($r['control_1_ram_prc']) ? (float)$r['control_1_ram_prc'] : null,
            'control_2_ram_prc' => isset($r['control_2_ram_prc']) ? (float)$r['control_2_ram_prc'] : null,
            'control_3_ram_prc' => isset($r['control_3_ram_prc']) ? (float)$r['control_3_ram_prc'] : null,
        ];
    }

    jsonOut([
        'meta' => [
            'table' => 'okd',
            'start' => $start->format(DateTimeInterface::ATOM),
            'end'   => $end->format(DateTimeInterface::ATOM),
            'points' => count($out),
        ],
        'rows' => $out,
    ]);
}

/* ---------------------------
   API mode
----------------------------*/
if (isset($_GET['api'])) {
    $defaultHours = envInt('DEFAULT_RANGE_HOURS', 2);
    $maxRangeHours = envInt('MAX_RANGE_HOURS', 72);
    $maxPoints = envInt('MAX_POINTS', 1500);

    $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
    $fallbackStart = $now->sub(new DateInterval('PT' . max(1, $defaultHours) . 'H'));

    $start = parseDt($_GET['start'] ?? null, $fallbackStart);
    $end   = parseDt($_GET['end'] ?? null, $now);

    [$start, $end] = clampRange($start, $end, $maxRangeHours);

    $reqMaxPoints = isset($_GET['max_points']) && is_numeric($_GET['max_points']) ? (int)$_GET['max_points'] : $maxPoints;
    $reqMaxPoints = max(200, min($reqMaxPoints, $maxPoints)); // dolny limit, górny limit

    $api = (string)$_GET['api'];
    if ($api === 'proxmox') apiProxmox($start, $end, $reqMaxPoints);
    if ($api === 'okd')     apiOkd($start, $end, $reqMaxPoints);

    jsonOut(['error' => 'Nieznane api. Użyj: ?api=proxmox albo ?api=okd'], 400);
}

/* ---------------------------
   UI mode (HTML)
----------------------------*/
$title = env('PAGE_TITLE', 'morsusl-gui • mors-gui-web');
$defaultHours = envInt('DEFAULT_RANGE_HOURS', 2);
$refreshSec   = envInt('REFRESH_SECONDS', 10);
$maxPoints    = envInt('MAX_POINTS', 1500);
$debug        = envInt('DEBUG', 0);
?>



  
  
  <?= htmlspecialchars($title, ENT_QUOTES, 'UTF-8') ?>

  
  

  
  
  

  


2 wykresy: proxmox i okd • domyślnie ostatnie h • auto-refresh co s • max punktów na serię
Proxmox

Serie (checkbox = pokaż/ukryj):
OKD

Serie (checkbox = pokaż/ukryj):
DEBUG=1 • DBA_SERVER= • DBA_NAME=