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-usl-proxmox
Aplikacja służy do zasilania danymi przez REST API w bazie danych PostgreSQL: dba_mors pochodzącymi z metryk serwera Proxmox 9.
- https://itadmin.vblog.ovh/proxmox-9-wystawienie-metryk-serwera-przez-mini-serwer-python-3/
- https://itadmin.vblog.ovh/proxmox-ve-9-1-4-wlasna-modyfikacja-okna-summary-w-konsoli-gui/
Struktura aplikacji mors-usl-proxmox
/repo/pakiety/morsusl/1.0.0/mors-usl-proxmox drwxr-xr-x 2 bastuser bastuser 21 Feb 12 21:25 app -rw-r--r-- 1 bastuser bastuser 448 Feb 12 21:39 cm-mors-usl-proxmox.yaml -rw-r--r-- 1 bastuser bastuser 406 Feb 12 21:24 Dockerfile -rw-r--r-- 1 bastuser bastuser 4 Feb 12 21:47 requirements.txt -rw-r--r-- 1 bastuser bastuser 155 Feb 12 21:27 secret-mors-usl-api-token.yaml
Przygotowanie Dockerfile na potrzeby zbudowania obrazu aplikacji
FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Minimalne zależności (requests)
RUN pip install --no-cache-dir requests
# Skopiuj kod aplikacji
COPY app/main.py /app/main.py
# Użytkownik nie-root (OpenShift i tak uruchamia jako losowy UID, ale to nie szkodzi)
RUN useradd -u 10001 -m appuser && chown -R 10001:0 /app
USER 10001
CMD ["python", "/app/main.py"]Kod aplikacji python mors-usl-proxmox
#!/usr/bin/env python3
import json
import os
import sys
import time
from datetime import datetime, timezone
from typing import Any, Dict, Optional, Tuple
import requests
import warnings
from urllib3.exceptions import InsecureRequestWarning
def env_int(name: str, default: int) -> int:
raw = os.getenv(name, str(default)).strip()
try:
return int(raw)
except ValueError:
return default
def env_bool(name: str, default: bool) -> bool:
raw = os.getenv(name, "1" if default else "0").strip().lower()
return raw in ("1", "true", "yes", "y", "on")
def log(msg: str, level: str = "INFO") -> None:
ts = datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
print(f"{ts} [{level}] {msg}", flush=True)
def debug_log(enabled: bool, msg: str) -> None:
if enabled:
log(msg, "DEBUG")
def normalize_payload(src: Dict[str, Any]) -> Dict[str, Any]:
"""
Wejście: JSON z /pve_stats.json
Wyjście: JSON pod API /rest-api/v1/stats/proxmox/create
Wymagane pola:
cpu_usage, cpu_temp_c, sys_load, ram_usage_prc, swap_usage_prc,
gpu_busy_prc, gpu_temp_c, nvme_temp_c
"""
def get_required(key: str) -> Any:
if key not in src:
raise KeyError(f"Brak pola '{key}' w danych źródłowych.")
return src[key]
cpu_usage = get_required("cpu_usage")
cpu_temp_c = get_required("cpu_temp_c")
sys_load = get_required("sys_load")
ram_usage_prc = get_required("ram_usage_prc")
swap_usage_prc = get_required("swap_usage_prc")
gpu_busy_percent = get_required("gpu_busy_percent")
gpu_temp_c = get_required("gpu_temp_c")
nvme_temp_c = get_required("nvme_temp_c")
# W Twoim curl przykładzie temperatury były stringami ("59.2", "47.0")
# więc ujednolicamy do stringów z 1 miejscem po przecinku.
def temp_to_str(v: Any) -> str:
try:
return f"{float(v):.1f}"
except Exception:
return str(v)
payload = {
"cpu_usage": int(cpu_usage),
"cpu_temp_c": temp_to_str(cpu_temp_c),
"sys_load": str(sys_load),
"ram_usage_prc": int(ram_usage_prc),
"swap_usage_prc": int(swap_usage_prc),
"gpu_busy_prc": int(gpu_busy_percent), # mapowanie nazwy pola
"gpu_temp_c": temp_to_str(gpu_temp_c),
"nvme_temp_c": int(nvme_temp_c),
}
return payload
def fetch_stats(source_url: str, timeout_s: int, debug: bool) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
try:
r = requests.get(source_url, timeout=timeout_s)
debug_log(debug, f"GET {source_url} -> HTTP {r.status_code}")
r.raise_for_status()
return r.json(), None
except Exception as e:
return None, f"Nie udało się pobrać danych z SOURCE_URL: {e}"
def post_stats(
api_url: str,
api_token: str,
payload: Dict[str, Any],
timeout_s: int,
tls_verify: bool,
debug: bool,
) -> Optional[str]:
headers = {
"Content-Type": "application/json",
"Authorization": api_token,
}
try:
debug_log(debug, f"POST {api_url} tls_verify={tls_verify} payload={json.dumps(payload, ensure_ascii=False)}")
r = requests.post(
api_url,
headers=headers,
json=payload,
timeout=timeout_s,
verify=tls_verify,
)
debug_log(debug, f"POST -> HTTP {r.status_code} body={r.text[:500]}")
r.raise_for_status()
return None
except Exception as e:
return f"Nie udało się wysłać danych do API: {e}"
def main() -> int:
# Wymagane/konfigurowalne ENV
source_url = os.getenv("SOURCE_URL", "http://192.168.8.20:8080/pve_stats.json").strip()
api_url = os.getenv("API_URL", "").strip()
api_token = os.getenv("API_TOKEN", "").strip()
update_interval = env_int("UPDATE_INTERVAL", 5)
debug = env_bool("DEBUG", False)
# Odpowiednik curl -k (czyli brak weryfikacji TLS)
tls_verify = env_bool("API_TLS_VERIFY", False)
if not tls_verify:
warnings.simplefilter("ignore", InsecureRequestWarning)
http_timeout_s = env_int("HTTP_TIMEOUT", 10)
if not api_url:
log("Brak zmiennej środowiskowej API_URL (ConfigMap).", "ERROR")
return 2
if not api_token:
log("Brak zmiennej środowiskowej API_TOKEN (Secret).", "ERROR")
return 2
log("Start mors-usl-proxmox")
log(f"SOURCE_URL={source_url}")
log(f"API_URL={api_url}")
log(f"UPDATE_INTERVAL={update_interval}s DEBUG={1 if debug else 0} API_TLS_VERIFY={1 if tls_verify else 0}")
while True:
src, err = fetch_stats(source_url, http_timeout_s, debug)
if err:
log(err, "ERROR")
time.sleep(update_interval)
continue
try:
payload = normalize_payload(src or {})
except Exception as e:
log(f"Błąd normalizacji danych: {e}", "ERROR")
if debug:
debug_log(True, f"Źródłowy JSON (fragment): {json.dumps(src, ensure_ascii=False)[:2000]}")
time.sleep(update_interval)
continue
err = post_stats(api_url, api_token, payload, http_timeout_s, tls_verify, debug)
if err:
log(err, "ERROR")
else:
log("OK: wysłano statystyki do API")
time.sleep(update_interval)
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
log("Stop (KeyboardInterrupt)", "INFO")
sys.exit(0)
Co robi Service (w praktyce)
Service to „stabilny punkt dostępu” do Podów w klastrze. Pody żyją i umierają, mają zmienne IP. Service ma stały DNS i stały virtual IP (ClusterIP). Service kieruje ruch do Podów przez selektory labeli (np. app=mors-gui-api). W projekcie morsgui-prod jest mors-gui-api. Najlepsza praktyka w klastrze:Inne workloady w klastrze (np. mors-usl-proxmox w morsusl-prod) powinny trafiać do API przez Service, nie przez Route.
Czyli zamiast:
https://mors-gui-api.morsgui-prod.apps… (Route / ingress / TLS / zewnętrzny endpoint)
np. https://mors-gui-api.morsgui-prod.apps.testcluster.okdlab.local/rest-api/v1/stats/proxmox/create
powinno się używać:
http://mors-gui-api.morsgui-prod.svc:8080/… (wewnętrzny Service DNS)
np. http://mors-gui-api.morsgui-prod.svc:8080/rest-api/v1/stats/proxmox/create
To ma duże plusy:
omijasz ingress/router, zwykle omijasz TLS (możesz iść po HTTP w sieci klastra, jeśli akceptujesz), unikasz warningów TLS typu InsecureRequestWarning, mniej zależności od certów/route.
Co robi Route
Route wystawia Service „na zewnątrz” klastra przez router/ingress OpenShift. Route mapuje hosta DNS (np. …apps.testcluster…) do Service. Route może robić TLS (edge/passthrough/reencrypt), host-based routing itd.
Route jest potrzebna, jeśli:
Chcesz, żeby API było dostępne z sieci spoza klastra, albo z innych środowisk (np. z laptopa/VM poza OCP).
Kiedy Route nie jest potrzebna
Jeżeli jedynym klientem API jest inny Pod w klastrze (u Ciebie: mors-usl-proxmox), to w wielu przypadkach Route jest zbędna.
Przygotowanie configmap dla aplikacji mors-usl-proxmox
apiVersion: v1 kind: ConfigMap metadata: name: cm-mors-usl-proxmox namespace: morsusl-prod data: API_URL: "https://mors-gui-api.morsgui-prod.apps.testcluster.okdlab.local/rest-api/v1/stats/proxmox/create" SOURCE_URL: "http://192.168.8.20:8080/pve_stats.json" UPDATE_INTERVAL: "5" DEBUG: "1" # 0 = jak curl -k (bez weryfikacji TLS), 1 = normalna weryfikacja certu API_TLS_VERIFY: "0" # timeouty HTTP (sekundy) HTTP_TIMEOUT: "10"
Przygotowanie secrets dla aplikacji mors-usl-proxmox
apiVersion: v1 kind: Secret metadata: name: secret-mors-usl-api-token namespace: morsusl-prod type: Opaque stringData: API_TOKEN: "cxbn762xamnMYt2d"
Uruchomienie aplikacji mors-usl-proxmox w projekcie morsusl-prod
oc new-project morsusl-prod # Build binarny z Dockerfile oc new-build --strategy=docker --binary --name=mors-usl-proxmox-build cd /repo/pakiety/morsusl/1.0.0/mors-usl-proxmox/ oc start-build mors-usl-proxmox-build --from-dir=. --follow # Konfiguracja (Secret + ConfigMap) oc apply -f secret-mors-usl-api-token.yaml oc apply -f cm-mors-usl-proxmox.yaml # Deployment aplikacji oc new-app mors-usl-proxmox-build:latest --name=mors-usl-proxmox oc set env deployment/mors-usl-proxmox --from=configmap/cm-mors-usl-proxmox oc set env deployment/mors-usl-proxmox --from=secret/secret-mors-usl-api-token oc rollout restart deployment/mors-usl-proxmox oc rollout status deployment/mors-usl-proxmox
Weryfikacja działania aplikacji mors-usl-proxmox
[bastuser@bastion mors-usl-proxmox]$ oc logs -f mors-usl-proxmox-849db9554d-qgqlp 2026-02-12T19:40:57+00:00 [INFO] Start mors-usl-proxmox 2026-02-12T19:40:57+00:00 [INFO] SOURCE_URL=http://192.168.8.20:8080/pve_stats.json 2026-02-12T19:40:57+00:00 [INFO] API_URL=https://mors-gui-api-morsgui-prod.apps.testcluster.okdlab.local/rest-api/v1/stats/proxmox/create 2026-02-12T19:40:57+00:00 [INFO] UPDATE_INTERVAL=5s DEBUG=0 API_TLS_VERIFY=0 2026-02-12T19:40:57+00:00 [INFO] OK: wysłano statystyki do API

