| Current Path : /proc/21979/root/opt/dedrads/ |
| Current File : //proc/21979/root/opt/dedrads/cpanel_postgres_manager.py |
#!/usr/lib/rads/venv/bin/python3
"""
Install, reset, or logically upgrade PostgreSQL for cPanel on AlmaLinux 8/9.
When a PostgreSQL version is passed, the script resets/enables that dnf module
stream before running /scripts/installpostgres. When no version is passed, it
prompts for one before continuing.
"""
import argparse
from contextlib import contextmanager
import datetime as _dt
import logging
import os
import re
import shutil
import socket
import subprocess
import sys
from pathlib import Path
from typing import Dict, Iterator, List, Optional, Sequence, Tuple
DATA_DIR = Path("/var/lib/pgsql/data")
PG_HBA = DATA_DIR / "pg_hba.conf"
PGSQL_BASE_DIR = DATA_DIR.parent
POSTGRES_SERVICE = "postgresql"
CPANEL_INSTALLPOSTGRES = Path("/scripts/installpostgres")
BACKUP_DIR = Path("/root/postgresql-backups")
POSTGRES_USER_COMMAND = ["runuser", "-u", "postgres", "--"]
POSTGRESQL_PACKAGE_QUERY_PATTERNS = [
"postgresql",
"postgresql-server",
"postgresql-libs",
"postgresql-private-libs",
"postgresql[0-9]*",
]
YUM_REPO_DIR = Path("/etc/yum.repos.d")
EXTERNAL_POSTGRES_REPO_PATTERNS = (
"pgdg",
"postgresql.org",
"yum.postgresql.org",
"ftp.postgresql.org",
)
LOGGER = logging.getLogger(__name__)
class Fatal(RuntimeError):
pass
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Install, reset, or logically upgrade PostgreSQL for cPanel."
)
parser.add_argument(
"version",
nargs="?",
type=int,
help=(
"PostgreSQL module stream to enable before install. Omit to display "
"available streams and choose interactively."
),
)
parser.add_argument(
"--upgrade",
action="store_true",
help="Backup the current cluster, reinstall/init the target version, then restore.",
)
parser.add_argument(
"--allow-logical-downgrade",
action="store_true",
help=(
"Allow --upgrade to attempt a pg_dumpall restore into an older major "
"version. This is best-effort and may fail if the dump uses newer "
"PostgreSQL features."
),
)
parser.add_argument(
"--reset-data",
action="store_true",
help="Destroy /var/lib/pgsql/data contents and initialize a fresh cluster.",
)
parser.add_argument(
"--backup-dir",
default=str(BACKUP_DIR),
help=f"Directory for pg_dumpall backups. Default: {BACKUP_DIR}",
)
parser.add_argument(
"--skip-backup",
action="store_true",
help="Allow destructive reset without creating a pg_dumpall backup.",
)
parser.add_argument(
"--yes",
action="store_true",
help="Answer yes to required prompts. Destructive prompts still require --i-understand-data-loss.",
)
parser.add_argument(
"--i-understand-data-loss",
action="store_true",
help="Required with --yes for destructive data directory reset.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Log commands without running them.",
)
return parser.parse_args()
def run(
command: Sequence[str],
*,
dry_run: bool = False,
check: bool = True,
capture: bool = False,
env: Optional[Dict[str, str]] = None,
) -> subprocess.CompletedProcess:
printable = " ".join(str(part) for part in command)
LOGGER.info("+ %s", printable)
if dry_run:
return subprocess.CompletedProcess(command, 0, "", "")
return subprocess.run(
list(command),
check=check,
text=True,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.PIPE if capture else None,
env=env,
)
def require_root() -> None:
if os.geteuid() != 0:
raise Fatal("Run this script as root.")
def command_exists(name: str) -> bool:
return shutil.which(name) is not None
def service_is_active(dry_run: bool) -> bool:
if dry_run:
return False
result = run(["systemctl", "is-active", "--quiet", POSTGRES_SERVICE], check=False)
return result.returncode == 0
def existing_data_dir_has_cluster() -> bool:
return (DATA_DIR / "PG_VERSION").exists()
def existing_cluster_version() -> Optional[str]:
pg_version = DATA_DIR / "PG_VERSION"
if pg_version.exists():
return pg_version.read_text().strip()
if command_exists("postgres"):
result = run(["postgres", "--version"], check=False, capture=True)
match = re.search(r"(\d+)(?:\.\d+)?", result.stdout or "")
if match:
return match.group(1)
return None
def installed_postgres_server_version() -> Optional[str]:
if not command_exists("postgres"):
return None
result = run(["postgres", "--version"], check=False, capture=True)
match = re.search(r"(\d+)(?:\.\d+)?", result.stdout or "")
if match:
return match.group(1)
return None
def require_installed_server_matches_target(version: Optional[int]) -> str:
installed = installed_postgres_server_version()
if installed is None:
raise Fatal("postgres executable was not found after package installation.")
if version is not None and installed != str(version):
raise Fatal(
f"Installed PostgreSQL server is version {installed}, but target module "
f"stream is {version}. Refusing to initialize, start, restore, or run "
"cPanel setup against the data directory. Check the enabled DNF module "
"stream and repository state."
)
return installed
def require_data_dir_matches_installed_server() -> None:
if not existing_data_dir_has_cluster():
return
cluster = existing_cluster_version()
installed = installed_postgres_server_version()
if installed is None or cluster is None:
return
if cluster != installed:
raise Fatal(
f"Existing PostgreSQL data directory is version {cluster}, but the "
f"installed PostgreSQL server is version {installed}. Refusing to run "
f"cPanel setup or start {POSTGRES_SERVICE} with an incompatible cluster. "
"Use --upgrade for a logical dump/restore or --reset-data for a fresh "
"cluster."
)
def prompt(message: str, *, default_no: bool = True, assume_yes: bool = False) -> bool:
if assume_yes:
LOGGER.info("%s yes", message)
return True
suffix = " [y/N]: " if default_no else " [Y/n]: "
answer = input(message + suffix).strip().lower()
if not answer:
return not default_no
return answer in {"y", "yes"}
def available_postgresql_versions() -> List[Tuple[int, str]]:
result = run(["dnf", "module", "list", "postgresql"], check=False, capture=True)
versions: List[Tuple[int, str]] = []
seen = set()
for line in (result.stdout or "").splitlines():
parts = line.split()
if len(parts) < 2 or parts[0] != "postgresql":
continue
if not parts[1].isdigit():
continue
version = int(parts[1])
if version in seen:
continue
seen.add(version)
flags = parts[2] if len(parts) > 2 and parts[2].startswith("[") else ""
versions.append((version, flags))
return versions
def version_flag_labels(flags: str) -> str:
labels = []
if "e" in flags:
labels.append("enabled")
if "d" in flags:
labels.append("default")
if "i" in flags:
labels.append("installed")
if "x" in flags:
labels.append("disabled")
return ", ".join(labels)
def log_available_postgresql_versions(versions: Sequence[Tuple[int, str]]) -> None:
if not versions:
LOGGER.info("No PostgreSQL module streams were found in DNF output.")
return
LOGGER.info("Available PostgreSQL module streams:")
for version, flags in versions:
labels = version_flag_labels(flags)
suffix = f" ({labels})" if labels else ""
LOGGER.info(" %s%s", version, suffix)
def prompt_for_version() -> Optional[int]:
versions = available_postgresql_versions()
log_available_postgresql_versions(versions)
answer = input(
"PostgreSQL version to install [press Enter for repository default]: "
).strip()
if not answer:
return None
if not answer.isdigit():
raise Fatal(f"Invalid PostgreSQL version: {answer}")
version = int(answer)
available = {available_version for available_version, _ in versions}
if available and version not in available:
available_list = ", ".join(str(available_version) for available_version in sorted(available))
raise Fatal(
f"PostgreSQL version {version} is not in the available module streams: "
f"{available_list}"
)
return version
def validate_selected_version(version: Optional[int]) -> None:
if version is None:
return
versions = available_postgresql_versions()
available = {available_version for available_version, _ in versions}
if available and version not in available:
available_list = ", ".join(str(available_version) for available_version in sorted(available))
raise Fatal(
f"PostgreSQL version {version} is not in the available module streams: "
f"{available_list}"
)
def require_cpanel() -> None:
if not CPANEL_INSTALLPOSTGRES.exists():
raise Fatal(f"{CPANEL_INSTALLPOSTGRES} was not found. This does not look like cPanel.")
def ensure_backup_directory(path: Path, dry_run: bool) -> None:
if dry_run:
LOGGER.info("+ mkdir -p %s", path)
return
path.mkdir(mode=0o700, parents=True, exist_ok=True)
def backup_cluster(backup_dir: Path, dry_run: bool) -> Optional[Path]:
ensure_backup_directory(backup_dir, dry_run)
stamp = _dt.datetime.now().strftime("%Y%m%d-%H%M%S")
backup_file = backup_dir / f"postgresql-all-{stamp}.sql"
if not command_exists("pg_dumpall"):
raise Fatal("pg_dumpall was not found; cannot create a PostgreSQL backup.")
if not service_is_active(dry_run):
run(["systemctl", "start", POSTGRES_SERVICE], dry_run=dry_run)
LOGGER.info("Creating logical backup at %s", backup_file)
with local_postgres_peer_auth(dry_run):
with backup_file.open("w") if not dry_run else open(os.devnull, "w") as output:
LOGGER.info("+ runuser -u postgres -- pg_dumpall")
if not dry_run:
subprocess.run(
[*POSTGRES_USER_COMMAND, "pg_dumpall"],
check=True,
cwd="/tmp",
stdout=output,
text=True,
)
return backup_file
def stop_postgresql(dry_run: bool) -> None:
run(["systemctl", "stop", POSTGRES_SERVICE], dry_run=dry_run, check=False)
def reload_postgresql(dry_run: bool) -> None:
run(["systemctl", "reload", POSTGRES_SERVICE], dry_run=dry_run, check=False)
@contextmanager
def local_postgres_peer_auth(dry_run: bool) -> Iterator[None]:
rule = "local all postgres peer\n"
if dry_run:
LOGGER.info("+ temporarily prepend to %s: %s", PG_HBA, rule.strip())
reload_postgresql(dry_run)
try:
yield
finally:
LOGGER.info("+ restore original %s", PG_HBA)
reload_postgresql(dry_run)
return
if not PG_HBA.exists():
yield
return
original = PG_HBA.read_text()
if original.startswith(rule):
yield
return
PG_HBA.write_text(rule + original)
reload_postgresql(dry_run)
try:
yield
finally:
PG_HBA.write_text(original)
reload_postgresql(dry_run)
def remove_external_postgres_libraries(dry_run: bool) -> None:
if dry_run:
LOGGER.info("+ rpm -q libpq5")
LOGGER.info("+ rpm -e --nodeps libpq5 # only if installed")
LOGGER.info("+ rpm -qa 'postgresql*-libs'")
LOGGER.info("+ rpm -e --nodeps postgresqlNN-libs # only for PGDG versioned libs")
return
installed = run(["rpm", "-q", "libpq5"], check=False, capture=True)
if installed.returncode == 0:
package = installed.stdout.strip().splitlines()[0]
LOGGER.info("Removing installed %s; it obsoletes AlmaLinux libpq.", package)
run(["rpm", "-e", "--nodeps", "libpq5"])
versioned_libs = run(["rpm", "-qa", "postgresql*-libs"], check=False, capture=True)
for package in (versioned_libs.stdout or "").splitlines():
if re.match(r"^postgresql\d+-libs-", package):
LOGGER.info(
"Removing installed %s; it is from an external PostgreSQL repo.",
package,
)
run(["rpm", "-e", "--nodeps", package])
def installed_postgresql_packages(dry_run: bool) -> List[str]:
if dry_run:
LOGGER.info(
"+ rpm -qa postgresql postgresql-server postgresql-libs "
"postgresql-private-libs 'postgresql[0-9]*'"
)
return ["postgresql", "postgresql-server", "postgresql-libs"]
packages: List[str] = []
seen = set()
for pattern in POSTGRESQL_PACKAGE_QUERY_PATTERNS:
result = run(["rpm", "-qa", pattern], check=False, capture=True)
for package in (result.stdout or "").splitlines():
if package and package not in seen:
seen.add(package)
packages.append(package)
return packages
def remove_installed_postgresql_packages(dry_run: bool) -> None:
packages = installed_postgresql_packages(dry_run)
if not packages:
LOGGER.info("No installed PostgreSQL RPM packages found to remove.")
return
LOGGER.info(
"Removing installed PostgreSQL RPM packages before installing the selected "
"module stream."
)
stop_postgresql(dry_run)
run(["rpm", "-e", "--nodeps", *packages], dry_run=dry_run)
def repo_section_matches(lines: Sequence[str]) -> bool:
text = "\n".join(lines).lower()
return any(pattern in text for pattern in EXTERNAL_POSTGRES_REPO_PATTERNS)
def disable_matching_repo_sections(text: str) -> Tuple[str, List[str]]:
lines = text.splitlines(keepends=True)
output: List[str] = []
changed_sections: List[str] = []
section: List[str] = []
section_name: Optional[str] = None
def flush() -> None:
nonlocal section, section_name
if not section:
return
if section_name is None or not repo_section_matches(section):
output.extend(section)
section = []
section_name = None
return
saw_enabled = False
changed = False
for index, line in enumerate(section):
if re.match(r"^\s*enabled\s*=", line):
saw_enabled = True
if not re.match(r"^\s*enabled\s*=\s*0\s*(?:[#;].*)?$", line):
newline = "\n" if line.endswith("\n") else ""
section[index] = "enabled=0" + newline
changed = True
break
if not saw_enabled:
insert_at = 1 if section and section[0].lstrip().startswith("[") else len(section)
section.insert(insert_at, "enabled=0\n")
changed = True
if changed:
changed_sections.append(section_name)
output.extend(section)
section = []
section_name = None
for line in lines:
match = re.match(r"^\s*\[([^\]]+)\]\s*$", line)
if match:
flush()
section = [line]
section_name = match.group(1)
else:
section.append(line)
flush()
return "".join(output), changed_sections
@contextmanager
def external_postgres_repos_disabled(dry_run: bool) -> Iterator[None]:
if dry_run:
LOGGER.info("+ temporarily disable external PostgreSQL repos during cPanel install")
yield
return
originals: Dict[Path, str] = {}
changed: List[str] = []
for repo_file in sorted(YUM_REPO_DIR.glob("*.repo")):
original = repo_file.read_text()
updated, sections = disable_matching_repo_sections(original)
if sections:
originals[repo_file] = original
repo_file.write_text(updated)
changed.extend(f"{repo_file.name}:{section}" for section in sections)
if changed:
LOGGER.info(
"Temporarily disabled external PostgreSQL repos: %s", ", ".join(changed)
)
try:
yield
finally:
for repo_file, original in originals.items():
repo_file.write_text(original)
if changed:
LOGGER.info("Restored external PostgreSQL repo configuration.")
def installpostgres_with_recovery(dry_run: bool, assume_yes: bool) -> None:
if not dry_run:
require_data_dir_matches_installed_server()
command = [str(CPANEL_INSTALLPOSTGRES)]
if assume_yes:
command.append("--yes")
result = run(command, dry_run=dry_run, check=False)
if result.returncode == 0:
if not dry_run:
require_data_dir_matches_installed_server()
return
if existing_data_dir_has_cluster():
if not dry_run:
require_data_dir_matches_installed_server()
raise subprocess.CalledProcessError(result.returncode, result.args)
if not command_exists("postgresql-setup"):
raise subprocess.CalledProcessError(result.returncode, result.args)
LOGGER.info(
"cPanel installpostgres failed before a PostgreSQL cluster existed; "
"initializing the cluster and rerunning cPanel configuration."
)
initdb_and_start(dry_run)
run(command, dry_run=dry_run)
if not dry_run:
require_data_dir_matches_installed_server()
def finalize_cpanel_postgresql(dry_run: bool, assume_yes: bool) -> None:
LOGGER.info("Finalizing PostgreSQL configuration for cPanel")
with external_postgres_repos_disabled(dry_run):
installpostgres_with_recovery(dry_run, assume_yes)
def ensure_cluster_running(dry_run: bool) -> None:
if not existing_data_dir_has_cluster():
initdb_and_start(dry_run)
return
if not dry_run:
require_data_dir_matches_installed_server()
run(["systemctl", "enable", "--now", POSTGRES_SERVICE], dry_run=dry_run)
run(["systemctl", "status", "--no-pager", POSTGRES_SERVICE], dry_run=dry_run, check=False)
def reset_module_and_install(
version: Optional[int], dry_run: bool, assume_yes: bool
) -> None:
if version is not None:
remove_installed_postgresql_packages(dry_run)
run(["dnf", "-y", "module", "reset", "postgresql"], dry_run=dry_run)
if version is not None:
run(["dnf", "-y", "module", "enable", f"postgresql:{version}"], dry_run=dry_run)
else:
LOGGER.info("No PostgreSQL module stream selected; using the repository default.")
remove_external_postgres_libraries(dry_run)
with external_postgres_repos_disabled(dry_run):
installpostgres_with_recovery(dry_run, assume_yes)
if not dry_run:
installed = require_installed_server_matches_target(version)
LOGGER.info("Installed PostgreSQL server version: %s", installed)
def clear_data_dir(dry_run: bool) -> None:
if not DATA_DIR.exists():
return
LOGGER.info("Clearing %s contents", DATA_DIR)
if dry_run:
LOGGER.info("+ remove all entries under %s", DATA_DIR)
return
for child in DATA_DIR.iterdir():
if child.is_dir() and not child.is_symlink():
shutil.rmtree(child)
else:
child.unlink()
def move_data_dir_aside(reason: str, dry_run: bool) -> Optional[Path]:
if not DATA_DIR.exists():
return None
stamp = _dt.datetime.now().strftime("%Y%m%d-%H%M%S")
destination = PGSQL_BASE_DIR / f"data.{reason}.{stamp}"
LOGGER.info("Moving existing PostgreSQL data directory to %s", destination)
if dry_run:
LOGGER.info("+ mv %s %s", DATA_DIR, destination)
return destination
DATA_DIR.rename(destination)
return destination
def initdb_and_start(dry_run: bool) -> None:
ensure_pgsql_base_dir(dry_run)
run(["postgresql-setup", "--initdb"], dry_run=dry_run)
if not dry_run:
require_data_dir_matches_installed_server()
run(["systemctl", "enable", "--now", POSTGRES_SERVICE], dry_run=dry_run)
run(["systemctl", "status", "--no-pager", POSTGRES_SERVICE], dry_run=dry_run, check=False)
def ensure_pgsql_base_dir(dry_run: bool) -> None:
if dry_run:
LOGGER.info("+ mkdir -p %s", PGSQL_BASE_DIR)
LOGGER.info("+ chown postgres:postgres %s", PGSQL_BASE_DIR)
LOGGER.info("+ chmod 700 %s", PGSQL_BASE_DIR)
return
PGSQL_BASE_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
shutil.chown(PGSQL_BASE_DIR, user="postgres", group="postgres")
PGSQL_BASE_DIR.chmod(0o700)
def restore_backup(backup_file: Path, dry_run: bool) -> None:
if not backup_file.exists() and not dry_run:
raise Fatal(f"Backup file does not exist: {backup_file}")
LOGGER.info("Restoring %s", backup_file)
LOGGER.info("+ runuser -u postgres -- psql < %s", backup_file)
with local_postgres_peer_auth(dry_run):
if not dry_run:
with backup_file.open() as input_file:
subprocess.run(
[*POSTGRES_USER_COMMAND, "psql"],
check=True,
cwd="/tmp",
stdin=input_file,
)
def destructive_confirmation(args: argparse.Namespace, reason: str) -> None:
if args.yes and args.i_understand_data_loss:
return
LOGGER.warning("WARNING: This operation destroys the current PostgreSQL data directory.")
LOGGER.warning("Reason: %s", reason)
LOGGER.warning("Target directory: %s", DATA_DIR)
if not prompt("Have you created and verified a backup?", assume_yes=False):
raise Fatal("Cancelled. Create a backup before continuing.")
phrase = input('Type "DESTROY POSTGRES DATA" to continue: ').strip()
if phrase != "DESTROY POSTGRES DATA":
raise Fatal("Cancelled.")
def logical_upgrade_confirmation(args: argparse.Namespace, backup_file: Path) -> None:
if args.yes:
LOGGER.info(
"Proceeding with logical upgrade using backup %s. The existing data "
"directory will be moved aside before the target cluster is initialized.",
backup_file,
)
return
LOGGER.warning("WARNING: Logical upgrade requires a clean target data directory.")
LOGGER.warning("Backup that will be restored: %s", backup_file)
LOGGER.warning(
"The current PostgreSQL data directory will be moved aside, not deleted: %s",
DATA_DIR,
)
if not prompt("Proceed with the logical upgrade?"):
raise Fatal("Cancelled.")
def maybe_backup_before_destructive(args: argparse.Namespace) -> Optional[Path]:
backup_dir = Path(args.backup_dir)
has_cluster = existing_data_dir_has_cluster()
if not has_cluster:
return None
if args.skip_backup:
if not prompt(
"You used --skip-backup. Continue without a pg_dumpall backup?",
assume_yes=args.yes and args.i_understand_data_loss,
):
raise Fatal("Cancelled.")
return None
if prompt("Create a pg_dumpall backup before changing PostgreSQL?", assume_yes=args.yes):
return backup_cluster(backup_dir, args.dry_run)
if not prompt("Continue without creating a new backup?", assume_yes=False):
raise Fatal("Cancelled.")
return None
def install_or_reset(args: argparse.Namespace, version: Optional[int]) -> None:
has_cluster = existing_data_dir_has_cluster()
current = existing_cluster_version() if has_cluster else None
if current:
LOGGER.info("Detected existing PostgreSQL cluster version: %s", current)
if (
has_cluster
and version is not None
and current is not None
and current != str(version)
and not args.reset_data
):
raise Fatal(
f"Existing PostgreSQL data directory is version {current}, but target "
f"module stream is {version}. Use --upgrade for a logical dump/restore "
"or --reset-data if you intend to destroy and recreate the cluster."
)
if args.reset_data and has_cluster:
maybe_backup_before_destructive(args)
destructive_confirmation(args, "Fresh initialization requested with --reset-data.")
stop_postgresql(args.dry_run)
clear_data_dir(args.dry_run)
reset_module_and_install(version, args.dry_run, args.yes)
if args.reset_data:
if args.dry_run or not existing_data_dir_has_cluster():
initdb_and_start(args.dry_run)
finalize_cpanel_postgresql(args.dry_run, args.yes)
elif not existing_data_dir_has_cluster():
initdb_and_start(args.dry_run)
finalize_cpanel_postgresql(args.dry_run, args.yes)
else:
if not args.dry_run:
require_data_dir_matches_installed_server()
run(["systemctl", "enable", "--now", POSTGRES_SERVICE], dry_run=args.dry_run)
run(["systemctl", "status", "--no-pager", POSTGRES_SERVICE], dry_run=args.dry_run, check=False)
def logical_upgrade(args: argparse.Namespace, version: Optional[int]) -> None:
if not existing_data_dir_has_cluster():
LOGGER.info("No existing PostgreSQL data directory found; running a normal install.")
install_or_reset(args, version)
return
current = existing_cluster_version()
LOGGER.info("Detected existing PostgreSQL cluster version: %s", current or "unknown")
target = str(version) if version is not None else "repository default"
LOGGER.info("Target PostgreSQL version: %s", target)
if (
current is not None
and version is not None
and int(current) > version
and not args.allow_logical_downgrade
):
raise Fatal(
f"Logical downgrade from PostgreSQL {current} to {version} is not supported. "
"Use --allow-logical-downgrade to attempt a best-effort pg_dumpall restore, "
"or --reset-data with a verified backup if you need to recreate the cluster."
)
if current is not None and version is not None and int(current) > version:
LOGGER.warning(
f"WARNING: Attempting logical downgrade from PostgreSQL {current} to {version}. "
"The restore can fail if the dump contains newer PostgreSQL features."
)
backup_file = maybe_backup_before_destructive(args)
if backup_file is None:
raise Fatal("Upgrade requires a pg_dumpall backup so the cluster can be restored.")
logical_upgrade_confirmation(args, backup_file)
moved_data_dir: Optional[Path] = None
try:
stop_postgresql(args.dry_run)
moved_data_dir = move_data_dir_aside("pre-upgrade", args.dry_run)
reset_module_and_install(version, args.dry_run, args.yes)
ensure_cluster_running(args.dry_run)
LOGGER.info("Restoring logical backup into the new PostgreSQL cluster.")
restore_backup(backup_file, args.dry_run)
run(["systemctl", "restart", POSTGRES_SERVICE], dry_run=args.dry_run)
run(["systemctl", "status", "--no-pager", POSTGRES_SERVICE], dry_run=args.dry_run, check=False)
finalize_cpanel_postgresql(args.dry_run, args.yes)
except Exception:
LOGGER.error("Upgrade did not complete.")
LOGGER.error("Logical backup: %s", backup_file)
if moved_data_dir is not None:
LOGGER.error("Previous data directory: %s", moved_data_dir)
LOGGER.error("Current data directory: %s", DATA_DIR)
raise
def main() -> int:
logging.basicConfig(level=logging.INFO, format="%(message)s")
args = parse_args()
try:
require_root()
require_cpanel()
version = args.version if args.version is not None else prompt_for_version()
validate_selected_version(version)
if args.upgrade:
logical_upgrade(args, version)
else:
install_or_reset(args, version)
LOGGER.info("Done on %s.", socket.gethostname())
return 0
except KeyboardInterrupt:
LOGGER.info("")
LOGGER.info("Cancelled.")
return 130
except subprocess.CalledProcessError as exc:
LOGGER.error(
"Command failed with exit code %s: %s",
exc.returncode,
" ".join(exc.cmd),
)
if exc.stdout:
LOGGER.error("%s", exc.stdout.rstrip())
if exc.stderr:
LOGGER.error("%s", exc.stderr.rstrip())
return exc.returncode or 1
except Fatal as exc:
LOGGER.error("ERROR: %s", exc)
return 1
if __name__ == "__main__":
sys.exit(main())