Repository: iterative/dvc
Pull request: remote: ADD sock attribute and pass it to SSHConnection constructor
The sock attribute is set to a `paramiko.ProxyCommand` object
if 'ProxyCommand' exists in config to `None` other-wise
(which is the default option in `paramiko.client.SSHClient.connect()`).

FIXES #3560

No tests added, but passes all current tests.
* [x] ❗ I have followed the [Contributing to DVC](https://dvc.org/doc/user-guide/contributing/core) checklist.

* [x] 📖 If this PR requires [documentation](https://dvc.org/doc) updates, I have created a separate PR (or issue, at least) in [dvc.org](https://github.com/iterative/dvc.org) and linked it here. If the CLI API is changed, I have updated [tab completion scripts](https://github.com/iterative/dvc/tree/master/scripts/completion).

* [x] ❌ I will check DeepSource, CodeClimate, and other sanity checks below. (We consider them recommendatory and don't expect everything to be addressed. Please fix things that actually improve code or fix bugs.)

Thank you for the contribution - we'll try to review it as soon as possible. 🙏


Problem statement:
ssh remote: Pass `ProxyCommand` to paramiko
If you want to set a ssh remote to a private server you should be able to do so by doing a proxy jump using the `ProxyCommand` option in your ssh config file, e.g:
```
Host <ON-PREMISES-SERVER>
        User <USER>
        ProxyCommand ssh -W %h:%p <PUBLIC-SERVER>

Host <PUBLIC-SERVER>
        HostName <PUBLIC-IP>
        User <USER>
        Port <PORT>
```
Currently the `ProxyCommand` is not passed to paramiko, so the on-premises server name is not found, e.g.:
```bash
$ dvc remote add -d <ON-PREMISES-SERVER> ssh://<ON-PREMISES-SERVER>/path/to/directory/
$ dvc push
ERROR: unexpected error - [Errno -2] Name or service not known
```

Hints from issue discussion:
Discord context https://discordapp.com/channels/485586884165107732/485596304961962003/694489311164235854

Commit message: remote: ADD sock attribute and pass it to SSHConnection constructor

The sock attribute is set to a `paramiko.ProxyCommand` object
if 'ProxyCommand' exists in config to `None` other-wise
(which is the default option in `paramiko.client.SSHClient.connect()`).

FIXES #3560

Patch to review:
diff --git a/dvc/remote/ssh/__init__.py b/dvc/remote/ssh/__init__.py
index 703617b2af..cb9225ec03 100644
--- a/dvc/remote/ssh/__init__.py
+++ b/dvc/remote/ssh/__init__.py
@@ -10,6 +10,7 @@
 from urllib.parse import urlparse
 
 from funcy import memoize, wrap_with, silent, first
+import paramiko
 
 import dvc.prompt as prompt
 from dvc.progress import Tqdm
@@ -85,6 +86,11 @@ def __init__(self, repo, config):
         self.password = config.get("password", None)
         self.ask_password = config.get("ask_password", False)
         self.gss_auth = config.get("gss_auth", False)
+        proxy_command = user_ssh_config.get("proxycommand", False)
+        if proxy_command:
+            self.sock = paramiko.ProxyCommand(proxy_command)
+        else:
+            self.sock = None
 
     @staticmethod
     def ssh_config_filename():
@@ -92,8 +98,6 @@ def ssh_config_filename():
 
     @staticmethod
     def _load_user_ssh_config(hostname):
-        import paramiko
-
         user_config_file = RemoteSSH.ssh_config_filename()
         user_ssh_config = {}
         if hostname and os.path.exists(user_config_file):
@@ -136,6 +140,7 @@ def ssh(self, path_info):
             timeout=self.timeout,
             password=self.password,
             gss_auth=self.gss_auth,
+            sock=self.sock,
         )
 
     def exists(self, path_info):


Code review comments:
diff_hunk: @@ -10,6 +10,7 @@
 from urllib.parse import urlparse
 
 from funcy import memoize, wrap_with, silent, first
+import paramiko
path: dvc/remote/ssh/__init__.py
text: @user1:
Let's move this under `RemoteSSH.__init__` as a dynamic import.

@author:
OK, I'll do that.

@author:
`RemoteSSH._load_user_ssh_config` is a static method and uses paramiko, this means that it can be called without executing the import inside the `RemoteSSH.__init__`. Should I place the import directly inside the class body? Meaning:
```python
class RemoteSSH(RemoteBASE):
    import paramiko
    ...
```

@user1:
@author Doesn't seem to work, judging by the tests. I would just keep it simple: just add `import paramiko` in `__init__` as initially requested.