diff --git a/lib/ansible/modules/crypto/openssl_dhparam.py b/lib/ansible/modules/crypto/openssl_dhparam.py new file mode 100644 index 0000000000..abc67767e1 --- /dev/null +++ b/lib/ansible/modules/crypto/openssl_dhparam.py @@ -0,0 +1,232 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Thom Wiggers +# 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: openssl_dhparam +author: "Thom Wiggers (@thomwiggers)" +version_added: "2.5" +short_description: Generate OpenSSL Diffie-Hellman Parameters +description: + - "This module allows one to (re)generate OpenSSL DH-params. + This module uses file common arguments to specify generated file permissions." +requirements: + - OpenSSL +options: + state: + required: false + default: "present" + choices: [ present, absent ] + description: + - Whether the parameters should exist or not, + taking action if the state is different from what is stated. + size: + required: false + default: 4096 + description: + - Size (in bits) of the generated DH-params + force: + required: false + default: False + choices: [ True, False ] + description: + - Should the parameters be regenerated even it it already exists + path: + required: true + description: + - Name of the file in which the generated parameters will be saved. +extends_documentation_fragment: files +''' + +EXAMPLES = ''' +# Generate Diffie-Hellman parameters with the default size (4096 bits) +- openssl_dhparam: + path: /etc/ssl/dhparams.pem + +# Generate DH Parameters with a different size (2048 bits) +- openssl_dhparam: + path: /etc/ssl/dhparams.pem + size: 2048 + +# Force regenerate an DH parameters if they already exist +- openssl_dhparam: + path: /etc/ssl/dhparams.pem + force: True + +''' + +RETURN = ''' +size: + description: Size (in bits) of the Diffie-Hellman parameters + returned: changed or success + type: int + sample: 4096 +filename: + description: Path to the generated Diffie-Hellman parameters + returned: changed or success + type: string + sample: /etc/ssl/dhparams.pem +''' + +import os +import re +import tempfile + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + + +class DHParameterError(Exception): + pass + + +class DHParameter(object): + + def __init__(self, module): + self.state = module.params['state'] + self.path = module.params['path'] + self.size = int(module.params['size']) + self.force = module.params['force'] + self.changed = False + self.openssl_bin = module.get_bin_path('openssl', True) + + def generate(self, module): + """Generate a keypair.""" + changed = False + + # ony generate when necessary + if self.force or not self._check_params_valid(module): + # create a tempfile + fd, tmpsrc = tempfile.mkstemp() + os.close(fd) + module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit + # openssl dhparam -out + command = [self.openssl_bin, 'dhparam', '-out', tmpsrc, str(self.size)] + rc, dummy, err = module.run_command(command, check_rc=False) + if rc != 0: + raise DHParameterError(to_native(err)) + try: + module.atomic_move(tmpsrc, self.path) + except Exception as e: + module.fail_json(msg="Failed to write to file %s: %s" % (self.path, str(e))) + changed = True + + # fix permissions (checking force not necessary as done above) + if not self._check_fs_attributes(module): + # Fix done implicitly by + # AnsibleModule.set_fs_attributes_if_different + changed = True + + self.changed = changed + + def check(self, module): + """Ensure the resource is in its desired state.""" + if self.force: + return False + return self._check_params_valid(module) and self._check_fs_attributes(module) + + def _check_params_valid(self, module): + """Check if the params are in the correct state""" + command = [self.openssl_bin, 'dhparam', '-check', '-text', '-noout', '-in', self.path] + rc, out, err = module.run_command(command, check_rc=False) + result = to_native(out) + if rc != 0: + # If the call failed the file probably doesn't exist or is + # unreadable + return False + # output contains "(xxxx bit)" + match = re.search(r"Parameters:\s+\((\d+) bit\).*", result) + if not match: + return False # No "xxxx bit" in output + else: + bits = int(match.group(1)) + + # if output contains "WARNING" we've got a problem + if "WARNING" in result or "WARNING" in to_native(err): + return False + + return bits == self.size + + def _check_fs_attributes(self, module): + """Checks (and changes if not in check mode!) fs attributes""" + file_args = module.load_file_common_arguments(module.params) + attrs_changed = module.set_fs_attributes_if_different(file_args, False) + + return not attrs_changed + + def dump(self): + """Serialize the object into a dictionary.""" + + result = { + 'size': self.size, + 'filename': self.path, + 'changed': self.changed, + } + return result + + +def main(): + """Main function""" + + module = AnsibleModule( + argument_spec=dict( + state=dict(default='present', choices=['present', 'absent'], type='str'), + size=dict(default=4096, type='int'), + force=dict(default=False, type='bool'), + path=dict(required=True, type='path'), + ), + supports_check_mode=True, + add_file_common_args=True, + ) + + base_dir = os.path.dirname(module.params['path']) + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg='The directory %s does not exist or the file is not a directory' % base_dir + ) + + dhparam = DHParameter(module) + + if dhparam.state == 'present': + + if module.check_mode: + result = dhparam.dump() + result['changed'] = module.params['force'] or not dhparam.check(module) + module.exit_json(**result) + + try: + dhparam.generate(module) + except DHParameterError as exc: + module.fail_json(msg=to_native(exc)) + else: + + if module.check_mode: + result = dhparam.dump() + result['changed'] = os.path.exists(module.params['path']) + module.exit_json(**result) + + try: + os.remove(module.params['path']) + except OSError as exc: + module.fail_json(msg=to_native(exc)) + + result = dhparam.dump() + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/openssl_dhparam/aliases b/test/integration/targets/openssl_dhparam/aliases new file mode 100644 index 0000000000..7978f4a6ce --- /dev/null +++ b/test/integration/targets/openssl_dhparam/aliases @@ -0,0 +1,2 @@ +posix/ci/group1 +destructive diff --git a/test/integration/targets/openssl_dhparam/tasks/main.yml b/test/integration/targets/openssl_dhparam/tasks/main.yml new file mode 100644 index 0000000000..0d05dc112b --- /dev/null +++ b/test/integration/targets/openssl_dhparam/tasks/main.yml @@ -0,0 +1,44 @@ +- block: + # This module generates unsafe parameters for testing purposes + # otherwise tests would be too slow + - name: Generate parameter + openssl_dhparam: + size: 768 + path: '{{ output_dir }}/dh768.pem' + + - name: Don't regenerate parameters with no change + openssl_dhparam: + size: 768 + path: '{{ output_dir }}/dh768.pem' + register: dhparam_changed + + - name: Generate parameters with size option + openssl_dhparam: + path: '{{ output_dir }}/dh512.pem' + size: 512 + + - name: Don't regenerate parameters with size option and no change + openssl_dhparam: + path: '{{ output_dir }}/dh512.pem' + size: 512 + register: dhparam_changed_512 + + - copy: + src: '{{ output_dir }}/dh768.pem' + remote_src: yes + dest: '{{ output_dir }}/dh512.pem' + + - name: Re-generate if size is different + openssl_dhparam: + path: '{{ output_dir }}/dh512.pem' + size: 512 + register: dhparam_changed_to_512 + + - name: Force re-generate parameters with size option + openssl_dhparam: + path: '{{ output_dir }}/dh512.pem' + size: 512 + force: yes + register: dhparam_changed_force + + - import_tasks: ../tests/validate.yml diff --git a/test/integration/targets/openssl_dhparam/tests/validate.yml b/test/integration/targets/openssl_dhparam/tests/validate.yml new file mode 100644 index 0000000000..f321ca7a5c --- /dev/null +++ b/test/integration/targets/openssl_dhparam/tests/validate.yml @@ -0,0 +1,32 @@ +--- +- name: Validate generated params + shell: 'openssl dhparam -in {{ output_dir }}/{{ item }}.pem -noout -check' + with_items: + - dh768 + - dh512 + +- name: Get bit size of 768 + shell: 'openssl dhparam -noout -in {{ output_dir }}/dh768.pem -text | head -n1 | sed -ne "s@.*(\\([[:digit:]]\{1,\}\\) bit).*@\\1@p"' + register: bit_size_dhparam + +- name: Check bit size of default + assert: + that: + - bit_size_dhparam.stdout == "768" + +- name: Get bit size of 512 + shell: 'openssl dhparam -noout -in {{ output_dir }}/dh512.pem -text | head -n1 | sed -ne "s@.*(\\([[:digit:]]\{1,\}\\) bit).*@\\1@p"' + register: bit_size_dhparam_512 + +- name: Check bit size of default + assert: + that: + - bit_size_dhparam_512.stdout == "512" + +- name: Check if changed works correctly + assert: + that: + - dhparam_changed is not changed + - dhparam_changed_512 is not changed + - dhparam_changed_to_512 is changed + - dhparam_changed_force is changed