diff --git a/odoo_openupgrade_wizard/cli/cli.py b/odoo_openupgrade_wizard/cli/cli.py index 6677acb..40b6a96 100644 --- a/odoo_openupgrade_wizard/cli/cli.py +++ b/odoo_openupgrade_wizard/cli/cli.py @@ -11,6 +11,7 @@ from loguru import logger import odoo_openupgrade_wizard from odoo_openupgrade_wizard.cli.cli_copydb import copydb from odoo_openupgrade_wizard.cli.cli_docker_build import docker_build +from odoo_openupgrade_wizard.cli.cli_dumpdb import dumpdb from odoo_openupgrade_wizard.cli.cli_estimate_workload import estimate_workload from odoo_openupgrade_wizard.cli.cli_execute_script_python import ( execute_script_python, @@ -107,6 +108,7 @@ def main(ctx, env_folder, filestore_folder, log_level): main.add_command(copydb) main.add_command(docker_build) +main.add_command(dumpdb) main.add_command(estimate_workload) main.add_command(execute_script_python) main.add_command(execute_script_sql) diff --git a/odoo_openupgrade_wizard/cli/cli_dumpdb.py b/odoo_openupgrade_wizard/cli/cli_dumpdb.py new file mode 100644 index 0000000..e092675 --- /dev/null +++ b/odoo_openupgrade_wizard/cli/cli_dumpdb.py @@ -0,0 +1,96 @@ +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_dump +from odoo_openupgrade_wizard.tools.tools_system import dump_filestore + + +@click.command() +@database_option_required +@click.option( + "--database-path", + type=click.Path(writable=True, resolve_path=True), + required=True, + help="Path to the database dump relative project folder.", +) +@click.option( + "--database-format", + type=click.Choice(("p", "c", "d", "t")), + default="c", + help="Database format (see pg_dump options): plain sql text (p), " + "custom format compressed (c), directory (d), tar file (t).", +) +@click.option( + "--filestore-path", + type=click.Path(writable=True, resolve_path=True), + required=True, + help="Path to the filestore backup.", +) +@click.option( + "--filestore-format", + type=click.Choice(("d", "t", "tgz")), + default="tgz", + help="Filestore format: directory (d), tar file (t), " + "tar file compressed with gzip (tgz)", +) +@click.option( + "--force", + is_flag=True, + default=False, + help="Overwrite file if they already exists.", +) +@click.pass_context +def dumpdb( + ctx, + database, + database_path, + database_format, + filestore_path, + filestore_format, + force, +): + """Create an dump of an Odoo database and its filestore.""" + database_path = Path(database_path) + filestore_path = Path(filestore_path) + + # Fail if dumps already exists and force argument not given + if not force and database_path.exists(): + ctx.fail(f"{database_path} exists, use --force to overwrite it.") + if not force and filestore_path.exists(): + ctx.fail(f"{filestore_path} exists, use --force to overwrite it.") + + # Check that database_path is inside the env_folder_path + absolute_database_path = database_path.absolute() + absolute_env_folder_path = ctx.obj["env_folder_path"].resolve().absolute() + if not str(absolute_database_path).startswith( + str(absolute_env_folder_path) + ): + ctx.fail( + "database-path should be inside the project path to allow " + "postgresql to write to it." + ) + + # Normalise database_path + database_path = absolute_database_path.relative_to( + absolute_env_folder_path + ) + + # dump the database + output = execute_pg_dump( + ctx, + database=database, + dumpformat=database_format, + filename=str(database_path), + ) + if output: + click.echo(output) + + # dump the filestore + dump_filestore( + ctx, + database=database, + destpath=filestore_path, + copyformat=filestore_format, + ) diff --git a/odoo_openupgrade_wizard/tools/tools_postgres.py b/odoo_openupgrade_wizard/tools/tools_postgres.py index 819a236..1e108ea 100644 --- a/odoo_openupgrade_wizard/tools/tools_postgres.py +++ b/odoo_openupgrade_wizard/tools/tools_postgres.py @@ -180,3 +180,58 @@ def execute_sql_files_pre_migration( for sql_file in sql_files: execute_sql_file(ctx, database, sql_file) + + +def chown_to_local_user(ctx, filepath: os.PathLike): + """Chown a filepath in the postgres container to the local user""" + container = get_postgres_container(ctx) + user_uid = os.getuid() + command = "chown -R {uid}:{uid} {filepath}".format( + uid=user_uid, filepath=filepath + ) + logger.debug( + "Executing the following command in postgres container: %s" + % (command,) + ) + chown_result = exec_container(container, command) + return chown_result.output.decode("utf8") + + +def execute_pg_dump( + ctx, + database: str, + dumpformat: str, + filename: str, + pg_dump_args="--no-owner", +): + """Execute pg_dump command on the postgres container and dump the + result to dumpfile. + """ + if pg_dump_args and not isinstance(pg_dump_args, str): + pg_dump_args = " ".join(pg_dump_args) + container = get_postgres_container(ctx) + # Generate path for the output file + filepath = Path("/env") / Path(filename) + # Generate pg_dump command + command = ( + "pg_dump" + " --username=odoo" + " --format {dumpformat}" + " --file {filepath}" + " {pg_dump_args}" + " {database}" + ).format( + dumpformat=dumpformat, + filepath=filepath, + database=database, + pg_dump_args=pg_dump_args, + ) + logger.debug( + "Executing the following command in postgres container: %s" + % (command,) + ) + pg_dump_result = exec_container(container, command) + + chown_to_local_user(ctx, filepath) + + 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 3e5e437..78eeb1a 100644 --- a/odoo_openupgrade_wizard/tools/tools_system.py +++ b/odoo_openupgrade_wizard/tools/tools_system.py @@ -1,6 +1,8 @@ import argparse import os +import shutil import subprocess +import tarfile from pathlib import Path import importlib_resources @@ -109,3 +111,36 @@ def get_local_user_id(): def execute_check_output(args_list, working_directory=False): logger.debug("Execute %s" % " ".join(args_list)) subprocess.check_output(args_list, cwd=working_directory) + + +def dump_filestore( + ctx, + database: str, + destpath: os.PathLike, + copyformat: str = "d", +): + """Copy filestore of database to destpath using copyformat. + copyformat can be 'd' for directory, a normal copy, or 't' for a + copy into a tar achive, or 'tgz' to copy to a compressed tar file. + """ + valid_format = ("d", "t", "tgz", "txz") + if copyformat not in valid_format: + raise ValueError( + f"copyformat should be one of the following {valid_format}" + ) + + filestore_folder_path = ctx.obj["env_folder_path"] / "filestore/filestore" + filestore_path = filestore_folder_path / database + + if copyformat == "d": + shutil.copytree(filestore_path, destpath) + + elif copyformat.startswith("t"): + wmode = "w" + if copyformat.endswith("gz"): + wmode += ":gz" + elif copyformat.endswith("xz"): + wmode += ":xz" + + with tarfile.open(destpath, wmode) as tar: + tar.add(filestore_path, arcname="filestore")