Repository: dask/distributed

Issue:
Support different usernames when starting an SSH cluster
I wanted to start an SSH cluster where my username is not same on all the nodes.  It would be great if this was supported.  The most obvious way to configure this would be to allow specifying hosts as `user@hostname`, either on the command line, or the hosts file.


Discussion:
Seems reasonable to me.  Is this something that you would like to work on?

On Mon, Nov 12, 2018 at 2:42 AM Suvayu Ali <notifications@github.com> wrote:

> I wanted to start an SSH cluster where my username is not same on all the
> nodes. It would be great if this was supported. The most obvious way to
> configure this would be to allow specifying hosts as user@hostname,
> either on the command line, or the hosts file.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/dask/distributed/issues/2350>, or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AASszHSSw1H0NP_WP6Ad4cMh0RY8jqluks5uuSZegaJpZM4YZHKn>
> .
>

Sure!  With some guidance of course :)

Resolving patch:
diff --git a/distributed/deploy/ssh.py b/distributed/deploy/ssh.py
--- a/distributed/deploy/ssh.py
+++ b/distributed/deploy/ssh.py
@@ -1,6 +1,6 @@
 import logging
 import sys
-from typing import List
+from typing import List, Union
 import warnings
 import weakref
 
@@ -230,7 +230,7 @@ async def start(self):
 
 def SSHCluster(
     hosts: List[str] = None,
-    connect_options: dict = {},
+    connect_options: Union[List[dict], dict] = {},
     worker_options: dict = {},
     scheduler_options: dict = {},
     worker_module: str = "distributed.cli.dask_worker",
@@ -261,8 +261,9 @@ def SSHCluster(
     hosts: List[str]
         List of hostnames or addresses on which to launch our cluster.
         The first will be used for the scheduler and the rest for workers.
-    connect_options: dict, optional
+    connect_options: dict or list of dict, optional
         Keywords to pass through to ``asyncssh.connect``.
+        If a list it must have the same length as ``hosts``.
     worker_options: dict, optional
         Keywords to pass on to workers.
     scheduler_options: dict, optional
@@ -311,11 +312,19 @@ def SSHCluster(
         kwargs.setdefault("worker_addrs", hosts)
         return OldSSHCluster(**kwargs)
 
+    if isinstance(connect_options, list) and len(connect_options) != len(hosts):
+        raise RuntimeError(
+            "When specifying a list of connect_options you must provide a "
+            "dictionary for each address."
+        )
+
     scheduler = {
         "cls": Scheduler,
         "options": {
             "address": hosts[0],
-            "connect_options": connect_options,
+            "connect_options": connect_options
+            if isinstance(connect_options, dict)
+            else connect_options[0],
             "kwargs": scheduler_options,
             "remote_python": remote_python,
         },
@@ -325,7 +334,9 @@ def SSHCluster(
             "cls": Worker,
             "options": {
                 "address": host,
-                "connect_options": connect_options,
+                "connect_options": connect_options
+                if isinstance(connect_options, dict)
+                else connect_options[i + 1],
                 "kwargs": worker_options,
                 "worker_module": worker_module,
                 "remote_python": remote_python,


Test patch:
diff --git a/distributed/deploy/tests/test_ssh.py b/distributed/deploy/tests/test_ssh.py
--- a/distributed/deploy/tests/test_ssh.py
+++ b/distributed/deploy/tests/test_ssh.py
@@ -115,3 +115,34 @@ async def test_unimplemented_options():
             scheduler_kwargs={"idle_timeout": "5s", "port": 0},
         ) as cluster:
             assert cluster
+
+
+@pytest.mark.asyncio
+async def test_list_of_connect_options():
+    async with SSHCluster(
+        ["127.0.0.1"] * 3,
+        connect_options=[dict(known_hosts=None)] * 3,
+        asynchronous=True,
+        scheduler_options={"port": 0, "idle_timeout": "5s"},
+        worker_options={"death_timeout": "5s"},
+    ) as cluster:
+        assert len(cluster.workers) == 2
+        async with Client(cluster, asynchronous=True) as client:
+            result = await client.submit(lambda x: x + 1, 10)
+            assert result == 11
+        assert not cluster._supports_scaling
+
+        assert "SSH" in repr(cluster)
+
+
+@pytest.mark.asyncio
+async def test_list_of_connect_options_raises():
+    with pytest.raises(RuntimeError):
+        async with SSHCluster(
+            ["127.0.0.1"] * 3,
+            connect_options=[dict(known_hosts=None)] * 4,  # Mismatch in length 4 != 3
+            asynchronous=True,
+            scheduler_options={"port": 0, "idle_timeout": "5s"},
+            worker_options={"death_timeout": "5s"},
+        ) as _:
+            pass