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()