generated from coulomb/repo-seed
Fix SSH auth: respect ~/.ssh/config instead of defaulting to root
CoulombCore (92.205.130.254) authenticates as tegwick via id_ops in ssh config, not root. Omit SANDBOXER_SSH_USER to let OpenSSH apply config; set SANDBOXER_SSH_USER only to override.
This commit is contained in:
@@ -16,9 +16,10 @@ Provision a compose-based e2e sandbox via `ext.compose-ssh` (e2e-framework linea
|
|||||||
- Sufficient disk for images
|
- Sufficient disk for images
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SANDBOXER_HOST=<ip-or-hostname> # e.g. coulombcore IP
|
export SANDBOXER_HOST=coulombcore # or 92.205.130.254
|
||||||
export SANDBOXER_SSH_USER=root # optional
|
# Omit SANDBOXER_SSH_USER to use ~/.ssh/config (CoulombCore: tegwick + id_ops)
|
||||||
export SANDBOXER_SSH_KEY=~/.ssh/id_rsa # optional
|
# export SANDBOXER_SSH_USER=tegwick # only if not in ssh config
|
||||||
|
# export SANDBOXER_SSH_KEY=~/.ssh/id_ops # only to override config
|
||||||
```
|
```
|
||||||
|
|
||||||
## Create
|
## Create
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ capabilities:
|
|||||||
pricing_model: self-hosted
|
pricing_model: self-hosted
|
||||||
config:
|
config:
|
||||||
base_dir: /tmp/sandboxer
|
base_dir: /tmp/sandboxer
|
||||||
ssh_user: root
|
# ssh_user omitted — uses SANDBOXER_SSH_USER or ~/.ssh/config (e.g. tegwick@coulombcore)
|
||||||
compose_timeout_s: 180
|
compose_timeout_s: 180
|
||||||
@@ -18,7 +18,7 @@ class ComposeSSHExtension:
|
|||||||
def __init__(self, config: dict[str, Any] | None = None) -> None:
|
def __init__(self, config: dict[str, Any] | None = None) -> None:
|
||||||
cfg = config or {}
|
cfg = config or {}
|
||||||
self.base_dir: str = cfg.get("base_dir", "/tmp/sandboxer")
|
self.base_dir: str = cfg.get("base_dir", "/tmp/sandboxer")
|
||||||
self.ssh_user: str = cfg.get("ssh_user", "root")
|
self.ssh_user: str | None = cfg.get("ssh_user")
|
||||||
self.compose_timeout_s: int = int(cfg.get("compose_timeout_s", 180))
|
self.compose_timeout_s: int = int(cfg.get("compose_timeout_s", 180))
|
||||||
|
|
||||||
def provision(
|
def provision(
|
||||||
@@ -33,7 +33,7 @@ class ComposeSSHExtension:
|
|||||||
|
|
||||||
sandbox_id = inputs.get("sandbox_id") or str(uuid.uuid4())[:8]
|
sandbox_id = inputs.get("sandbox_id") or str(uuid.uuid4())[:8]
|
||||||
remote_dir = f"{self.base_dir}/{sandbox_id}"
|
remote_dir = f"{self.base_dir}/{sandbox_id}"
|
||||||
ssh = SSHConfig.from_env(host, user=self.ssh_user)
|
ssh = SSHConfig.from_env(host, user=self.ssh_user or None)
|
||||||
|
|
||||||
rc, out = ssh.run(f"mkdir -p {remote_dir}")
|
rc, out = ssh.run(f"mkdir -p {remote_dir}")
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
@@ -64,7 +64,8 @@ class ComposeSSHExtension:
|
|||||||
|
|
||||||
def wait_ready(self, handle: dict[str, str]) -> dict[str, str]:
|
def wait_ready(self, handle: dict[str, str]) -> dict[str, str]:
|
||||||
"""Confirm compose services are running (no HTTP health polling)."""
|
"""Confirm compose services are running (no HTTP health polling)."""
|
||||||
ssh = SSHConfig.from_env(handle["host"], user=handle.get("ssh_user", self.ssh_user))
|
ssh_user = handle.get("ssh_user") or self.ssh_user or None
|
||||||
|
ssh = SSHConfig.from_env(handle["host"], user=ssh_user)
|
||||||
project = handle["compose_project"]
|
project = handle["compose_project"]
|
||||||
remote_dir = handle["remote_dir"]
|
remote_dir = handle["remote_dir"]
|
||||||
compose_file = handle["compose_file"]
|
compose_file = handle["compose_file"]
|
||||||
@@ -83,7 +84,8 @@ class ComposeSSHExtension:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def teardown(self, handle: dict[str, str]) -> dict[str, str]:
|
def teardown(self, handle: dict[str, str]) -> dict[str, str]:
|
||||||
ssh = SSHConfig.from_env(handle["host"], user=handle.get("ssh_user", self.ssh_user))
|
ssh_user = handle.get("ssh_user") or self.ssh_user or None
|
||||||
|
ssh = SSHConfig.from_env(handle["host"], user=ssh_user)
|
||||||
project = handle.get("compose_project")
|
project = handle.get("compose_project")
|
||||||
remote_dir = handle.get("remote_dir")
|
remote_dir = handle.get("remote_dir")
|
||||||
compose_file = handle.get("compose_file")
|
compose_file = handle.get("compose_file")
|
||||||
|
|||||||
@@ -11,19 +11,27 @@ from pathlib import Path
|
|||||||
@dataclass
|
@dataclass
|
||||||
class SSHConfig:
|
class SSHConfig:
|
||||||
host: str
|
host: str
|
||||||
user: str = "root"
|
user: str | None = None
|
||||||
key: str | None = None
|
key: str | None = None
|
||||||
connect_timeout: int = 15
|
connect_timeout: int = 15
|
||||||
|
|
||||||
|
@property
|
||||||
|
def destination(self) -> str:
|
||||||
|
"""SSH destination; omit user when unset so ~/.ssh/config applies."""
|
||||||
|
if self.user:
|
||||||
|
return f"{self.user}@{self.host}"
|
||||||
|
return self.host
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target(self) -> str:
|
def target(self) -> str:
|
||||||
return f"{self.user}@{self.host}"
|
return self.destination
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls, host: str, *, user: str | None = None, key: str | None = None) -> SSHConfig:
|
def from_env(cls, host: str, *, user: str | None = None, key: str | None = None) -> SSHConfig:
|
||||||
|
env_user = os.environ.get("SANDBOXER_SSH_USER")
|
||||||
return cls(
|
return cls(
|
||||||
host=host,
|
host=host,
|
||||||
user=user or os.environ.get("SANDBOXER_SSH_USER", "root"),
|
user=user if user is not None else (env_user or None),
|
||||||
key=key or os.environ.get("SANDBOXER_SSH_KEY"),
|
key=key or os.environ.get("SANDBOXER_SSH_KEY"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,7 +47,7 @@ class SSHConfig:
|
|||||||
]
|
]
|
||||||
if self.key:
|
if self.key:
|
||||||
args += ["-i", self.key]
|
args += ["-i", self.key]
|
||||||
args.append(self.target)
|
args.append(self.destination)
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def run(self, cmd: str, *, timeout: int = 60) -> tuple[int, str]:
|
def run(self, cmd: str, *, timeout: int = 60) -> tuple[int, str]:
|
||||||
|
|||||||
13
tests/test_ssh.py
Normal file
13
tests/test_ssh.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""SSH config behavior."""
|
||||||
|
|
||||||
|
from sandboxer.extensions.ssh import SSHConfig
|
||||||
|
|
||||||
|
|
||||||
|
def test_destination_uses_ssh_config_when_user_unset() -> None:
|
||||||
|
ssh = SSHConfig(host="92.205.130.254")
|
||||||
|
assert ssh.destination == "92.205.130.254"
|
||||||
|
|
||||||
|
|
||||||
|
def test_destination_includes_explicit_user() -> None:
|
||||||
|
ssh = SSHConfig(host="92.205.130.254", user="tegwick")
|
||||||
|
assert ssh.destination == "tegwick@92.205.130.254"
|
||||||
Reference in New Issue
Block a user