191 lines
6.4 KiB
Python
191 lines
6.4 KiB
Python
import os
|
||
import yaml
|
||
import logging
|
||
from .config_loader import load_config as loader_load_config, get_config as loader_get_config
|
||
|
||
log = logging.getLogger("ainventory")
|
||
|
||
class ConfigManager:
|
||
"""Manages backend.yaml configuration file updates."""
|
||
|
||
@staticmethod
|
||
def get_config_path():
|
||
"""Returns the absolute path to backend.yaml."""
|
||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||
project_root = os.path.dirname(base_dir)
|
||
return os.path.join(project_root, "config", "backend.yaml")
|
||
|
||
@staticmethod
|
||
def read_config() -> dict:
|
||
"""Read backend.yaml and return current config."""
|
||
path = ConfigManager.get_config_path()
|
||
if not os.path.exists(path):
|
||
log.warning(f"ℹ️ {path} not found. Returning empty dict.")
|
||
return {}
|
||
|
||
try:
|
||
with open(path, 'r', encoding='utf-8') as f:
|
||
return yaml.safe_load(f) or {}
|
||
except Exception as e:
|
||
log.error(f"❌ Failed to read {path}: {e}")
|
||
return {}
|
||
|
||
@staticmethod
|
||
def update_config(updates: dict) -> dict:
|
||
"""
|
||
Update backend.yaml with new values and return updated config.
|
||
Only updates sections in backend.yaml.
|
||
"""
|
||
path = ConfigManager.get_config_path()
|
||
current_yaml = ConfigManager.read_config()
|
||
|
||
# Merge updates (deep merge for sections)
|
||
for section, values in updates.items():
|
||
if isinstance(values, dict) and section in current_yaml and isinstance(current_yaml[section], dict):
|
||
current_yaml[section].update(values)
|
||
else:
|
||
current_yaml[section] = values
|
||
|
||
try:
|
||
with open(path, 'w', encoding='utf-8') as f:
|
||
yaml.safe_dump(current_yaml, f, default_flow_style=False, sort_keys=False)
|
||
log.info(f"✅ Updated {path} with new values.")
|
||
|
||
# Reload the global config
|
||
loader_load_config()
|
||
return loader_get_config()
|
||
except Exception as e:
|
||
log.error(f"❌ Failed to write {path}: {e}")
|
||
raise
|
||
|
||
@staticmethod
|
||
def validate_config_file() -> bool:
|
||
"""Validate backend.yaml syntax and required fields."""
|
||
path = ConfigManager.get_config_path()
|
||
if not os.path.exists(path):
|
||
return False
|
||
|
||
try:
|
||
with open(path, 'r', encoding='utf-8') as f:
|
||
yaml.safe_load(f)
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
@staticmethod
|
||
def get_secrets_path():
|
||
"""Returns the absolute path to secrets.yaml."""
|
||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||
project_root = os.path.dirname(base_dir)
|
||
return os.path.join(project_root, "config", "secrets.yaml")
|
||
|
||
@staticmethod
|
||
def update_secrets(updates: dict) -> dict:
|
||
"""Update secrets.yaml with new values (flattened structure)."""
|
||
path = ConfigManager.get_secrets_path()
|
||
|
||
current_secrets = {}
|
||
if os.path.exists(path):
|
||
try:
|
||
with open(path, 'r', encoding='utf-8') as f:
|
||
current_secrets = yaml.safe_load(f) or {}
|
||
except Exception as e:
|
||
log.error(f"❌ Failed to read {path}: {e}")
|
||
|
||
# Update flattened secrets
|
||
current_secrets.update(updates)
|
||
|
||
try:
|
||
with open(path, 'w', encoding='utf-8') as f:
|
||
# Header for clarity
|
||
f.write("# TFM aInventory - Managed Secrets (Updated via Admin UI)\n")
|
||
yaml.dump(current_secrets, f, default_flow_style=False, sort_keys=True)
|
||
log.info(f"✅ Updated {path} with new secrets.")
|
||
|
||
# Reload the global config
|
||
loader_load_config()
|
||
return loader_get_config()
|
||
except Exception as e:
|
||
log.error(f"❌ Failed to write {path}: {e}")
|
||
raise
|
||
|
||
@staticmethod
|
||
def update_keys(updates: dict) -> dict:
|
||
"""
|
||
Intelligent key update: routes sensitive AI keys to secrets.yaml
|
||
and others to backend.yaml.
|
||
"""
|
||
secrets_updates = {}
|
||
backend_updates = {}
|
||
|
||
sensitive_keys = ["GEMINI_API_KEY", "CLAUDE_API_KEY", "JWT_SECRET_KEY", "LDAP_PASSWORD"]
|
||
|
||
for key, val in updates.items():
|
||
if key in sensitive_keys:
|
||
secrets_updates[key] = val
|
||
else:
|
||
# Non-sensitive or complex nested settings
|
||
backend_updates[key] = val
|
||
|
||
if secrets_updates:
|
||
ConfigManager.update_secrets(secrets_updates)
|
||
|
||
if backend_updates:
|
||
ConfigManager.update_config(backend_updates)
|
||
|
||
return loader_get_config()
|
||
|
||
@staticmethod
|
||
def get_config() -> dict:
|
||
"""Return the current global configuration."""
|
||
return loader_get_config()
|
||
|
||
@staticmethod
|
||
def get_masked_key(key_name: str):
|
||
"""Returns a masked version of a configuration value or env var."""
|
||
config = loader_get_config()
|
||
|
||
# Try to find in config first
|
||
val = None
|
||
if key_name == "JWT_SECRET_KEY":
|
||
val = config.get("auth", {}).get("jwt_secret_key")
|
||
elif key_name == "GEMINI_API_KEY":
|
||
val = config.get("ai", {}).get("gemini_api_key")
|
||
elif key_name == "CLAUDE_API_KEY":
|
||
val = config.get("ai", {}).get("claude_api_key")
|
||
|
||
# Fallback to env
|
||
if not val:
|
||
val = os.environ.get(key_name)
|
||
|
||
if not val:
|
||
return None
|
||
|
||
prefix = ""
|
||
if "GEMINI" in key_name:
|
||
prefix = "G-"
|
||
elif "CLAUDE" in key_name:
|
||
prefix = "sk-"
|
||
|
||
if len(val) <= 8:
|
||
return f"{prefix}****"
|
||
|
||
return f"{prefix}****{val[-4:]}"
|
||
|
||
# Module-level functions for backward compatibility and direct import
|
||
def read_config() -> dict:
|
||
"""Read backend.yaml and return current config."""
|
||
return ConfigManager.read_config()
|
||
|
||
def update_config(updates: dict) -> dict:
|
||
"""Update backend.yaml with new values and return updated config."""
|
||
return ConfigManager.update_config(updates)
|
||
|
||
def get_config() -> dict:
|
||
"""Return the current global configuration."""
|
||
return loader_get_config()
|
||
|
||
def validate_config_file() -> bool:
|
||
"""Validate backend.yaml syntax and required fields."""
|
||
return ConfigManager.validate_config_file()
|