Repository: dask/distributed

Issue:
dashboard_link conflicts with environment variables (schema, host, and port)
I incidentally had an environment variable named `host` which prevented a the `cluster.dashboard_link` from rendering. I'm not suggesting a change, just making the error and workaround available in case someone else encounters it.

## Error
```
Traceback (most recent call last):
  File "start_cluster.py", line 4, in <module>
    print(f"dashboard: {cluster.dashboard_link}")
  File "/home/andy/.pyenv/versions/3.7.4/lib/python3.7/site-packages/distributed/deploy/cluster.py", line 201, in dashboard_link
    return format_dashboard_link(host, port)
  File "/home/andy/.pyenv/versions/3.7.4/lib/python3.7/site-packages/distributed/utils.py", line 1437, in format_dashboard_link
    return template.format(scheme=scheme, host=host, port=port, **os.environ)
TypeError: format() got multiple values for keyword argument 'host'
```

## Reproducing
### Script: start_cluster.py
```
from distributed import Client, LocalCluster
if __name__ == '__main__':
    cluster = LocalCluster()
    print(f"dashboard: {cluster.dashboard_link}")
```
### Terminal
```
% export host=xyz
% python start_cluster.py 
```

## Cause
The trouble is that the [format call](https://github.com/dask/distributed/blob/cf26e1a559e2c89c4a4b14b6622111eaf0954f12/distributed/utils.py#L1437) specifies a `host=` and the `**os.environ` also includes a `host=` (from the outside environment). These two conflict. Similar conflicts would happen if you had lower case schema= or port= in your outer environment.

## The workaround
Remove the conflicting variables from the environment
```
unset host
unset port
unset schema
python start_cluster.py
```




Discussion:
jcrist: LGTM, merging.

Patch:
diff --git a/distributed/utils.py b/distributed/utils.py
index 9f14d58d..b7f6631c 100644
--- a/distributed/utils.py
+++ b/distributed/utils.py
@@ -1434,7 +1434,9 @@ def format_dashboard_link(host, port):
         scheme = "https"
     else:
         scheme = "http"
-    return template.format(scheme=scheme, host=host, port=port, **os.environ)
+    return template.format(
+        **toolz.merge(os.environ, dict(scheme=scheme, host=host, port=port))
+    )
 
 
 def is_coroutine_function(f):


Test patch:
diff --git a/distributed/tests/test_utils.py b/distributed/tests/test_utils.py
index bf2d8456..ff733a1a 100644
--- a/distributed/tests/test_utils.py
+++ b/distributed/tests/test_utils.py
@@ -2,6 +2,7 @@ import array
 import datetime
 from functools import partial
 import io
+import os
 import queue
 import socket
 import sys
@@ -40,6 +41,7 @@ from distributed.utils import (
     parse_bytes,
     parse_timedelta,
     warn_on_duration,
+    format_dashboard_link,
 )
 from distributed.utils_test import loop, loop_in_thread  # noqa: F401
 from distributed.utils_test import div, has_ipv6, inc, throws, gen_test, captured_logger
@@ -582,3 +584,17 @@ def test_is_valid_xml():
     assert is_valid_xml("<a>foo</a>")
     with pytest.raises(Exception):
         assert is_valid_xml("<a>foo")
+
+
+def test_format_dashboard_link():
+    with dask.config.set({"distributed.dashboard.link": "foo"}):
+        assert format_dashboard_link("host", 1234) == "foo"
+
+    assert "host" in format_dashboard_link("host", 1234)
+    assert "1234" in format_dashboard_link("host", 1234)
+
+    try:
+        os.environ["host"] = "hello"
+        assert "hello" not in format_dashboard_link("host", 1234)
+    finally:
+        del os.environ["host"]