#!/usr/bin/env python from functools import lru_cache from getpass import getpass import os from pathlib import Path import subprocess import sys from ansible.module_utils.six.moves import configparser from ansible.plugins.vars import BaseVarsPlugin DOCUMENTATION = """ module: pass vars: vault version_added: 2.9 short_description: Load vault passwords from pass description: - Works exactly as a vault, loading variables from pass. - Decrypts the YAML file `ansible_vault` from cranspasswords. - Loads the secret variables. - Makes use of data caching in order to avoid calling cranspasswords multiple times. - Uses the local gpg key from the user running ansible on the Control node. """ class VarsModule(BaseVarsPlugin): @staticmethod @lru_cache def decrypt_password(name, crans_submodule=False): """ Passwords are decrypted from the local password store, then are cached. By that way, we don't decrypt these passwords everytime. """ # Load config config = configparser.ConfigParser() config.read(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'pass.ini')) password_store = Path(config.get('pass', 'password_store_dir', fallback=os.getenv('PASSWORD_STORE_DIR', Path.home() / '.password-store'))) if crans_submodule: password_store /= config.get('pass', 'crans_password_store_submodule', fallback=os.getenv('CRANS_PASSWORD_STORE_SUBMODULE', 'crans')) full_command = ['gpg', '-d', password_store / f'{name}.gpg'] proc = subprocess.run(full_command, capture_output=True, close_fds=True) clear_text = proc.stdout.decode('UTF-8') sys.stderr.write(proc.stderr.decode('UTF-8')) return clear_text @staticmethod @lru_cache def become_password(entity): """ Query the become password that should be used for the given entity. If entity is the whole group that has no default password, the become password will be prompted. The configuration should be given in pass.ini, in the `pass_become` group. You have only to write `group=pass-filename`. """ # Load config config = configparser.ConfigParser() config.read(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'pass.ini')) if config.has_option('pass_become', entity.get_name()): return VarsModule.decrypt_password( config.get('pass_become', entity.get_name())).split('\n')[0] if entity.get_name() == "all": return getpass("BECOME password: ", stream=None) return None def get_vars(self, loader, path, entities): """ Get all vars for entities, called by Ansible. loader: Ansible's DataLoader. path: Current play's playbook directory. entities: Host or group names pertinent to the variables needed. """ # VarsModule objects are called every time you need host vars, per host, # and per group the host is part of. # It is about 6 times per host per task in current state # of Ansible Crans configuration. # It is way to much. # So we cache the data into the DataLoader (see parsing/DataLoader). passwords = {} for entity in entities: # Load vault passwords if entity.get_name() == 'all': passwords['vault'] = loader.load( VarsModule.decrypt_password('ansible_vault', True)) # Load become password become_password = VarsModule.become_password(entity) if become_password is not None: passwords['ansible_become_password'] = become_password return passwords