Files
tfm_ainventory/backend/config_manager.py

191 lines
6.3 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import yaml
import logging
from .config_loader import load_config, 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
load_config()
return 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
load_config()
return 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 get_config()
@staticmethod
def get_config() -> dict:
"""Return the current global configuration."""
return get_config()
@staticmethod
def get_masked_key(key_name: str):
"""Returns a masked version of a configuration value or env var."""
config = 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 ConfigManager.get_config()
def validate_config_file() -> bool:
"""Validate backend.yaml syntax and required fields."""
return ConfigManager.validate_config_file()