Run every command on the build VM over SSH (M1.3)
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 55s
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 55s
execution.SSHHost.execute is now the only way the agent runs anything (L13). Commands travel as a script on SSH's stdin, so the secrets handed to a call are never on a command line, and every value handed out is redacted from the output. Timeouts kill the remote process group, and an unreachable host is reported as such, not as a failed command. Workspace and GitRepo run on the build VM. The clone uses the repository's SSH URL, so no credential is stored in it (#89); fetches and pushes get the push key over stdin, in a short-lived ssh-agent, and PRs are still opened from the agent's host with its API token. The bench runs on the build host too. Tests reach "the build VM" through a throwaway local sshd (nisse_dev .testing); CI installs openssh-server. The live push test now pushes with the sandbox's deploy key (NISSE_PUSH_KEY). Fixes #89. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
b7f1f3371c
commit
761ebdb857
32 files changed
+1098
-280
No files matched your search
@@ -0,0 +1,101 @@
|
||||
"""Test support: a throwaway sshd standing in for a tool VM (L13).
|
||||
|
||||
Runs the system's sshd as the current user, on a free local port, with a
|
||||
host key and client key made for the occasion. Tests then reach "the build
|
||||
VM" over real SSH, so the code path under test is the production path.
|
||||
"""
|
||||
|
||||
import getpass
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from nisse_dev.execution import SSHHost
|
||||
|
||||
SSHD = "/usr/sbin/sshd"
|
||||
|
||||
|
||||
def _free_port() -> int:
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
return int(sock.getsockname()[1])
|
||||
|
||||
|
||||
def _keygen(path: Path) -> None:
|
||||
subprocess.run(
|
||||
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(path)], check=True
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def local_sshd(root: Path) -> Iterator[SSHHost]:
|
||||
"""An SSHHost for a private sshd on 127.0.0.1, torn down afterwards."""
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
host_key, client_key = root / "host_key", root / "client_key"
|
||||
_keygen(host_key)
|
||||
_keygen(client_key)
|
||||
authorized = root / "authorized_keys"
|
||||
authorized.write_text(client_key.with_suffix(".pub").read_text())
|
||||
authorized.chmod(0o600)
|
||||
port = _free_port()
|
||||
config = root / "sshd_config"
|
||||
config.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
f"Port {port}",
|
||||
"ListenAddress 127.0.0.1",
|
||||
f"HostKey {host_key}",
|
||||
f"AuthorizedKeysFile {authorized}",
|
||||
f"PidFile {root / 'sshd.pid'}",
|
||||
"PasswordAuthentication no",
|
||||
"KbdInteractiveAuthentication no",
|
||||
"PubkeyAuthentication yes",
|
||||
"PermitRootLogin prohibit-password",
|
||||
"StrictModes no",
|
||||
"UsePAM no",
|
||||
"MaxSessions 50",
|
||||
"MaxStartups 50",
|
||||
# Stands in for a provisioned build VM: sessions see the test
|
||||
# runner's PATH, so its Python and tools are "installed".
|
||||
f"SetEnv PATH={os.environ.get('PATH', '/usr/bin:/bin')}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
)
|
||||
process = subprocess.Popen(
|
||||
[SSHD, "-D", "-e", "-f", str(config)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
user = getpass.getuser()
|
||||
host = SSHHost(
|
||||
"127.0.0.1", key=client_key, state_dir=root / "client", user=user, port=port
|
||||
)
|
||||
try:
|
||||
deadline = time.monotonic() + 10
|
||||
while True:
|
||||
if process.poll() is not None:
|
||||
raise RuntimeError(
|
||||
"sshd exited: " + (process.stderr.read() or b"").decode()
|
||||
if process.stderr
|
||||
else "sshd exited"
|
||||
)
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=0.5):
|
||||
break
|
||||
except OSError:
|
||||
if time.monotonic() > deadline:
|
||||
raise RuntimeError("sshd did not start") from None
|
||||
time.sleep(0.05)
|
||||
yield host
|
||||
finally:
|
||||
host.close()
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
Reference in new issue
Block a user