diff --git a/auto_backup/README.rst b/auto_backup/README.rst index 1b5485c70c3..57bf142d684 100644 --- a/auto_backup/README.rst +++ b/auto_backup/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ==================== Database Auto-Backup ==================== @@ -17,7 +13,7 @@ Database Auto-Backup .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github @@ -46,7 +42,7 @@ Before installing this module, you need to execute: :: - pip3 install pysftp==0.2.9 + pip3 install "paramiko<4.0.0" Configuration ============= @@ -71,7 +67,7 @@ Keep your data safe, through an SSH tunnel! Want to go even further and write your backups to an external server? You can with this module! Specify the credentials to the server, specify a path and everything will be backed up automatically. This is done -through an SSH (encrypted) tunnel, thanks to pysftp, so your data is +through an SSH (encrypted) tunnel, thanks to Paramiko, so your data is safe! Test connection @@ -108,12 +104,12 @@ manually execute the selected processes. Known issues / Roadmap ====================== -- On larger databases, it is possible that backups will die due to Odoo - server settings. In order to circumvent this without frivolously - changing settings, you need to run the backup from outside of the main - Odoo instance. How to do this is outlined in `this blog - post `__. -- Backups won't work if list_db=False is configured in the instance. +- On larger databases, it is possible that backups will die due to Odoo + server settings. In order to circumvent this without frivolously + changing settings, you need to run the backup from outside of the + main Odoo instance. How to do this is outlined in `this blog + post `__. +- Backups won't work if list_db=False is configured in the instance. Bug Tracker =========== @@ -140,15 +136,15 @@ Authors Contributors ------------ -- Yenthe Van Ginneken -- Alessio Gerace -- Jairo Llopis -- Dave Lasley -- Andrea Stirpe -- Aitor Bouzas -- Simone Vanin -- Vu Nguyen Anh -- Alex Comba +- Yenthe Van Ginneken +- Alessio Gerace +- Jairo Llopis +- Dave Lasley +- Andrea Stirpe +- Aitor Bouzas +- Simone Vanin +- Vu Nguyen Anh +- Alex Comba Maintainers ----------- diff --git a/auto_backup/__manifest__.py b/auto_backup/__manifest__.py index 3c7ab7471ac..fc295ec55e8 100644 --- a/auto_backup/__manifest__.py +++ b/auto_backup/__manifest__.py @@ -24,5 +24,5 @@ "view/db_backup_view.xml", ], "installable": True, - "external_dependencies": {"python": ["paramiko<4.0.0", "pysftp", "cryptography"]}, + "external_dependencies": {"python": ["paramiko<4.0.0", "cryptography"]}, } diff --git a/auto_backup/models/db_backup.py b/auto_backup/models/db_backup.py index 042038e6183..6ce06afce3d 100644 --- a/auto_backup/models/db_backup.py +++ b/auto_backup/models/db_backup.py @@ -10,8 +10,9 @@ from contextlib import contextmanager from datetime import datetime, timedelta from glob import iglob +from pathlib import PurePosixPath -import pysftp +import paramiko from odoo import _, api, exceptions, fields, models, tools from odoo.exceptions import UserError @@ -129,9 +130,9 @@ def action_sftp_test_connection(self): with self.sftp_connection(): raise UserError(_("Connection Test Succeeded!")) except ( - pysftp.CredentialException, - pysftp.ConnectionException, - pysftp.SSHException, + OSError, + paramiko.AuthenticationException, + paramiko.SSHException, ) as exc: _logger.info("Connection Test Failed!", exc_info=True) raise UserError(self.env._("Connection Test Failed!")) from exc @@ -176,10 +177,7 @@ def action_backup(self): with cached: with rec.sftp_connection() as remote: - try: - remote.makedirs(rec.folder) - except pysftp.ConnectionException as exc: - _logger.exception(f"pysftp ConnectionException: {exc}") + rec._sftp_makedirs(remote, rec.folder) # Copy cached backup to remote server with remote.open( @@ -270,22 +268,53 @@ def filename(when, ext="zip"): when, ext="dump.zip" if ext == "zip" else ext ) + @contextmanager def sftp_connection(self): """Return a new SFTP connection with found parameters.""" self.ensure_one() params = { - "host": self.sftp_host, + "hostname": self.sftp_host, "username": self.sftp_user, "port": self.sftp_port, + "allow_agent": False, + "look_for_keys": False, } _logger.debug( - "Trying to connect to sftp://%(username)s@%(host)s:%(port)d", extra=params + "Trying to connect to sftp://%s@%s:%d", + self.sftp_user, + self.sftp_host, + self.sftp_port, ) if self.sftp_private_key: - params["private_key"] = self.sftp_private_key + params["key_filename"] = self.sftp_private_key if self.sftp_password: - params["private_key_pass"] = self.sftp_password + params["password"] = self.sftp_password else: params["password"] = self.sftp_password - return pysftp.Connection(**params) + ssh = paramiko.SSHClient() + ssh.load_system_host_keys() + sftp = None + try: + ssh.connect(**params) + sftp = ssh.open_sftp() + yield sftp + finally: + if sftp: + sftp.close() + ssh.close() + + @staticmethod + def _sftp_makedirs(sftp, path): + """Create a remote directory and its parents when missing.""" + missing = [] + current = PurePosixPath(path) + while current != current.parent: + try: + sftp.stat(str(current)) + break + except FileNotFoundError: + missing.append(current) + current = current.parent + for directory in reversed(missing): + sftp.mkdir(str(directory)) diff --git a/auto_backup/readme/INSTALL.md b/auto_backup/readme/INSTALL.md index 39c51f6db89..b15eca6d4a8 100644 --- a/auto_backup/readme/INSTALL.md +++ b/auto_backup/readme/INSTALL.md @@ -1,3 +1,3 @@ Before installing this module, you need to execute: - pip3 install pysftp==0.2.9 + pip3 install "paramiko<4.0.0" diff --git a/auto_backup/readme/USAGE.md b/auto_backup/readme/USAGE.md index c314fad9b85..48f8e365018 100644 --- a/auto_backup/readme/USAGE.md +++ b/auto_backup/readme/USAGE.md @@ -10,7 +10,7 @@ and external backups should be kept, automatically! Want to go even further and write your backups to an external server? You can with this module! Specify the credentials to the server, specify a path and everything will be backed up automatically. This is done -through an SSH (encrypted) tunnel, thanks to pysftp, so your data is +through an SSH (encrypted) tunnel, thanks to Paramiko, so your data is safe! ## Test connection diff --git a/auto_backup/static/description/index.html b/auto_backup/static/description/index.html index 997ce0aa76c..569d039205d 100644 --- a/auto_backup/static/description/index.html +++ b/auto_backup/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Database Auto-Backup -
+
+

Database Auto-Backup

- - -Odoo Community Association - -
-

Database Auto-Backup

-

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

A tool for all your back-ups, internal and external!

Table of contents

@@ -408,38 +403,38 @@

Database Auto-Backup

-

Installation

+

Installation

Before installing this module, you need to execute:

-pip3 install pysftp==0.2.9
+pip3 install "paramiko<4.0.0"
 
-

Configuration

+

Configuration

Go to Settings -> Database Structure -> Automated Backup to create your configurations for each database that you needed to backups.

-

Usage

+

Usage

Keep your Odoo data safe with this module. Take automated back-ups, remove them automatically and even write them to an external server through an encrypted tunnel. You can even specify how long local backups and external backups should be kept, automatically!

-

Connect with an FTP Server

+

Connect with an FTP Server

-

Keep your data safe, through an SSH tunnel!

+

Keep your data safe, through an SSH tunnel!

Want to go even further and write your backups to an external server? You can with this module! Specify the credentials to the server, specify a path and everything will be backed up automatically. This is done -through an SSH (encrypted) tunnel, thanks to pysftp, so your data is +through an SSH (encrypted) tunnel, thanks to Paramiko, so your data is safe!

-

Test connection

+

Test connection

-

Checks your credentials in one click

+

Checks your credentials in one click

Want to make sure if the connection details are correct and if Odoo can automatically write them to the remote server? Simply click on the ‘Test SFTP Connection’ button and you will get message telling you if @@ -447,33 +442,33 @@

Checks your credentials in one cl

-

E-mail on backup failure

+

E-mail on backup failure

-

Stay informed of problems, automatically!

+

Stay informed of problems, automatically!

Do you want to know if the database backup succeeded or failed? Subscribe to the corresponding backup setting notification type.

-

Run backups when you want

+

Run backups when you want

From the backups configuration list, press More > Execute backup(s) to manually execute the selected processes.

Try me on Runbot

-

Known issues / Roadmap

+

Known issues / Roadmap

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -481,9 +476,9 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Yenthe Van Ginneken
  • Agile Business Group
  • @@ -493,7 +488,7 @@

    Authors

-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -520,6 +515,5 @@

Maintainers

-
diff --git a/auto_backup/tests/test_db_backup.py b/auto_backup/tests/test_db_backup.py index 6ab7236ebfa..4bced2e200f 100644 --- a/auto_backup/tests/test_db_backup.py +++ b/auto_backup/tests/test_db_backup.py @@ -8,9 +8,9 @@ import os from contextlib import contextmanager from datetime import datetime, timedelta -from unittest.mock import PropertyMock, patch +from unittest.mock import MagicMock, PropertyMock, call, patch -import pysftp +import paramiko from odoo import tools from odoo.exceptions import UserError @@ -23,9 +23,8 @@ class_name = f"{model}.DbBackup" -class TestConnectionException(pysftp.ConnectionException): - def __init__(self): - super().__init__("test", "test") +class TestConnectionException(paramiko.SSHException): + pass class TestDbBackup(common.TransactionCase): @@ -181,40 +180,55 @@ def test_action_backup_all_return(self): res = rec_id.action_backup_all() self.assertEqual(rec_id.search().action_backup(), res) - @patch(f"{model}.pysftp") - def test_sftp_connection_init_passwd(self, pysftp): + @patch(f"{model}.paramiko.SSHClient") + def test_sftp_connection_init_passwd(self, ssh_client): """It should initiate SFTP connection w/ proper args and pass""" rec_id = self.new_record() - rec_id.sftp_connection() - pysftp.Connection.assert_called_once_with( - host=rec_id.sftp_host, + with rec_id.sftp_connection(): + pass + ssh_client().connect.assert_called_once_with( + hostname=rec_id.sftp_host, username=rec_id.sftp_user, port=rec_id.sftp_port, + allow_agent=False, + look_for_keys=False, password=rec_id.sftp_password, ) - @patch(f"{model}.pysftp") - def test_sftp_connection_init_key(self, pysftp): + @patch(f"{model}.paramiko.SSHClient") + def test_sftp_connection_init_key(self, ssh_client): """It should initiate SFTP connection w/ proper args and key""" rec_id = self.new_record() rec_id.write({"sftp_private_key": "pkey", "sftp_password": "pkeypass"}) - rec_id.sftp_connection() - pysftp.Connection.assert_called_once_with( - host=rec_id.sftp_host, + with rec_id.sftp_connection(): + pass + ssh_client().connect.assert_called_once_with( + hostname=rec_id.sftp_host, username=rec_id.sftp_user, port=rec_id.sftp_port, - private_key=rec_id.sftp_private_key, - private_key_pass=rec_id.sftp_password, + allow_agent=False, + look_for_keys=False, + key_filename=rec_id.sftp_private_key, + password=rec_id.sftp_password, ) - @patch(f"{model}.pysftp") - def test_sftp_connection_return(self, pysftp): - """It should return new sftp connection""" + @patch(f"{model}.paramiko.SSHClient") + def test_sftp_connection_closes_clients(self, ssh_client): + """It should close both SFTP and SSH clients.""" rec_id = self.new_record() - res = rec_id.sftp_connection() + with rec_id.sftp_connection() as connection: + self.assertEqual(ssh_client().open_sftp(), connection) + ssh_client().open_sftp().close.assert_called_once_with() + ssh_client().close.assert_called_once_with() + + def test_sftp_makedirs(self): + """It should create missing remote directories from top to bottom.""" + sftp = MagicMock() + sftp.stat.side_effect = [FileNotFoundError, FileNotFoundError, None] + self.Model._sftp_makedirs(sftp, "/parent/child/grandchild") self.assertEqual( - pysftp.Connection(), - res, + [call("/parent/child"), call("/parent/child/grandchild")], + sftp.mkdir.call_args_list, ) def test_filename_default(self): diff --git a/requirements.txt b/requirements.txt index 5d1fefa6f23..c423566761b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,5 @@ openpyxl openupgradelib paramiko<4.0.0 pygount -pysftp sentry_sdk>=2.0.0,<=2.22.0 unittest-xml-reporting