import argparse import subprocess import sys from pathlib import Path import yaml from typing import Dict, Any from module_utils.manager.inventory import InventoryManager from module_utils.handler.vault import VaultHandler, VaultScalar from module_utils.handler.yaml import YamlHandler from yaml.dumper import SafeDumper def ask_for_confirmation(key: str) -> bool: """Prompt the user for confirmation to overwrite an existing value.""" confirmation = input( f"Are you sure you want to overwrite the value for '{key}'? (y/n): " ).strip().lower() return confirmation == 'y' def main(): parser = argparse.ArgumentParser( description="Selectively vault credentials + become-password in your inventory." ) parser.add_argument( "--role-path", required=True, help="Path to your role" ) parser.add_argument( "--inventory-file", required=True, help="Host vars file to update" ) parser.add_argument( "--vault-password-file", required=True, help="Vault password file" ) parser.add_argument( "--set", nargs="*", default=[], help="Override values key.subkey=VALUE" ) parser.add_argument( "-f", "--force", action="store_true", help="Force overwrite without confirmation" ) args = parser.parse_args() # Parse overrides overrides = { k.strip(): v.strip() for pair in args.set for k, v in [pair.split("=", 1)] } # Initialize inventory manager manager = InventoryManager( role_path=Path(args.role_path), inventory_path=Path(args.inventory_file), vault_pw=args.vault_password_file, overrides=overrides ) # Load existing credentials to preserve existing_apps = manager.inventory.get("applications", {}) existing_creds = {} if manager.app_id in existing_apps: existing_creds = existing_apps[manager.app_id].get("credentials", {}).copy() # Apply schema (may generate defaults) updated_inventory = manager.apply_schema() # Restore existing database_password if present apps = updated_inventory.setdefault("applications", {}) app_block = apps.setdefault(manager.app_id, {}) creds = app_block.setdefault("credentials", {}) if "database_password" in existing_creds: creds["database_password"] = existing_creds["database_password"] # Store original plaintext values original_plain = {key: str(val) for key, val in creds.items()} for key, raw_val in list(creds.items()): # Skip if already vaulted if isinstance(raw_val, VaultScalar) or str(raw_val).lstrip().startswith("$ANSIBLE_VAULT"): continue # Determine plaintext plain = original_plain.get(key, "") if key in overrides and (args.force or ask_for_confirmation(key)): plain = overrides[key] # Encrypt the plaintext encrypted = manager.vault_handler.encrypt_string(plain, key) lines = encrypted.splitlines() indent = len(lines[1]) - len(lines[1].lstrip()) body = "\n".join(line[indent:] for line in lines[1:]) creds[key] = VaultScalar(body) # Vault top-level become password if present if "ansible_become_password" in updated_inventory: val = str(updated_inventory["ansible_become_password"]) if val.lstrip().startswith("$ANSIBLE_VAULT"): updated_inventory["ansible_become_password"] = VaultScalar(val) else: snippet = manager.vault_handler.encrypt_string( val, "ansible_become_password" ) lines = snippet.splitlines() indent = len(lines[1]) - len(lines[1].lstrip()) body = "\n".join(line[indent:] for line in lines[1:]) updated_inventory["ansible_become_password"] = VaultScalar(body) # Write back to file with open(args.inventory_file, "w", encoding="utf-8") as f: yaml.dump(updated_inventory, f, sort_keys=False, Dumper=SafeDumper) print(f"✅ Inventory selectively vaulted → {args.inventory_file}") if __name__ == "__main__": main()