From e32b0aa0fa624545ebc7849b8e2dac4faa611fab Mon Sep 17 00:00:00 2001 From: Bombar Maxime Date: Wed, 15 Apr 2020 02:39:16 +0200 Subject: [PATCH] [vault_cranspasswords] Documentation and formatting --- vars_plugins/vault_cranspassword.py | 112 -------------- ...spassword.ini => vault_cranspasswords.ini} | 4 +- vars_plugins/vault_cranspasswords.py | 139 ++++++++++++++++++ 3 files changed, 141 insertions(+), 114 deletions(-) delete mode 100755 vars_plugins/vault_cranspassword.py rename vars_plugins/{vault_cranspassword.ini => vault_cranspasswords.ini} (75%) create mode 100755 vars_plugins/vault_cranspasswords.py diff --git a/vars_plugins/vault_cranspassword.py b/vars_plugins/vault_cranspassword.py deleted file mode 100755 index 886b69f6..00000000 --- a/vars_plugins/vault_cranspassword.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/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 deploiement - -""" -Ansible Vault CransPassword script. -======================================== - -Returns Ansible vault from CransPassword. - -Configuration is read from `vault_cranspassword.ini`. -""" - -import json -import os -import subprocess -import sys - -from ansible.module_utils.six.moves import configparser -from ansible.plugins.vars import BaseVarsPlugin - - -class VarsModule(BaseVarsPlugin): - @staticmethod - def gpg_decrypt(crypt_text): - 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): - """Exécute la commande distante, et retourne la sortie de cette - commande""" - # Get full command from settings file - command = self.config.get('cranspassword', 'server_cmd').split(" ") - 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: - print("Mauvais code retour côté serveur", file=sys.stderr) - sys.exit(ret) - - try: - answer = json.loads(raw_out.strip()) - except ValueError: - print("Impossible de parser le résultat", file=sys.stderr) - sys.exit(42) - - return answer[0] - - def get_encrypted(self, filename): - """ - Get encrypted content of a cranspassword file - """ - gotit, value = self.getfile_command(filename) - if not gotit: - print(value, file=sys.stderr) # value contient le message d'erreur - 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_cranspassword.ini') - - def get_vars(self, loader, path, entities, cache=True): - """ - Get all vars for entities, called by Ansible - """ - super().get_vars(loader, path, entities) - - # We do not want to request N time the same file from cranspassword - # But VarsModule object get instanced each time - # So the hack is to use loader._FILE_CACHE that *should* be private - # Sorry for this, don't judge me on this please <3 - - if 'cranspassword' 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['cranspassword'] = data - else: - data = loader._FILE_CACHE['cranspassword'] - - return data diff --git a/vars_plugins/vault_cranspassword.ini b/vars_plugins/vault_cranspasswords.ini similarity index 75% rename from vars_plugins/vault_cranspassword.ini rename to vars_plugins/vault_cranspasswords.ini index 2df78e29..97db11c4 100644 --- a/vars_plugins/vault_cranspassword.ini +++ b/vars_plugins/vault_cranspasswords.ini @@ -1,6 +1,6 @@ -# Ansible Vault CransPassword settings +# Ansible Vault CransPasswords settings # -[cranspassword] +[cranspasswords] #: Commande exécutée sur le client pour appeler le script sur le serveur distant. server_cmd=/usr/bin/ssh odlyd.crans.org sudo -n /usr/local/bin/cranspasswords-server diff --git a/vars_plugins/vault_cranspasswords.py b/vars_plugins/vault_cranspasswords.py new file mode 100755 index 00000000..9f5c6ddb --- /dev/null +++ b/vars_plugins/vault_cranspasswords.py @@ -0,0 +1,139 @@ +#!/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