Ctrl K
ComunidadGuías…

scripts/fixtures/sandbox-environment-seed.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Small pinned-image QA service: SQLite JSON/TCP 5432 and HTTP backend 8080.

This deliberately uses a bounded fixture protocol, not PostgreSQL. Auto-role
selection is only for a fresh dedicated /24 with Debug/Test/Backend/DB leases
in that order (.2/.3/.4/.5). Production discovery never relies on these offsets.
"""
import argparse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import ipaddress
import json
from pathlib import Path
import socket
import socketserver
import sqlite3

SEED = "firecrab-environment-seed-414-v1"
LIMIT = 8192


def initialize(path):
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    with sqlite3.connect(path) as database:
        database.execute("CREATE TABLE IF NOT EXISTS seed(identity TEXT PRIMARY KEY)")
        database.execute("INSERT OR IGNORE INTO seed(identity) VALUES(?)", (SEED,))
        database.execute("CREATE TABLE IF NOT EXISTS writes(key TEXT PRIMARY KEY,value TEXT NOT NULL)")


def database_request(path, request):
    if not isinstance(request, dict):
        raise ValueError("request must be an object")
    operation = request.get("operation")
    if operation not in ("read", "write"):
        raise ValueError("unsupported operation")
    with sqlite3.connect(path, timeout=3) as database:
        if operation == "write":
            key, value = request.get("key"), request.get("value")
            if not isinstance(key, str) or not 1 <= len(key) <= 64 or not all(char.isalnum() or char in "_.-" for char in key):
                raise ValueError("invalid key")
            if not isinstance(value, str) or len(value.encode()) > 4096:
                raise ValueError("invalid value")
            database.execute("INSERT INTO writes(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", (key, value))
        return {"seed": database.execute("SELECT identity FROM seed ORDER BY identity").fetchall(),
                "values": dict(database.execute("SELECT key,value FROM writes ORDER BY key"))}


class DatabaseHandler(socketserver.StreamRequestHandler):
    def handle(self):
        self.request.settimeout(3)
        try:
            payload = self.rfile.readline(LIMIT + 1)
            if len(payload) > LIMIT or not payload.endswith(b"\n"):
                raise ValueError("request limit exceeded")
            reply = database_request(self.server.database_path, json.loads(payload))
            self.wfile.write(json.dumps(reply, separators=(",", ":")).encode() + b"\n")
        except (ValueError, sqlite3.Error, TimeoutError):
            self.wfile.write(b'{"error":"fixture_request_failed"}\n')


class DatabaseServer(socketserver.ThreadingTCPServer):
    allow_reuse_address = True
    daemon_threads = True

    def __init__(self, address, path):
        initialize(path)
        self.database_path = path
        super().__init__(address, DatabaseHandler)


def remote_database(host, port, request):
    payload = json.dumps(request, separators=(",", ":")).encode() + b"\n"
    if len(payload) > LIMIT:
        raise ValueError("request limit exceeded")
    with socket.create_connection((host, port), timeout=3) as connection:
        connection.sendall(payload)
        with connection.makefile("rb") as stream:
            response = stream.readline(LIMIT + 1)
    if len(response) > LIMIT or not response.endswith(b"\n"):
        raise ValueError("response limit exceeded")
    return json.loads(response)


class BackendHandler(BaseHTTPRequestHandler):
    def log_message(self, *_):
        pass

    def reply(self, operation):
        try:
            body = remote_database(self.server.database_host, self.server.database_port, operation)
            status = 200 if "error" not in body else 503
        except (OSError, ValueError):
            status, body = 503, {"error": "fixture_database_unavailable"}
        payload = json.dumps(body, separators=(",", ":")).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

    def do_GET(self):
        if self.path not in ("/health", "/seed"):
            self.send_error(404)
            return
        self.reply({"operation": "read"})

    def do_POST(self):
        if self.path != "/write":
            self.send_error(404)
            return
        try:
            length = int(self.headers.get("Content-Length", "0"))
            if not 1 <= length <= LIMIT:
                raise ValueError("request limit exceeded")
            body = json.loads(self.rfile.read(length))
            self.reply({"operation": "write", "key": body["key"], "value": body["value"]})
        except (ValueError, KeyError, TypeError):
            self.send_error(400)


class BackendServer(ThreadingHTTPServer):
    daemon_threads = True

    def __init__(self, address, database_host, database_port):
        self.database_host, self.database_port = database_host, database_port
        super().__init__(address, BackendHandler)


def auto_role():
    # UDP connect only selects a local route. It does not send an Internet packet.
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as route:
        route.connect(("192.0.2.1", 9))
        address = ipaddress.IPv4Address(route.getsockname()[0])
    role = {2: "idle", 3: "idle", 4: "backend", 5: "db"}.get(address.packed[-1])
    if role is None:
        raise ValueError("auto fixture requires fresh /24 leases .2/.3/.4/.5")
    database_host = str(ipaddress.IPv4Address((int(address) & 0xffffff00) + 5))
    return role, database_host


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--role", choices=("db", "backend", "auto"), default="auto")
    parser.add_argument("--listen", default="0.0.0.0")
    parser.add_argument("--database", default="/var/lib/firecrab-environment-seed/state.sqlite")
    parser.add_argument("--database-host")
    parser.add_argument("--database-port", type=int, default=5432)
    parser.add_argument("--http-port", type=int, default=8080)
    parser.add_argument("--init-only", action="store_true")
    args = parser.parse_args()
    if args.init_only:
        initialize(args.database)
        return
    role, database_host = auto_role() if args.role == "auto" else (args.role, args.database_host)
    if role == "idle":
        return
    if role == "db":
        server = DatabaseServer((args.listen, args.database_port), args.database)
    else:
        if not database_host:
            parser.error("backend requires --database-host")
        server = BackendServer((args.listen, args.http_port), database_host, args.database_port)
    with server:
        server.serve_forever()


if __name__ == "__main__":
    main()