mirror of
https://github.com/rustfs/rustfs.git
synced 2026-10-26 22:07:53 +00:00
177 lines
8.7 KiB
Python
177 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Node-local service control for isolated scanner benchmarks (Linux/systemd)."""
|
|
|
|
import argparse
|
|
import fcntl
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
|
|
|
|
def require(condition, message):
|
|
if not condition:
|
|
raise ValueError(message)
|
|
|
|
|
|
def positive(value, name):
|
|
require(type(value) is int and value > 0, "invalid " + name)
|
|
return value
|
|
|
|
|
|
def local_path(value):
|
|
require(isinstance(value, str) and re.fullmatch(r"/[A-Za-z0-9_./-]+", value), "invalid local path")
|
|
path = Path(value)
|
|
require(path == path.resolve(), "local path must be canonical")
|
|
return path
|
|
|
|
|
|
def command(*args):
|
|
# Never include argv, environment-file contents or command output in errors.
|
|
try:
|
|
result = subprocess.run(args, capture_output=True, text=True, timeout=45, check=False)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
raise ValueError("service command failed or timed out; reconcile with status/stop") from None
|
|
require(result.returncode == 0, "service command failed; reconcile with status/stop")
|
|
return result.stdout
|
|
|
|
|
|
class Service:
|
|
def __init__(self, plan):
|
|
require(type(plan) is dict and set(plan) == {
|
|
"run_id", "root", "binary", "binary_sha256", "environment_file", "args",
|
|
"runtime_seconds", "cpu_percent", "memory_bytes",
|
|
}, "invalid service plan fields")
|
|
require(str(uuid.UUID(plan["run_id"])) == plan["run_id"], "invalid run ID")
|
|
require(os.getuid() != 0, "run as the benchmark user, not root")
|
|
self.plan = plan
|
|
self.root = local_path(plan["root"])
|
|
root_stat = self.root.stat()
|
|
require(self.root.is_dir() and root_stat.st_uid == os.getuid()
|
|
and stat.S_IMODE(root_stat.st_mode) == 0o700, "benchmark root must be private and owned")
|
|
self.binary = local_path(plan["binary"])
|
|
self.environment = local_path(plan["environment_file"])
|
|
require(self.binary.is_relative_to(self.root) and self.environment.is_relative_to(self.root),
|
|
"binary and environment file must be inside benchmark root")
|
|
require(isinstance(plan["binary_sha256"], str)
|
|
and re.fullmatch(r"[0-9a-f]{64}", plan["binary_sha256"]), "invalid binary digest")
|
|
require(type(plan["args"]) is list and all(isinstance(arg, str) and arg
|
|
and not any(ord(c) < 32 or c == "%" for c in arg) for arg in plan["args"]), "invalid service arguments")
|
|
for key in ("runtime_seconds", "cpu_percent", "memory_bytes"):
|
|
positive(plan[key], key)
|
|
identity = hashlib.sha256(json.dumps(plan, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
self.unit = "rustfs-scanner-" + plan["run_id"] + ".service"
|
|
self.description = "RustFS scanner acceptance " + identity
|
|
|
|
def state(self):
|
|
raw = command("systemctl", "show", self.unit,
|
|
"--property=LoadState,ActiveState,MainPID,ControlGroup,Description,User,WorkingDirectory,Transient")
|
|
state = dict(line.split("=", 1) for line in raw.splitlines() if "=" in line)
|
|
if state.get("LoadState") != "not-found":
|
|
require(state.get("Description") == self.description and state.get("User") == str(os.getuid())
|
|
and state.get("WorkingDirectory") == str(self.root) and state.get("Transient") == "yes",
|
|
"service ownership mismatch; refusing operation")
|
|
return state
|
|
|
|
def observe(self):
|
|
state = self.state()
|
|
pid = int(state["MainPID"])
|
|
result = {"unit": self.unit, "active": state["ActiveState"], "pid": pid,
|
|
"stopped": state["ActiveState"] in ("inactive", "failed") and pid == 0,
|
|
"cpu_max": None, "memory_max": None}
|
|
if state["ActiveState"] == "active":
|
|
require(pid > 0 and state["ControlGroup"] == "/system.slice/" + self.unit, "unexpected service cgroup")
|
|
cgroup = Path("/sys/fs/cgroup") / state["ControlGroup"].lstrip("/")
|
|
result["cpu_max"] = (cgroup / "cpu.max").read_text().strip()
|
|
result["memory_max"] = int((cgroup / "memory.max").read_text())
|
|
return result
|
|
|
|
def start(self):
|
|
require(self.state()["LoadState"] == "not-found", "service already exists; use status/stop to reconcile")
|
|
for path in (self.binary, self.environment):
|
|
info = path.stat()
|
|
require(stat.S_ISREG(info.st_mode) and info.st_uid == os.getuid()
|
|
and not info.st_mode & 0o022, "service input must be an owned regular file")
|
|
require(stat.S_IMODE(self.environment.stat().st_mode) == 0o600, "environment file must have mode 0600")
|
|
require(os.access(self.binary, os.X_OK), "binary is not executable")
|
|
with self.binary.open("rb") as source:
|
|
require(hashlib.file_digest(source, "sha256").hexdigest() == self.plan["binary_sha256"], "binary digest mismatch")
|
|
command("sudo", "-n", "systemd-run", "--quiet", "--collect", "--service-type=exec",
|
|
"--expand-environment=no", "--unit=" + self.unit, "--description=" + self.description,
|
|
"--uid=" + str(os.getuid()), "--gid=" + str(os.getgid()),
|
|
"--property=WorkingDirectory=" + str(self.root),
|
|
"--property=EnvironmentFile=" + str(self.environment),
|
|
"--property=KillMode=control-group", "--property=SendSIGKILL=yes",
|
|
"--property=TimeoutStopSec=30", "--property=Restart=no", "--property=NoNewPrivileges=yes",
|
|
"--property=RuntimeMaxSec=" + str(self.plan["runtime_seconds"]),
|
|
"--property=CPUAccounting=yes", "--property=MemoryAccounting=yes", "--property=IOAccounting=yes",
|
|
"--property=CPUQuota=" + str(self.plan["cpu_percent"]) + "%",
|
|
"--property=MemoryMax=" + str(self.plan["memory_bytes"]),
|
|
"--", str(self.binary), *self.plan["args"])
|
|
result = self.observe()
|
|
require(result["active"] == "active", "service exited before observation")
|
|
return result
|
|
|
|
def resize(self, cpu_percent, memory_bytes):
|
|
positive(cpu_percent, "cpu_percent")
|
|
positive(memory_bytes, "memory_bytes")
|
|
before = self.observe()
|
|
require(before["active"] == "active", "cannot resize inactive service")
|
|
command("sudo", "-n", "systemctl", "set-property", "--runtime", self.unit,
|
|
"CPUQuota=" + str(cpu_percent) + "%", "MemoryMax=" + str(memory_bytes))
|
|
after = self.observe()
|
|
require(after["active"] == "active" and after["pid"] == before["pid"], "process changed during resize")
|
|
quota, period = map(int, after["cpu_max"].split())
|
|
require(quota * 100 == period * cpu_percent and after["memory_max"] == memory_bytes,
|
|
"kernel resource limits do not match request")
|
|
return after
|
|
|
|
def stop(self):
|
|
if self.state()["LoadState"] != "not-found":
|
|
command("sudo", "-n", "systemctl", "stop", self.unit)
|
|
result = self.observe()
|
|
require(result["stopped"], "service has not stopped")
|
|
return result
|
|
|
|
def execute(self, action, cpu_percent=None, memory_bytes=None):
|
|
# Separate SSH controllers serialize mutations without relying on a PID file.
|
|
lock_path = self.root / ("." + self.unit + ".lock")
|
|
fd = os.open(lock_path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
with os.fdopen(fd, "w") as lock:
|
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
if action == "start":
|
|
return self.start()
|
|
if action == "stop":
|
|
return self.stop()
|
|
if action == "resize":
|
|
return self.resize(cpu_percent, memory_bytes)
|
|
require(action == "status", "unknown action")
|
|
return self.observe()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("action", choices=("start", "status", "resize", "stop"))
|
|
parser.add_argument("plan", type=Path, help="immutable plan; credentials belong in its private environment file")
|
|
parser.add_argument("--cpu-percent", type=int)
|
|
parser.add_argument("--memory-bytes", type=int)
|
|
args = parser.parse_args()
|
|
try:
|
|
require(args.plan.stat().st_size <= 65536, "service plan is too large")
|
|
result = Service(json.loads(args.plan.read_text())).execute(args.action, args.cpu_percent, args.memory_bytes)
|
|
except (ValueError, TypeError, AttributeError, KeyError, OSError):
|
|
# A malformed plan or a tool error may contain secret-bearing input.
|
|
print("service control failed; retain the plan and reconcile with status/stop", file=sys.stderr)
|
|
return 1
|
|
print(json.dumps(result, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|