chore: Enhance database connection handling and configuration in installer.py

This commit is contained in:
SINeV 2025-08-27 20:23:17 +02:00
parent 7fc66c1159
commit 9031032360
4 changed files with 234 additions and 47 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -1,6 +0,0 @@
[
{
"url": "https://github.com/frappe/erpnext.git",
"branch": "version-15"
}
]

View file

@ -2,6 +2,23 @@
import argparse import argparse
import os import os
import subprocess import subprocess
import time
import socket
from typing import Tuple, Optional
def load_env_file(env_file_path: str = ".env") -> None:
"""Load environment variables from a .env file if it exists."""
if os.path.exists(env_file_path):
cprint(f"Loading environment variables from {env_file_path}", level=3)
with open(env_file_path, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
# Only set if not already in environment
if key not in os.environ:
os.environ[key] = value
def cprint(*args, level: int = 1): def cprint(*args, level: int = 1):
@ -26,11 +43,101 @@ def cprint(*args, level: int = 1):
print(CYLW, message, reset) # noqa: T001, T201 print(CYLW, message, reset) # noqa: T001, T201
def check_database_connection(host, port, timeout=30):
"""
Check if database service is reachable
"""
cprint(f"Checking database connection to {host}:{port}...", level=3)
start_time = time.time()
while time.time() - start_time < timeout:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex((host, port))
sock.close()
if result == 0:
cprint(f"✓ Database is reachable at {host}:{port}", level=2)
return True
except Exception as e:
pass
cprint(f"→ Database not reachable. Waiting... ({int(time.time() - start_time)}s)", level=3)
time.sleep(2)
cprint(f"✗ Database connection timeout after {timeout}s", level=1)
return False
def normalize_db_type(db_type: str) -> str:
"""
Normalize database type to match Frappe's expected values.
Frappe expects 'postgres' not 'postgresql'.
"""
db_type = db_type.lower().strip()
if db_type in ["postgresql", "postgres", "pg"]:
return "postgres"
elif db_type in ["mariadb", "mysql"]:
return "mariadb"
else:
cprint(f"Warning: Unknown database type '{db_type}', defaulting to 'mariadb'", level=3)
return "mariadb"
def get_database_config(args) -> Tuple[str, int, str, str]:
"""
Get database configuration from args or environment variables.
Returns: (host, port, username, password)
"""
# Normalize database type to ensure compatibility with Frappe
normalized_db_type = normalize_db_type(args.db_type)
args.db_type = normalized_db_type
# Set defaults based on database type
if normalized_db_type == "postgres":
default_host = os.getenv("POSTGRES_HOST", "db")
default_port = int(os.getenv("POSTGRES_PORT", "5432"))
default_username = os.getenv("POSTGRES_USER", "postgres")
default_password = os.getenv("POSTGRES_PASSWORD", "123")
else: # mariadb/mysql
default_host = os.getenv("MYSQL_HOST", "db")
default_port = int(os.getenv("MYSQL_PORT", "3306"))
default_username = os.getenv("MYSQL_USER", "root")
default_password = os.getenv("MYSQL_ROOT_PASSWORD", "123")
# Use command line args if provided, otherwise use defaults
host = args.db_host or default_host
port = args.db_port or default_port
username = args.db_username or default_username
password = args.db_password or default_password
return host, port, username, password
def main(): def main():
# Load environment variables from .env file if it exists
load_env_file()
parser = get_args_parser() parser = get_args_parser()
args = parser.parse_args() args = parser.parse_args()
# Display configuration summary
cprint("=== Frappe Bench Setup Configuration ===", level=2)
db_host, db_port, db_username, _ = get_database_config(args)
cprint(f"Database Type: {args.db_type}", level=3)
cprint(f"Database Host: {db_host}:{db_port}", level=3)
cprint(f"Database User: {db_username}", level=3)
cprint(f"Site Name: {args.site_name}", level=3)
cprint(f"Bench Name: {args.bench_name}", level=3)
cprint("=========================================", level=2)
init_bench_if_not_exist(args) init_bench_if_not_exist(args)
create_site_in_bench(args) success = create_site_in_bench(args)
if not success:
cprint("Site creation failed. Please check the database connection and try again.", level=1)
exit(1)
def get_args_parser(): def get_args_parser():
@ -111,7 +218,42 @@ def get_args_parser():
action="store", action="store",
type=str, type=str,
help="Database type to use (e.g., mariadb or postgres)", help="Database type to use (e.g., mariadb or postgres)",
default="postgres", # Set your default database type here default="postgres", # Changed to postgres for PostgreSQL setup
)
parser.add_argument(
"--db-host",
action="store",
type=str,
help="Database host, default: db (for Docker) or localhost",
default=None,
)
parser.add_argument(
"--db-port",
action="store",
type=int,
help="Database port, default: 5432 for postgres, 3306 for mariadb",
default=None,
)
parser.add_argument(
"--db-username",
action="store",
type=str,
help="Database username, default: postgres for postgres, root for mariadb",
default=None,
)
parser.add_argument(
"--db-password",
action="store",
type=str,
help="Database password, default: 123",
default=None,
)
parser.add_argument(
"--db-name",
action="store",
type=str,
help="Database name for the site (optional)",
default=None,
) )
return parser return parser
@ -193,52 +335,100 @@ def init_bench_if_not_exist(args):
["bench", "set-config", "-gp", "developer_mode", "1"], ["bench", "set-config", "-gp", "developer_mode", "1"],
cwd=os.getcwd() + "/" + args.bench_name, cwd=os.getcwd() + "/" + args.bench_name,
) )
# Configure additional PostgreSQL-specific settings if needed
if args.db_type == "postgres":
configure_postgres_settings(args)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
cprint(e.output, level=1) cprint(e.output, level=1)
def configure_postgres_settings(args):
"""Configure PostgreSQL-specific settings for the bench."""
cprint("Configuring PostgreSQL-specific settings...", level=3)
# Get database configuration
db_host, db_port, db_username, db_password = get_database_config(args)
# Set PostgreSQL connection parameters in common_site_config.json
postgres_configs = [
("db_host", db_host),
("db_type", "postgres"),
]
# Add port configuration if non-standard
if db_port != 5432:
postgres_configs.append(("db_port", str(db_port)))
for config_key, config_value in postgres_configs:
cprint(f"Setting {config_key} to {config_value}", level=3)
subprocess.call(
["bench", "set-config", "-g", config_key, config_value],
cwd=os.getcwd() + "/" + args.bench_name,
)
def create_site_in_bench(args): def create_site_in_bench(args):
if "mariadb" == args.db_type: # Get database configuration
cprint("Set db_host", level=3) db_host, db_port, db_username, db_password = get_database_config(args)
subprocess.call(
["bench", "set-config", "-g", "db_host", "mariadb"], # Check database connectivity before proceeding
cwd=os.getcwd() + "/" + args.bench_name, if not check_database_connection(db_host, db_port):
) db_type_name = "PostgreSQL" if args.db_type == "postgres" else "MariaDB"
new_site_cmd = [ cprint(f"Cannot connect to {db_type_name} database at {db_host}:{db_port}. Please ensure the database service is running.", level=1)
"bench", cprint("If using Docker, run: docker-compose up -d db", level=3)
"new-site", return False
f"--db-root-username=root",
f"--db-host=db", # Should match the compose service name # Set database host configuration
f"--db-type={args.db_type}", # Add the selected database type cprint(f"Setting db_host to {db_host}", level=3)
f"--mariadb-user-host-login-scope=%", subprocess.call(
f"--db-root-password=123", # Replace with your MariaDB password ["bench", "set-config", "-g", "db_host", db_host],
f"--admin-password={args.admin_password}", cwd=os.getcwd() + "/" + args.bench_name,
] )
else:
cprint("Set db_host", level=3) # Build new site command
subprocess.call( new_site_cmd = [
["bench", "set-config", "-g", "db_host", "db"], "bench",
cwd=os.getcwd() + "/" + args.bench_name, "new-site",
) f"--db-root-username={db_username}",
new_site_cmd = [ f"--db-host={db_host}",
"bench", f"--db-type={args.db_type}",
"new-site", f"--db-root-password={db_password}",
f"--db-root-username=postgres", f"--admin-password={args.admin_password}",
f"--db-host=db", # Should match the compose service name ]
f"--db-type={args.db_type}", # Add the selected database type
f"--db-root-password=123", # Replace with your PostgreSQL password # Add database-specific options
f"--admin-password={args.admin_password}", if args.db_type == "mariadb":
] new_site_cmd.append("--mariadb-user-host-login-scope=%")
# Add custom database name if specified
if args.db_name:
new_site_cmd.append(f"--db-name={args.db_name}")
# Add database port if non-standard
if (args.db_type == "postgres" and db_port != 5432) or (args.db_type == "mariadb" and db_port != 3306):
new_site_cmd.append(f"--db-port={db_port}")
apps = os.listdir(f"{os.getcwd()}/{args.bench_name}/apps") apps = os.listdir(f"{os.getcwd()}/{args.bench_name}/apps")
apps.remove("frappe") apps.remove("frappe")
for app in apps: for app in apps:
new_site_cmd.append(f"--install-app={app}") new_site_cmd.append(f"--install-app={app}")
new_site_cmd.append(args.site_name) new_site_cmd.append(args.site_name)
cprint(f"Creating Site {args.site_name} ...", level=2) cprint(f"Creating Site {args.site_name} ...", level=2)
subprocess.call( try:
new_site_cmd, result = subprocess.call(
cwd=os.getcwd() + "/" + args.bench_name, new_site_cmd,
) cwd=os.getcwd() + "/" + args.bench_name,
)
if result == 0:
cprint(f"✓ Site {args.site_name} created successfully!", level=2)
return True
else:
cprint(f"✗ Site creation failed with exit code {result}", level=1)
return False
except subprocess.CalledProcessError as e:
cprint(f"✗ Site creation failed: {e}", level=1)
return False
if __name__ == "__main__": if __name__ == "__main__":

11
pwd.yml
View file

@ -35,10 +35,10 @@ services:
bench set-config -gp socketio_port $$SOCKETIO_PORT; bench set-config -gp socketio_port $$SOCKETIO_PORT;
environment: environment:
DB_HOST: db DB_HOST: db
DB_PORT: "5432" DB_PORT: '5432'
REDIS_CACHE: redis-cache:6379 REDIS_CACHE: redis-cache:6379
REDIS_QUEUE: redis-queue:6379 REDIS_QUEUE: redis-queue:6379
SOCKETIO_PORT: "9000" SOCKETIO_PORT: '9000'
depends_on: depends_on:
- db - db
volumes: volumes:
@ -115,14 +115,14 @@ services:
SOCKETIO: websocket:9000 SOCKETIO: websocket:9000
UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1 UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
UPSTREAM_REAL_IP_HEADER: X-Forwarded-For UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
UPSTREAM_REAL_IP_RECURSIVE: "off" UPSTREAM_REAL_IP_RECURSIVE: 'off'
PROXY_READ_TIMEOUT: 120 PROXY_READ_TIMEOUT: 120
CLIENT_MAX_BODY_SIZE: 50m CLIENT_MAX_BODY_SIZE: 50m
volumes: volumes:
- sites:/home/frappe/frappe-bench/sites - sites:/home/frappe/frappe-bench/sites
- logs:/home/frappe/frappe-bench/logs - logs:/home/frappe/frappe-bench/logs
ports: ports:
- "8080:8080" - '8080:8080'
queue-long: queue-long:
image: frappe/erpnext:v15.75.1 image: frappe/erpnext:v15.75.1
@ -192,6 +192,9 @@ services:
image: frappe/erpnext:v15.75.1 image: frappe/erpnext:v15.75.1
networks: networks:
- frappe_network - frappe_network
depends_on:
- redis_cache
- redis_queue
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure