diff --git a/cli/verify_inventory.py b/cli/verify_inventory.py new file mode 100644 index 00000000..84c5d43b --- /dev/null +++ b/cli/verify_inventory.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +import argparse +import sys +import yaml +import re +from pathlib import Path + + +def load_yaml_file(path): + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + content = re.sub(r'(?m)^([ \t]*[^\s:]+):\s*!vault[\s\S]+?(?=^\S|\Z)', r'\1: ""\n', content) + return yaml.safe_load(content) + except Exception as e: + print(f"Warning: Could not parse {path}: {e}", file=sys.stderr) + return None + + +def recursive_keys(d, prefix=""): + keys = set() + if isinstance(d, dict): + for k, v in d.items(): + full_key = f"{prefix}.{k}" if prefix else k + keys.add(full_key) + keys.update(recursive_keys(v, full_key)) + return keys + + +def compare_application_keys(applications, defaults, source_file): + errors = [] + for app_id, app_conf in applications.items(): + if app_id not in defaults: + errors.append(f"{source_file}: Unknown application '{app_id}' (not in defaults_applications)") + continue + + default_conf = defaults.get(app_id, {}) + app_keys = recursive_keys(app_conf) + default_keys = recursive_keys(default_conf) + + for key in app_keys: + if key.startswith("credentials."): + continue # explicitly ignore credentials + if key not in default_keys: + errors.append(f"{source_file}: Missing default for {app_id}: {key}") + return errors + + +def load_inventory_files(inventory_dir): + all_data = {} + inventory_path = Path(inventory_dir) + + for path in inventory_path.glob("*.yml"): + data = load_yaml_file(path) + if isinstance(data, dict): + applications = data.get("applications") or data.get("defaults_applications") + if applications: + all_data[path] = applications + + for vars_folder in inventory_path.glob("*_vars"): + if vars_folder.is_dir(): + for subfile in vars_folder.rglob("*.yml"): + data = load_yaml_file(subfile) + if isinstance(data, dict): + applications = data.get("applications") or data.get("defaults_applications") + if applications: + all_data[subfile] = applications + + return all_data + + +def main(): + parser = argparse.ArgumentParser(description="Verify application variable consistency with defaults.") + parser.add_argument("inventory_dir", help="Path to inventory directory (contains inventory.yml and *_vars/") + parser.add_argument("--defaults", default="group_vars/all/04_applications.yml", help="Path to defaults_applications file") + args = parser.parse_args() + + defaults_data = load_yaml_file(args.defaults) + defaults = defaults_data.get("defaults_applications", {}) if defaults_data else {} + + if not defaults: + print("Error: No 'defaults_applications' found in defaults file.", file=sys.stderr) + sys.exit(1) + + all_errors = [] + inventory_files = load_inventory_files(args.inventory_dir) + for source_path, app_data in inventory_files.items(): + errors = compare_application_keys(app_data, defaults, str(source_path)) + all_errors.extend(errors) + + if all_errors: + print("Validation failed with the following issues:") + for err in all_errors: + print("-", err) + sys.exit(1) + else: + print("Inventory directory is valid against defaults.") + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file