@@ -63,7 +63,8 @@ def load_config() -> Dict[str, Any]:
6363 """Load configuration from the default config file.
6464
6565 Returns:
66- Dictionary with configuration values (url, auth_user, auth_password)
66+ Dictionary with configuration values (url, auth_user, auth_password,
67+ auth_token)
6768
6869 """
6970 if not os .path .exists (DEFAULT_CONFIG_PATH ):
@@ -371,12 +372,29 @@ def __init__(
371372 base_url : str ,
372373 auth_user : Optional [str ] = None ,
373374 auth_password : Optional [str ] = None ,
375+ auth_token : Optional [str ] = None ,
374376 ):
375- """Initialize the client with a base URL and optional auth credentials."""
377+ """Initialize the client with a base URL and optional auth credentials.
378+
379+ Two credential forms are supported for a VMM with `[auth]` enabled:
380+ a bearer token (sent as `Authorization: Bearer <token>`), or HTTP Basic
381+ (user + password). A bearer token takes precedence when both are set.
382+ """
376383 self .base_url = base_url .rstrip ("/" )
377384 self .use_uds = self .base_url .startswith ("unix:" )
378- self .auth_user = auth_user
379- self .auth_password = auth_password
385+ self .auth_user = auth_user or None
386+ self .auth_password = auth_password or None
387+ self .auth_token = auth_token or None
388+ # fail fast on a half-configured Basic credential: silently sending no
389+ # auth header would make requests fail against `[auth] enabled = true`
390+ # in a way that is hard to diagnose.
391+ if not self .auth_token and bool (self .auth_user ) != bool (self .auth_password ):
392+ missing = "password" if self .auth_user else "username"
393+ raise ValueError (
394+ f"incomplete VMM Basic auth credentials: { missing } is missing. "
395+ "set both username and password, or use a bearer token "
396+ "(--token / DSTACK_VMM_TOKEN)."
397+ )
380398
381399 if self .use_uds :
382400 self .uds_path = self .base_url [5 :] # Remove 'unix:' prefix
@@ -410,13 +428,17 @@ def request(
410428 if headers is None :
411429 headers = {}
412430
413- # Add Basic Authentication header if credentials are provided
414- if self .auth_user and self .auth_password :
415- credentials = f"{ self .auth_user } :{ self .auth_password } "
416- encoded_credentials = base64 .b64encode (credentials .encode ("utf-8" )).decode (
417- "ascii"
418- )
419- headers ["Authorization" ] = f"Basic { encoded_credentials } "
431+ # Add an auth header when credentials are provided. A bearer token wins
432+ # over Basic when both happen to be set.
433+ if "Authorization" not in headers :
434+ if self .auth_token :
435+ headers ["Authorization" ] = f"Bearer { self .auth_token } "
436+ elif self .auth_user and self .auth_password :
437+ credentials = f"{ self .auth_user } :{ self .auth_password } "
438+ encoded_credentials = base64 .b64encode (
439+ credentials .encode ("utf-8" )
440+ ).decode ("ascii" )
441+ headers ["Authorization" ] = f"Basic { encoded_credentials } "
420442
421443 # Prepare the body
422444 if isinstance (body , dict ):
@@ -480,11 +502,12 @@ def __init__(
480502 base_url : str ,
481503 auth_user : Optional [str ] = None ,
482504 auth_password : Optional [str ] = None ,
505+ auth_token : Optional [str ] = None ,
483506 ):
484507 """Initialize the CLI with a base URL and optional auth credentials."""
485508 self .base_url = base_url .rstrip ("/" )
486509 self .headers = {"Content-Type" : "application/json" }
487- self .client = VmmClient (base_url , auth_user , auth_password )
510+ self .client = VmmClient (base_url , auth_user , auth_password , auth_token )
488511
489512 def rpc_call (self , method : str , params : Optional [Dict ] = None ) -> Dict :
490513 """Make an RPC call to the dstack-vmm API."""
@@ -1519,6 +1542,7 @@ def main():
15191542 default_auth_password = os .environ .get (
15201543 "DSTACK_VMM_AUTH_PASSWORD" , config .get ("auth_password" )
15211544 )
1545+ default_auth_token = os .environ .get ("DSTACK_VMM_TOKEN" , config .get ("auth_token" ))
15221546
15231547 parser .add_argument (
15241548 "--url" ,
@@ -1537,6 +1561,13 @@ def main():
15371561 default = default_auth_password ,
15381562 help = "Basic auth password (can also be set via DSTACK_VMM_AUTH_PASSWORD env var or config file)" ,
15391563 )
1564+ parser .add_argument (
1565+ "--token" ,
1566+ default = default_auth_token ,
1567+ help = "bearer token for a VMM with `[auth]` enabled, sent as "
1568+ "`Authorization: Bearer` (can also be set via DSTACK_VMM_TOKEN env var "
1569+ "or config file). Takes precedence over --auth-user/--auth-password." ,
1570+ )
15401571
15411572 subparsers = parser .add_subparsers (dest = "command" , help = "Commands" )
15421573
@@ -1939,7 +1970,11 @@ def _patched_format_help():
19391970
19401971 # Resolve the URL with auto-discovery
19411972 url = resolve_vmm_url (instances , config , args .url )
1942- cli = VmmCLI (url , args .auth_user , args .auth_password )
1973+ try :
1974+ cli = VmmCLI (url , args .auth_user , args .auth_password , args .token )
1975+ except ValueError as e :
1976+ print (f"error: { e } " , file = sys .stderr )
1977+ sys .exit (1 )
19431978
19441979 if args .command == "lsvm" :
19451980 cli .list_vms (args .verbose , args .json )
0 commit comments