11import base64
2- import hashlib
3- import os
42import queue
53import tempfile
64import threading
1715from websocket import WebSocketApp
1816
1917import dstack .api as api
20- from dstack ._internal .cli .utils .common import warn
2118from dstack ._internal .core .consts import DSTACK_RUNNER_HTTP_PORT , DSTACK_RUNNER_SSH_PORT
2219from dstack ._internal .core .errors import ClientError , ConfigurationError , ResourceNotExistsError
2320from dstack ._internal .core .models .backends .base import BackendType
4845 get_service_port ,
4946)
5047from dstack ._internal .core .models .runs import Run as RunModel
51- from dstack ._internal .core .models .users import UserWithCreds
5248from dstack ._internal .core .services .configs import ConfigManager
5349from dstack ._internal .core .services .logs import URLReplacer
5450from dstack ._internal .core .services .ssh .attach import SSHAttach
51+ from dstack ._internal .core .services .ssh .key_manager import UserSSHKeyManager
5552from dstack ._internal .core .services .ssh .ports import PortsLock
5653from dstack ._internal .server .schemas .logs import PollLogsRequest
5754from dstack ._internal .utils .common import get_or_error , make_proxy_url
@@ -88,7 +85,7 @@ def __init__(
8885 self ._ports_lock : Optional [PortsLock ] = ports_lock
8986 self ._ssh_attach : Optional [SSHAttach ] = None
9087 if ssh_identity_file is not None :
91- warn (
88+ logger . warning (
9289 "[code]ssh_identity_file[/code] in [code]Run[/code] is deprecated and ignored; will be removed"
9390 " since 0.19.40"
9491 )
@@ -281,31 +278,20 @@ def attach(
281278 dstack.api.PortUsedError: If ports are in use or the run is attached by another process.
282279 """
283280 if not ssh_identity_file :
284- user = self ._api_client .users .get_my_user ()
285- run_ssh_key_pub = self ._run .run_spec .ssh_key_pub
286281 config_manager = ConfigManager ()
287- if isinstance (user , UserWithCreds ) and user .ssh_public_key == run_ssh_key_pub :
288- token_hash = hashlib .sha1 (user .creds .token .encode ()).hexdigest ()[:8 ]
289- config_manager .dstack_ssh_dir .mkdir (parents = True , exist_ok = True )
290- ssh_identity_file = config_manager .dstack_ssh_dir / token_hash
291-
292- def key_opener (path , flags ):
293- return os .open (path , flags , 0o600 )
294-
295- with open (ssh_identity_file , "wb" , opener = key_opener ) as f :
296- assert user .ssh_private_key
297- f .write (user .ssh_private_key .encode ())
282+ key_manager = UserSSHKeyManager (self ._api_client , config_manager .dstack_ssh_dir )
283+ if (
284+ user_key := key_manager .get_user_key ()
285+ ) and user_key .public_key == self ._run .run_spec .ssh_key_pub :
286+ ssh_identity_file = user_key .private_key_path
298287 else :
299288 if config_manager .dstack_key_path .exists ():
300289 # TODO: Remove since 0.19.40
301- warn (
302- f"Using legacy [code]{ config_manager .dstack_key_path } [/code]."
303- " Future versions will use the user SSH key from the server." ,
304- )
290+ logger .debug (f"Using legacy [code]{ config_manager .dstack_key_path } [/code]." )
305291 ssh_identity_file = config_manager .dstack_key_path
306292 else :
307293 raise ConfigurationError (
308- f"User SSH key doen 't match; default SSH key ({ config_manager .dstack_key_path } ) doesn't exist"
294+ f"User SSH key doesn 't match; default SSH key ({ config_manager .dstack_key_path } ) doesn't exist"
309295 )
310296 ssh_identity_file = str (ssh_identity_file )
311297
@@ -504,15 +490,19 @@ def get_run_plan(
504490 ssh_key_pub = Path (ssh_identity_file ).with_suffix (".pub" ).read_text ()
505491 else :
506492 config_manager = ConfigManager ()
507- if not config_manager .dstack_key_path .exists ():
508- generate_rsa_key_pair (private_key_path = config_manager .dstack_key_path )
509- warn (
510- f"Using legacy [code]{ config_manager .dstack_key_path .with_suffix ('.pub' )} [/code]."
511- " Future versions will use the user SSH key from the server." ,
512- )
513- ssh_key_pub = config_manager .dstack_key_path .with_suffix (".pub" ).read_text ()
514- # TODO: Uncomment after 0.19.40
515- # ssh_key_pub = None
493+ key_manager = UserSSHKeyManager (self ._api_client , config_manager .dstack_ssh_dir )
494+ if key_manager .get_user_key ():
495+ ssh_key_pub = None # using the server-managed user key
496+ else :
497+ if not config_manager .dstack_key_path .exists ():
498+ generate_rsa_key_pair (private_key_path = config_manager .dstack_key_path )
499+ logger .warning (
500+ f"Using legacy [code]{ config_manager .dstack_key_path .with_suffix ('.pub' )} [/code]."
501+ " You will only be able to attach to the run from this client."
502+ " Update the [code]dstack[/] server to [code]0.19.34[/]+ to switch to user keys"
503+ " automatically replicated to all clients." ,
504+ )
505+ ssh_key_pub = config_manager .dstack_key_path .with_suffix (".pub" ).read_text ()
516506 run_spec = RunSpec (
517507 run_name = configuration .name ,
518508 repo_id = repo .repo_id ,
@@ -760,12 +750,19 @@ def get_plan(
760750 idle_duration = idle_duration , # type: ignore[assignment]
761751 )
762752 config_manager = ConfigManager ()
763- if not config_manager .dstack_key_path .exists ():
764- generate_rsa_key_pair (private_key_path = config_manager .dstack_key_path )
765- warn (
766- f"Using legacy [code]{ config_manager .dstack_key_path .with_suffix ('.pub' )} [/code]."
767- " Future versions will use the user SSH key from the server." ,
768- )
753+ key_manager = UserSSHKeyManager (self ._api_client , config_manager .dstack_ssh_dir )
754+ if key_manager .get_user_key ():
755+ ssh_key_pub = None # using the server-managed user key
756+ else :
757+ if not config_manager .dstack_key_path .exists ():
758+ generate_rsa_key_pair (private_key_path = config_manager .dstack_key_path )
759+ logger .warning (
760+ f"Using legacy [code]{ config_manager .dstack_key_path .with_suffix ('.pub' )} [/code]."
761+ " You will only be able to attach to the run from this client."
762+ " Update the [code]dstack[/] server to [code]0.19.34[/]+ to switch to user keys"
763+ " automatically replicated to all clients." ,
764+ )
765+ ssh_key_pub = config_manager .dstack_key_path .with_suffix (".pub" ).read_text ()
769766 run_spec = RunSpec (
770767 run_name = run_name ,
771768 repo_id = repo .repo_id ,
@@ -775,7 +772,7 @@ def get_plan(
775772 configuration_path = configuration_path ,
776773 configuration = configuration ,
777774 profile = profile ,
778- ssh_key_pub = config_manager . dstack_key_path . with_suffix ( ".pub" ). read_text () ,
775+ ssh_key_pub = ssh_key_pub ,
779776 )
780777 logger .debug ("Getting run plan" )
781778 run_plan = self ._api_client .runs .get_plan (self ._project , run_spec )
0 commit comments