mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-16 12:47:19 +08:00
Feat: add local & ssh provider in admin panel (#15039)
### What problem does this PR solve? Feat: add local & ssh provider in admin panel ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@@ -3,12 +3,10 @@ import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.sandbox.providers.base import SandboxProviderConfigError
|
||||
from agent.sandbox.providers.local import LocalProvider
|
||||
|
||||
|
||||
def _make_provider(monkeypatch, tmp_path, **overrides):
|
||||
monkeypatch.setenv("SANDBOX_LOCAL_ENABLED", "true")
|
||||
def _make_provider(tmp_path, **overrides):
|
||||
config = {
|
||||
"python_bin": sys.executable,
|
||||
"work_dir": str(tmp_path),
|
||||
@@ -24,16 +22,14 @@ def _make_provider(monkeypatch, tmp_path, **overrides):
|
||||
return provider
|
||||
|
||||
|
||||
def test_local_provider_requires_explicit_env_enable(monkeypatch, tmp_path):
|
||||
monkeypatch.delenv("SANDBOX_LOCAL_ENABLED", raising=False)
|
||||
def test_local_provider_initializes_from_config(tmp_path):
|
||||
provider = LocalProvider()
|
||||
|
||||
with pytest.raises(SandboxProviderConfigError):
|
||||
provider.initialize({"work_dir": str(tmp_path)})
|
||||
provider.initialize({"python_bin": sys.executable, "work_dir": str(tmp_path)})
|
||||
assert provider.health_check() is True
|
||||
|
||||
|
||||
def test_local_provider_executes_python_main(monkeypatch, tmp_path):
|
||||
provider = _make_provider(monkeypatch, tmp_path)
|
||||
def test_local_provider_executes_python_main(tmp_path):
|
||||
provider = _make_provider(tmp_path)
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
try:
|
||||
@@ -53,8 +49,8 @@ def test_local_provider_executes_python_main(monkeypatch, tmp_path):
|
||||
assert result.metadata["result_value"] == {"message": "hello ragflow"}
|
||||
|
||||
|
||||
def test_local_provider_collects_artifacts(monkeypatch, tmp_path):
|
||||
provider = _make_provider(monkeypatch, tmp_path)
|
||||
def test_local_provider_collects_artifacts(tmp_path):
|
||||
provider = _make_provider(tmp_path)
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
try:
|
||||
@@ -82,8 +78,8 @@ def test_local_provider_collects_artifacts(monkeypatch, tmp_path):
|
||||
]
|
||||
|
||||
|
||||
def test_local_provider_times_out(monkeypatch, tmp_path):
|
||||
provider = _make_provider(monkeypatch, tmp_path, timeout=1)
|
||||
def test_local_provider_times_out(tmp_path):
|
||||
provider = _make_provider(tmp_path, timeout=1)
|
||||
instance = provider.create_instance("python")
|
||||
|
||||
try:
|
||||
|
||||
43
test/unit_test/agent/sandbox/test_sandbox_client.py
Normal file
43
test/unit_test/agent/sandbox/test_sandbox_client.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import pytest
|
||||
from agent.sandbox import client as sandbox_client
|
||||
from agent.sandbox.providers.self_managed import SelfManagedProvider
|
||||
|
||||
pytestmark = pytest.mark.p2
|
||||
|
||||
|
||||
def test_client_defaults_to_self_managed(monkeypatch):
|
||||
class FakeSettingsService:
|
||||
@staticmethod
|
||||
def get_by_name(name):
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(sandbox_client, "SystemSettingsService", FakeSettingsService)
|
||||
monkeypatch.setattr(SelfManagedProvider, "initialize", lambda self, config: True)
|
||||
monkeypatch.setattr(sandbox_client, "_provider_manager", None)
|
||||
|
||||
provider_manager = sandbox_client.get_provider_manager()
|
||||
|
||||
assert provider_manager.get_provider_name() == "self_managed"
|
||||
assert isinstance(provider_manager.get_provider(), SelfManagedProvider)
|
||||
|
||||
|
||||
def test_self_managed_schema_uses_env_for_deployment_defaults(monkeypatch):
|
||||
monkeypatch.setenv("SANDBOX_EXECUTOR_MANAGER_IMAGE", "custom-executor:latest")
|
||||
monkeypatch.setenv("SANDBOX_EXECUTOR_MANAGER_POOL_SIZE", "7")
|
||||
monkeypatch.setenv("SANDBOX_BASE_PYTHON_IMAGE", "custom-python:latest")
|
||||
monkeypatch.setenv("SANDBOX_BASE_NODEJS_IMAGE", "custom-node:latest")
|
||||
monkeypatch.setenv("SANDBOX_EXECUTOR_MANAGER_PORT", "19485")
|
||||
monkeypatch.setenv("SANDBOX_ENABLE_SECCOMP", "true")
|
||||
monkeypatch.setenv("SANDBOX_MAX_MEMORY", "512m")
|
||||
monkeypatch.setenv("SANDBOX_TIMEOUT", "25s")
|
||||
|
||||
schema = SelfManagedProvider.get_config_schema()
|
||||
|
||||
assert schema["executor_manager_image"]["default"] == "custom-executor:latest"
|
||||
assert schema["executor_manager_pool_size"]["default"] == 7
|
||||
assert schema["base_python_image"]["default"] == "custom-python:latest"
|
||||
assert schema["base_nodejs_image"]["default"] == "custom-node:latest"
|
||||
assert schema["executor_manager_port"]["default"] == 19485
|
||||
assert schema["enable_seccomp"]["default"] is True
|
||||
assert schema["max_memory"]["default"] == "512m"
|
||||
assert schema["sandbox_timeout"]["default"] == "25s"
|
||||
174
test/unit_test/agent/sandbox/test_ssh_provider.py
Normal file
174
test/unit_test/agent/sandbox/test_ssh_provider.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import base64
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.sandbox.providers.ssh import SSHProvider
|
||||
from agent.sandbox.result_protocol import RESULT_MARKER_PREFIX
|
||||
|
||||
pytestmark = pytest.mark.p3
|
||||
|
||||
|
||||
class _FakeWritableFile:
|
||||
def __init__(self, sftp, path: str):
|
||||
self._sftp = sftp
|
||||
self._path = path
|
||||
self._chunks: list[str] = []
|
||||
|
||||
def write(self, content: str):
|
||||
self._chunks.append(content)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
self._sftp.files[self._path] = "".join(self._chunks).encode("utf-8")
|
||||
return False
|
||||
|
||||
|
||||
class _FakeReadableFile:
|
||||
def __init__(self, payload: bytes):
|
||||
self._payload = payload
|
||||
|
||||
def read(self):
|
||||
return self._payload
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
|
||||
class _FakeSFTP:
|
||||
def __init__(self):
|
||||
self.files: dict[str, bytes] = {}
|
||||
self.closed = False
|
||||
|
||||
def file(self, path: str, mode: str):
|
||||
if "w" in mode:
|
||||
return _FakeWritableFile(self, path)
|
||||
return _FakeReadableFile(self.files[path])
|
||||
|
||||
def listdir_attr(self, path: str):
|
||||
prefix = path.rstrip("/") + "/"
|
||||
names = []
|
||||
for file_path, payload in self.files.items():
|
||||
if not file_path.startswith(prefix):
|
||||
continue
|
||||
relative = file_path[len(prefix):]
|
||||
if "/" in relative:
|
||||
continue
|
||||
names.append(
|
||||
SimpleNamespace(
|
||||
filename=relative,
|
||||
st_mode=0o100644,
|
||||
st_size=len(payload),
|
||||
)
|
||||
)
|
||||
return names
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
def __init__(self, sftp: _FakeSFTP):
|
||||
self._sftp = sftp
|
||||
self.closed = False
|
||||
|
||||
def open_sftp(self):
|
||||
return self._sftp
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
|
||||
def _build_provider():
|
||||
provider = SSHProvider()
|
||||
provider.host = "example.com"
|
||||
provider.port = 22
|
||||
provider.username = "ragflow"
|
||||
provider.password = "secret"
|
||||
provider.work_dir = "/tmp"
|
||||
provider.command_template = "cd {workspace} && python3 {script_path}"
|
||||
provider.timeout = 5
|
||||
provider.max_output_bytes = 1024 * 1024
|
||||
provider.max_artifacts = 20
|
||||
provider.max_artifact_bytes = 1024 * 1024
|
||||
provider._initialized = True
|
||||
return provider
|
||||
|
||||
|
||||
def test_ssh_provider_executes_python_main_and_collects_artifacts(monkeypatch):
|
||||
provider = _build_provider()
|
||||
fake_sftp = _FakeSFTP()
|
||||
fake_client = _FakeClient(fake_sftp)
|
||||
executed_commands: list[str] = []
|
||||
|
||||
monkeypatch.setattr(provider, "_create_ssh_client", lambda: fake_client)
|
||||
monkeypatch.setattr(provider, "_create_remote_workspace", lambda client: "/tmp/ws-123")
|
||||
|
||||
def _run_remote_command(client, command: str, timeout: int):
|
||||
executed_commands.append(command)
|
||||
if command.startswith("mkdir -p "):
|
||||
return "", "", 0
|
||||
if command.startswith("cd /tmp/ws-123 && python3 /tmp/ws-123/main.py"):
|
||||
fake_sftp.files["/tmp/ws-123/artifacts/chart.png"] = b"PNGDATA"
|
||||
payload = base64.b64encode(
|
||||
b'{"present":true,"value":{"message":"hello ssh"},"type":"json"}'
|
||||
).decode("ascii")
|
||||
return f"debug line\n{RESULT_MARKER_PREFIX}{payload}\n", "", 0
|
||||
if command.startswith("rm -rf "):
|
||||
return "", "", 0
|
||||
raise AssertionError(f"Unexpected command: {command}")
|
||||
|
||||
monkeypatch.setattr(provider, "_run_remote_command", _run_remote_command)
|
||||
|
||||
instance = provider.create_instance("python")
|
||||
result = provider.execute_code(
|
||||
instance.instance_id,
|
||||
'def main() -> dict:\n return {"message": "hello ssh"}\n',
|
||||
"python",
|
||||
timeout=5,
|
||||
)
|
||||
provider.destroy_instance(instance.instance_id)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout == "debug line\n"
|
||||
assert result.metadata["result_present"] is True
|
||||
assert result.metadata["result_value"] == {"message": "hello ssh"}
|
||||
assert result.metadata["artifacts"] == [
|
||||
{
|
||||
"name": "chart.png",
|
||||
"content_b64": base64.b64encode(b"PNGDATA").decode("ascii"),
|
||||
"mime_type": "image/png",
|
||||
"size": 7,
|
||||
}
|
||||
]
|
||||
assert "cd /tmp/ws-123 && python3 /tmp/ws-123/main.py" in executed_commands
|
||||
assert fake_sftp.closed is True
|
||||
assert fake_client.closed is True
|
||||
|
||||
|
||||
def test_ssh_provider_propagates_timeouts():
|
||||
provider = _build_provider()
|
||||
provider._instances["instance-1"] = {
|
||||
"client": object(),
|
||||
"sftp": _FakeSFTP(),
|
||||
"remote_work_dir": "/tmp/ws-123",
|
||||
"language": "python",
|
||||
}
|
||||
|
||||
def _timeout(*args, **kwargs):
|
||||
raise TimeoutError("Execution timed out after 5 seconds")
|
||||
|
||||
provider._run_remote_command = _timeout # type: ignore[method-assign]
|
||||
|
||||
with pytest.raises(TimeoutError, match="Execution timed out"):
|
||||
provider.execute_code(
|
||||
"instance-1",
|
||||
'def main() -> dict:\n return {"ok": True}\n',
|
||||
"python",
|
||||
timeout=5,
|
||||
)
|
||||
Reference in New Issue
Block a user