diff --git a/odoo_openupgrade_wizard/cli/cli.py b/odoo_openupgrade_wizard/cli/cli.py index c6f1508..8d8ac8c 100644 --- a/odoo_openupgrade_wizard/cli/cli.py +++ b/odoo_openupgrade_wizard/cli/cli.py @@ -28,6 +28,7 @@ from odoo_openupgrade_wizard.cli.cli_init import init from odoo_openupgrade_wizard.cli.cli_install_from_csv import install_from_csv from odoo_openupgrade_wizard.cli.cli_psql import psql from odoo_openupgrade_wizard.cli.cli_pull_submodule import pull_submodule +from odoo_openupgrade_wizard.cli.cli_restoredb import restoredb from odoo_openupgrade_wizard.cli.cli_run import run from odoo_openupgrade_wizard.cli.cli_upgrade import upgrade from odoo_openupgrade_wizard.tools.tools_system import ensure_folder_exists @@ -114,6 +115,7 @@ def main(ctx, env_folder, filestore_folder, log_level): main.add_command(copydb) +main.add_command(restoredb) main.add_command(docker_build) main.add_command(dropdb) main.add_command(dumpdb) diff --git a/odoo_openupgrade_wizard/cli/cli_restoredb.py b/odoo_openupgrade_wizard/cli/cli_restoredb.py new file mode 100644 index 0000000..2f479a1 --- /dev/null +++ b/odoo_openupgrade_wizard/cli/cli_restoredb.py @@ -0,0 +1,78 @@ +from pathlib import Path + +import click + +from odoo_openupgrade_wizard.cli.cli_options import database_option_required +from odoo_openupgrade_wizard.tools.tools_postgres import execute_pg_restore +from odoo_openupgrade_wizard.tools.tools_system import restore_filestore + + +@click.command() +@database_option_required +@click.option( + "--database-path", + required=True, + type=click.Path(readable=True, resolve_path=True), + help="Path to the database dump relative project folder.", +) +@click.option( + "--database-format", + required=True, + type=click.Choice(("c", "d", "t")), + default="c", + help="Database format (see pg_dump options): " + "custom format compressed (c), directory (d), tar file (t)." + "plain sql text (p) is not implemented", +) +@click.option( + "--filestore-path", + required=True, + type=click.Path(readable=True, resolve_path=True), + help="Path to the filestore backup.", +) +@click.option( + "--filestore-format", + required=True, + type=click.Choice(("d", "t", "tgz")), + default="tgz", + help="Filestore format: directory (d), tar file (t), " + "tar file compressed with gzip (tgz)", +) +@click.pass_context +def restoredb( + ctx, + database, + database_path, + database_format, + filestore_path, + filestore_format, +): + """Restore an Odoo database and associated filestore.""" + + database_path = Path(database_path) + filestore_path = Path(filestore_path) + + # Check that database_path is inside the env_folder_path + absolute_env_folder_path = ctx.obj["env_folder_path"].resolve().absolute() + if not str(database_path).startswith(str(absolute_env_folder_path)): + ctx.fail( + "database-path should be inside the project path to allow " + "postgresql to read to it." + ) + # Restore the database + output = execute_pg_restore( + ctx, + database_path.relative_to(absolute_env_folder_path), + database, + database_format, + ) + if output: + click.echo(output) + + # Restore the filestore + restore_filestore( + ctx, + database, + filestore_path, + filestore_format, + ) diff --git a/odoo_openupgrade_wizard/tools/tools_postgres.py b/odoo_openupgrade_wizard/tools/tools_postgres.py index cfe95d1..e2ad0d8 100644 --- a/odoo_openupgrade_wizard/tools/tools_postgres.py +++ b/odoo_openupgrade_wizard/tools/tools_postgres.py @@ -235,5 +235,28 @@ def execute_pg_dump( pg_dump_result = exec_container(container, command) chown_to_local_user(ctx, filepath) - + return pg_dump_result.output.decode("utf8") + + +def execute_pg_restore( + ctx, + filepath: Path, + database: str, + database_format: str, +): + """Execute pg_restore command on the postgres container""" + container = get_postgres_container(ctx) + ensure_database(ctx, database, "absent") + ensure_database(ctx, database, "present") + command = ( + "pg_restore" + f" {Path('/env') / filepath}" + f" --dbname={database}" + " --schema=public" + " --username=odoo" + " --no-owner" + f" --format {database_format}" + ) + logger.info(f"Restoring database '{database}'...") + pg_dump_result = exec_container(container, command) return pg_dump_result.output.decode("utf8") diff --git a/odoo_openupgrade_wizard/tools/tools_system.py b/odoo_openupgrade_wizard/tools/tools_system.py index db2bdc7..3de3816 100644 --- a/odoo_openupgrade_wizard/tools/tools_system.py +++ b/odoo_openupgrade_wizard/tools/tools_system.py @@ -143,3 +143,33 @@ def dump_filestore( with tarfile.open(destpath, wmode) as tar: tar.add(filestore_path, arcname="filestore") + + +def restore_filestore( + ctx, + database: str, + src_path: Path, + file_format: str = "d", +): + """Restore filestore of database from src_path using file_format. + file_format can be : + 'd' for 'directory': a normal copy, + 't' / 'tgz' for 'tar': an extraction from a tar achive + """ + valid_format = ("d", "t", "tgz") + if file_format not in valid_format: + raise ValueError( + f"file_format should be one of the following {valid_format}" + ) + + filestore_path = ( + ctx.obj["env_folder_path"] / "filestore/filestore" / database + ) + + logger.info(f"Restoring filestore of '{database}'...") + if file_format == "d": + shutil.copytree(src_path, filestore_path) + + else: # works for "t" and "tgz" + tar = tarfile.open(src_path) + tar.extractall(path=filestore_path) diff --git a/tests/__init__.py b/tests/__init__.py index 0765cbc..b1fe69d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,6 +7,7 @@ from click.testing import CliRunner from plumbum.cmd import mkdir from odoo_openupgrade_wizard.cli.cli import main +from odoo_openupgrade_wizard.tools.tools_postgres import execute_sql_request _logger = logging.getLogger() @@ -91,3 +92,12 @@ def mock_odoo_rpc_url(mocker): "odoo_openupgrade_wizard.tools.tools_odoo_instance._ODOO_RPC_URL", odoo_rpc_url, ) + + +def assert_database(ctx, db_name, state): + request = "select datname FROM pg_database WHERE datistemplate = false;" + results = execute_sql_request(ctx, request) + if state == "present": + assert [db_name] in results + else: + assert [db_name] not in results diff --git a/tests/cli_23_restoredb_test.py b/tests/cli_23_restoredb_test.py new file mode 100644 index 0000000..c1e4f91 --- /dev/null +++ b/tests/cli_23_restoredb_test.py @@ -0,0 +1,54 @@ +import pathlib +import shutil + +from odoo_openupgrade_wizard.tools.tools_postgres import ensure_database + +from . import ( + assert_database, + build_ctx_from_config_file, + cli_runner_invoke, + mock_odoo_rpc_url, + move_to_test_folder, +) + + +def test_cli_restoredb(mocker): + move_to_test_folder() + mock_odoo_rpc_url(mocker) + + db_name = "database_test_cli___restoredb" + ctx = build_ctx_from_config_file() + + # Ensure environment is clean + ensure_database(ctx, db_name, state="absent") + dest_filestore_path = pathlib.Path(f"./filestore/filestore/{db_name}") + shutil.rmtree(dest_filestore_path, ignore_errors=True) + + # Copy database and filestore data in a accessible folder + database_path = pathlib.Path("./restoredb.dump") + filestore_path = pathlib.Path("./restoredb.tar.gz") + + shutil.copyfile(pathlib.Path("../restoredb/test.dump"), database_path) + shutil.copyfile(pathlib.Path("../restoredb/test.tar.gz"), filestore_path) + + cli_runner_invoke( + [ + "--log-level=DEBUG", + "restoredb", + f"--database={db_name}", + f"--database-path={database_path}", + "--database-format=c", + f"--filestore-path={filestore_path}", + "--filestore-format=tgz", + ], + ) + + # check filestore exists + assert dest_filestore_path.exists() + + # Check database exists + assert_database(ctx, db_name, "present") + + # Delete filestore and database + shutil.rmtree(dest_filestore_path) + ensure_database(ctx, db_name, state="absent") diff --git a/tests/cli_31_copydb_test.py b/tests/cli_31_copydb_test.py index 025c704..29db47a 100644 --- a/tests/cli_31_copydb_test.py +++ b/tests/cli_31_copydb_test.py @@ -4,6 +4,7 @@ import shutil from odoo_openupgrade_wizard.tools.tools_postgres import ensure_database from . import ( + assert_database, build_ctx_from_config_file, cli_runner_invoke, mock_odoo_rpc_url, @@ -46,5 +47,9 @@ def test_cli_copydb(mocker): # check filestore exists assert dest_filestore_path.exists() - # Delete filestore + # Check database exists + assert_database(ctx, db_dest_name, "present") + + # Delete filestore and database shutil.rmtree(dest_filestore_path) + ensure_database(ctx, db_dest_name, state="absent") diff --git a/tests/cli_32_dropdb_test.py b/tests/cli_32_dropdb_test.py index 631cba4..671edb4 100644 --- a/tests/cli_32_dropdb_test.py +++ b/tests/cli_32_dropdb_test.py @@ -1,12 +1,10 @@ import pathlib import shutil -from odoo_openupgrade_wizard.tools.tools_postgres import ( - ensure_database, - execute_sql_request, -) +from odoo_openupgrade_wizard.tools.tools_postgres import ensure_database from . import ( + assert_database, build_ctx_from_config_file, cli_runner_invoke, mock_odoo_rpc_url, @@ -45,9 +43,7 @@ def test_cli_dropdb(mocker): ) # Check database does not exists - request = "select datname FROM pg_database WHERE datistemplate = false;" - results = execute_sql_request(ctx, request) - assert [db_name] not in results + assert_database(ctx, db_name, "absent") # Check filestore does not exists assert not filestore_path.exists() diff --git a/tests/data/restoredb/test.dump b/tests/data/restoredb/test.dump new file mode 100644 index 0000000..4fc9c9b Binary files /dev/null and b/tests/data/restoredb/test.dump differ diff --git a/tests/data/restoredb/test.tar.gz b/tests/data/restoredb/test.tar.gz new file mode 100644 index 0000000..7f5af3f Binary files /dev/null and b/tests/data/restoredb/test.tar.gz differ