diff --git a/dvc/config.py b/dvc/config.py index 87311842f..c7925aec2 100644 --- a/dvc/config.py +++ b/dvc/config.py @@ -253,6 +253,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes SECTION_REMOTE_TIMEOUT = "timeout" SECTION_REMOTE_PASSWORD = "password" SECTION_REMOTE_ASK_PASSWORD = "ask_password" + SECTION_REMOTE_GSS_AUTH = "gss_auth" SECTION_REMOTE_NO_TRAVERSE = "no_traverse" SECTION_REMOTE_SCHEMA = { SECTION_REMOTE_URL: str, @@ -273,6 +274,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes Optional(SECTION_REMOTE_TIMEOUT): Use(int), Optional(SECTION_REMOTE_PASSWORD): str, Optional(SECTION_REMOTE_ASK_PASSWORD): BOOL_SCHEMA, + Optional(SECTION_REMOTE_GSS_AUTH): BOOL_SCHEMA, Optional(SECTION_AZURE_CONNECTION_STRING): str, Optional(SECTION_OSS_ACCESS_KEY_ID): str, Optional(SECTION_OSS_ACCESS_KEY_SECRET): str, diff --git a/dvc/remote/ssh/__init__.py b/dvc/remote/ssh/__init__.py index 98ee172bc..d5df6a75e 100644 --- a/dvc/remote/ssh/__init__.py +++ b/dvc/remote/ssh/__init__.py @@ -87,6 +87,7 @@ class RemoteSSH(RemoteBASE): self.ask_password = config.get( Config.SECTION_REMOTE_ASK_PASSWORD, False ) + self.gss_auth = config.get(Config.SECTION_REMOTE_GSS_AUTH, False) @staticmethod def ssh_config_filename(): @@ -145,6 +146,7 @@ class RemoteSSH(RemoteBASE): key_filename=self.keyfile, timeout=self.timeout, password=self.password, + gss_auth=self.gss_auth, ) def exists(self, path_info): diff --git a/setup.py b/setup.py index e98ef6be8..0edd7e972 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ gs = ["google-cloud-storage==1.13.0"] s3 = ["boto3==1.9.115"] azure = ["azure-storage-blob==2.1.0"] oss = ["oss2==2.6.1"] -ssh = ["paramiko>=2.5.0"] +ssh = ["paramiko[gssapi]>=2.5.0"] hdfs = ["pyarrow==0.14.0"] all_remotes = gs + s3 + azure + ssh + oss diff --git a/tests/unit/remote/ssh/test_ssh.py b/tests/unit/remote/ssh/test_ssh.py index b48bf082c..0dee1ac3e 100644 --- a/tests/unit/remote/ssh/test_ssh.py +++ b/tests/unit/remote/ssh/test_ssh.py @@ -160,3 +160,25 @@ def test_ssh_keyfile(mock_file, mock_exists, config, expected_keyfile): mock_exists.assert_called_with(RemoteSSH.ssh_config_filename()) mock_file.assert_called_with(RemoteSSH.ssh_config_filename()) assert remote.keyfile == expected_keyfile + + +@pytest.mark.parametrize( + "config,expected_gss_auth", + [ + ({"url": "ssh://example.com", "gss_auth": True}, True), + ({"url": "ssh://example.com", "gss_auth": False}, False), + ({"url": "ssh://not_in_ssh_config.com"}, False), + ], +) +@patch("os.path.exists", return_value=True) +@patch( + "{}.open".format(builtin_module_name), + new_callable=mock_open, + read_data=mock_ssh_config, +) +def test_ssh_gss_auth(mock_file, mock_exists, config, expected_gss_auth): + remote = RemoteSSH(None, config) + + mock_exists.assert_called_with(RemoteSSH.ssh_config_filename()) + mock_file.assert_called_with(RemoteSSH.ssh_config_filename()) + assert remote.gss_auth == expected_gss_auth