Repository: iterative/dvc

Issue:
[Remote SSH] Add a parameter allow_agent
Hello,

For some reason, when I use `dvc pull` or `dvc push` with an SSH remote, sometimes I get an error like this:
```
ERROR: unexpected error - No existing session
```

This is only on one specific machine that I use. On other machines, I don't have this problem.

To fix it, I have to give the parameter `allow_agent=False` to paramiko. If I edit manually the line https://github.com/iterative/dvc/blob/master/dvc/remote/ssh/__init__.py#L148 to add it, it works.

I was thinking that DVC could pass it through its config file, similarly to `port` or `ask_password`. For instance, I could add the line `allow_agent = false` in `.dvc/config.local`. I wanted to make a PR for that but I am too busy currently to do this myself :(

What do you think?


Comments:
@gcoter Sounds good! Let's do that!

Unfortunately, we don't have the capacity for this right now as well :slightly_frowning_face: So it might have to wait until someone has time for it. 
@gcoter Btw, i think you can already do that using your ssh config:
```
Host example.com
  ForwardAgent no
```
Hi @efiop, thanks for your answer! I tried to modify `ForwardAgent` but it doesn't seem to work in my case...
I will try to make the PR myself

Patch:
diff --git a/dvc/config.py b/dvc/config.py
--- a/dvc/config.py
+++ b/dvc/config.py
@@ -190,6 +190,7 @@ class RelPath(str):
                     "keyfile": str,
                     "timeout": Coerce(int),
                     "gss_auth": Bool,
+                    "allow_agent": Bool,
                     **REMOTE_COMMON,
                 },
                 "hdfs": {"user": str, **REMOTE_COMMON},
diff --git a/dvc/tree/ssh/__init__.py b/dvc/tree/ssh/__init__.py
--- a/dvc/tree/ssh/__init__.py
+++ b/dvc/tree/ssh/__init__.py
@@ -91,6 +91,7 @@ def __init__(self, repo, config):
             self.sock = paramiko.ProxyCommand(proxy_command)
         else:
             self.sock = None
+        self.allow_agent = config.get("allow_agent", True)
 
     @staticmethod
     def ssh_config_filename():
@@ -143,6 +144,7 @@ def ssh(self, path_info):
             password=self.password,
             gss_auth=self.gss_auth,
             sock=self.sock,
+            allow_agent=self.allow_agent,
         )
 
     @contextmanager


Test patch:
diff --git a/tests/unit/remote/ssh/test_ssh.py b/tests/unit/remote/ssh/test_ssh.py
--- a/tests/unit/remote/ssh/test_ssh.py
+++ b/tests/unit/remote/ssh/test_ssh.py
@@ -183,6 +183,31 @@ def test_ssh_gss_auth(mock_file, mock_exists, dvc, config, expected_gss_auth):
     assert tree.gss_auth == expected_gss_auth
 
 
+@pytest.mark.parametrize(
+    "config,expected_allow_agent",
+    [
+        ({"url": "ssh://example.com"}, True),
+        ({"url": "ssh://not_in_ssh_config.com"}, True),
+        ({"url": "ssh://example.com", "allow_agent": True}, True),
+        ({"url": "ssh://example.com", "allow_agent": False}, False),
+    ],
+)
+@patch("os.path.exists", return_value=True)
+@patch(
+    f"{builtin_module_name}.open",
+    new_callable=mock_open,
+    read_data=mock_ssh_config,
+)
+def test_ssh_allow_agent(
+    mock_file, mock_exists, dvc, config, expected_allow_agent
+):
+    tree = SSHTree(dvc, config)
+
+    mock_exists.assert_called_with(SSHTree.ssh_config_filename())
+    mock_file.assert_called_with(SSHTree.ssh_config_filename())
+    assert tree.allow_agent == expected_allow_agent
+
+
 def test_hardlink_optimization(dvc, tmp_dir, ssh):
     tree = SSHTree(dvc, ssh.config)