Read ~/.ssh/config in the SSH pre-check

The paramiko pre-check ignored HostName, Port and IdentityFile, so hosts that rsync's ssh reached through ~/.ssh/config failed before the backup started.
This commit is contained in:
Fuxino
2026-10-21 22:31:01 +02:00
parent fa3ba2a6bd
commit dc50ef1b07
2 changed files with 65 additions and 7 deletions
+37 -6
View File
@@ -34,7 +34,7 @@ from shutil import get_terminal_size, rmtree, which
from subprocess import PIPE, STDOUT, Popen
from tempfile import mkstemp
from timeit import default_timer
from typing import ParamSpec, TypedDict, TypeVar
from typing import Any, ParamSpec, TypedDict, TypeVar
try:
import paramiko
@@ -469,6 +469,36 @@ class Backup:
if connected is None:
ssh.close()
def _ssh_config(self, homedir: str) -> dict[str, Any]:
"""connect() arguments for ssh_host from ~/.ssh/config (HostName, Port, IdentityFile), as rsync's ssh uses it"""
assert self.ssh_host is not None
params: dict[str, Any] = {'hostname': self.ssh_host}
try:
host = paramiko.SSHConfig.from_path(f'{homedir}/.ssh/config').lookup(self.ssh_host)
params['hostname'] = host['hostname']
if 'port' in host:
params['port'] = int(host['port'])
except FileNotFoundError:
return params
except (OSError, paramiko.SSHException, ImportError, ValueError) as e:
# ImportError: 'Match exec' needs the invoke module
logger.warning('Cannot read %s/.ssh/config: %s', homedir, e)
return {'hostname': self.ssh_host}
# Like ssh, skip identity files that don't exist
keys = [k for k in host.get('identityfile', []) if os.path.isfile(k)]
if keys:
params['key_filename'] = keys
return params
def _ssh_login(self, ssh: paramiko.client.SSHClient, homedir: str) -> paramiko.client.SSHClient | None:
try:
ssh.load_host_keys(filename=f'{homedir}/.ssh/known_hosts')
@@ -477,11 +507,13 @@ class Backup:
ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
host = self._ssh_config(homedir)
try:
# WarningPolicy emits a UserWarning for unknown host keys
with warnings.catch_warnings():
warnings.simplefilter('error', UserWarning)
ssh.connect(self.ssh_host, username=self.ssh_user, timeout=SSH_TIMEOUT)
ssh.connect(**host, username=self.ssh_user, timeout=SSH_TIMEOUT)
return ssh
except UserWarning:
@@ -507,7 +539,7 @@ class Backup:
pass
try:
ssh.connect(self.ssh_host, username=self.ssh_user, timeout=SSH_TIMEOUT)
ssh.connect(**host, username=self.ssh_user, timeout=SSH_TIMEOUT)
return ssh
except paramiko.SSHException:
@@ -521,8 +553,7 @@ class Backup:
password = getpass(
f'{self.ssh_user}@{self.ssh_host}\'s password: ')
ssh.connect(self.ssh_host, username=self.ssh_user, timeout=SSH_TIMEOUT,
password=password)
ssh.connect(**host, username=self.ssh_user, timeout=SSH_TIMEOUT, password=password)
self._password_auth = True
os.environ['SSHPASS'] = password
@@ -545,7 +576,7 @@ class Backup:
return None
try:
ssh.connect(self.ssh_host, username=self.ssh_user, timeout=SSH_TIMEOUT, pkey=pkey)
ssh.connect(**host, username=self.ssh_user, timeout=SSH_TIMEOUT, pkey=pkey)
except paramiko.SSHException:
logger.critical('SSH connection to server failed')