From 9033145285007d77c6986e326fadcd34b7139dce Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Fri, 19 May 2017 19:18:51 +0100 Subject: [PATCH] Support updates of the kolla passwords.yml file This also adds support for vault encryption of the passwords, allowing them to be safely stored with other kayobe configuration. --- ansible/group_vars/all/kolla | 3 + ansible/roles/kolla-ansible/defaults/main.yml | 3 + .../kolla-ansible/library/kolla_passwords.py | 200 ++++++++++++++++++ ansible/roles/kolla-ansible/tasks/config.yml | 36 ++-- 4 files changed, 218 insertions(+), 24 deletions(-) create mode 100644 ansible/roles/kolla-ansible/library/kolla_passwords.py diff --git a/ansible/group_vars/all/kolla b/ansible/group_vars/all/kolla index 2c61a9985..df0ed7854 100644 --- a/ansible/group_vars/all/kolla +++ b/ansible/group_vars/all/kolla @@ -149,6 +149,9 @@ kolla_build_customizations: {} ############################################################################### # Kolla-ansible configuration. +# Password to use to encrypt the kolla-ansible passwords.yml file. +kolla_ansible_vault_password: "{{ ansible_env.KAYOBE_VAULT_PASSWORD }}" + # Whether TLS is enabled for the external API endpoints. kolla_enable_tls_external: "no" diff --git a/ansible/roles/kolla-ansible/defaults/main.yml b/ansible/roles/kolla-ansible/defaults/main.yml index 2f29c5385..5541ec43d 100644 --- a/ansible/roles/kolla-ansible/defaults/main.yml +++ b/ansible/roles/kolla-ansible/defaults/main.yml @@ -15,6 +15,9 @@ kolla_ansible_source_version: # Virtualenv directory where Kolla will be installed. kolla_venv: "{{ ansible_env['PWD'] }}/kolla-venv" +# Password to use to encrypt the passwords.yml file. +kolla_ansible_vault_password: + # Directory where Kolla config files will be installed. kolla_config_path: diff --git a/ansible/roles/kolla-ansible/library/kolla_passwords.py b/ansible/roles/kolla-ansible/library/kolla_passwords.py new file mode 100644 index 000000000..43276a12f --- /dev/null +++ b/ansible/roles/kolla-ansible/library/kolla_passwords.py @@ -0,0 +1,200 @@ +#!/usr/bin/python + +# Copyright (c) 2017 StackHPC Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +DOCUMENTATION = ''' +--- +module: kolla_passwords +short_description: Generates a kolla-ansible passwords file +''' + +from ansible.module_utils.basic import * + +import os.path +import shutil +import tempfile + +IMPORT_ERRORS = [] +try: + import yaml +except ImportError as e: + IMPORT_ERRORS.append(e) + + +def virtualenv_path_prefix(module): + return "%s/bin" % module.params['virtualenv'] + + +def kolla_genpwd(module, file_path): + """Run the kolla-genpwd command.""" + cmd = ["kolla-genpwd", "--passwords", file_path] + module.run_command(cmd, check_rc=True, + path_prefix=virtualenv_path_prefix(module)) + + +def kolla_mergepwd(module, old_path, new_path, final_path): + """Run the kolla-mergepwd command.""" + cmd = ["kolla-mergepwd", + "--old", old_path, + "--new", new_path, + "--final", final_path] + module.run_command(cmd, check_rc=True, + path_prefix=virtualenv_path_prefix(module)) + + +def create_vault_password_file(module): + """Create a vault password file.""" + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(module.params['vault_password']) + return f.name + + +def vault_encrypt(module, file_path): + """Encrypt a file using Ansible vault""" + password_path = create_vault_password_file(module) + try: + cmd = ["ansible-vault", "encrypt", + "--vault-password-file", password_path, + file_path] + module.run_command(cmd, check_rc=True, + path_prefix=virtualenv_path_prefix(module)) + finally: + os.unlink(password_path) + + +def vault_decrypt(module, file_path): + """Decrypt a file using Ansible vault""" + password_path = create_vault_password_file(module) + try: + cmd = ["ansible-vault", "decrypt", + "--vault-password-file", password_path, + file_path] + module.run_command(cmd, check_rc=True, + path_prefix=virtualenv_path_prefix(module)) + finally: + os.unlink(password_path) + + +def create_named_tempfile(): + """Create a named temporary file and return its name.""" + with tempfile.NamedTemporaryFile(delete=False) as f: + temp_file_path = f.name + f.close() + return temp_file_path + + +def kolla_passwords(module): + """Generate a kolla-ansible passwords.yml file. + + We use the sample passwords.yml file as a base to determine which passwords + to generate. This gets merged with an existing passwords file if one + exists. We then apply any custom password overrides. Finally, we generate + any passwords that are missing. If requested, the final file will be + encrypted using ansible vault. + """ + if not os.path.isfile(module.params['sample']): + module.fail_json(msg="Sample passwords.yml file %s does not exist" % + module.params['sample']) + + temp_file_path = create_named_tempfile() + try: + # Start with kolla's sample password file. + shutil.copy2(module.params['sample'], temp_file_path) + + # If passwords exist, decrypt and merge these in. + if module.params['src'] and os.path.isfile(module.params['src']): + src_path = create_named_tempfile() + try: + shutil.copy2(module.params['src'], src_path) + if module.params['vault_password']: + vault_decrypt(module, src_path) + kolla_mergepwd(module, src_path, temp_file_path, temp_file_path) + finally: + os.unlink(src_path) + + # Merge in overrides. + if module.params['overrides']: + with tempfile.NamedTemporaryFile(delete=False) as f: + yaml.dump(module.params['overrides'], f) + overrides_path = f.name + try: + kolla_mergepwd(module, overrides_path, temp_file_path, temp_file_path) + finally: + os.unlink(overrides_path) + + # Generate null passwords. + kolla_genpwd(module, temp_file_path) + + # Compare with the decrypted destination file. + if os.path.isfile(module.params['dest']): + if module.params['vault_password']: + dest_path = create_named_tempfile() + try: + shutil.copy2(module.params['dest'], dest_path) + vault_decrypt(module, dest_path) + checksum_dest = module.sha1(dest_path) + finally: + os.unlink(dest_path) + else: + checksum_dest = module.sha1(module.params['dest']) + checksum_temp_file = module.sha1(temp_file_path) + changed = checksum_dest != checksum_temp_file + else: + changed = True + + # Encrypt the file. + if changed and module.params['vault_password']: + vault_encrypt(module, temp_file_path) + + # Move into place. + if changed and not module.check_mode: + module.atomic_move(temp_file_path, module.params['dest']) + except Exception as e: + try: + os.unlink(temp_file_path) + finally: + module.fail_json(msg="Failed to generate kolla passwords: %s" % repr(e)) + + if not module.check_mode: + # Update the file's attributes. + file_args = module.load_file_common_arguments(module.params) + changed = module.set_fs_attributes_if_different(file_args, changed) + + return {'changed': changed} + + +def main(): + module = AnsibleModule( + argument_spec = dict( + dest=dict(default='/etc/kolla/passwords.yml', type='str'), + overrides=dict(default={}, type='dict'), + sample=dict(default='/usr/share/kolla-ansible/etc_examples/kolla/passwords.yml', type='str'), + src=dict(default='/etc/kolla/passwords.yml', type='str'), + vault_password=dict(type='str'), + virtualenv=dict(type='str'), + ), + add_file_common_args=True, + supports_check_mode=True, + ) + + if IMPORT_ERRORS: + errors = ", ".join([repr(e) for e in IMPORT_ERRORS]) + module.fail_json(msg="Failed to import modules: %s" % errors) + + result = kolla_passwords(module) + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/ansible/roles/kolla-ansible/tasks/config.yml b/ansible/roles/kolla-ansible/tasks/config.yml index d7d9c42ed..97f7abcf6 100644 --- a/ansible/roles/kolla-ansible/tasks/config.yml +++ b/ansible/roles/kolla-ansible/tasks/config.yml @@ -22,30 +22,18 @@ - { src: overcloud.j2, dest: inventory/overcloud } - { src: globals.yml.j2, dest: globals.yml } -- name: Check whether the Kolla passwords file exists - stat: - path: "{{ kolla_config_path }}/passwords.yml" - get_checksum: False - get_md5: False - mime: False - register: kolla_passwords_stat +- name: Ensure the Kolla passwords file exists + kolla_passwords: + src: "{{ kayobe_config_path }}/kolla/passwords.yml" + dest: "{{ kayobe_config_path }}/kolla/passwords.yml" + mode: 0640 + sample: "{{ kolla_ansible_install_dir }}/etc_examples/kolla/passwords.yml" + overrides: "{{ kolla_ansible_custom_passwords }}" + vault_password: "{{ kolla_ansible_vault_password }}" + virtualenv: "{{ kolla_venv or omit }}" -- name: Generate Kolla passwords - shell: > - cp {{ kolla_ansible_install_dir }}/etc_examples/kolla/passwords.yml {{ kolla_config_path }}/passwords.yml.generated - && chmod 640 {{ kolla_config_path }}/passwords.yml.generated - && {{ kolla_venv }}/bin/kolla-genpwd -p {{ kolla_config_path }}/passwords.yml.generated - && mv {{ kolla_config_path }}/passwords.yml.generated {{ kolla_config_path }}/passwords.yml - when: not kolla_passwords_stat.stat.exists - -- name: Read the Kolla passwords file - slurp: - src: "{{ kolla_config_path }}/passwords.yml" - register: passwords_result - when: "{{ kolla_ansible_custom_passwords }}" - -- name: Ensure the Kolla passwords file contains the required custom passwords +- name: Ensure the Kolla passwords file is copied into place copy: - content: "{{ passwords_result.content | b64decode | from_yaml | combine(kolla_ansible_custom_passwords) | to_nice_yaml }}" + src: "{{ kayobe_config_path }}/kolla/passwords.yml" dest: "{{ kolla_config_path }}/passwords.yml" - when: "{{ kolla_ansible_custom_passwords }}" + remote_src: True