|
1 | 1 | from collections.abc import Iterable |
2 | | -from typing import Optional |
3 | 2 |
|
4 | | -import dstack._internal.server.services.jobs as jobs_services |
5 | 3 | from dstack._internal.core.consts import DSTACK_RUNNER_SSH_PORT |
6 | 4 | from dstack._internal.core.models.backends.base import BackendType |
7 | 5 | from dstack._internal.core.models.instances import SSHConnectionParams |
8 | | -from dstack._internal.core.models.runs import JobProvisioningData |
9 | 6 | from dstack._internal.core.services.ssh.tunnel import SSH_DEFAULT_OPTIONS, SocketPair, SSHTunnel |
10 | 7 | from dstack._internal.server.models import JobModel |
11 | 8 | from dstack._internal.server.services.instances import get_instance_remote_connection_info |
| 9 | +from dstack._internal.server.services.jobs import get_job_provisioning_data, get_job_runtime_data |
12 | 10 | from dstack._internal.utils.common import get_or_error |
13 | 11 | from dstack._internal.utils.path import FileContent |
14 | 12 |
|
15 | 13 |
|
16 | | -def container_ssh_tunnel( |
17 | | - job: JobModel, |
18 | | - forwarded_sockets: Iterable[SocketPair] = (), |
19 | | - options: dict[str, str] = SSH_DEFAULT_OPTIONS, |
20 | | -) -> SSHTunnel: |
| 14 | +def get_container_ssh_credentials(job: JobModel) -> list[tuple[SSHConnectionParams, FileContent]]: |
21 | 15 | """ |
22 | | - Build SSHTunnel for connecting to the container running the specified job. |
| 16 | + Returns the information needed to connect to the SSH server inside the job container. |
| 17 | +
|
| 18 | + The user of the target host (container) is set to: |
| 19 | + * VM-based backends and SSH instances: "root" |
| 20 | + * container-based backends: `JobProvisioningData.username`, which is, as of 2026-03-10, |
| 21 | + is always "root" on all supported backends (Runpod, Vast.ai, Kubernetes) |
| 22 | +
|
| 23 | + Args: |
| 24 | + job: `JobModel` with `instance` and `instance.project` fields loaded. |
| 25 | +
|
| 26 | + Returns: |
| 27 | + A list of hosts credentials as (host's `SSHConnectionParams`, private key's `FileContent`) |
| 28 | + pairs ordered from the first proxy jump (if any) to the target host (container). |
23 | 29 | """ |
24 | | - jpd: JobProvisioningData = JobProvisioningData.__response__.parse_raw( |
25 | | - job.job_provisioning_data |
26 | | - ) |
| 30 | + hosts: list[tuple[SSHConnectionParams, FileContent]] = [] |
| 31 | + |
| 32 | + instance = get_or_error(job.instance) |
| 33 | + project_key = FileContent(instance.project.ssh_private_key) |
| 34 | + |
| 35 | + rci = get_instance_remote_connection_info(instance) |
| 36 | + if rci is not None and (head_proxy := rci.ssh_proxy) is not None: |
| 37 | + head_key = FileContent(get_or_error(get_or_error(rci.ssh_proxy_keys)[0].private)) |
| 38 | + hosts.append((head_proxy, head_key)) |
| 39 | + |
| 40 | + jpd = get_job_provisioning_data(job) |
| 41 | + assert jpd is not None |
27 | 42 | assert jpd.hostname is not None |
28 | 43 | assert jpd.ssh_port is not None |
29 | | - if not jpd.dockerized: |
30 | | - ssh_destination = f"{jpd.username}@{jpd.hostname}" |
31 | | - ssh_port = jpd.ssh_port |
32 | | - ssh_proxy = jpd.ssh_proxy |
33 | | - else: |
34 | | - ssh_destination = "root@localhost" |
| 44 | + |
| 45 | + if jpd.dockerized: |
| 46 | + if jpd.backend != BackendType.LOCAL: |
| 47 | + instance_proxy = SSHConnectionParams( |
| 48 | + hostname=jpd.hostname, |
| 49 | + username=jpd.username, |
| 50 | + port=jpd.ssh_port, |
| 51 | + ) |
| 52 | + hosts.append((instance_proxy, project_key)) |
35 | 53 | ssh_port = DSTACK_RUNNER_SSH_PORT |
36 | | - job_submission = jobs_services.job_model_to_job_submission(job) |
37 | | - jrd = job_submission.job_runtime_data |
| 54 | + jrd = get_job_runtime_data(job) |
38 | 55 | if jrd is not None and jrd.ports is not None: |
39 | 56 | ssh_port = jrd.ports.get(ssh_port, ssh_port) |
40 | | - ssh_proxy = SSHConnectionParams( |
| 57 | + target_host = SSHConnectionParams( |
| 58 | + hostname="localhost", |
| 59 | + username="root", |
| 60 | + port=ssh_port, |
| 61 | + ) |
| 62 | + hosts.append((target_host, project_key)) |
| 63 | + else: |
| 64 | + if jpd.ssh_proxy is not None: |
| 65 | + hosts.append((jpd.ssh_proxy, project_key)) |
| 66 | + target_host = SSHConnectionParams( |
41 | 67 | hostname=jpd.hostname, |
42 | 68 | username=jpd.username, |
43 | 69 | port=jpd.ssh_port, |
44 | 70 | ) |
45 | | - if jpd.backend == BackendType.LOCAL: |
46 | | - ssh_proxy = None |
47 | | - ssh_head_proxy: Optional[SSHConnectionParams] = None |
48 | | - ssh_head_proxy_private_key: Optional[str] = None |
49 | | - instance = get_or_error(job.instance) |
50 | | - rci = get_instance_remote_connection_info(instance) |
51 | | - if rci is not None and rci.ssh_proxy is not None: |
52 | | - ssh_head_proxy = rci.ssh_proxy |
53 | | - ssh_head_proxy_private_key = get_or_error(rci.ssh_proxy_keys)[0].private |
54 | | - ssh_proxies = [] |
55 | | - if ssh_head_proxy is not None: |
56 | | - ssh_head_proxy_private_key = get_or_error(ssh_head_proxy_private_key) |
57 | | - ssh_proxies.append((ssh_head_proxy, FileContent(ssh_head_proxy_private_key))) |
58 | | - if ssh_proxy is not None: |
59 | | - ssh_proxies.append((ssh_proxy, None)) |
| 71 | + hosts.append((target_host, project_key)) |
| 72 | + |
| 73 | + return hosts |
| 74 | + |
| 75 | + |
| 76 | +def container_ssh_tunnel( |
| 77 | + job: JobModel, |
| 78 | + forwarded_sockets: Iterable[SocketPair] = (), |
| 79 | + options: dict[str, str] = SSH_DEFAULT_OPTIONS, |
| 80 | +) -> SSHTunnel: |
| 81 | + """ |
| 82 | + Build SSHTunnel for connecting to the container running the specified job. |
| 83 | + """ |
| 84 | + hosts = get_container_ssh_credentials(job) |
| 85 | + target, identity = hosts[-1] |
60 | 86 | return SSHTunnel( |
61 | | - destination=ssh_destination, |
62 | | - port=ssh_port, |
63 | | - ssh_proxies=ssh_proxies, |
64 | | - identity=FileContent(instance.project.ssh_private_key), |
| 87 | + destination=f"{target.username}@{target.hostname}", |
| 88 | + port=target.port, |
| 89 | + ssh_proxies=hosts[:-1], |
| 90 | + identity=identity, |
65 | 91 | forwarded_sockets=forwarded_sockets, |
66 | 92 | options=options, |
67 | 93 | ) |
0 commit comments