8000 [17.0][REF] auto_backup: replace pysftp with paramiko by rrebollo · Pull Request #3817 · OCA/server-tools · GitHub
[go: up one dir, main page]

Skip to content

[17.0][REF] auto_backup: replace pysftp with paramiko - #3817

Draft
rrebollo wants to merge 2 commits into
OCA:17.0from
BinhexTeam:17.0-imp-auto_backup-paramiko
Draft

rrebollo wants to merge 2 commits into
OCA:17.0from
BinhexTeam:17.0-imp-auto_backup-paramiko

Conversation

@rrebollo
@rrebollo rrebollo commented Oct 27, 2026 •
Copy link
Copy Markdown

What

Replace the abandoned pysftp with paramiko in auto_backup, keeping the host key policy added
in #3462.

Why

  • Last pysftp release is 0.2.9 (2016), and its module-level from paramiko import DSSKey breaks
    import pysftp on paramiko >= 4, which is why this module had to pin paramiko<4.0.0.
  • It only loads RSA/DSS private keys: Ed25519 and ECDSA key files are rejected.
  • Its makedirs raises OSError, never ConnectionException, so the handler wrapped around that
    call was dead code.

How

  • _SftpClient: a context manager over paramiko.Transport built from an explicit socket
    (timeout=60), so the per-record hostkey can be passed to Transport.connect; it exposes
    open, listdir, unlink and a recursive makedirs that refuses a regular file where a
    directory is expected.
  • _get_sftp_host_keys keeps the policy unchanged: pinned keys, then ~/.ssh/known_hosts, then the
    explicit Ignore Host Key bypass, and a UserError when no key is available at all.
    _SftpConnectionOptions disappears with pysftp.
  • The test-connection button now catches AuthenticationException, SSHException and OSError.

Compatibility

No field, view, ACL or data change: no migration script is needed.

paramiko<4.0.0 stays for now: paramiko 4 renamed PKey.from_path(passphrase=...) to password=...
and this was validated against 3.5.1. Dropping the bound is a follow-up once an environment with
paramiko >= 4 is available (see #3705, which starts that migration on 18.0).

Tests

18 -> 22. The pysftp mocks are replaced by assertions on the transport (hostkey is the pinned key),
the socket timeout, the private-key loading, the bypass (hostkey=None), an unknown host raising
SSHException, and makedirs top-down plus the "not a directory" case. A mutation check (drop
hostkey=, or create directories bottom-up) makes the suite fail.

Depends on:

@BinhexTeam T26503

pysftp 0.2.9 verifies the host key of the SFTP server by default against
~/.ssh/known_hosts, and fails with "No hostkey for host X found" when the
Odoo user has no such file. Until now the check could not be configured at
all: a missing known_hosts file only produced a UserWarning, and an empty one
raised HostKeysException before the connection test could handle it.

Replace it with an explicit policy:

- SFTP Host Keys accepts one or more OpenSSH public keys, one per line (the
  output of ssh-keyscan, or bare keys), and enforces exactly those keys when
  set.
- Ignore Host Key skips the verification on purpose: it logs a warning and
  the form shows a banner while it is enabled.
- With neither of them configured, the keys of ~/.ssh/known_hosts are used,
  and when no key is available at all the connection is refused with a
  UserError explaining how to provide one.

The connection options no longer build pysftp.CnOpts, whose constructor
loads known_hosts before any policy can be applied: a missing file emits a
UserWarning and an empty file raises HostKeysException outside the UserError
handling of the connection test.
pysftp's last release is 0.2.9, from 2016. Its module-level DSSKey import
breaks on paramiko >= 4.0, which is why this module had to pin
paramiko<4.0.0, and it only tries RSA and DSS private keys, so Ed25519 and
ECDSA key files are rejected. Its makedirs raises OSError, never
ConnectionException, so the handler wrapped around that call was dead code.

Talk to paramiko directly through a small _SftpClient wrapper that stays a
context manager, so sftp_connection() keeps returning something usable in a
"with" block, as all its callers already expect. paramiko.SSHClient cannot
express a host key pinned per record, so the wrapper builds the transport
from a socket itself and passes hostkey to Transport.connect; the socket
also carries the read timeout.

The host key policy is unchanged: pinned keys first, then
~/.ssh/known_hosts, then the explicit "Ignore Host Key" bypass, and the
connection is refused with a UserError when no key is available at all.

Validated against paramiko 3.5.1, the version shipped in the images. No
model field, view, ACL or data changes, so no migration script is needed.

The paramiko<4.0.0 upper bound stays for now: paramiko 4 renamed the
passphrase argument of PKey.from_path to password.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0