chore: give cli invocations of dbtools a dedicated interface

This commit is contained in:
David Arnold 2023-04-06 20:47:20 -05:00 committed by David Arnold
parent cfbe6da959
commit 40a2daf84c
No known key found for this signature in database
GPG key ID: AB15A6AF1101390D
6 changed files with 183 additions and 158 deletions

View file

@ -2,17 +2,16 @@ import json
import os
import subprocess
import sys
from shutil import which
import click
import frappe
from frappe import _
from frappe.commands import get_site, pass_context
from frappe.coverage import CodeCoverage
from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import cint, update_progress_bar
find_executable = which # backwards compatibility
EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
@ -465,19 +464,11 @@ def database(context, extra_args):
Enter into the Database console for given site.
"""
site = get_site(context)
if not site:
raise SiteNotSpecifiedError
frappe.init(site=site)
if frappe.conf.db_type == "mariadb":
_mariadb(extra_args=extra_args)
elif frappe.conf.db_type == "postgres":
_psql(extra_args=extra_args)
_enter_console(extra_args=extra_args)
@click.command(
"mariadb",
context_settings=EXTRA_ARGS_CTX,
)
@click.command("mariadb", context_settings=EXTRA_ARGS_CTX)
@click.argument("extra_args", nargs=-1)
@pass_context
def mariadb(context, extra_args):
@ -485,10 +476,9 @@ def mariadb(context, extra_args):
Enter into mariadb console for a given site.
"""
site = get_site(context)
if not site:
raise SiteNotSpecifiedError
frappe.init(site=site)
_mariadb(extra_args=extra_args)
frappe.conf.db_type = "mariadb"
_enter_console(extra_args=extra_args)
@click.command("postgres", context_settings=EXTRA_ARGS_CTX)
@ -500,42 +490,27 @@ def postgres(context, extra_args):
"""
site = get_site(context)
frappe.init(site=site)
_psql(extra_args=extra_args)
frappe.conf.db_type = "postgres"
_enter_console(extra_args=extra_args)
def _mariadb(extra_args=None):
mariadb = which("mariadb") or which("mysql")
command = [
mariadb,
"--port",
str(frappe.conf.db_port),
"-u",
frappe.conf.db_name,
f"-p{frappe.conf.db_password}",
frappe.conf.db_name,
"-h",
frappe.conf.db_host,
"--pager=less -SFX",
"--safe-updates",
"-A",
]
if extra_args:
command += list(extra_args)
os.execv(mariadb, command)
def _enter_console(extra_args=None):
from frappe.database import get_command
def _psql(extra_args=None):
psql = which("psql")
host = frappe.conf.db_host
port = frappe.conf.db_port
env = os.environ.copy()
env["PGPASSWORD"] = frappe.conf.db_password
conn_string = f"postgresql://{frappe.conf.db_name}@{host}:{port}/{frappe.conf.db_name}"
psql_cmd = [psql, conn_string]
if extra_args:
psql_cmd = psql_cmd + list(extra_args)
subprocess.run(psql_cmd, check=True, env=env)
bin, args, bin_name = get_command(
host=frappe.conf.db_host,
port=frappe.conf.db_port,
user=frappe.conf.db_name,
password=frappe.conf.db_password,
db_name=frappe.conf.db_name,
extra=list(extra_args) if extra_args else [],
)
if not bin:
frappe.throw(
_("{} not found in PATH! This is required to access the console.").format(bin_name),
exc=frappe.ExecutableNotFound,
)
os.execv(bin, [bin] + args)
@click.command("jupyter")

View file

@ -3,6 +3,7 @@
# Database Module
# --------------------
from shutil import which
from frappe.database.database import savepoint
@ -50,3 +51,75 @@ def get_db(host=None, user=None, password=None, port=None):
import frappe.database.mariadb.database
return frappe.database.mariadb.database.MariaDBDatabase(host, user, password, port=port)
def get_command(
host=None, port=None, user=None, password=None, db_name=None, extra=None, dump=False
):
import frappe
if frappe.conf.db_type == "postgres":
if dump:
bin, bin_name = which("pg_dump"), "pg_dump"
else:
bin, bin_name = which("psql"), "psql"
host = frappe.utils.esc(host, "$ ")
user = frappe.utils.esc(user, "$ ")
db_name = frappe.utils.esc(db_name, "$ ")
conn_string = str
if password:
password = frappe.utils.esc(password, "$ ")
conn_string = f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
else:
conn_string = f"postgresql://{user}@{host}:{port}/{db_name}"
command = [conn_string]
if extra:
command.extend(extra)
else:
if dump:
bin, bin_name = which("mariadb-dump") or which("mysqldump"), "mariadb-dump"
else:
bin, bin_name = which("mariadb") or which("mysql"), "mariadb"
host = frappe.utils.esc(host, "$ ")
user = frappe.utils.esc(user, "$ ")
db_name = frappe.utils.esc(db_name, "$ ")
command = [
f"--user={user}",
f"--host={host}",
f"--port={port}",
]
if password:
password = frappe.utils.esc(password, "$ ")
command.append(f"--password={password}")
if dump:
command.extend(
[
"--single-transaction",
"--quick",
"--lock-tables=false",
]
)
else:
command.extend(
[
"--pager='less -SFX'",
"--safe-updates",
"--no-auto-rehash",
]
)
command.append(db_name)
if extra:
command.extend(extra)
return bin, command, bin_name

View file

@ -1,4 +1,5 @@
import frappe
from frappe import _
class DbManager:
@ -49,37 +50,37 @@ class DbManager:
return self.db.sql("SHOW DATABASES", pluck=True)
@staticmethod
def restore_database(target, source, user, password):
import os
def restore_database(verbose, target, source, user, password):
from shutil import which
from frappe.utils import make_esc
from frappe.database import get_command
from frappe.utils import execute_in_shell
esc = make_esc("$ ")
pv = which("pv")
mariadb_cli = which("mariadb") or which("mysql")
command = []
if pv:
pipe = f"{pv} {source} |"
source = ""
else:
pipe = ""
source = f"< {source}"
if pipe:
command.extend([f"{pv}", f"{source}", "|"])
source = []
print("Restoring Database file...")
else:
source = ["<", f"{source}"]
command = "{pipe} {mariadb_cli} -u {user} -p{password} -h{host} -P{port} {target} {source}"
command = command.format(
pipe=pipe,
user=esc(user),
password=esc(password),
host=esc(frappe.conf.db_host),
target=esc(target),
source=source,
bin, args, bin_name = get_command(
host=frappe.conf.db_host,
port=frappe.conf.db_port,
mariadb_cli=mariadb_cli,
user=user,
password=password,
db_name=target,
)
os.system(command)
if not bin:
frappe.throw(
_("{} not found in PATH! This is required to restore the database.").format(bin_name),
exc=frappe.ExecutableNotFound,
)
command.append(bin)
command.extend(args)
command.extend(source)
execute_in_shell(" ".join(command), check_exit_code=True, verbose=verbose)
frappe.cache.delete_keys("") # Delete all keys associated with this site.

View file

@ -97,7 +97,9 @@ def import_db_from_sql(source_sql=None, verbose=False):
db_name = frappe.conf.db_name
if not source_sql:
source_sql = os.path.join(os.path.dirname(__file__), "framework_mariadb.sql")
DbManager(frappe.local.db).restore_database(db_name, source_sql, db_name, frappe.conf.db_password)
DbManager(frappe.local.db).restore_database(
verbose, db_name, source_sql, db_name, frappe.conf.db_password
)
if verbose:
print("Imported from database %s" % source_sql)

View file

@ -1,6 +1,7 @@
import os
import frappe
from frappe import _
def setup_database(force, source_sql=None, verbose=False):
@ -39,12 +40,9 @@ def bootstrap_database(db_name, verbose, source_sql=None):
def import_db_from_sql(source_sql=None, verbose=False):
from shutil import which
from subprocess import PIPE, run
# we can't pass psql password in arguments in postgresql as mysql. So
# set password connection parameter in environment variable
subprocess_env = os.environ.copy()
subprocess_env["PGPASSWORD"] = str(frappe.conf.db_password)
from frappe.database import get_command
from frappe.utils import execute_in_shell
# bootstrap db
if not source_sql:
@ -52,27 +50,33 @@ def import_db_from_sql(source_sql=None, verbose=False):
pv = which("pv")
_command = (
f"psql {frappe.conf.db_name} "
f"-h {frappe.conf.db_host} -p {str(frappe.conf.db_port)} "
f"-U {frappe.conf.db_name}"
)
command = []
if pv:
command = f"{pv} {source_sql} | " + _command
command.extend([f"{pv}", f"{source_sql}", "|"])
source = []
print("Restoring Database file...")
else:
command = _command + f" -f {source_sql}"
source = ["-f", f"{source_sql}"]
print("Restoring Database file...")
if verbose:
print(command)
bin, args, bin_name = get_command(
host=frappe.conf.db_host,
port=frappe.conf.db_port,
user=frappe.conf.db_name,
password=frappe.conf.db_password,
db_name=frappe.conf.db_name,
)
restore_proc = run(command, env=subprocess_env, shell=True, stdout=PIPE)
if verbose:
print(
f"\nSTDOUT by psql:\n{restore_proc.stdout.decode()}\nImported from Database File: {source_sql}"
if not bin:
frappe.throw(
_("{} not found in PATH! This is required to restore the database.").format(bin_name),
exc=frappe.ExecutableNotFound,
)
command.append(bin)
command.extend(args)
command.extend(source)
execute_in_shell(" ".join(command), check_exit_code=True, verbose=verbose)
frappe.cache.delete_keys("") # Delete all keys associated with this site.
def get_root_connection(root_login=None, root_password=None):

View file

@ -16,7 +16,7 @@ from cryptography.fernet import Fernet
# imports - module imports
import frappe
import frappe.utils
from frappe import conf
from frappe import _, conf
from frappe.utils import cint, get_file_size, get_url, now, now_datetime
# backup variable for backwards compatibility
@ -362,37 +362,14 @@ class BackupGenerator:
with open(site_config_backup_path, "w") as n, open(site_config_path) as c:
n.write(c.read())
def get_db_dump_exeuctable(self) -> str:
db_exc, exists = None, False
if self.db_type == "mariadb":
if mariadb_dump_path := which("mariadb-dump"):
exists = bool(mariadb_dump_path)
db_exc = "mariadb-dump"
else:
# Fallback to mysqldump if mariadb-dump is not available.
db_exc = "mysqldump"
exists = bool(which(db_exc))
elif self.db_type == "postgres":
db_exc = "pg_dump"
exists = bool(which(db_exc))
if not exists:
frappe.throw(
f"{db_exc} not found in PATH! This is required to take a backup.",
exc=frappe.ExecutableNotFound,
)
return db_exc
def take_dump(self):
import frappe.utils
from frappe.utils.change_log import get_app_branch
db_exc = self.get_db_dump_exeuctable()
gzip_exc = which("gzip")
if not gzip_exc:
frappe.throw(
"`gzip` not found in PATH! This is required to take a backup.", exc=frappe.ExecutableNotFound
_("gzip not found in PATH! This is required to take a backup."), exc=frappe.ExecutableNotFound
)
database_header_content = [
@ -400,11 +377,6 @@ class BackupGenerator:
"",
]
# escape reserved characters
args = frappe._dict(
[item[0], frappe.utils.esc(str(item[1]), "$ ")] for item in self.__dict__.copy().items()
)
if self.backup_includes:
backup_info = ("Backing Up Tables: ", ", ".join(self.backup_includes))
elif self.backup_excludes:
@ -423,54 +395,52 @@ class BackupGenerator:
generated_header = "\n".join(f"-- {x}" for x in database_header_content) + "\n"
with gzip.open(args.backup_path_db, "wt") as f:
with gzip.open(self.backup_path_db, "wt") as f:
f.write(generated_header)
if self.db_type == "postgres":
# Remember process of this shell and kill it if mysqldump exits w/ non-zero code
def wrap(cmd):
ret = ["self=$$;", "("]
ret.extend(cmd)
ret.extend(["||", "kill", "$self", ")", "|", gzip_exc, ">>", self.backup_path_db])
return ret
cmd = []
extra = []
if self.db_type == "mariadb":
if self.backup_includes:
args["include"] = " ".join([f"--table='public.\"{table}\"'" for table in self.backup_includes])
extra.extend([f"'{x}'" for x in self.backup_includes])
elif self.backup_excludes:
args["exclude"] = " ".join(
[f"--exclude-table-data='public.\"{table}\"'" for table in self.backup_excludes]
)
extra.extend([f"--ignore-table='{self.db_name}.{table}'" for table in self.backup_excludes])
cmd_string = (
"self=$$; "
"( {db_exc} postgres://{user}:{password}@{db_host}:{db_port}/{db_name}"
" {include} {exclude} || kill $self ) | {gzip} >> {backup_path_db}"
)
else:
elif self.db_type == "postgres":
if self.backup_includes:
args["include"] = " ".join([f"'{x}'" for x in self.backup_includes])
extra.extend([f"--table='public.\"{table}\"'" for table in self.backup_includes])
elif self.backup_excludes:
args["exclude"] = " ".join(
[f"--ignore-table='{self.db_name}.{table}'" for table in self.backup_excludes]
)
extra.extend([f"--exclude-table-data='public.\"{table}\"'" for table in self.backup_excludes])
cmd_string = (
# Remember process of this shell and kill it if mysqldump exits w/ non-zero code
"self=$$; "
" ( {db_exc} --single-transaction --quick --lock-tables=false -u {user}"
" -p{password} {db_name} -h {db_host} -P {db_port} {include} {exclude} || kill $self ) "
" | {gzip} >> {backup_path_db}"
)
from frappe.database import get_command
command = cmd_string.format(
user=args.user,
password=args.password,
db_exc=db_exc,
db_host=args.db_host,
db_port=args.db_port,
db_name=args.db_name,
backup_path_db=args.backup_path_db,
exclude=args.get("exclude", ""),
include=args.get("include", ""),
gzip=gzip_exc,
bin, args, bin_name = get_command(
host=self.db_host,
port=self.db_port,
user=self.user,
password=self.password,
db_name=self.db_name,
extra=extra,
dump=True,
)
if not bin:
frappe.throw(
_("{} not found in PATH! This is required to take a backup.").format(bin_name),
exc=frappe.ExecutableNotFound,
)
cmd.append(bin)
cmd.extend(args)
command = " ".join(wrap(cmd))
if self.verbose:
print(command.replace(args.password, "*" * 10) + "\n")
print(command.replace(frappe.utils.esc(self.password, "$ "), "*" * 10) + "\n")
frappe.utils.execute_in_shell(command, low_priority=True, check_exit_code=True)