gOdoo-cli 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- godoo_cli/__init__.py +2 -0
- godoo_cli/__main__.py +5 -0
- godoo_cli/cli.py +87 -0
- godoo_cli/cli_common.py +183 -0
- godoo_cli/commands/__init__.py +9 -0
- godoo_cli/commands/backup/__init__.py +1 -0
- godoo_cli/commands/backup/cli.py +17 -0
- godoo_cli/commands/backup/load.py +110 -0
- godoo_cli/commands/backup/pull.py +134 -0
- godoo_cli/commands/backup/util.py +35 -0
- godoo_cli/commands/bootstrap.py +187 -0
- godoo_cli/commands/config.py +43 -0
- godoo_cli/commands/db/__init__.py +1 -0
- godoo_cli/commands/db/cli.py +18 -0
- godoo_cli/commands/db/connection.py +94 -0
- godoo_cli/commands/db/passwords.py +56 -0
- godoo_cli/commands/db/query.py +41 -0
- godoo_cli/commands/launch.py +325 -0
- godoo_cli/commands/rpc/__init__.py +8 -0
- godoo_cli/commands/rpc/cli.py +34 -0
- godoo_cli/commands/rpc/config_parameters.py +48 -0
- godoo_cli/commands/rpc/importer.py +70 -0
- godoo_cli/commands/rpc/modules.py +144 -0
- godoo_cli/commands/rpc/translations.py +129 -0
- godoo_cli/commands/shell.py +45 -0
- godoo_cli/commands/source_get.py +279 -0
- godoo_cli/commands/test.py +126 -0
- godoo_cli/git/__init__.py +4 -0
- godoo_cli/git/git_odoo.py +89 -0
- godoo_cli/git/git_odoo_addons.py +95 -0
- godoo_cli/git/git_repo.py +207 -0
- godoo_cli/git/git_url.py +152 -0
- godoo_cli/git/zip_download.py +49 -0
- godoo_cli/helpers/__init__.py +1 -0
- godoo_cli/helpers/bootstrap.py +61 -0
- godoo_cli/helpers/cli.py +15 -0
- godoo_cli/helpers/odoo_files.py +278 -0
- godoo_cli/helpers/odoo_manifest.py +112 -0
- godoo_cli/helpers/system.py +176 -0
- godoo_cli/version.py +1 -0
- godoo_cli-0.7.0.dist-info/LICENSE +165 -0
- godoo_cli-0.7.0.dist-info/METADATA +197 -0
- godoo_cli-0.7.0.dist-info/RECORD +45 -0
- godoo_cli-0.7.0.dist-info/WHEEL +4 -0
- godoo_cli-0.7.0.dist-info/entry_points.txt +3 -0
godoo_cli/__init__.py
ADDED
godoo_cli/__main__.py
ADDED
godoo_cli/cli.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Main CLI."""
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
from rich import print as rich_print
|
|
7
|
+
|
|
8
|
+
from .cli_common import CommonCLI
|
|
9
|
+
from .commands import (
|
|
10
|
+
backup_cli_app,
|
|
11
|
+
bootstrap_odoo,
|
|
12
|
+
db_cli_app,
|
|
13
|
+
launch_import,
|
|
14
|
+
launch_odoo,
|
|
15
|
+
odoo_shell,
|
|
16
|
+
odoo_test,
|
|
17
|
+
rpc_cli_app,
|
|
18
|
+
set_odoo_config,
|
|
19
|
+
source_cli_app,
|
|
20
|
+
uninstall_modules,
|
|
21
|
+
)
|
|
22
|
+
from .helpers.odoo_files import odoo_bin_get_version
|
|
23
|
+
from .helpers.system import set_logging
|
|
24
|
+
from .version import __version__
|
|
25
|
+
|
|
26
|
+
CLI = CommonCLI()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main_callback(
|
|
30
|
+
verbose: Optional[bool] = typer.Option(
|
|
31
|
+
False,
|
|
32
|
+
"--verbose",
|
|
33
|
+
"-v",
|
|
34
|
+
envvar="GODOO_VERBOSE",
|
|
35
|
+
help="Verbose Logging with Error stacktraces",
|
|
36
|
+
)
|
|
37
|
+
):
|
|
38
|
+
set_logging(verbose=verbose)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@CLI.arg_annotator
|
|
42
|
+
def print_versions(
|
|
43
|
+
odoo_main_path=CLI.odoo_paths.bin_path,
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Print gOdoo and Odoo Version info.
|
|
47
|
+
"""
|
|
48
|
+
rich_print(f"gOdoo Version: [bold green]{__version__}[/bold green]")
|
|
49
|
+
odoo_version = odoo_bin_get_version(odoo_main_path)
|
|
50
|
+
rich_print(f"Odoo Version: [bold green]{odoo_version}[/bold green]")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main_cli():
|
|
54
|
+
load_dotenv(".env", override=True)
|
|
55
|
+
|
|
56
|
+
help_text = "gOdoo CLI for Interacting with Odoo"
|
|
57
|
+
app = typer.Typer(no_args_is_help=True, callback=main_callback, rich_markup_mode="rich", help=help_text)
|
|
58
|
+
# Nested Subcommands
|
|
59
|
+
app.add_typer(
|
|
60
|
+
typer_instance=rpc_cli_app(),
|
|
61
|
+
name="rpc",
|
|
62
|
+
)
|
|
63
|
+
app.add_typer(
|
|
64
|
+
typer_instance=db_cli_app(),
|
|
65
|
+
name="db",
|
|
66
|
+
)
|
|
67
|
+
app.add_typer(
|
|
68
|
+
typer_instance=source_cli_app(),
|
|
69
|
+
name="source",
|
|
70
|
+
)
|
|
71
|
+
app.add_typer(typer_instance=backup_cli_app(), name="backup")
|
|
72
|
+
|
|
73
|
+
# Normal Subcommands
|
|
74
|
+
app.command("version")(print_versions)
|
|
75
|
+
app.command("bootstrap")(bootstrap_odoo)
|
|
76
|
+
app.command("launch")(launch_odoo)
|
|
77
|
+
app.command("launch-import")(launch_import)
|
|
78
|
+
app.command("test")(odoo_test)
|
|
79
|
+
app.command("config")(set_odoo_config)
|
|
80
|
+
app.command("shell")(odoo_shell)
|
|
81
|
+
app.command("uninstall")(uninstall_modules)
|
|
82
|
+
return app
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def launch_cli():
|
|
86
|
+
app = main_cli()
|
|
87
|
+
app()
|
godoo_cli/cli_common.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from typer import Option
|
|
6
|
+
from typer_common_functions import get_type_from_default, typer_retuner, typer_unpacker
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class OdooLaunchArgs:
|
|
11
|
+
extra_cmd_args: List[str] = Option(None, help="extra agruments to pass to odoo-bin", rich_help_panel="Odoo")
|
|
12
|
+
multithread_worker_count: int = Option(
|
|
13
|
+
-1, help="count of worker threads. will enable proxy_mode if >0. (Autodetect with -1)", rich_help_panel="Odoo"
|
|
14
|
+
)
|
|
15
|
+
languages: str = Option("de_DE,en_US", help="languages to load by default", rich_help_panel="Odoo")
|
|
16
|
+
no_install_base: bool = Option(
|
|
17
|
+
False,
|
|
18
|
+
"--no-install-base",
|
|
19
|
+
help="dont install [bold]base[/bold] and [bold]web[/bold] module",
|
|
20
|
+
rich_help_panel="Odoo",
|
|
21
|
+
)
|
|
22
|
+
no_install_workspace_modules: bool = Option(
|
|
23
|
+
False,
|
|
24
|
+
"--no-install-workspace-modules",
|
|
25
|
+
help="dont automatically install modules found in [bold cyan]--workspace_path[/bold cyan]",
|
|
26
|
+
rich_help_panel="Odoo",
|
|
27
|
+
)
|
|
28
|
+
no_update_source: bool = Option(
|
|
29
|
+
False, "--no-update-source", help="Update Odoo Source and Thirdparty Addons", rich_help_panel="Source Code"
|
|
30
|
+
)
|
|
31
|
+
no_addons_remove_unspecified: bool = Option(
|
|
32
|
+
False,
|
|
33
|
+
"--no-addons-remove-unspecified",
|
|
34
|
+
help="don't remove unspecified addons if not '[bold cyan]--no-update-source[/bold cyan]'",
|
|
35
|
+
rich_help_panel="Source Code",
|
|
36
|
+
)
|
|
37
|
+
odoo_demo: bool = Option(False, "--odoo-demo", help="Load Demo Data", rich_help_panel="Odoo")
|
|
38
|
+
dev_mode: bool = Option(
|
|
39
|
+
False,
|
|
40
|
+
"--dev-mode",
|
|
41
|
+
help="Passes '[bold cyan]--dev xml,qweb,reload[/bold cyan]' to odoo",
|
|
42
|
+
rich_help_panel="Odoo",
|
|
43
|
+
)
|
|
44
|
+
log_file_path: Path = Option(None, dir_okay=False, writable=True, help="Logfile Path", rich_help_panel="Odoo")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class OdooPathCLIArgs:
|
|
49
|
+
bin_path: Path = Option(
|
|
50
|
+
...,
|
|
51
|
+
envvar="ODOO_MAIN_FOLDER",
|
|
52
|
+
help="folder that contains odoo-bin",
|
|
53
|
+
rich_help_panel="Path Options",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
conf_path: Path = Option(
|
|
57
|
+
...,
|
|
58
|
+
envvar="ODOO_CONF_PATH",
|
|
59
|
+
help="odoo.conf path",
|
|
60
|
+
rich_help_panel="Path Options",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
workspace_addon_path: Path = Option(
|
|
64
|
+
...,
|
|
65
|
+
envvar="ODOO_WORKSPACE_ADDON_LOCATION",
|
|
66
|
+
help="path to dev workspace addons",
|
|
67
|
+
rich_help_panel="Path Options",
|
|
68
|
+
)
|
|
69
|
+
thirdparty_addon_path: Path = Option(
|
|
70
|
+
...,
|
|
71
|
+
envvar="ODOO_THIRDPARTY_LOCATION",
|
|
72
|
+
help="folder that contains thirdparty repos like OCA",
|
|
73
|
+
rich_help_panel="Path Options",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class RpcCLIArgs:
|
|
79
|
+
rpc_host: str = Option(
|
|
80
|
+
...,
|
|
81
|
+
envvar="ODOO_RPC_HOST",
|
|
82
|
+
help="Odoo RPC Host",
|
|
83
|
+
rich_help_panel="RPC Options",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
rpc_user: str = Option(
|
|
87
|
+
...,
|
|
88
|
+
envvar="ODOO_RPC_USER",
|
|
89
|
+
help="User for RPC login",
|
|
90
|
+
rich_help_panel="RPC Options",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
rpc_password: str = Option(
|
|
94
|
+
...,
|
|
95
|
+
envvar="ODOO_RPC_PASSWORD",
|
|
96
|
+
help="Password RPC Login Password",
|
|
97
|
+
rich_help_panel="RPC Options",
|
|
98
|
+
)
|
|
99
|
+
rpc_db_name: str = Option(
|
|
100
|
+
...,
|
|
101
|
+
envvar=["ODOO_RPC_DB_NAME", "ODOO_MAIN_DB"],
|
|
102
|
+
help="RPC database name",
|
|
103
|
+
rich_help_panel="RPC Options",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class DatabaseCLIArgs:
|
|
109
|
+
db_filter: str = Option(
|
|
110
|
+
...,
|
|
111
|
+
envvar="ODOO_DB_FILTER",
|
|
112
|
+
help="database filter for odoo_conf",
|
|
113
|
+
rich_help_panel="Database Options",
|
|
114
|
+
)
|
|
115
|
+
db_host: str = Option(
|
|
116
|
+
"",
|
|
117
|
+
envvar="ODOO_DB_HOST",
|
|
118
|
+
help="db hostname (empty for default socket)",
|
|
119
|
+
rich_help_panel="Database Options",
|
|
120
|
+
)
|
|
121
|
+
db_name: str = Option(
|
|
122
|
+
...,
|
|
123
|
+
envvar="ODOO_MAIN_DB",
|
|
124
|
+
help="launch database name",
|
|
125
|
+
rich_help_panel="Database Options",
|
|
126
|
+
)
|
|
127
|
+
db_user: str = Option(
|
|
128
|
+
...,
|
|
129
|
+
envvar="ODOO_DB_USER",
|
|
130
|
+
help="db user",
|
|
131
|
+
rich_help_panel="Database Options",
|
|
132
|
+
)
|
|
133
|
+
db_password: str = Option(
|
|
134
|
+
...,
|
|
135
|
+
envvar="ODOO_DB_PASSWORD",
|
|
136
|
+
help="db password",
|
|
137
|
+
rich_help_panel="Database Options",
|
|
138
|
+
)
|
|
139
|
+
db_port: str = Option(
|
|
140
|
+
"",
|
|
141
|
+
envvar="ODOO_DB_PORT",
|
|
142
|
+
help="db host port (empty for socket)",
|
|
143
|
+
rich_help_panel="Database Options",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class SourceGetArgs:
|
|
149
|
+
mainfest_path: Path = Option(
|
|
150
|
+
"",
|
|
151
|
+
envvar="ODOO_MANIFEST",
|
|
152
|
+
help="godoo manifest path, when downloading odoo source (skip repo_url)",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
source_download_archive: Optional[bool] = Option(
|
|
156
|
+
False,
|
|
157
|
+
"--source-download-archive",
|
|
158
|
+
envvar="SOURCE_CLONE_ARCHIVE",
|
|
159
|
+
help="When using a HTTPs Repo Url for Github we can download a snapshop without the Repo history",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class CommonCLI:
|
|
164
|
+
def __init__(self) -> None:
|
|
165
|
+
self.odoo_paths = OdooPathCLIArgs
|
|
166
|
+
self.odoo_launch = OdooLaunchArgs
|
|
167
|
+
self.database = DatabaseCLIArgs
|
|
168
|
+
self.rpc = RpcCLIArgs
|
|
169
|
+
self.source = SourceGetArgs
|
|
170
|
+
self.returner = typer_retuner
|
|
171
|
+
self.unpacker = typer_unpacker
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def arg_annotator(self):
|
|
175
|
+
"""Add type annotations for
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
Callback
|
|
180
|
+
Annotator function with CommonCLI Props as arguments
|
|
181
|
+
"""
|
|
182
|
+
my_args = self.__dict__
|
|
183
|
+
return get_type_from_default(*list(my_args.values()))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .bootstrap import bootstrap_odoo
|
|
2
|
+
from .config import set_odoo_config
|
|
3
|
+
from .db import db_cli_app
|
|
4
|
+
from .launch import launch_import, launch_odoo
|
|
5
|
+
from .rpc import rpc_cli_app
|
|
6
|
+
from .shell import odoo_shell, uninstall_modules
|
|
7
|
+
from .source_get import source_cli_app
|
|
8
|
+
from .test import odoo_test
|
|
9
|
+
from .backup import backup_cli_app
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .cli import backup_cli_app
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from .load import load_instance_data
|
|
4
|
+
from .pull import InstancePuller
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def backup_cli_app():
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
no_args_is_help=True,
|
|
10
|
+
help="Functions around Backing up and Restoring Odoo",
|
|
11
|
+
)
|
|
12
|
+
puller = InstancePuller()
|
|
13
|
+
|
|
14
|
+
app.command("pull")(puller.pull_instance_data)
|
|
15
|
+
app.command("load")(load_instance_data)
|
|
16
|
+
|
|
17
|
+
return app
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
|
|
7
|
+
from ...cli_common import CommonCLI
|
|
8
|
+
from ..db.connection import DBConnection
|
|
9
|
+
from .util import call_rsync, create_db, drop_db
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
CLI = CommonCLI()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_pg_dump(
|
|
16
|
+
dump_path: Path,
|
|
17
|
+
db_name: str,
|
|
18
|
+
db_host: str = None,
|
|
19
|
+
db_port: int = 0,
|
|
20
|
+
db_user: str = None,
|
|
21
|
+
db_password: str = None,
|
|
22
|
+
):
|
|
23
|
+
"""Drop and recreate db_name and restore dump_path into it."""
|
|
24
|
+
if not dump_path.exists():
|
|
25
|
+
raise FileNotFoundError("Cannot find PG dump @ %s" % dump_path)
|
|
26
|
+
|
|
27
|
+
conn = DBConnection(
|
|
28
|
+
db_name="postgres",
|
|
29
|
+
hostname=db_host,
|
|
30
|
+
port=db_port,
|
|
31
|
+
username=db_user,
|
|
32
|
+
password=db_password,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
drop_db(conn, db_name)
|
|
36
|
+
create_db(conn, db_name)
|
|
37
|
+
|
|
38
|
+
conn.db_name = db_name
|
|
39
|
+
|
|
40
|
+
LOGGER.info(
|
|
41
|
+
"Loading DB Dump: %s [%s mb] --> %s",
|
|
42
|
+
dump_path,
|
|
43
|
+
round(dump_path.stat().st_size / (1024 * 1024), 2),
|
|
44
|
+
db_name,
|
|
45
|
+
)
|
|
46
|
+
if dump_path.suffix == ".dump":
|
|
47
|
+
command = "pg_restore --no-owner --format c --dbname %s --jobs 8 {} %s" % (db_name, dump_path)
|
|
48
|
+
else:
|
|
49
|
+
command = "cat %s | psql {} >/dev/null" % dump_path
|
|
50
|
+
load_return = conn.run_psql_shell_command(
|
|
51
|
+
command=command,
|
|
52
|
+
log_name="Reading DB",
|
|
53
|
+
text=True,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
)
|
|
56
|
+
if load_return.returncode != 0:
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
LOGGER.info("Deleting RPC Import Cache using plain SQL")
|
|
59
|
+
with conn.connect() as cur:
|
|
60
|
+
cur.execute("DELETE FROM ir_config_parameter WHERE key='godoo_rpc_import_cache';")
|
|
61
|
+
return load_return.returncode
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@CLI.arg_annotator
|
|
65
|
+
def load_instance_data(
|
|
66
|
+
source_folder: Annotated[
|
|
67
|
+
Path,
|
|
68
|
+
typer.Argument(
|
|
69
|
+
...,
|
|
70
|
+
help="Source Folder to load data from.",
|
|
71
|
+
file_okay=False,
|
|
72
|
+
dir_okay=True,
|
|
73
|
+
exists=True,
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
filestore_target_folder: Annotated[
|
|
77
|
+
Path,
|
|
78
|
+
typer.Argument(
|
|
79
|
+
...,
|
|
80
|
+
help="Target Folder to load filestore to.",
|
|
81
|
+
file_okay=False,
|
|
82
|
+
dir_okay=True,
|
|
83
|
+
),
|
|
84
|
+
],
|
|
85
|
+
db_host=CLI.database.db_host,
|
|
86
|
+
db_port=CLI.database.db_port,
|
|
87
|
+
db_name=CLI.database.db_name,
|
|
88
|
+
db_user=CLI.database.db_user,
|
|
89
|
+
db_password=CLI.database.db_password,
|
|
90
|
+
dump_file_name: Annotated[
|
|
91
|
+
str,
|
|
92
|
+
typer.Option(help="Optional Changeable filename of Odoo SQL Dump within source_folder"),
|
|
93
|
+
] = "odoo.dump",
|
|
94
|
+
):
|
|
95
|
+
"""Copy Filestore cache and load SQL Dump into Postgres for gOdoo."""
|
|
96
|
+
load_pg_dump(
|
|
97
|
+
dump_path=source_folder / dump_file_name,
|
|
98
|
+
db_host=db_host,
|
|
99
|
+
db_port=db_port,
|
|
100
|
+
db_name=db_name,
|
|
101
|
+
db_user=db_user,
|
|
102
|
+
db_password=db_password,
|
|
103
|
+
)
|
|
104
|
+
LOGGER.info("Done Loading Odoo instance from %s", source_folder)
|
|
105
|
+
|
|
106
|
+
filestore_target_folder.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
call_rsync(
|
|
108
|
+
source_folder=source_folder / "odoo_filestore",
|
|
109
|
+
target_folder=filestore_target_folder,
|
|
110
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.progress import track
|
|
7
|
+
from typing_extensions import Annotated
|
|
8
|
+
|
|
9
|
+
from ...cli_common import CommonCLI
|
|
10
|
+
from ...helpers.system import typer_ask_overwrite_path
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
CLI = CommonCLI()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InstancePuller:
|
|
17
|
+
ssh_hostname: str
|
|
18
|
+
ssh_user: str
|
|
19
|
+
pg_container: str # Does Postgres Live in a container?
|
|
20
|
+
pg_user: str
|
|
21
|
+
|
|
22
|
+
def set_exec_target(self, command):
|
|
23
|
+
"""Prepend ssh execution, if ssh args are provided"""
|
|
24
|
+
if self.ssh_hostname:
|
|
25
|
+
return f"ssh {self.ssh_user}@{self.ssh_hostname} '{command}'"
|
|
26
|
+
return command
|
|
27
|
+
|
|
28
|
+
def get_docker_volume_path(self, volume_name: str):
|
|
29
|
+
command = f"docker volume inspect {volume_name} | jq -r .[0].Mountpoint"
|
|
30
|
+
command = self.set_exec_target(command)
|
|
31
|
+
LOGGER.debug("Running: '%s'", command)
|
|
32
|
+
path = subprocess.run(
|
|
33
|
+
command,
|
|
34
|
+
shell=True,
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
).stdout.strip("\n")
|
|
38
|
+
LOGGER.debug("Docker Volume Path: %s", path)
|
|
39
|
+
return path
|
|
40
|
+
|
|
41
|
+
def rsync_filestore(self, filestore_folder, target_folder: Path):
|
|
42
|
+
src_path = filestore_folder
|
|
43
|
+
if self.ssh_hostname:
|
|
44
|
+
src_path = f"{self.ssh_user}@{self.ssh_hostname}:{src_path}/"
|
|
45
|
+
|
|
46
|
+
command = f"rsync --rsync-path 'sudo rsync' -a --no-perms --no-owner --no-group --delete --info=progress2 {src_path} {target_folder}"
|
|
47
|
+
LOGGER.info("Rysnc filestore from: %s", filestore_folder)
|
|
48
|
+
LOGGER.debug("Running: %s", command)
|
|
49
|
+
return subprocess.run(command, shell=True).returncode
|
|
50
|
+
|
|
51
|
+
def download_db_dump(self, db_name, target_sql_path: Path):
|
|
52
|
+
command = f"pg_dump --no-owner -Fc {db_name}"
|
|
53
|
+
if n := self.pg_container:
|
|
54
|
+
command = f"docker exec {n} {command}"
|
|
55
|
+
if u := self.pg_user:
|
|
56
|
+
command += f" -U {u}"
|
|
57
|
+
else:
|
|
58
|
+
# Lets assume we run pg_dump locally here.
|
|
59
|
+
# Totally need to think about a neat way to handle the multiple combinations of this.
|
|
60
|
+
if u := self.pg_user:
|
|
61
|
+
command = f"sudo -u {u} {command}"
|
|
62
|
+
|
|
63
|
+
command = self.set_exec_target(command)
|
|
64
|
+
LOGGER.info("Downloading DB Dump: %s", db_name)
|
|
65
|
+
target_sql_path.touch(exist_ok=True)
|
|
66
|
+
with target_sql_path.open("wb") as sql_dump_file:
|
|
67
|
+
LOGGER.debug("Running: %s", command)
|
|
68
|
+
dbdumper = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
|
|
69
|
+
for line in track(dbdumper.stdout, description="Downloading DB Dump", show_speed=False):
|
|
70
|
+
sql_dump_file.write(line)
|
|
71
|
+
|
|
72
|
+
@CLI.arg_annotator
|
|
73
|
+
def pull_instance_data(
|
|
74
|
+
self,
|
|
75
|
+
target_folder: Annotated[
|
|
76
|
+
Path, typer.Argument(..., help="Target Folder to pull data to.", file_okay=False, dir_okay=True)
|
|
77
|
+
],
|
|
78
|
+
ssh_user: Annotated[
|
|
79
|
+
str,
|
|
80
|
+
typer.Option(
|
|
81
|
+
..., help="SSH User of remote instance.", rich_help_panel="Remote Options", envvar="ODOO_PULL_SSH_USER"
|
|
82
|
+
),
|
|
83
|
+
] = None,
|
|
84
|
+
ssh_hostname: Annotated[
|
|
85
|
+
str,
|
|
86
|
+
typer.Option(
|
|
87
|
+
help="SSH Hostname of remote instance.", rich_help_panel="Remote Options", envvar="ODOO_PULL_HOST"
|
|
88
|
+
),
|
|
89
|
+
] = None,
|
|
90
|
+
filestore_folder: Annotated[
|
|
91
|
+
Path,
|
|
92
|
+
typer.Option(help="Path to odoo web folder.", rich_help_panel="Filestore Options"),
|
|
93
|
+
] = None,
|
|
94
|
+
filestore_volume: Annotated[
|
|
95
|
+
str,
|
|
96
|
+
typer.Option(
|
|
97
|
+
help="Alternative to valib-folder. A Docker volume name, that gets used to derive filestore-folder",
|
|
98
|
+
rich_help_panel="Filestore Options",
|
|
99
|
+
),
|
|
100
|
+
] = None,
|
|
101
|
+
pg_container: Annotated[
|
|
102
|
+
str,
|
|
103
|
+
typer.Option(
|
|
104
|
+
help="Postgres Container Name. (Optional)",
|
|
105
|
+
rich_help_panel="Database Options",
|
|
106
|
+
),
|
|
107
|
+
] = None,
|
|
108
|
+
pg_db_user=CLI.database.db_user,
|
|
109
|
+
pg_db_name=CLI.database.db_name,
|
|
110
|
+
):
|
|
111
|
+
"""Pull filestore folder and Database Dump and save to TARGET_FOLDER"""
|
|
112
|
+
self.ssh_hostname = ssh_hostname
|
|
113
|
+
self.ssh_user = ssh_user
|
|
114
|
+
filestore_target = target_folder / "odoo_filestore"
|
|
115
|
+
sql_target = target_folder / "odoo.dump"
|
|
116
|
+
|
|
117
|
+
if filestore_folder:
|
|
118
|
+
volume_path = filestore_folder
|
|
119
|
+
elif filestore_volume:
|
|
120
|
+
volume_path = self.get_docker_volume_path(filestore_volume)
|
|
121
|
+
else:
|
|
122
|
+
LOGGER.error("You need to either Supply a filestore-folder or a filestore-volume")
|
|
123
|
+
raise typer.Exit(1)
|
|
124
|
+
|
|
125
|
+
if not typer_ask_overwrite_path([filestore_target, sql_target]):
|
|
126
|
+
raise typer.Exit(2)
|
|
127
|
+
|
|
128
|
+
filestore_target.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
self.rsync_filestore(volume_path, filestore_target)
|
|
130
|
+
|
|
131
|
+
self.pg_container = pg_container
|
|
132
|
+
self.pg_user = pg_db_user
|
|
133
|
+
self.download_db_dump(pg_db_name, sql_target)
|
|
134
|
+
LOGGER.info("Done Pulling Odoo Instance")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..db.connection import DBConnection
|
|
6
|
+
|
|
7
|
+
LOGGER = getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def call_rsync(source_folder: Path, target_folder: Path, rsync_delete: bool = True):
|
|
11
|
+
"""Copy Folder to anohter using Rsync"""
|
|
12
|
+
if not source_folder.exists() and not source_folder.glob("*"):
|
|
13
|
+
raise FileNotFoundError("Cannot find filestore Backup @ %s" % source_folder)
|
|
14
|
+
args = ["rsync", "-a", "--no-perms", "--no-owner", "--no-group", "--info=progress2"]
|
|
15
|
+
if rsync_delete:
|
|
16
|
+
args += ["--delete"]
|
|
17
|
+
args += [f"{source_folder}/", f"{target_folder}/"]
|
|
18
|
+
command = " ".join(map(str, args))
|
|
19
|
+
LOGGER.info("Rysnc filestore to: %s", source_folder)
|
|
20
|
+
LOGGER.debug("Running: %s", command)
|
|
21
|
+
return subprocess.run(command, shell=True).returncode
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def drop_db(connection: DBConnection, db_name: str):
|
|
25
|
+
"""Drop a DB in postgres"""
|
|
26
|
+
with connection.connect() as cur:
|
|
27
|
+
LOGGER.info("Dropping DB: %s", db_name)
|
|
28
|
+
cur.execute("DROP DATABASE IF EXISTS %s" % db_name)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def create_db(connection: DBConnection, db_name: str):
|
|
32
|
+
"""Create DB In Postgres"""
|
|
33
|
+
with connection.connect() as cur:
|
|
34
|
+
LOGGER.info("Creating DB: %s", db_name)
|
|
35
|
+
cur.execute("CREATE DATABASE %s" % db_name)
|