Skip to content

Commit 74de53d

Browse files
committed
Add new mila login command
Signed-off-by: Fabrice Normandin <[email protected]>
1 parent 9b4a7cd commit 74de53d

File tree

6 files changed

+103
-7
lines changed

6 files changed

+103
-7
lines changed

milatools/cli/commands.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@
3232
from typing_extensions import TypedDict
3333

3434
from milatools.cli import console
35+
from milatools.cli.login import login
3536
from milatools.utils.local_v1 import LocalV1
3637
from milatools.utils.remote_v1 import RemoteV1, SlurmRemote
37-
from milatools.utils.remote_v2 import RemoteV2
38+
from milatools.utils.remote_v2 import SSH_CONFIG_FILE, RemoteV2
3839
from milatools.utils.vscode_utils import (
3940
get_code_command,
40-
# install_local_vscode_extensions_on_remote,
4141
sync_vscode_extensions,
4242
sync_vscode_extensions_with_hostnames,
4343
)
@@ -170,6 +170,15 @@ def mila():
170170

171171
init_parser.set_defaults(function=init)
172172

173+
# ----- mila login ------
174+
login_parser = subparsers.add_parser(
175+
"login",
176+
help="Sets up reusable SSH connections to the entries of the SSH config.",
177+
formatter_class=SortingHelpFormatter,
178+
)
179+
login_parser.add_argument("--ssh_config_path", type=Path, default=SSH_CONFIG_FILE)
180+
login_parser.set_defaults(function=login)
181+
173182
# ----- mila forward ------
174183

175184
forward_parser = subparsers.add_parser(

milatools/cli/login.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from pathlib import Path
5+
6+
from paramiko import SSHConfig
7+
8+
from milatools.cli import console
9+
from milatools.utils.remote_v2 import SSH_CONFIG_FILE, RemoteV2
10+
11+
12+
async def login(
13+
ssh_config_path: Path = SSH_CONFIG_FILE,
14+
) -> list[RemoteV2]:
15+
"""Logs in and sets up reusable SSH connections to all the hosts in the SSH config.
16+
17+
Returns the list of remotes where the connection was successfully established.
18+
"""
19+
ssh_config = SSHConfig.from_path(str(ssh_config_path.expanduser()))
20+
potential_clusters = [
21+
host
22+
for host in ssh_config.get_hostnames()
23+
if not any(c in host for c in ["*", "?", "!"])
24+
]
25+
# take out entries like `mila-cpu` that have a proxy and remote command.
26+
potential_clusters = [
27+
hostname
28+
for hostname in potential_clusters
29+
if not (
30+
(config := ssh_config.lookup(hostname)).get("proxycommand")
31+
and config.get("remotecommand")
32+
)
33+
]
34+
remotes = await asyncio.gather(
35+
*(
36+
RemoteV2.connect(hostname, ssh_config_path=ssh_config_path)
37+
for hostname in potential_clusters
38+
),
39+
return_exceptions=True,
40+
)
41+
remotes = [remote for remote in remotes if isinstance(remote, RemoteV2)]
42+
console.log(f"Successfully connected to {[remote.hostname for remote in remotes]}")
43+
return remotes
44+
45+
46+
if __name__ == "__main__":
47+
asyncio.run(login())

tests/cli/test_commands/test_help_mila_.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
usage: mila [-h] [--version] [-v]
2-
{docs,intranet,init,forward,code,sync,serve} ...
2+
{docs,intranet,init,login,forward,code,sync,serve} ...
33

44
Tools to connect to and interact with the Mila cluster. Cluster documentation:
55
https://docs.mila.quebec/
66

77
positional arguments:
8-
{docs,intranet,init,forward,code,sync,serve}
8+
{docs,intranet,init,login,forward,code,sync,serve}
99
docs Open the Mila cluster documentation.
1010
intranet Open the Mila intranet in a browser.
1111
init Set up your configuration and credentials.
12+
login Sets up reusable SSH connections to the entries of the
13+
SSH config.
1214
forward Forward a port on a compute node to your local
1315
machine.
1416
code Open a remote VSCode session on a compute node.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
usage: mila [-h] [--version] [-v]
2-
{docs,intranet,init,forward,code,sync,serve} ...
2+
{docs,intranet,init,login,forward,code,sync,serve} ...
33
mila: error: the following arguments are required: <command>
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
usage: mila [-h] [--version] [-v]
2-
{docs,intranet,init,forward,code,sync,serve} ...
3-
mila: error: argument <command>: invalid choice: 'search' (choose from 'docs', 'intranet', 'init', 'forward', 'code', 'sync', 'serve')
2+
{docs,intranet,init,login,forward,code,sync,serve} ...
3+
mila: error: argument <command>: invalid choice: 'search' (choose from 'docs', 'intranet', 'init', 'login', 'forward', 'code', 'sync', 'serve')

tests/cli/test_login.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import textwrap
2+
from logging import getLogger as get_logger
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from milatools.cli.login import login
8+
from milatools.utils.remote_v2 import SSH_CACHE_DIR, RemoteV2
9+
10+
from .common import requires_ssh_to_localhost
11+
12+
logger = get_logger(__name__)
13+
14+
15+
@requires_ssh_to_localhost
16+
@pytest.mark.asyncio
17+
async def test_login(tmp_path: Path): # ssh_config_file: Path):
18+
assert SSH_CACHE_DIR.exists()
19+
ssh_config_path = tmp_path / "ssh_config"
20+
ssh_config_path.write_text(
21+
textwrap.dedent(
22+
"""\
23+
Host foo
24+
hostname localhost
25+
Host bar
26+
hostname localhost
27+
"""
28+
)
29+
+ "\n"
30+
)
31+
32+
# Should create a connection to every host in the ssh config file.
33+
remotes = await login(ssh_config_path=ssh_config_path)
34+
assert all(isinstance(remote, RemoteV2) for remote in remotes)
35+
assert set(remote.hostname for remote in remotes) == {"foo", "bar"}
36+
for remote in remotes:
37+
logger.info(f"Removing control socket at {remote.control_path}")
38+
remote.control_path.unlink()

0 commit comments

Comments
 (0)