Created
August 5, 2025 23:03
-
-
Save ellisgeek/b6be9797f8b90669981eaf786f5a445b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Ansible Vault password client script that returns the vault password for a given vault ID. | |
| This script provides a flexible system for retrieving Ansible vault passwords using | |
| different backend implementations. Each backend must inherit from VaultPasswordBackend | |
| and implement the required interface. | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| from typing import Optional, Dict, Type, Set | |
| class VaultPasswordBackend: | |
| """Base class for vault password backends. | |
| This class defines the interface that all password backend implementations must follow. | |
| Subclasses should override the class attributes and methods as needed. | |
| Attributes: | |
| name: The unique identifier for this backend type. | |
| description: A human-readable description of what this backend does. | |
| config_help: Additional configuration help text for this backend. | |
| """ | |
| name: Optional[str] = None | |
| description: Optional[str] = None | |
| config_help: Optional[str] = None | |
| def get_password(self, vault_id: str) -> Optional[str]: | |
| """Retrieve the vault password for the given vault ID. | |
| Args: | |
| vault_id: The identifier for the vault password to retrieve. | |
| Returns: | |
| The password string if found, None otherwise. | |
| Raises: | |
| NotImplementedError: This is an abstract method that must be implemented. | |
| """ | |
| raise NotImplementedError | |
| def get_missing_password_message(self, vault_id: str) -> str: | |
| """Generate an error message for when a password cannot be found. | |
| Args: | |
| vault_id: The vault ID that was searched for. | |
| Returns: | |
| A human-readable error message explaining why the password couldn't be found. | |
| """ | |
| return f"Vault password not found for vault ID '{vault_id}'" | |
| class EnvVarBackend(VaultPasswordBackend): | |
| """Backend that retrieves vault passwords from environment variables. | |
| This backend looks for passwords in environment variables following the pattern | |
| ANSIBLE_VAULT_PASSWORD_<VAULT_ID>. The prefix can be customized during initialization. | |
| Attributes: | |
| name: The identifier for this backend ('env'). | |
| description: Human-readable description of this backend's functionality. | |
| config_help: Configuration instructions for this backend. | |
| prefix: The environment variable prefix used when looking up passwords. | |
| """ | |
| name: str = "env" | |
| description: str = "Fetches the password from an environment variable." | |
| config_help: str = ( | |
| " - The environment variable name is constructed as ANSIBLE_VAULT_PASSWORD_<VAULT_ID>.\n" | |
| " - You can override the prefix by modifying the EnvVarBackend class (default: ANSIBLE_VAULT_PASSWORD_)." | |
| ) | |
| def __init__(self, prefix: str = "ANSIBLE_VAULT_PASSWORD_") -> None: | |
| """Initialize the backend with the given environment variable prefix. | |
| Args: | |
| prefix: The prefix to use when constructing environment variable names. | |
| Defaults to "ANSIBLE_VAULT_PASSWORD_". | |
| """ | |
| self.prefix: str = prefix | |
| def get_password(self, vault_id: str) -> Optional[str]: | |
| """Retrieve a vault password from an environment variable. | |
| The environment variable name is constructed by appending the uppercase vault_id | |
| to the configured prefix. | |
| Args: | |
| vault_id: The vault ID to look up. | |
| Returns: | |
| The password if found in the environment, None otherwise. | |
| """ | |
| env_var = f"{self.prefix}{vault_id.upper()}" | |
| val = os.environ.get(env_var) | |
| if not val: | |
| env_var = env_var.replace("-","_") | |
| val = os.environ.get(env_var) | |
| return val | |
| def get_missing_password_message(self, vault_id: str) -> str: | |
| """Generate a detailed error message when a password is not found. | |
| Args: | |
| vault_id: The vault ID that was searched for. | |
| Returns: | |
| An error message including the environment variable that was checked. | |
| """ | |
| env_var = f"{self.prefix}{vault_id.upper()}" | |
| return f"Vault password not found for vault ID '{vault_id}' (env var '{env_var}' or '{env_var.replace('-','_')}')" | |
| def get_all_backends() -> Dict[str, Type[VaultPasswordBackend]]: | |
| """Dynamically discover and register all available password backend implementations. | |
| This function recursively finds all subclasses of VaultPasswordBackend that have | |
| a name attribute defined, and maps their names to the implementing classes. | |
| Returns: | |
| A dictionary mapping backend names to their implementing classes. | |
| """ | |
| def all_subclasses(cls: Type) -> Set[Type]: | |
| """Recursively find all subclasses of a class. | |
| Args: | |
| cls: The base class to find subclasses of. | |
| Returns: | |
| A set of all subclass types found. | |
| """ | |
| subclasses = set(cls.__subclasses__()) | |
| for subclass in cls.__subclasses__(): | |
| subclasses.update(all_subclasses(subclass)) | |
| return subclasses | |
| return { | |
| backend_cls.name: backend_cls | |
| for backend_cls in all_subclasses(VaultPasswordBackend) | |
| if getattr(backend_cls, 'name', None) | |
| } | |
| BACKENDS: Dict[str, Type[VaultPasswordBackend]] = get_all_backends() | |
| def build_description() -> str: | |
| """Build the help text description including backend-specific documentation. | |
| This function generates comprehensive help text that includes: | |
| - Documentation for each available backend | |
| - Example usage instructions | |
| Returns: | |
| A formatted string containing the complete help text. | |
| """ | |
| desc = [ | |
| "\n", | |
| "Currently implemented backends:" | |
| ] | |
| for name, backend_cls in BACKENDS.items(): | |
| desc.append(f" {name}: {backend_cls.description}") | |
| if backend_cls.config_help: | |
| for line in backend_cls.config_help.split("\n"): | |
| desc.append(f" {line}") | |
| desc.append("\nExample usage:") | |
| desc.append(" ./vault_password_client.py --vault-id <vault_id> [--backend <backend>]") | |
| return "\n".join(desc) | |
| def main() -> None: | |
| """Entry point for the vault password client script. | |
| Exit codes: | |
| 0: Success - password was found and printed to stdout | |
| 1: Password not found - backend-specific error message printed to stderr | |
| 2: Invalid backend specified - error message printed to stderr | |
| """ | |
| parser = argparse.ArgumentParser( | |
| description="Return the Ansible Vault password for a given vault ID using a selectable backend.", | |
| epilog=build_description(), | |
| formatter_class=argparse.RawDescriptionHelpFormatter | |
| ) | |
| parser.add_argument( | |
| '--vault-id', '-i', | |
| required=True, | |
| help="The vault ID to fetch the password for." | |
| ) | |
| parser.add_argument( | |
| '--backend', '-b', | |
| default='env', | |
| choices=list(BACKENDS.keys()), | |
| help=f"Password backend to use (default: env). Choices: {', '.join(BACKENDS.keys())}." | |
| ) | |
| args = parser.parse_args() | |
| backend_cls = BACKENDS.get(args.backend) | |
| if backend_cls is None: | |
| print(f"Unsupported backend: {args.backend}", file=sys.stderr) | |
| sys.exit(2) | |
| backend = backend_cls() | |
| password = backend.get_password(args.vault_id) | |
| if password is None: | |
| msg = backend.get_missing_password_message(args.vault_id) | |
| print(msg, file=sys.stderr) | |
| sys.exit(1) | |
| print(password) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment