diff --git a/src/common/db/Database.py b/src/common/db/Database.py index 4ad12cb01e..1e4ec8ecca 100644 --- a/src/common/db/Database.py +++ b/src/common/db/Database.py @@ -12,7 +12,7 @@ from os import _exit, getenv, sep from os.path import join as os_join from pathlib import Path -from re import DOTALL, Match, compile as re_compile, escape, error as RegexError, search +from re import DOTALL, Match, compile as re_compile, escape, error as RegexError, search, sub as re_sub from sys import argv, path as sys_path from tarfile import open as tar_open from threading import Lock @@ -207,6 +207,21 @@ def validate_and_update_db_string(db_string: str) -> Tuple[str, Optional[Match]] sqlalchemy_string, main_match = validate_and_update_db_string(sqlalchemy_string) sqlalchemy_string_readonly, readonly_match = validate_and_update_db_string(sqlalchemy_string_readonly) + def _mask_db_uri(uri: str) -> str: + """Best-effort mask password in SQLAlchemy URI for logs.""" + if not uri: + return uri + try: + from sqlalchemy.engine.url import make_url + + u = make_url(uri) + if u.password: + u = u.set(password="***") + return str(u) + except Exception: + # Fallback: mask :password@ in authority section + return re_sub(r"(//[^:/@]+:)[^@]*@", r"\1***@", uri) + self.database_uri = "" if sqlalchemy_string == sqlalchemy_string_readonly else sqlalchemy_string self.database_uri_readonly = sqlalchemy_string_readonly error = False @@ -261,6 +276,27 @@ def validate_and_update_db_string(db_string: str) -> Tuple[str, Optional[Match]] current_time = datetime.now().astimezone() not_connected = True fallback = False + ssl_ca_hint_logged = False + + def _ssl_expectations(uri: str) -> Tuple[bool, bool]: + """Return (ssl_enabled, has_explicit_root_ca) for a DB URI.""" + if not uri: + return False, False + try: + from sqlalchemy.engine.url import make_url + + u = make_url(uri) + q = {str(k).lower(): str(v) for k, v in dict(u.query or {}).items()} + ssl_flag = q.get("ssl", "").strip().lower() + sslmode = q.get("sslmode", "").strip().lower() + has_ca = bool((q.get("ssl_ca", "") or q.get("sslrootcert", "")).strip()) + ssl_enabled = has_ca or ssl_flag in ("1", "true", "yes", "on") or (sslmode not in ("", "disable", "off", "no", "false", "0")) + return ssl_enabled, has_ca + except Exception: + _lower = uri.lower() + has_ca = ("ssl_ca=" in _lower) or ("sslrootcert=" in _lower) + ssl_enabled = has_ca or ("ssl=true" in _lower) or ("sslmode=" in _lower and "sslmode=disable" not in _lower) + return ssl_enabled, has_ca while not_connected: try: @@ -275,6 +311,36 @@ def validate_and_update_db_string(db_string: str) -> Tuple[str, Optional[Match]] not_connected = False except (OperationalError, DatabaseError) as e: + # Helpful TLS diagnostics: SSL enabled but no CA configured. + # Print once to avoid log spam during retries. + if not ssl_ca_hint_logged: + _uri_for_check = self.database_uri or sqlalchemy_string + ssl_enabled, has_root_ca = _ssl_expectations(_uri_for_check) + err_l = str(e).lower() + looks_like_tls_error = any( + token in err_l + for token in ( + "ssl", + "tls", + "certificate", + "cert", + "verify failed", + "unknown ca", + "self signed", + ) + ) + if ssl_enabled and not has_root_ca and looks_like_tls_error: + self.logger.error( + "Database connection failed with SSL/TLS enabled but no root CA configured. " + "Reason: %s", + e, + ) + self.logger.error( + "Set a CA file in DATABASE_URI (ssl_ca=... for MySQL/MariaDB, sslrootcert=... for PostgreSQL), " + "or disable SSL for testing only." + ) + ssl_ca_hint_logged = True + if (datetime.now().astimezone() - current_time).total_seconds() > DATABASE_RETRY_TIMEOUT: if not fallback and self.database_uri_readonly: self.logger.error(f"Can't connect to database after {DATABASE_RETRY_TIMEOUT} seconds. Falling back to read-only database connection") @@ -283,7 +349,12 @@ def validate_and_update_db_string(db_string: str) -> Tuple[str, Optional[Match]] self.readonly = True fallback = True continue - self.logger.error(f"Can't connect to database after {DATABASE_RETRY_TIMEOUT} seconds: {e}") + self.logger.error( + "Can't connect to database after %s seconds: %s (DATABASE_URI=%s)", + DATABASE_RETRY_TIMEOUT, + e, + _mask_db_uri(self.database_uri or self.database_uri_readonly), + ) _exit(1) if any(error in str(e) for error in self.READONLY_ERROR): @@ -296,10 +367,18 @@ def validate_and_update_db_string(db_string: str) -> Tuple[str, Optional[Match]] not_connected = False continue elif log: - self.logger.warning("Can't connect to database, retrying in 5 seconds ...") + self.logger.warning( + "Can't connect to database (%s). Retrying in 5 seconds ... (DATABASE_URI=%s)", + e, + _mask_db_uri(self.database_uri or self.database_uri_readonly), + ) sleep(5) except BaseException as e: - self.logger.error(f"Error when trying to connect to the database: {e}") + self.logger.error( + "Error when trying to connect to the database: %s (DATABASE_URI=%s)", + e, + _mask_db_uri(self.database_uri or self.database_uri_readonly), + ) exit(1) if log: diff --git a/src/common/helpers/db-tls-validate.sh b/src/common/helpers/db-tls-validate.sh new file mode 100644 index 0000000000..b1531f7160 --- /dev/null +++ b/src/common/helpers/db-tls-validate.sh @@ -0,0 +1,337 @@ +#!/bin/bash + +# Shared Database Configuration and TLS Certificate Validation Helper +# Sourced by both systemd (bunkerweb-scheduler.sh) and container (entrypoint.sh) deployments +# +# Functions: +# parse_database_uri() - Extract connection info from DATABASE_URI +# validate_db_tls() - Validate DB TLS connection with system CA fallback +# validate_db_tls_certificate() - Test TLS connection via openssl s_client +# +# shellcheck disable=SC1091 +source /usr/share/bunkerweb/helpers/utils.sh + +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# parse_database_uri +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Parse DATABASE_URI and extract connection information. +# Outputs key=value pairs suitable for eval or sourcing. +# +# Output format: +# type= +# database= +# host= +# port= +# user= +# ssl_enabled= +# pem_path= +# +# Usage: +# eval "$(parse_database_uri)" # Imports: type, database, host, port, user, ssl_enabled, pem_path +# +# Returns: 0 always (fails gracefully with safe defaults) +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +parse_database_uri() { + timeout 2s python3 << 'PY' +from os import getenv +from urllib.parse import urlparse, parse_qs, unquote + +uri = getenv("DATABASE_URI", "") +db_type = host = user = db_name = port = "" +ssl_enabled = "no" +pem_path = "" + +try: + p = urlparse(uri) + db_type = (p.scheme or "").split("+", 1)[0] + host = p.hostname or "" + port = str(p.port) if p.port else "" + user = p.username or "" + if db_type == "sqlite": + db_name = p.path or "" + else: + db_name = (p.path or "").lstrip("/") + + if db_type != "sqlite": + q = parse_qs(p.query or "", keep_blank_values=True) + + def _q(name: str) -> str: + vals = q.get(name) or [] + return vals[0] if vals else "" + + ssl_flag = (_q("ssl") or "").strip().lower() + sslmode = (_q("sslmode") or "").strip().lower() + pem_path = unquote((_q("ssl_ca") or _q("sslrootcert") or "").strip()) + + if pem_path: + ssl_enabled = "yes" + elif ssl_flag in ("1", "true", "yes", "on"): + ssl_enabled = "yes" + elif sslmode and sslmode not in ("disable", "off", "no", "false", "0"): + ssl_enabled = "yes" +except Exception: + pass + +print(f'type="{db_type or "(unknown)"}"') +print(f'database="{db_name or "(none)"}"') +print(f'host="{host or "(local)"}"') +print(f'port="{port or "(default)"}"') +print(f'user="{user or "(none)"}"') +print(f'ssl_enabled="{ssl_enabled}"') +print(f'pem_path="{pem_path or "(none)"}"') +PY +} + +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# validate_db_tls +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Comprehensive DB TLS validation with system CA fallback. +# Handles: explicit PEM files, system CA discovery, and graceful fallback. +# +# Arguments: +# $1: LOG_CONTEXT - Logging prefix (e.g., "ENTRYPOINT", "SYSTEMCTL") +# $2: ssl_enabled - "yes" if SSL is enabled, "no" otherwise +# $3: pem_path - Path to PEM file or "(none)" +# $4: host - Database server hostname +# $5: port - Database server port (or "(default)" to use protocol default) +# $6: db_type - Database type (postgres, mysql, etc) +# $7: run_as_user - (Optional) "yes" for nginx user execution, "no" or omit for direct +# +# Returns: +# 0 = validation passed or skipped (no SSL) +# 1 = validation failed (continues gracefully) +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +validate_db_tls() { + local LOG_CONTEXT="${1:-UNKNOWN}" + local ssl_enabled="${2:-no}" + local pem_path="${3:-(none)}" + local host="${4:-(none)}" + local port="${5:-(default)}" + local db_type="${6:-(none)}" + local run_as_user="${7:-no}" + + # Log SSL status + log "$LOG_CONTEXT" "ℹ️" "DB SSL enabled: ${ssl_enabled}" + + # Skip validation if neither SSL nor PEM + if [ "$ssl_enabled" != "yes" ] && ([ -z "$pem_path" ] || [ "$pem_path" = "(none)" ]); then + log "$LOG_CONTEXT" "ℹ️" "DB TLS certificate provided: no" + return 0 + fi + + # If no PEM provided but SSL enabled, try system CA certs + local is_system_ca="no" + if [ "$ssl_enabled" = "yes" ] && ([ -z "$pem_path" ] || [ "$pem_path" = "(none)" ]); then + # Find system CA bundle + for ca_file in /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-bundle.crt /etc/ssl/certs/ca.crt; do + if [ -f "$ca_file" ]; then + pem_path="$ca_file" + is_system_ca="yes" + log "$LOG_CONTEXT" "ℹ️" "No PEM provided, using system CA bundle: $pem_path" + break + fi + done + # If no system CA found, proceed with validation (will use system defaults) + if [ -z "$pem_path" ] || [ "$pem_path" = "(none)" ]; then + log "$LOG_CONTEXT" "ℹ️" "No system CA bundle found, proceeding with SSL (system default verification)" + fi + else + [ -n "$pem_path" ] && [ "$pem_path" != "(none)" ] && log "$LOG_CONTEXT" "ℹ️" "DB TLS certificate provided: $pem_path" + fi + + # Validate TLS connection + validate_db_tls_certificate "$LOG_CONTEXT" "$host" "$port" "$db_type" "$pem_path" "$run_as_user" "$is_system_ca" +} + +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# validate_db_tls_certificate +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Test TLS connection to database server using openssl s_client. +# +# Arguments: +# $1: LOG_CONTEXT - Logging prefix (e.g., "SYSTEMCTL", "ENTRYPOINT") +# $2: host - Database server hostname +# $3: port - Database server port (or "(default)" to use protocol default) +# $4: db_type - Database type (postgres, mysql, etc) +# $5: pem_path - Path to CA certificate file or "(none)" +# $6: run_as_user - (Optional) "yes" for nginx user, "no" for direct execution +# $7: is_system_ca - (Optional) "yes" if using system CA bundle, "no" for user cert +# +# Returns: +# 0 = TLS connection successful or skipped +# 1 = TLS connection failed (continues gracefully) +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +validate_db_tls_certificate() { + local LOG_CONTEXT="${1:-UNKNOWN}" + local host="${2:-(none)}" + local port="${3:-(default)}" + local db_type="${4:-(none)}" + local pem_path="${5:-(none)}" + local run_as_user="${6:-no}" + local is_system_ca="${7:-no}" + + # Skip if no host or db_type + if [ "$host" = "(none)" ] || [ -z "$host" ] || [ "$db_type" = "(none)" ] || [ -z "$db_type" ]; then + return 0 + fi + + # Skip if no PEM and no certificate to validate + if [ "$pem_path" = "(none)" ] || [ -z "$pem_path" ]; then + log "$LOG_CONTEXT" "ℹ️" "No explicit certificate to validate, TLS verification delegated to system" + return 0 + fi + + # ── PEM File Validation ─────────────────────────────────────────────────── + # Validate the PEM file before testing connections + log "$LOG_CONTEXT" "ℹ️" "Validating PEM certificate: $pem_path" + + # File existence check + if [ ! -f "$pem_path" ]; then + log "$LOG_CONTEXT" "❌" "PEM certificate does not exist: $pem_path" + return 1 + fi + + # File readability check + if [ ! -r "$pem_path" ]; then + log "$LOG_CONTEXT" "❌" "PEM certificate is NOT readable: $pem_path" + return 1 + fi + + # PEM/DER format validation + if ! timeout 2s openssl x509 -in "$pem_path" -noout >/dev/null 2>&1; then + # Try DER format + if ! timeout 2s openssl x509 -inform DER -in "$pem_path" -noout >/dev/null 2>&1; then + log "$LOG_CONTEXT" "❌" "PEM certificate is not valid X.509 format: $pem_path" + return 1 + fi + fi + + # Certificate expiry validation - skip for system CA bundles (they may contain expired certs) + if [ "$is_system_ca" != "yes" ]; then + local cert_check + cert_check=$(timeout 2s env PEM_FILE="$pem_path" python3 - << 'PY' +from os import getenv +from pathlib import Path +from datetime import datetime, timezone +from cryptography import x509 + +try: + pem_file = getenv("PEM_FILE", "") + data = Path(pem_file).read_bytes() + certs = x509.load_pem_x509_certificates(data) + if not certs: + print("NO_CERTS") + exit(1) + + now = datetime.now(timezone.utc) + for idx, cert in enumerate(certs, start=1): + if now < cert.not_valid_before_utc: + print(f"NOT_VALID_YET|{idx}") + exit(1) + if now > cert.not_valid_after_utc: + print(f"EXPIRED|{idx}") + exit(1) + + print("VALID") +except Exception as e: + print(f"ERROR|{e}") + exit(1) +PY +) + + if [ "$cert_check" != "VALID" ]; then + log "$LOG_CONTEXT" "❌" "PEM certificate validation failed: $cert_check" + return 1 + fi + + log "$LOG_CONTEXT" "✅" "PEM certificate is valid" + else + log "$LOG_CONTEXT" "ℹ️" "Skipping expiry validation for system CA bundle (validation delegated to TLS handshake)" + fi + + # Determine port: use provided port or fall back to database type default + if [ "$port" = "(default)" ] || [ -z "$port" ]; then + case "$db_type" in + postgres) port=5432 ;; + mysql|mariadb) port=3306 ;; + *) port=5432 ;; # Default to postgres + esac + fi + + # Test TLS connection + log "$LOG_CONTEXT" "ℹ️" "Testing TLS connection to ${host}:${port} (${db_type})" + + # Resolve hostname to IP addresses + local ips + ips=$(getent hosts "$host" 2>/dev/null | awk '{print $1}') + + if [ -z "$ips" ]; then + # If hostname doesn't resolve, try direct connection to hostname + local direct_cmd="openssl s_client -connect $host:$port" + [ -f "$pem_path" ] && direct_cmd="$direct_cmd -CAfile $pem_path" + case "$db_type" in + postgres) direct_cmd="$direct_cmd -starttls postgres" ;; + mysql|mariadb) direct_cmd="$direct_cmd -starttls mysql" ;; + esac + + log "$LOG_CONTEXT" "ℹ️" "No DNS resolution for ${host}, testing direct connection" + if echo "Q" | timeout 2s $direct_cmd >/dev/null 2>&1; then + log "$LOG_CONTEXT" "✅" "TLS connection successful to ${host}:${port}" + return 0 + else + log "$LOG_CONTEXT" "❌" "TLS connection failed to ${host}:${port}" + return 1 + fi + fi + + # Try each IP address and report all results + local ip + local found_success=0 + local success_ips="" + local failed_ips="" + local total_ips=0 + local passed_ips=0 + + while IFS= read -r ip; do + ((total_ips++)) + local ip_cmd="openssl s_client -connect $ip:$port" + + # Add CA certificate verification + if [ -f "$pem_path" ]; then + ip_cmd="$ip_cmd -CAfile $pem_path" + fi + + # Add STARTTLS for database protocols + case "$db_type" in + postgres) + ip_cmd="$ip_cmd -starttls postgres" + ;; + mysql|mariadb) + ip_cmd="$ip_cmd -starttls mysql" + ;; + esac + + log "$LOG_CONTEXT" "ℹ️" "Testing TLS connection to ${ip}:${port} (resolved from ${host})" + if echo "Q" | timeout 2s $ip_cmd >/dev/null 2>&1; then + log "$LOG_CONTEXT" "✅" "TLS connection successful to ${ip}:${port}" + success_ips="${success_ips}${ip}:${port} " + found_success=1 + ((passed_ips++)) + else + log "$LOG_CONTEXT" "❌" "TLS connection failed to ${ip}:${port}" + failed_ips="${failed_ips}${ip}:${port} " + fi + done <<< "$ips" + + # Report overall results + if [ "$found_success" = "1" ]; then + log "$LOG_CONTEXT" "✅" "TLS validation passed (${passed_ips}/${total_ips} IPs: ${success_ips%% })" + return 0 + else + log "$LOG_CONTEXT" "❌" "TLS validation failed for all IPs (${total_ips} failed: ${failed_ips%% }), continuing startup" + return 1 + fi +} diff --git a/src/linux/scripts/bunkerweb-scheduler.sh b/src/linux/scripts/bunkerweb-scheduler.sh index 89d6c56055..1a5178d419 100644 --- a/src/linux/scripts/bunkerweb-scheduler.sh +++ b/src/linux/scripts/bunkerweb-scheduler.sh @@ -135,6 +135,32 @@ function start() { # Extract and validate database type DATABASE=$(echo "$DATABASE_URI" | awk -F: '{print $1}' | awk -F+ '{print $1}') + # Parse DB connection target first (needed for TLS validation) + # shellcheck disable=SC1091 + source /usr/share/bunkerweb/helpers/db-tls-validate.sh + if parse_output=$(parse_database_uri); then + eval "$parse_output" + else + log "SYSTEMCTL" "⚠️" "Failed to parse DATABASE_URI (error), using defaults" + type="(unknown)" + database="(none)" + host="(local)" + user="(none)" + ssl_enabled="no" + pem_path="(none)" + fi + db_ssl_enabled="$ssl_enabled" + db_pem_path="$pem_path" + db_port="$port" + + # Skip TLS validation for SQLite (local file-based, no network) + if [ "$type" != "sqlite" ]; then + # Validate TLS certificate before any database connection (with system CA fallback) + if ! validate_db_tls "SYSTEMCTL" "$db_ssl_enabled" "$db_pem_path" "$host" "$db_port" "$type" "yes"; then + log "SYSTEMCTL" "⚠️" "TLS validation failed but continuing" + fi + fi + # Check current version and stamp log "SYSTEMCTL" "ℹ️" "Checking database version..." installed_version=$(cat /usr/share/bunkerweb/VERSION) @@ -197,6 +223,9 @@ EOL export DATABASE_URI rm -f /var/tmp/bunkerweb/database_uri + # Log parsed database connection info + log "SYSTEMCTL" "ℹ️" "Scheduler DB target: type=${type} database=${database} host=${host} port=${port} user=${user} ssl=${ssl_enabled} pem=${pem_path}" + # Update configuration files if [ "$current_version" != "$installed_version" ]; then if [ "$current_version" != "dev" ] && [ "$current_version" != "testing" ]; then diff --git a/src/scheduler/entrypoint.sh b/src/scheduler/entrypoint.sh index 6066e764c6..201af164b5 100644 --- a/src/scheduler/entrypoint.sh +++ b/src/scheduler/entrypoint.sh @@ -51,6 +51,32 @@ DATABASE_URI=${DATABASE_URI:-sqlite:////var/lib/bunkerweb/db.sqlite3} export DATABASE_URI DATABASE=$(echo "$DATABASE_URI" | awk -F: '{print $1}' | awk -F+ '{print $1}') +# Parse DB connection target first (needed for TLS validation) +# shellcheck disable=SC1091 +source /usr/share/bunkerweb/helpers/db-tls-validate.sh +if parse_output=$(parse_database_uri); then + eval "$parse_output" +else + log "ENTRYPOINT" "⚠️" "Failed to parse DATABASE_URI (error), using defaults" + type="(unknown)" + database="(none)" + host="(local)" + user="(none)" + ssl_enabled="no" + pem_path="(none)" +fi +db_ssl_enabled="$ssl_enabled" +db_pem_path="$pem_path" +db_port="$port" + +# Skip TLS validation for SQLite (local file-based, no network) +if [ "$type" != "sqlite" ]; then + # Validate TLS certificate before any database connection (with system CA fallback) + if ! validate_db_tls "ENTRYPOINT" "$db_ssl_enabled" "$db_pem_path" "$host" "$db_port" "$type" "no"; then + log "ENTRYPOINT" "⚠️" "TLS validation failed but continuing" + fi +fi + # Check current version and stamp log "ENTRYPOINT" "ℹ️" "Checking database version..." installed_version=$(cat /usr/share/bunkerweb/VERSION) @@ -99,6 +125,9 @@ DATABASE_URI=$(cat /var/tmp/bunkerweb/database_uri) export DATABASE_URI rm -f /var/tmp/bunkerweb/database_uri +# Log parsed database connection info +log "ENTRYPOINT" "ℹ️" "Scheduler DB target: type=${type} database=${database} host=${host} port=${port} user=${user} ssl=${ssl_enabled} pem=${pem_path}" + # Update configuration files if [ "$current_version" != "$installed_version" ]; then if [ "$current_version" != "dev" ] && [ "$current_version" != "testing" ]; then diff --git a/src/scheduler/main.py b/src/scheduler/main.py index 50742e70d9..66306e63a1 100644 --- a/src/scheduler/main.py +++ b/src/scheduler/main.py @@ -685,7 +685,28 @@ def backup_failover(): with tmp_variables_path.open() as f: dotenv_env = dict(line.strip().split("=", 1) for line in f if line.strip() and not line.startswith("#") and "=" in line) - SCHEDULER = JobScheduler(LOGGER, db=Database(LOGGER, sqlalchemy_string=dotenv_env.get("DATABASE_URI", getenv("DATABASE_URI", None)))) # type: ignore + _db_uri = dotenv_env.get("DATABASE_URI", getenv("DATABASE_URI", None)) + + def _db_target_details(uri: str) -> str: + if not uri: + return "type=(unknown) database=(none) host=(local) user=(none)" + try: + from sqlalchemy.engine.url import make_url + + u = make_url(uri) + driver = (u.drivername or "").split("+", 1)[0] + host = u.host or "(local)" + user = u.username or "(none)" + if driver == "sqlite": + db_name = u.database or "(none)" + else: + db_name = (u.database or "(none)") + return f"type={driver or '(unknown)'} database={db_name} host={host} user={user}" + except Exception: + return "type=(unknown) database=(none) host=(local) user=(none)" + + LOGGER.info("Scheduler DB target: %s", _db_target_details(_db_uri or "")) + SCHEDULER = JobScheduler(LOGGER, db=Database(LOGGER, sqlalchemy_string=_db_uri)) # type: ignore JOB = Job(LOGGER, __file__, SCHEDULER.db)