diff --git a/services/bstein-dev-home/backend-deployment.yaml b/services/bstein-dev-home/backend-deployment.yaml index b0d59f551..fbaa425ee 100644 --- a/services/bstein-dev-home/backend-deployment.yaml +++ b/services/bstein-dev-home/backend-deployment.yaml @@ -123,6 +123,8 @@ spec: value: "" - name: HTTP_CHECK_TIMEOUT_SEC value: "15" + - name: MONERO_GET_INFO_URL + value: http://monerod.crypto.svc.cluster.local:18084/get_info - name: PORTAL_DB_POOL_MIN value: "0" - name: PORTAL_DB_POOL_MAX diff --git a/services/crypto/monerod/deployment.yaml b/services/crypto/monerod/deployment.yaml index c69343d30..7c24efcee 100644 --- a/services/crypto/monerod/deployment.yaml +++ b/services/crypto/monerod/deployment.yaml @@ -57,6 +57,9 @@ spec: - --rpc-bind-ip=0.0.0.0 - --rpc-bind-port=18081 - --confirm-external-bind + - --rpc-ssl=disabled + - --log-file=/tmp/monerod.log + - --max-log-files=2 - --p2p-bind-ip=0.0.0.0 - --p2p-bind-port=18080 - --no-igd @@ -106,8 +109,45 @@ spec: volumeMounts: - { name: data, mountPath: /data } - { name: tmp, mountPath: /tmp } + - name: status-proxy + image: python:3.11-alpine + command: ["python", "/app/status_proxy.py"] + ports: + - { name: status, containerPort: 18084 } + env: + - name: MONEROD_LOG_FILE + value: /tmp/monerod.log + - name: MONEROD_STATUS_RPC_TIMEOUT_SEC + value: "1.25" + readinessProbe: + httpGet: + path: /healthz + port: status + initialDelaySeconds: 2 + periodSeconds: 10 + timeoutSeconds: 2 + livenessProbe: + httpGet: + path: /healthz + port: status + initialDelaySeconds: 10 + periodSeconds: 20 + timeoutSeconds: 2 + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 128Mi + volumeMounts: + - { name: tmp, mountPath: /tmp } + - { name: status-proxy, mountPath: /app/status_proxy.py, subPath: status_proxy.py } volumes: - name: data persistentVolumeClaim: { claimName: monerod-chain } - name: tmp emptyDir: {} + - name: status-proxy + configMap: + name: monerod-status-proxy diff --git a/services/crypto/monerod/kustomization.yaml b/services/crypto/monerod/kustomization.yaml index d6c1b19a1..d849663b0 100644 --- a/services/crypto/monerod/kustomization.yaml +++ b/services/crypto/monerod/kustomization.yaml @@ -4,6 +4,7 @@ kind: Kustomization resources: - pvc.yaml - cm-release-keys.yaml + - status-proxy-configmap.yaml - deployment.yaml - service.yaml - ingress.yaml diff --git a/services/crypto/monerod/service.yaml b/services/crypto/monerod/service.yaml index cc97c92de..99f691dfa 100644 --- a/services/crypto/monerod/service.yaml +++ b/services/crypto/monerod/service.yaml @@ -10,5 +10,6 @@ spec: selector: { app: monerod } ports: - { name: rpc, port: 18081, targetPort: 18081 } + - { name: status, port: 18084, targetPort: 18084 } - { name: p2p, port: 18080, targetPort: 18080 } - { name: zmq, port: 18083, targetPort: 18083 } diff --git a/services/crypto/monerod/status-proxy-configmap.yaml b/services/crypto/monerod/status-proxy-configmap.yaml new file mode 100644 index 000000000..ea6ce3dcd --- /dev/null +++ b/services/crypto/monerod/status-proxy-configmap.yaml @@ -0,0 +1,94 @@ +# services/crypto/monerod/status-proxy-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: monerod-status-proxy + namespace: crypto +data: + status_proxy.py: | + from __future__ import annotations + + import json + import os + import re + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + from urllib import error, request + + LOG_FILE = os.environ.get("MONEROD_LOG_FILE", "/tmp/monerod.log") + LISTEN_PORT = int(os.environ.get("MONEROD_STATUS_PORT", "18084")) + RPC_TIMEOUT_SEC = float(os.environ.get("MONEROD_STATUS_RPC_TIMEOUT_SEC", "1.25")) + RPC_URL = os.environ.get("MONEROD_RPC_URL", "http://127.0.0.1:18081/get_info") + + SYNC_RE = re.compile(r"Synced\s+(\d+)/(\d+).*?(\d+)\s+left") + CANDIDATE_RE = re.compile( + r"candidate:\s+(\d+)\s+->\s+(\d+).*?node is\s+(\d+)\s+blocks", + re.IGNORECASE, + ) + + + def _tail_log(path: str, limit: int = 262_144) -> str: + try: + size = os.path.getsize(path) + with open(path, "rb") as handle: + handle.seek(max(0, size - limit)) + return handle.read().decode("utf-8", "ignore") + except OSError: + return "" + + + def _sync_from_logs() -> tuple[int, int]: + for line in reversed(_tail_log(LOG_FILE).splitlines()): + match = SYNC_RE.search(line) or CANDIDATE_RE.search(line) + if match: + return int(match.group(1)), int(match.group(2)) + return 0, 0 + + + def _rpc_get_info() -> dict: + with request.urlopen(RPC_URL, timeout=RPC_TIMEOUT_SEC) as response: + payload = json.loads(response.read().decode("utf-8")) + if isinstance(payload, dict): + payload.setdefault("rpc_status", "ok") + return payload + return {"status": "BAD_UPSTREAM", "rpc_status": "bad_upstream"} + + + def _busy_status() -> dict: + height, target_height = _sync_from_logs() + return { + "nettype": "mainnet", + "status": "SYNCING_RPC_BUSY", + "height": height, + "target_height": target_height, + "synchronized": False, + "rpc_status": "busy", + } + + + class Handler(BaseHTTPRequestHandler): + def _json(self, status: int, payload: dict) -> None: + body = json.dumps(payload).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self) -> None: # noqa: N802 + if self.path == "/healthz": + self._json(200, {"status": "ok"}) + return + if self.path.split("?", 1)[0] != "/get_info": + self._json(404, {"status": "NOT_FOUND"}) + return + try: + self._json(200, _rpc_get_info()) + except (OSError, TimeoutError, ValueError, error.URLError): + self._json(200, _busy_status()) + + def log_message(self, fmt: str, *args) -> None: + return + + + if __name__ == "__main__": + ThreadingHTTPServer(("", LISTEN_PORT), Handler).serve_forever()