Refactor: Modularize configuration and implement robust DB bootstrap
This commit is contained in:
@@ -67,6 +67,36 @@ try:
|
||||
except Exception:
|
||||
log.exception('Could not connect to database.')
|
||||
|
||||
def reconnect_db() -> bool:
|
||||
"""
|
||||
Re-initializes the global database engine and connection using current settings.
|
||||
Useful after bootstrapping new credentials from the 'cfg' table.
|
||||
"""
|
||||
global engine, db, db_uri
|
||||
|
||||
log.info("Refreshing database connection engine...")
|
||||
try:
|
||||
if engine:
|
||||
engine.dispose()
|
||||
log.info("Disposed of previous database engine.")
|
||||
|
||||
db_uri = settings.SQLALCHEMY_DB_URI
|
||||
engine = create_engine(
|
||||
url = db_uri,
|
||||
echo = False,
|
||||
pool_use_lifo = True,
|
||||
pool_pre_ping = True,
|
||||
pool_recycle = settings.DB['pool_recycle'],
|
||||
isolation_level = 'READ COMMITTED',
|
||||
connect_args = {'connect_timeout': settings.DB['connect_timeout']}
|
||||
)
|
||||
db = engine.connect()
|
||||
log.info(f"Database connection re-established successfully: {db_uri}")
|
||||
return True
|
||||
except Exception:
|
||||
log.exception("FAILED to refresh database connection!")
|
||||
return False
|
||||
|
||||
# ### BEGIN ### API DB SQL ### sql_connect() ###
|
||||
@logger_reset
|
||||
def sql_connect(current_db, log_lvl: int = logging.INFO) -> None|bool|int:
|
||||
|
||||
87
app/lib_config_v3.py
Normal file
87
app/lib_config_v3.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
log = logging.getLogger('root')
|
||||
|
||||
def validate_critical_config(settings: Any):
|
||||
"""
|
||||
Validates that essential settings are populated and not using placeholders.
|
||||
Logs warnings or errors for missing critical infrastructure.
|
||||
"""
|
||||
log.info("Checking critical system configuration...")
|
||||
|
||||
# 1. Database Check
|
||||
db = getattr(settings, 'DB', {})
|
||||
if not db.get('server') or db.get('server') == 'mariadb':
|
||||
# 'mariadb' is the default in .env, usually fine, but worth noting
|
||||
log.info(f"Database server: {db.get('server')}")
|
||||
|
||||
# 2. SMTP Check
|
||||
smtp = getattr(settings, 'SMTP', {})
|
||||
if not smtp.get('server'):
|
||||
log.warning("CRITICAL: SMTP server not configured. Email features will fail.")
|
||||
if smtp.get('password') == 'set-in-ae-sql-db-cnf-tbl':
|
||||
log.error("CRITICAL: SMTP password is still set to placeholder. Email authentication will fail.")
|
||||
|
||||
# 3. Security Check
|
||||
jwt_key = getattr(settings, 'JWT_KEY', '')
|
||||
if not jwt_key or jwt_key == 'fake-super-secret-token':
|
||||
log.error("SECURITY: JWT_KEY is missing or using a known fake token!")
|
||||
|
||||
log.info("Configuration validation complete.")
|
||||
|
||||
def bootstrap_db_config(settings: Any) -> bool:
|
||||
"""
|
||||
Loads dynamic settings from the 'cfg' table and updates the settings object.
|
||||
Uses deferred import of sql_select to avoid circular dependencies.
|
||||
"""
|
||||
# CRITICAL: Deferred import to prevent boot-time circular dependencies
|
||||
from app.db_sql import sql_select
|
||||
|
||||
cfg_id = settings.AETHER_CFG.get('id', '0')
|
||||
log.info(f"Bootstrapping system configuration from DB (cfg_id={cfg_id})...")
|
||||
|
||||
try:
|
||||
# Fetch the config record
|
||||
aether_cfg_sql = sql_select(
|
||||
table_name='cfg',
|
||||
record_id=int(cfg_id),
|
||||
as_list=False,
|
||||
max_count=1,
|
||||
)
|
||||
|
||||
# In some cases sql_select might return a single-item list even with as_list=False
|
||||
if isinstance(aether_cfg_sql, list):
|
||||
if len(aether_cfg_sql) > 0:
|
||||
aether_cfg_sql = aether_cfg_sql[0]
|
||||
else:
|
||||
aether_cfg_sql = None
|
||||
|
||||
if not aether_cfg_sql or not isinstance(aether_cfg_sql, dict):
|
||||
log.error(f"FAILED to load system config from DB for ID {cfg_id}. Table 'cfg' might be empty or ID missing.")
|
||||
return False
|
||||
|
||||
# --- Update Database settings ---
|
||||
# Safety: Only update if the values are provided in the DB record
|
||||
if aether_cfg_sql.get('db_server'): settings.DB_SERVER = aether_cfg_sql.get('db_server')
|
||||
if aether_cfg_sql.get('db_port'): settings.DB_PORT = str(aether_cfg_sql.get('db_port'))
|
||||
if aether_cfg_sql.get('db_name'): settings.DB_NAME = aether_cfg_sql.get('db_name')
|
||||
if aether_cfg_sql.get('db_username'): settings.DB_USER = aether_cfg_sql.get('db_username')
|
||||
if aether_cfg_sql.get('db_password'): settings.DB_PASS = aether_cfg_sql.get('db_password')
|
||||
|
||||
# --- Update SMTP Settings ---
|
||||
if aether_cfg_sql.get('smtp_server'): settings.SMTP['server'] = aether_cfg_sql.get('smtp_server')
|
||||
if aether_cfg_sql.get('smtp_port'): settings.SMTP['port'] = str(aether_cfg_sql.get('smtp_port'))
|
||||
if aether_cfg_sql.get('smtp_username'): settings.SMTP['username'] = aether_cfg_sql.get('smtp_username')
|
||||
if aether_cfg_sql.get('smtp_password'): settings.SMTP['password'] = aether_cfg_sql.get('smtp_password')
|
||||
|
||||
# --- Update File Paths ---
|
||||
if aether_cfg_sql.get('path_hosted_files_root'): settings.FILES_PATH['hosted_files_root'] = aether_cfg_sql.get('path_hosted_files_root')
|
||||
if aether_cfg_sql.get('path_hosted_tmp_root'): settings.FILES_PATH['hosted_tmp_root'] = aether_cfg_sql.get('path_hosted_tmp_root')
|
||||
|
||||
log.info("System configuration successfully synchronized with DB.")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
log.exception(f"Unexpected error during system bootstrap: {e}")
|
||||
return False
|
||||
52
app/main.py
52
app/main.py
@@ -21,7 +21,8 @@ from app.middleware import add_process_time_header as process_time_middleware
|
||||
# Centralized router registry
|
||||
from app.routers.registry import setup_routers
|
||||
|
||||
from app.db_sql import sql_select, reset_redis # , sql_connect
|
||||
from app.db_sql import sql_select, reset_redis, reconnect_db
|
||||
from app.lib_config_v3 import bootstrap_db_config, validate_critical_config
|
||||
|
||||
|
||||
print('### **** *** ** * The Aether API v4 using FastAPI is loading... * ** *** **** ###')
|
||||
@@ -47,41 +48,26 @@ app = FastAPI(
|
||||
log.setLevel(logging.INFO)
|
||||
# log.debug(config.settings)
|
||||
|
||||
print('### **** *** ** * Aether API v4 using FastAPI - About to try first SQL SELECT (sql_select()) while loading... * ** *** **** ###')
|
||||
if aether_cfg_sql_result := sql_select(
|
||||
table_name = 'cfg',
|
||||
record_id = config.settings.AETHER_CFG['id'],
|
||||
as_list = False,
|
||||
max_count = 1,
|
||||
):
|
||||
aether_cfg_sql = aether_cfg_sql_result
|
||||
print('### **** *** ** * Aether API v4 using FastAPI - Bootstrapping Configuration... * ** *** **** ###')
|
||||
|
||||
config.settings.DB['server'] = aether_cfg_sql.get('db_server')
|
||||
config.settings.DB['port'] = aether_cfg_sql.get('db_port')
|
||||
config.settings.DB['name'] = aether_cfg_sql.get('db_name')
|
||||
config.settings.DB['username'] = aether_cfg_sql.get('db_username')
|
||||
config.settings.DB['password'] = aether_cfg_sql.get('db_password')
|
||||
# Sync settings from DB with robust error handling
|
||||
try:
|
||||
if bootstrap_db_config(config.settings):
|
||||
log.info("Successfully bootstrapped configuration from database.")
|
||||
# Re-initialize the database engine with new credentials/URI
|
||||
if reconnect_db():
|
||||
log.info("Database connection re-established with production configuration.")
|
||||
else:
|
||||
log.warning("FAILED to re-establish database connection after bootstrap. Falling back to .env settings.")
|
||||
else:
|
||||
log.warning("System bootstrap from DB returned no results. Using environment defaults.")
|
||||
except Exception as e:
|
||||
log.error(f"Unexpected error during configuration bootstrap: {e}. Falling back to .env settings.")
|
||||
|
||||
DB = config.settings.DB
|
||||
config.settings.SQLALCHEMY_DB_URI = 'mysql://'+DB['username']+':'+DB['password']+'@'+DB['server']+'/'+DB['name']
|
||||
# db_result = sql_connect(config.settings.SQLALCHEMY_DB_URI)
|
||||
log.debug(config.settings.DB)
|
||||
# Perform final validation of critical infrastructure
|
||||
validate_critical_config(config.settings)
|
||||
|
||||
config.settings.SMTP['server'] = aether_cfg_sql.get('smtp_server')
|
||||
config.settings.SMTP['port'] = aether_cfg_sql.get('smtp_port')
|
||||
config.settings.SMTP['username'] = aether_cfg_sql.get('smtp_username')
|
||||
config.settings.SMTP['password'] = aether_cfg_sql.get('smtp_password')
|
||||
|
||||
# config.settings.FILES_PATH['hosted_files_root'] = aether_cfg_sql.get('PATH_HOSTED_FILES_ROOT')
|
||||
# config.settings.FILES_PATH['hosted_tmp_root'] = aether_cfg_sql.get('PATH_HOSTED_TMP_ROOT')
|
||||
|
||||
config.settings.FILES_PATH['hosted_files_root'] = aether_cfg_sql.get('path_hosted_files_root')
|
||||
config.settings.FILES_PATH['hosted_tmp_root'] = aether_cfg_sql.get('path_hosted_tmp_root')
|
||||
else:
|
||||
# aether_cfg_sql_result
|
||||
pass
|
||||
print('### **** *** ** * Aether API v4 using FastAPI - Finished first SQL SELECT (sql_select()) while loading... * ** *** **** ###' )
|
||||
log.debug(aether_cfg_sql_result)
|
||||
print('### **** *** ** * Aether API v4 using FastAPI - Finished Configuration Phase * ** *** **** ###' )
|
||||
log.debug(config.settings)
|
||||
|
||||
# @lru_cache()
|
||||
|
||||
Reference in New Issue
Block a user