From cad882d58b19d353840133f9c65911d49fec4494 Mon Sep 17 00:00:00 2001 From: Simon Maillard Date: Wed, 4 Dec 2024 14:23:24 +0000 Subject: [PATCH 1/5] =?UTF-8?q?[IMP]=C2=A0split=20generate=5Fodoo=5Fcomman?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In 2 functions: - generate_odoo_command_options - generate_odoo_command To be able to use generate_odoo_command_options from other places --- odoo_openupgrade_wizard/tools/tools_odoo.py | 105 ++++++++++++-------- 1 file changed, 64 insertions(+), 41 deletions(-) diff --git a/odoo_openupgrade_wizard/tools/tools_odoo.py b/odoo_openupgrade_wizard/tools/tools_odoo.py index 2cd3093..f63a287 100644 --- a/odoo_openupgrade_wizard/tools/tools_odoo.py +++ b/odoo_openupgrade_wizard/tools/tools_odoo.py @@ -121,7 +121,7 @@ def get_docker_container_name(ctx, database: str, migration_step: dict) -> str: ) -def generate_odoo_command( +def generate_odoo_command_options( ctx, migration_step: dict, execution_context: str, @@ -130,13 +130,13 @@ def generate_odoo_command( update: str = False, init: str = False, stop_after_init: bool = False, - shell: bool = False, -) -> str: +) -> list: + """ + Generate Odoo command options as a string to append to any command. + """ odoo_env_path = get_odoo_env_path(ctx, migration_step["version"]) # Compute 'server_wide_modules' - # For that purpose, read the custom odoo.conf file - # to know if server_wide_modules is defined custom_odoo_config_file = odoo_env_path / "odoo.conf" parser = configparser.RawConfigParser() parser.read(custom_odoo_config_file) @@ -147,7 +147,7 @@ def generate_odoo_command( migration_step, execution_context ) - # compute 'addons_path' + # Compute 'addons_path' addons_path_list, empty_addons_path_list = get_odoo_addons_path( ctx, odoo_env_path, migration_step, execution_context ) @@ -161,49 +161,72 @@ def generate_odoo_command( " because it doesn't contain any odoo module." ) - # compute 'log_file' - log_file_name = "{}____{}.log".format( - ctx.obj["log_prefix"], migration_step["complete_name"] + # Compute 'log_file' + log_file_name = ( + f"{ctx.obj['log_prefix']}____{migration_step['complete_name']}.log" ) - log_file_docker_path = "/env/log/%s" % log_file_name + log_file_docker_path = f"/env/log/{log_file_name}" - database_cmd = database and "--database %s" % database or "" - load_cmd = ( - server_wide_modules - and "--load %s" % ",".join(server_wide_modules) - or "" + # Build options string + options = [ + "--config=/odoo_env/odoo.conf", + "--data-dir=/env/filestore/", + f"--addons-path={addons_path}", + f"--logfile={log_file_docker_path}", + "--db_host=db", + "--db_port=5432", + "--db_user=odoo", + "--db_password=odoo", + "--workers=0", + f"{'--without-demo=all' if not demo else ''}", + f"{'--load ' + ','.join(server_wide_modules) if server_wide_modules else ''}", # noqa + f"{'--database=' + database if database else ''}", + f"{'--update ' + update if update else ''}", + f"{'--init ' + init if init else ''}", + f"{'--stop-after-init' if stop_after_init else ''}", + ] + + # remove empty strings + return [x for x in options if x] + + +def generate_odoo_command( + ctx, + migration_step: dict, + execution_context: str, + database: str, + demo: bool = False, + update: str = False, + init: str = False, + stop_after_init: bool = False, + shell: bool = False, +) -> str: + """ + Generate the full Odoo command using options from + generate_odoo_command_options. + """ + options = generate_odoo_command_options( + ctx, + migration_step, + execution_context, + database, + demo, + update, + init, + stop_after_init, ) - update_cmd = update and "--update %s" % update or "" - init_cmd = init and "--init %s" % init or "" - stop_after_init_cmd = stop_after_init and "--stop-after-init" or "" - shell_cmd = shell and "shell" or "" - demo_cmd = not demo and "--without-demo all" or "" - command = ( + + base_command = ( Path("/odoo_env") / Path(get_odoo_folder(migration_step, execution_context)) / Path(get_odoo_run_command(migration_step)) ) - result = ( - f" {command}" - f" {shell_cmd}" - f" --config=/odoo_env/odoo.conf" - f" --data-dir=/env/filestore/" - f" --addons-path={addons_path}" - f" --logfile={log_file_docker_path}" - f" --db_host=db" - f" --db_port=5432" - f" --db_user=odoo" - f" --db_password=odoo" - f" --workers=0" - f" {demo_cmd}" - f" {load_cmd}" - f" {database_cmd}" - f" {update_cmd}" - f" {init_cmd}" - f" {stop_after_init_cmd}" - ) - return result + options_as_string = " ".join(options) + if shell: + return f"{base_command} shell {options_as_string}" + else: + return f"{base_command} {options_as_string}" def run_odoo( From 5a854287c77616057c4e5d7f85e6d31aa9a64e27 Mon Sep 17 00:00:00 2001 From: Simon Maillard Date: Thu, 5 Dec 2024 09:24:25 +0000 Subject: [PATCH 2/5] =?UTF-8?q?[ADD]=C2=A0Add=20shell=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To jump in an odoo shell on an instance already started using odoo run --- odoo_openupgrade_wizard/cli/cli.py | 2 + odoo_openupgrade_wizard/cli/cli_shell.py | 107 +++++++++++++++++++++++ tests/cli_33_shell_test.py | 79 +++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 odoo_openupgrade_wizard/cli/cli_shell.py create mode 100644 tests/cli_33_shell_test.py diff --git a/odoo_openupgrade_wizard/cli/cli.py b/odoo_openupgrade_wizard/cli/cli.py index 0d73c0e..7ae59de 100644 --- a/odoo_openupgrade_wizard/cli/cli.py +++ b/odoo_openupgrade_wizard/cli/cli.py @@ -31,6 +31,7 @@ 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_shell import shell from odoo_openupgrade_wizard.cli.cli_upgrade import upgrade from odoo_openupgrade_wizard.tools.tools_system import ensure_folder_exists @@ -166,4 +167,5 @@ main.add_command(install_from_csv) main.add_command(psql) main.add_command(pull_submodule) main.add_command(run) +main.add_command(shell) main.add_command(upgrade) diff --git a/odoo_openupgrade_wizard/cli/cli_shell.py b/odoo_openupgrade_wizard/cli/cli_shell.py new file mode 100644 index 0000000..2c59eb9 --- /dev/null +++ b/odoo_openupgrade_wizard/cli/cli_shell.py @@ -0,0 +1,107 @@ +import os +import subprocess +import sys + +import click +import docker +from loguru import logger + +from odoo_openupgrade_wizard.cli.cli_options import ( + database_option_required, + get_migration_step_from_options, + step_option, +) +from odoo_openupgrade_wizard.tools.tools_odoo import ( + generate_odoo_command_options, +) +from odoo_openupgrade_wizard.tools.tools_postgres import ensure_database + + +@click.command() +@step_option +@database_option_required +@click.option( + "--code", + default=None, + help="Python code to execute in the Odoo shell. " + "Example: 'print(env.user.name)'", +) +@click.pass_context +def shell(ctx, step, database, code): + """Run an Odoo shell in the running Odoo container.""" + + migration_step = get_migration_step_from_options(ctx, step) + ensure_database(ctx, database, state="present") + + config = ctx.obj.get("config", {}) + project_name = config.get("project_name") + if not project_name: + click.echo("Unable to find the project name.") + sys.exit(1) + + expected_container_name_prefix = f"oow-{project_name}-{database}" + + # Connect to the Docker daemon + docker_client = docker.from_env() + + # List all running containers + running_containers = docker_client.containers.list( + filters={"status": "running"} + ) + + # Find the running Odoo container + matching_container = next( + ( + c + for c in running_containers + if expected_container_name_prefix in c.name and "step" in c.name + ), + None, + ) + + if not matching_container: + logger.error( + "No running Odoo container found. " + "Please run oow run before running the shell." + ) + sys.exit(1) + + logger.info(f"Execute shell in {matching_container.name} container...") + + common_options = generate_odoo_command_options( + ctx, migration_step, "regular", database + ) + + # Build the docker exec command + command = [ + "docker", + "exec", + "-i", # Interactive mode to enable stdin + matching_container.name, + "/odoo_env/src/odoo/odoo-bin", + "shell", + "--no-http", + ] + common_options + + logger.info(f"Command: {' '.join(command)}") + + # If code is provided, send it via stdin + if code: + logger.info(f"Executing code: {code}") + process = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, # Enables text mode + ) + stdout, stderr = process.communicate(code) + if process.returncode != 0: + logger.error(f"Error executing code: {stderr}") + sys.exit(1) + else: + click.echo(stdout) + else: + # Interactive session requires TTY + command.insert(2, "-t") + os.execvp(command[0], command) diff --git a/tests/cli_33_shell_test.py b/tests/cli_33_shell_test.py new file mode 100644 index 0000000..feb391d --- /dev/null +++ b/tests/cli_33_shell_test.py @@ -0,0 +1,79 @@ +import pathlib +import shutil + +from pytest import raises + +from odoo_openupgrade_wizard.cli.cli_options import ( + get_migration_step_from_options, +) +from odoo_openupgrade_wizard.tools.tools_odoo import run_odoo +from odoo_openupgrade_wizard.tools.tools_postgres import ensure_database + +from . import ( + build_ctx_from_config_file, + cli_runner_invoke, + move_to_test_folder, +) + + +def test_cli_shell(): + move_to_test_folder() + ctx = build_ctx_from_config_file() + migration_step = get_migration_step_from_options(ctx, 1) + stop_after_init = False + + # Ensure environment is clean + db_name = "database_test_cli___shell" + 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) + + # Set the log prefix + ctx.obj["log_prefix"] = "test_cli_shell" + + # Initialize the database + run_odoo( + ctx, + migration_step, + database=db_name, + detached_container=not stop_after_init, + stop_after_init=stop_after_init, + init="base", + execution_context="regular", + publish_ports=False, + ) + + # Test that a simple script executes successfully + result = cli_runner_invoke( + [ + "shell", + f"--database={db_name}", + "--step=1", + '--code="print("Hello, World!")"', + ] + ) + assert result.exit_code == 0 + assert "Hello, World!" in result.output + + # Test with a script that queries a model + result = cli_runner_invoke( + [ + "shell", + f"--database={db_name}", + "--step=1", + "--code=\"print(env['res.partner'].search_count([]))\"", + ] + ) + assert result.exit_code == 0 + assert result.output.strip().isdigit() # Should return a number + + # Test with missing database + with raises(Exception): + cli_runner_invoke( + [ + "shell", + "--database=nonexistent_database", + "--step=1", + '--script="print(env.user.name)"', + ] + ) From 8b75c94d22bb8c78268eede80d890357ed19ed31 Mon Sep 17 00:00:00 2001 From: Simon Maillard Date: Mon, 9 Dec 2024 09:40:34 +0000 Subject: [PATCH 3/5] [FIX] Mount volume in docker need absolute path Fixes E docker.errors.APIError: 400 Client Error for http://dind:2375/v1.47/containers/create?name=oow-test-cli-database_test_cli___shell-14.0-step-01: Bad Request ("create .: volume name is too short, names should be at least two alphanumeric characters") /root/.cache/pypoetry/virtualenvs/odoo-openupgrade-wizard-0zCHEn-Y-py3.9/lib/python3.9/site-packages/docker/errors.py:39: APIError --- tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index b6de448..aaa6966 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -65,7 +65,7 @@ def cli_runner_invoke(cmd, expect_success=True): def build_ctx_from_config_file() -> dict: - env_folder_path = Path(".") + env_folder_path = Path(".").absolute() class context: pass From 0ba79d59ba88ff214a1292d70d5f75f571b6cf8a Mon Sep 17 00:00:00 2001 From: Simon Maillard Date: Thu, 12 Dec 2024 10:40:10 +0000 Subject: [PATCH 4/5] [IMP] code layout --- tests/cli_33_shell_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cli_33_shell_test.py b/tests/cli_33_shell_test.py index feb391d..ac195b2 100644 --- a/tests/cli_33_shell_test.py +++ b/tests/cli_33_shell_test.py @@ -20,7 +20,6 @@ def test_cli_shell(): move_to_test_folder() ctx = build_ctx_from_config_file() migration_step = get_migration_step_from_options(ctx, 1) - stop_after_init = False # Ensure environment is clean db_name = "database_test_cli___shell" @@ -32,6 +31,7 @@ def test_cli_shell(): ctx.obj["log_prefix"] = "test_cli_shell" # Initialize the database + stop_after_init = False run_odoo( ctx, migration_step, From ba4274dea435b17b2f4a192cdf2a3c73934af8a4 Mon Sep 17 00:00:00 2001 From: Simon Maillard Date: Thu, 12 Dec 2024 10:47:46 +0000 Subject: [PATCH 5/5] =?UTF-8?q?[IMP]=C2=A0shell=20command:=20use=20ipython?= =?UTF-8?q?=20as=20python=20shell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- odoo_openupgrade_wizard/cli/cli_shell.py | 1 + .../templates/odoo/extra_python_requirements.txt.j2 | 1 + 2 files changed, 2 insertions(+) diff --git a/odoo_openupgrade_wizard/cli/cli_shell.py b/odoo_openupgrade_wizard/cli/cli_shell.py index 2c59eb9..6dbb572 100644 --- a/odoo_openupgrade_wizard/cli/cli_shell.py +++ b/odoo_openupgrade_wizard/cli/cli_shell.py @@ -81,6 +81,7 @@ def shell(ctx, step, database, code): "/odoo_env/src/odoo/odoo-bin", "shell", "--no-http", + "--shell-interface=iptyhon", ] + common_options logger.info(f"Command: {' '.join(command)}") diff --git a/odoo_openupgrade_wizard/templates/odoo/extra_python_requirements.txt.j2 b/odoo_openupgrade_wizard/templates/odoo/extra_python_requirements.txt.j2 index 75f29a2..66a122d 100644 --- a/odoo_openupgrade_wizard/templates/odoo/extra_python_requirements.txt.j2 +++ b/odoo_openupgrade_wizard/templates/odoo/extra_python_requirements.txt.j2 @@ -7,3 +7,4 @@ git+https://github.com/OCA/openupgradelib@master#egg=openupgradelib # dependencies of the module OCA/server-tools 'upgrade_analysis' odoorpc mako +ipython # used by default by shell command