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
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-na-proxmox-9-opis-instalacjii-konfiguracji/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-na-proxmox-9-opis-postgresql-baza-danych-dla-aplikacji-openshift/
Prawidłowa kolejność przygotowania projektu MORS na klastrze OKD 4.19
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-morsusl-aplikacja-mors-usl-proxmox/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-morsusl-aplikacja-mors-usl-okd/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-morsgui-aplikacja-mors-gui-api/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-projekt-morsgui-aplikacja-mors-gui-web/
Opis aplikacji mors-gui-api
Aplikacja służy do zasilania danymi przez REST API w bazie danych PostgreSQL: dba_mors generowanymi przez 2 aplikacje
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-morsusl-aplikacja-mors-usl-proxmox/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-monitorujacy-serwer-pve-9-i-klaster-okd-morsusl-aplikacja-mors-usl-okd/
Aplikacja wystawia 4 endpointy, służące do odczytu (read) i dodawania nowych rekordów (create)
- https://mors-gui-api-morsgui-prod.apps.testcluster.okdlab.local/rest-api/v1/stats/okd/read/1
- https://mors-gui-api-morsgui-prod.apps.testcluster.okdlab.local/rest-api/v1/stats/proxmox/read/1
- https://mors-gui-api-morsgui-prod.apps.testcluster.okdlab.local/rest-api/v1/stats/okd/create
- https://mors-gui-api-morsgui-prod.apps.testcluster.okdlab.local/rest-api/v1/stats/proxmox/create
Struktura aplikacji mors-gui-web
/repo/pakiety/morsgui/1.0.0/mors-gui-api drwxr-xr-x 2 bastuser bastuser 21 Feb 12 21:05 app -rw-r--r-- 1 bastuser bastuser 192 Feb 12 21:54 cm-mors-gui-api.yaml -rw-r--r-- 1 bastuser bastuser 508 Feb 12 21:11 Dockerfile -rw-r--r-- 1 bastuser bastuser 84 Feb 12 21:20 requirements.txt -rw-r--r-- 1 bastuser bastuser 176 Feb 12 21:54 secret-mors-gui-api.yaml
Przygotowanie schematu bazy danych dba_mors
Po wcześniejszym przygotowaniu serwera baz danych PostgreSQL wg opisu https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-na-proxmox-9-opis-postgresql-baza-danych-dla-aplikacji-openshift/ można przystąpić do utworzenia 2 tabel: okd i proxmox oraz nadania wybranych uprawnień dla użytkownika usr_mors.
# zalogowanie do serwera PSQL [root@database-1 ~]# sudo -iu postgres psql CREATE DATABASE dba_mors; -- Dalej uruchom już po połączeniu do bazy dba_mors (np. w psql: \c dba_mors) CREATE TABLE public.proxmox ( entry_id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, cpu_usage integer NOT NULL, cpu_temp_c varchar(5) NOT NULL, sys_load varchar(24) NOT NULL, ram_usage_prc integer NOT NULL, swap_usage_prc integer NOT NULL, gpu_busy_prc integer NOT NULL, gpu_temp_c varchar(24) NOT NULL, nvme_temp_c integer NOT NULL ); CREATE TABLE public.okd ( entry_id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, compute_1_cpu_prc integer NOT NULL, compute_2_cpu_prc integer NOT NULL, compute_3_cpu_prc integer NOT NULL, control_1_cpu_prc integer NOT NULL, control_2_cpu_prc integer NOT NULL, control_3_cpu_prc integer NOT NULL, compute_1_ram_prc integer NOT NULL, compute_2_ram_prc integer NOT NULL, compute_3_ram_prc integer NOT NULL, control_1_ram_prc integer NOT NULL, control_2_ram_prc integer NOT NULL, control_3_ram_prc integer NOT NULL ); CREATE ROLE usr_mors WITH LOGIN PASSWORD 'MorSdaT2578!'; GRANT CONNECT ON DATABASE dba_mors TO usr_mors; GRANT USAGE ON SCHEMA public TO usr_mors; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.proxmox TO usr_mors; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.okd TO usr_mors;
Przygotowanie Dockerfile na potrzeby zbudowania obrazu aplikacji
FROM python:3.13-slim
ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# zależności systemowe (minimalne) – psycopg potrzebuje libpq
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY app /app/app
EXPOSE 8080
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]Kod aplikacji python mors-gui-api
from __future__ import annotations
import os
from typing import Optional, List, Dict, Any, Literal
from fastapi import FastAPI, Header, HTTPException, Path
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel, Field, constr
import psycopg
# =========================================
# ENV (ConfigMap cm-mors-gui-api)
# =========================================
DBA_SERVER = os.getenv("DBA_SERVER", "127.0.0.1")
DBA_PORT = int(os.getenv("DBA_PORT", "5432"))
DBA_NAME = os.getenv("DBA_NAME", "postgres")
DBA_USER = os.getenv("DBA_USER", "postgres")
DBA_PASS = os.getenv("DBA_PASS", "")
API_TOKEN = (os.getenv("API_TOKEN") or "").strip()
APP_TITLE = "mors-gui-api"
APP_VERSION = "1.0.0"
def db_conn_str() -> str:
return (
f"host={DBA_SERVER} port={DBA_PORT} dbname={DBA_NAME} "
f"user={DBA_USER} password={DBA_PASS}"
)
# =========================================
# Auth
# =========================================
def normalize_auth_header(value: Optional[str]) -> str:
if not value:
return ""
v = value.strip()
# akceptuj "Bearer <token>" albo sam token
if v.lower().startswith("bearer "):
return v.split(" ", 1)[1].strip()
return v
def require_token(authorization: Optional[str]) -> None:
if not API_TOKEN:
raise HTTPException(status_code=500, detail="API token is not configured (API_TOKEN missing)")
got = normalize_auth_header(authorization)
if got != API_TOKEN:
raise HTTPException(status_code=401, detail="Unauthorized")
# =========================================
# Pydantic models (dopasowane do kolumn)
# =========================================
# public.proxmox:
# cpu_temp_c varchar(5), sys_load varchar(24), gpu_temp_c varchar(24)
# trzymamy to jako string (żeby nie wywaliło się na "59.2" vs "59,2" itp.)
class ProxmoxPayload(BaseModel):
cpu_usage: int = Field(..., ge=0)
cpu_temp_c: constr(max_length=5) # np. "59.2"
sys_load: constr(max_length=24) # np. "6.17,5.68,5.46"
ram_usage_prc: int = Field(..., ge=0, le=100)
swap_usage_prc: int = Field(..., ge=0, le=100)
gpu_busy_prc: int = Field(..., ge=0, le=100)
gpu_temp_c: constr(max_length=24) # wg DDL varchar(24)
nvme_temp_c: int = Field(..., ge=-100, le=200) # arbitralnie szerzej, żeby nie blokować
# public.okd:
# wszystkie pola integer NOT NULL
class OkdPayload(BaseModel):
compute_1_cpu_prc: int = Field(..., ge=0, le=100)
compute_2_cpu_prc: int = Field(..., ge=0, le=100)
compute_3_cpu_prc: int = Field(..., ge=0, le=100)
control_1_cpu_prc: int = Field(..., ge=0, le=100)
control_2_cpu_prc: int = Field(..., ge=0, le=100)
control_3_cpu_prc: int = Field(..., ge=0, le=100)
compute_1_ram_prc: int = Field(..., ge=0, le=100)
compute_2_ram_prc: int = Field(..., ge=0, le=100)
compute_3_ram_prc: int = Field(..., ge=0, le=100)
control_1_ram_prc: int = Field(..., ge=0, le=100)
control_2_ram_prc: int = Field(..., ge=0, le=100)
control_3_ram_prc: int = Field(..., ge=0, le=100)
StatsKind = Literal["okd", "proxmox"]
# =========================================
# DB operations (bez DDL)
# =========================================
def insert_proxmox(payload: ProxmoxPayload) -> None:
sql = """
INSERT INTO public.proxmox
(cpu_usage, cpu_temp_c, sys_load, ram_usage_prc, swap_usage_prc,
gpu_busy_prc, gpu_temp_c, nvme_temp_c)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
"""
with psycopg.connect(db_conn_str()) as conn:
with conn.cursor() as cur:
cur.execute(
sql,
(
payload.cpu_usage,
payload.cpu_temp_c,
payload.sys_load,
payload.ram_usage_prc,
payload.swap_usage_prc,
payload.gpu_busy_prc,
payload.gpu_temp_c,
payload.nvme_temp_c,
),
)
conn.commit()
def insert_okd(payload: OkdPayload) -> None:
sql = """
INSERT INTO public.okd
(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)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
with psycopg.connect(db_conn_str()) as conn:
with conn.cursor() as cur:
cur.execute(
sql,
(
payload.compute_1_cpu_prc,
payload.compute_2_cpu_prc,
payload.compute_3_cpu_prc,
payload.control_1_cpu_prc,
payload.control_2_cpu_prc,
payload.control_3_cpu_prc,
payload.compute_1_ram_prc,
payload.compute_2_ram_prc,
payload.compute_3_ram_prc,
payload.control_1_ram_prc,
payload.control_2_ram_prc,
payload.control_3_ram_prc,
),
)
conn.commit()
def read_last_proxmox(limit: int) -> List[Dict[str, Any]]:
sql = """
SELECT entry_id, 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 public.proxmox
ORDER BY entry_id DESC
LIMIT %s
"""
with psycopg.connect(db_conn_str()) as conn:
with conn.cursor() as cur:
cur.execute(sql, (limit,))
rows = cur.fetchall()
out: List[Dict[str, Any]] = []
for r in rows:
out.append(
{
"entry_id": r[0],
"created_at": r[1].strftime("%Y-%m-%d %H:%M:%S"),
"cpu_usage": r[2],
"cpu_temp_c": r[3], # varchar(5)
"sys_load": r[4], # varchar(24)
"ram_usage_prc": r[5],
"swap_usage_prc": r[6],
"gpu_busy_prc": r[7],
"gpu_temp_c": r[8], # varchar(24)
"nvme_temp_c": r[9],
}
)
return out
def read_last_okd(limit: int) -> List[Dict[str, Any]]:
sql = """
SELECT entry_id, 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 public.okd
ORDER BY entry_id DESC
LIMIT %s
"""
with psycopg.connect(db_conn_str()) as conn:
with conn.cursor() as cur:
cur.execute(sql, (limit,))
rows = cur.fetchall()
out: List[Dict[str, Any]] = []
for r in rows:
out.append(
{
"entry_id": r[0],
"created_at": r[1].strftime("%Y-%m-%d %H:%M:%S"),
"compute_1_cpu_prc": r[2],
"compute_2_cpu_prc": r[3],
"compute_3_cpu_prc": r[4],
"control_1_cpu_prc": r[5],
"control_2_cpu_prc": r[6],
"control_3_cpu_prc": r[7],
"compute_1_ram_prc": r[8],
"compute_2_ram_prc": r[9],
"compute_3_ram_prc": r[10],
"control_1_ram_prc": r[11],
"control_2_ram_prc": r[12],
"control_3_ram_prc": r[13],
}
)
return out
def db_smoke_test() -> None:
# szybki test połączenia (bez DDL)
with psycopg.connect(db_conn_str()) as conn:
with conn.cursor() as cur:
cur.execute("SELECT 1;")
cur.fetchone()
# =========================================
# FastAPI endpoints
# =========================================
app = FastAPI(title=APP_TITLE, version=APP_VERSION)
@app.on_event("startup")
def startup() -> None:
try:
db_smoke_test()
except Exception as e:
raise RuntimeError(f"Database connection failed: {e}") from e
@app.get("/healthz", response_class=PlainTextResponse)
def healthz():
return "OK"
# POST proxmox create
@app.post("/rest-api/v1/stats/proxmox/create", response_class=PlainTextResponse)
def proxmox_create(
payload: ProxmoxPayload,
authorization: Optional[str] = Header(default=None, alias="Authorization"),
):
require_token(authorization)
try:
insert_proxmox(payload)
return "OK"
except Exception:
return PlainTextResponse("ERROR", status_code=500)
# POST okd create
@app.post("/rest-api/v1/stats/okd/create", response_class=PlainTextResponse)
def okd_create(
payload: OkdPayload,
authorization: Optional[str] = Header(default=None, alias="Authorization"),
):
require_token(authorization)
try:
insert_okd(payload)
return "OK"
except Exception:
return PlainTextResponse("ERROR", status_code=500)
# GET proxmox read
@app.get("/rest-api/v1/stats/proxmox/read/{limit}")
def proxmox_read(
limit: int = Path(..., ge=1, le=10000),
authorization: Optional[str] = Header(default=None, alias="Authorization"),
):
require_token(authorization)
try:
return read_last_proxmox(limit)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Read failed: {e}")
# GET okd read
@app.get("/rest-api/v1/stats/okd/read/{limit}")
def okd_read(
limit: int = Path(..., ge=1, le=10000),
authorization: Optional[str] = Header(default=None, alias="Authorization"),
):
require_token(authorization)
try:
return read_last_okd(limit)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Read failed: {e}")Przygotowanie projektu morsgui-prod
oc new-project morsgui-prod oc new-build --strategy=docker --binary --name=mors-gui-api-build cd /repo/pakiety/morsgui/1.0.0/mors-gui-api/ oc start-build mors-gui-api-build --from-dir=. --follow # tylko w celach testowych, współczesną dobrą praktyką jest configmapa i secret jako manifest YAML tak jak poniżej: mors-gui-api.yaml, secret-mors-gui-api.yaml oc create configmap cm-mors-gui-api \ --from-literal=DBA_SERVER=192.168.40.25 \ --from-literal=DBA_NAME=dba_mors \ --from-literal=DBA_USER=usr_mors \ --from-literal=DBA_PASS='MorSdaT2578!' \ --from-literal=DBA_PORT=5432 \ --from-literal=API_TOKEN=cxbn762xamnMYt2d
Przygotowanie configmap dla aplikacji mors-gui-api
cd /repo/pakiety/morsgui/1.0.0/mors-gui-api oc apply -f cm-mors-gui-api.yaml
apiVersion: v1 kind: ConfigMap metadata: name: cm-mors-gui-api namespace: morsgui-prod data: DBA_SERVER: "192.168.40.25" DBA_PORT: "5432" DBA_NAME: "dba_mors" DBA_USER: "usr_mors"
Przygotowanie secrets dla aplikacji mors-gui-api
cd /repo/pakiety/morsgui/1.0.0/mors-gui-api oc apply -f secret-mors-gui-api.yaml
apiVersion: v1 kind: Secret metadata: name: secret-mors-gui-api namespace: morsgui-prod type: Opaque stringData: DBA_PASS: "MorSdaT2578!" API_TOKEN: "cxbn762xamnMYt2d"
Uruchomienie aplikacji mors-gui-api w projekcie morsgui-prod
# Utworzenie aplikacji z obrazu buildu (Deployment)
oc new-app mors-gui-api-build:latest --name=mors-gui-api
# Podpięcie env do Deployment
oc set env deployment/mors-gui-api --from=configmap/cm-mors-gui-api
oc set env deployment/mors-gui-api --from=secret/secret-mors-gui-api
# Jeśli Service już istnieje
oc get svc mors-gui-api -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
mors-gui-api ClusterIP 172.30.40.251 <none> 8080/TCP 31s deployment=mors-gui-api
# Jeśli serwis by nie istniał to należy go utworzyć (jeśli OpenShift nie wykrył automatycznie)
oc expose deployment/mors-gui-api --port=8080
# Uwtórz zwykłą Route http
oc expose svc/mors-gui-api
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
mors-gui-api mors-gui-api-morsgui-prod.apps.testcluster.okdlab.local mors-gui-api 8080-tcp None
# lub zdecydowanie lepiej Route https edge TLS
oc create route edge mors-gui-api --service=mors-gui-api --port=8080
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
mors-gui-api mors-gui-api-morsgui-prod.apps.testcluster.okdlab.local mors-gui-api 8080 edge None
# zmiana ze zwykłej na edget TLS
oc patch route mors-gui-api --type=merge -p '{"spec":{"tls":{"termination":"edge"}}}'
# Rollout / restart po zmianach. W Deployment nie ma rollout latest, tylko np. restart:
oc rollout restart deployment/mors-gui-api
oc rollout status deployment/mors-gui-api
# Logi poda
oc get pods
oc logs -f mors-gui-api-6f65797d45-q6dxp
INFO: Started server process [1]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)Co dokładnie robi Route z edge TLS
Bez TLS (to co masz po oc expose svc/mors-gui-api). Route jest HTTP (terminacja: None). Ruch idzie: klient → router OpenShift → service → pod. Po drodze nic nie jest szyfrowane na wejściu (połączenie klient↔router jest plain HTTP)
Konsekwencje:
token w nagłówku Authorization leci w sieci jawnie (to jest realnie słabe, jeśli to nie jest sieć w pełni zaufana). Przeglądarki i część klientów będą kręcić nosem (brak HTTPS)
Z TLS edge. Klient łączy się HTTPS do routera. Router robi terminację TLS (odszyfrowuje) i dalej do Twojego serwisu/poda idzie już HTTP. Schemat: klient (HTTPS) → router (TLS termination) → service (HTTP) → pod.
Co to daje:
szyfrowanie na odcinku klient↔router (najważniejszy odcinek). Sensowniejsze bezpieczeństwo (token, payload, itp.). Łatwiej potem wpiąć normalne certyfikaty i działać „jak człowiek” produkcyjnie
Weryfikacja działania aplikacji mors-gui-api
Z dowolnej maszyny sieci OKDLAB 192.168.40.0/24 np. maszyny bastion:
[bastuser@bastion mors-gui-api]$ curl -s -k -X GET -H "Content-Type: application/json" -H "Authorization: cxbn762xamnMYt2d" https://mors-gui-api-morsgui-prod.apps.testcluster.okdlab.local/rest-api/v1/stats/okd/read/1 | jq '.'
[
{
"entry_id": 1,
"created_at": "2026-02-12 21:26:38",
"compute_1_cpu_prc": 8,
"compute_2_cpu_prc": 12,
"compute_3_cpu_prc": 4,
"control_1_cpu_prc": 25,
"control_2_cpu_prc": 15,
"control_3_cpu_prc": 17,
"compute_1_ram_prc": 31,
"compute_2_ram_prc": 27,
"compute_3_ram_prc": 18,
"control_1_ram_prc": 71,
"control_2_ram_prc": 43,
"control_3_ram_prc": 42
}
]w logach poda aplikacji powinno być widać reakcję na zapytanie do API
oc logs -f mors-gui-api-6f65797d45-q6dxp INFO: Started server process [1] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) INFO: 10.129.2.2:37972 - "GET /rest-api/v1/stats/okd/read/1 HTTP/1.1" 200 OK INFO: 10.129.2.2:44244 - "GET /rest-api/v1/stats/okd/read/1 HTTP/1.1" 401 Unauthorized INFO: 10.130.0.2:52108 - "GET /rest-api/v1/stats/okd/read/1 HTTP/1.1" 200 OK
[bastuser@bastion mors-usl-proxmox]$ curl -s -k -X POST \
-H "Content-Type: application/json" \
-H "Authorization: cxbn762xamnMYt2d" \
-d '{
"compute_1_cpu_prc": 12,
"compute_2_cpu_prc": 9,
"compute_3_cpu_prc": 15,
"control_1_cpu_prc": 7,
"control_2_cpu_prc": 11,
"control_3_cpu_prc": 6,
"compute_1_ram_prc": 55,
"compute_2_ram_prc": 49,
"compute_3_ram_prc": 62,
"control_1_ram_prc": 38,
"control_2_ram_prc": 41,
"control_3_ram_prc": 35
}' \
https://mors-gui-api-morsgui-prod.apps.testcluster.okdlab.local/rest-api/v1/stats/okd/create
oc logs -f mors-gui-api-6f65797d45-q6dxp INFO: 10.129.2.2:60622 - "POST /rest-api/v1/stats/okd/create HTTP/1.1" 200 OK
curl -s -k -X POST \
-H "Content-Type: application/json" \
-H "Authorization: cxbn762xamnMYt2d" \
-d '{
"cpu_usage": 17,
"cpu_temp_c": "59.2",
"sys_load": "6.17,5.68,5.46",
"ram_usage_prc": 61,
"swap_usage_prc": 0,
"gpu_busy_prc": 0,
"gpu_temp_c": "47.0",
"nvme_temp_c": 39
}' \
https://mors-gui-api-morsgui-prod.apps.testcluster.okdlab.local/rest-api/v1/stats/proxmox/create

