Repository: dask/distributed

Issue:
Dependendy asyncssh not installed, when installing distributed
**What happened**:
asyncssh was not installed, but is used by distributed.

**What you expected to happen**:
asyncssh is installed, when installing distributed. May be through an ssh flag?

**Minimal Complete Verifiable Example**:

Freshly install:
```bash
pip install distributed
```

Import ssh cluster:
```python
from distributed import SSHCluster
```

```python-traceback
distributed\deploy\ssh.py in start(self)
    165 
    166     async def start(self):
--> 167         import asyncssh  # import now to avoid adding to module startup time
    168 
    169         logger.debug("Created Scheduler Connection")

ModuleNotFoundError: No module named 'asyncssh'
```

**Anything else we need to know?**:
No :)

**Environment**:

- Distributed version: 2021.09.1
- Python version: 3.7.7
- Operating System: Windows 10
- Install method (conda, pip, source): pip



Discussion:
Thanks for reporting @JulianWgs. `distributed` has several possible optional dependencies, depending on what functionality one wants to use. In these cases when an optional dependency isn't installed, we tend to raise an informative error message. For example:

https://github.com/dask/distributed/blob/7a3ea4c34ae0af6af1137fe3e3bda927aa9eac26/distributed/security.py#L106-L109

We should do something similar for `SSHCluster`s use of `asyncssh`. Additionally, while the `SSHCluster` docstring mentions `asyncssh`, it doesn't state that `asyncssh` is required to be able to use `SSHCluster` -- we should update the docstring to indicate this. 

Is this something you're interested in contributing? (no obligation though)
I ran into this import dependency issue whilst attempting to use SSHCluster. After fumbling about for few hours, a manual install of asyncssh package (I used conda-forge) seemed to resolve it. But, I am [seeing odd behavior](https://github.com/dask/distributed/issues/5439) from SSHCluster. I wonder if it could be because of an asyncssh incompatibility issue? 
My intent isn't to hijack this thread. So, if I've spoken out of turn then my apologies. Indeed, an informative error of sorts would be helpful.

Resolving patch:
diff --git a/distributed/deploy/ssh.py b/distributed/deploy/ssh.py
--- a/distributed/deploy/ssh.py
+++ b/distributed/deploy/ssh.py
@@ -91,7 +91,13 @@ def __init__(
         self.nprocs = self.kwargs.pop("nprocs", 1)
 
     async def start(self):
-        import asyncssh  # import now to avoid adding to module startup time
+        try:
+            import asyncssh  # import now to avoid adding to module startup time
+        except ImportError:
+            raise ImportError(
+                "Dask's SSHCluster requires the `asyncssh` package to be installed. "
+                "Please install it using pip or conda."
+            )
 
         self.connection = await asyncssh.connect(self.address, **self.connect_options)
 
@@ -180,7 +186,13 @@ def __init__(
         self.remote_python = remote_python
 
     async def start(self):
-        import asyncssh  # import now to avoid adding to module startup time
+        try:
+            import asyncssh  # import now to avoid adding to module startup time
+        except ImportError:
+            raise ImportError(
+                "Dask's SSHCluster requires the `asyncssh` package to be installed. "
+                "Please install it using pip or conda."
+            )
 
         logger.debug("Created Scheduler Connection")
 


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
@@ -26,6 +26,20 @@ def test_ssh_hosts_empty_list():
         SSHCluster(hosts=[])
 
 
+@pytest.mark.asyncio
+async def test_ssh_cluster_raises_if_asyncssh_not_installed(monkeypatch, cleanup):
+    monkeypatch.setitem(sys.modules, "asyncssh", None)
+    with pytest.raises(ImportError, match="SSHCluster requires the `asyncssh` package"):
+        async with SSHCluster(
+            ["127.0.0.1"] * 3,
+            connect_options=[dict(known_hosts=None)] * 3,
+            asynchronous=True,
+            scheduler_options={"idle_timeout": "5s"},
+            worker_options={"death_timeout": "5s"},
+        ) as cluster:
+            assert not cluster
+
+
 @pytest.mark.asyncio
 async def test_basic():
     async with SSHCluster(