#!/usr/bin/env python # (c) 2019 Cr@ns # Authors : Alexandre IOOSS # Based on cranspasswords by : Daniel Stan # Vincent Le Gallic # # This file is part of Cr@ns ansible deployment """ Ansible Vault CransPasswords script. ======================================== Returns Ansible variables gpg encrypted and stored within cranspasswords. See https://gitlab.crans.org/nounous/cranspasswords Configuration is read from `vault_cranspasswords.ini`. """ import json import os import subprocess import sys from ansible.errors import AnsibleError, AnsibleParserError from ansible.module_utils._text import to_native from ansible.module_utils.six.moves import configparser from ansible.plugins.vars import BaseVarsPlugin DOCUMENTATION = ''' module: vault_cranspasswords vars: vault_cranspasswords version_added: "2.7" short_description: In charge of loading variables stored within cranspasswords description: - Works exactly as a vault, loading variables from cranspasswords. - 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. options: {} ''' class VarsModule(BaseVarsPlugin): @staticmethod def gpg_decrypt(crypt_text): """ Decrypt the text in argument using gpg. """ full_command = ['gpg', '-d'] proc = subprocess.Popen(full_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr, close_fds=True) proc.stdin.write(crypt_text.encode()) proc.stdin.close() clear_text = proc.stdout.read().decode() return clear_text def getfile_command(self, filename): """ Run the command on the remote cranspasswords server, and return the output. """ # Get full command from settings file try: command = self.config.get('cranspasswords', 'server_cmd').split(" ") except configparser.NoSectionError as e: raise AnsibleParserError(to_native(e)) command.append("getfiles") proc = subprocess.Popen( command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=sys.stderr, close_fds=True ) proc.stdin.write(json.dumps([filename]).encode()) proc.stdin.flush() raw_out, raw_err = proc.communicate() ret = proc.returncode if ret != 0: raise AnsibleError("Bad return code on the serveur side") try: answer = json.loads(raw_out.strip()) return answer[0] except ValueError: raise AnsibleError("Unable to parse the result") def get_encrypted(self, filename): """ Get encrypted content of a cranspasswords file """ gotit, value = self.getfile_command(filename) # if not gotit, value contains the error message if not gotit: raise AnsibleError("Unable to get the file : {}".format(to_native(value))) else: crypt_text = value['contents'] return crypt_text def __init__(self): super().__init__() # Load config self.config = configparser.ConfigParser() self.config.read(os.path.dirname(os.path.realpath(__file__)) + '/vault_cranspasswords.ini') 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). super().get_vars(loader, path, entities) if 'cranspasswords' not in loader._FILE_CACHE: # Get text then decrypt and return crypt_text = self.get_encrypted('ansible_vault') clear_text = self.gpg_decrypt(crypt_text) data = loader.load(clear_text) loader._FILE_CACHE['cranspasswords'] = data else: data = loader._FILE_CACHE['cranspasswords'] return data