#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: (c) 2020, Yo # Use ansible-vault python library from Tomohiro Nakamura (Prefer v2) # Vastly inspired by the lineinfile module # # v.0.5 19/05/2020 # # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: putinvault short_description: Manage Ansible vault file description: - Manages passwords in Ansible Vault files version_added: "0.1" options: path: description: - Ansible vault File path type: path required: true aliases: [ name, vault ] password: description: - The password to access the vault. - Required for C(state=present). type: str required: true aliases: [ vault_pass ] key: description: - The password name to insert/replace into the vault file. - Required for C(state=present). type: str required: true value: description: - The password value to insert/replace into the vault file. - Required for C(state=present). type: str state: description: - Whether the password should be there or not. type: str choices: [ absent, present ] default: present replace: description: - Used with C(state=present). - If specified and the password already exist in vault file, it will be overwritten with value. - By default it will fail if password exist in vault. type: bool default: no aliases: [ overwrite ] strict: description: - Used with C(state=absent). - If specified to no, remove password whatever value is. - By default it will remove password only when key and value match specified. type: bool default: yes create: description: - Used with C(state=present). - If specified, the vault file will be created if it does not already exist. - By default it will fail if the vault file is missing. type: bool default: no backup: description: - Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly. type: bool default: no author: - Johan Vaucourt (johan.vaucourt@gmail.com) ''' EXAMPLES = ''' - name: Add a new password to a vault if the file does not exist putinvault: path: /tmp/vault.yml key: 'vaulted_ldap_admin_password' value: 'admin123' create: yes - name: Add a password to an existing vault. If the file does not exist or key already exists, it will trig an error putinvault: path: vault.yml password: 'vault123' key: 'vaulted_ldap_reader_password' value: 'read123' - name: Overwrite LDAP admin password putinvault: path: vault.yml key: 'vaulted_ldap_admin_password' value: 'newadmin123' replace: yes ''' import os import re import tempfile from ansible_vault import Vault # import module snippets from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_bytes, to_native def check_file_attrs(module, changed, message, diff): file_args = module.load_file_common_arguments(module.params) if module.set_fs_attributes_if_different(file_args, False, diff=diff): if changed: message += " and " changed = True message += "ownership, perms or SE linux context changed" return message, changed def present(module, dest, password, key, value, replace, create, backup): diff = {'before': '', 'after': '', 'before_header': '%s (content)' % dest, 'after_header': '%s (content)' % dest} b_dest = to_bytes(dest, errors='surrogate_or_strict') if not os.path.exists(b_dest): if not create: module.fail_json(rc=257, msg='Destination %s does not exist !' % dest) b_destpath = os.path.dirname(b_dest) if b_destpath and not os.path.exists(b_destpath) and not module.check_mode: try: os.makedirs(b_destpath) except Exception as e: module.fail_json(msg='Error creating %s Error code: %s Error description: %s' % (b_destpath, e[0], e[1])) vault = Vault(password) v_data = {} else: # TODO : Manage erroneous password vault = Vault(password) with open(b_dest, 'rb') as f: v_data = vault.load(f.read()) if module._diff: diff['before'] = to_native(v_data) msg ='' changed = False if key in v_data: if replace == False: module.warn( "The password already exist in vault. If you want to replace it disregarding it current value, set replace to True.") pass else: v_data[key] = value msg = 'line replaced' changed = True else: v_data[key] = value msg = 'line added' changed = True if module._diff: diff['after'] = to_native(v_data) backupdest = "" if changed and not module.check_mode: if backup and os.path.exists(b_dest): backupdest = module.backup_local(dest) vault.dump(v_data, open(b_dest, 'w')) if module.check_mode and not os.path.exists(b_dest): module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=diff) attr_diff = {} msg, changed = check_file_attrs(module, changed, msg, attr_diff) attr_diff['before_header'] = '%s (file attributes)' % dest attr_diff['after_header'] = '%s (file attributes)' % dest difflist = [diff, attr_diff] module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=difflist) def absent(module, dest, password, key, value, strict, backup): b_dest = to_bytes(dest, errors='surrogate_or_strict') if not os.path.exists(b_dest): module.exit_json(changed=False, msg="file not present") msg = '' changed = False diff = {'before': '', 'after': '', 'before_header': '%s (content)' % dest, 'after_header': '%s (content)' % dest} # TODO : Manage erroneous password vault = Vault(password) with open(b_dest, 'rb') as f: v_data = vault.load(f.read()) if module._diff: diff['before'] = to_native(v_data) if key in v_data: if strict == True: if v_data[key] == value: del v_data[key] changed = True else: module.fail_json(msg='value should match what is in vault with strict=true') else: del v_data[key] changed = True if module._diff: diff['after'] = to_native(v_data) backupdest = "" if changed and not module.check_mode: if backup: backupdest = module.backup_local(dest) vault.dump(v_data, open(b_dest, 'w')) if changed: msg = "Password removed" attr_diff = {} msg, changed = check_file_attrs(module, changed, msg, attr_diff) attr_diff['before_header'] = '%s (file attributes)' % dest attr_diff['after_header'] = '%s (file attributes)' % dest difflist = [diff, attr_diff] module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=difflist) def main(): module = AnsibleModule( argument_spec=dict( path=dict(type='path', required=True, aliases=['dest', 'destfile', 'name']), password=dict(type='str', required=True, aliases=['vault_pass']), state=dict(type='str', default='present', choices=['absent', 'present']), key=dict(type='str', required=True), value=dict(type='str'), replace=dict(type='bool', aliases=['overwrite'], default=False), strict=dict(type='bool', default=True), create=dict(type='bool', default=False), backup=dict(type='bool', default=False), ), add_file_common_args=True, supports_check_mode=True, ) params = module.params path = params['path'] password = params['password'] state = params['state'] key = params['key'] value = params['value'] replace = params['replace'] strict = params['strict'] create = params['create'] backup = params['backup'] b_path = to_bytes(path, errors='surrogate_or_strict') if os.path.isdir(b_path): module.fail_json(rc=256, msg='Path %s is a directory !' % path) if params['state'] == 'present': if key is None or value is None: module.fail_json(msg='key and value are required with state=present') present(module, path, password, key, value, replace, create, backup) else: if value is None and strict is True: module.fail_json(msg='value is required with strict=True') absent(module, path, password, key, value, strict, backup) if __name__ == '__main__': main()