From f9a36962bd47cb7969d407b84556491bbd15be9d Mon Sep 17 00:00:00 2001
From: Yanis Guenane <yguenane@redhat.com>
Date: Fri, 27 May 2016 12:16:14 +0200
Subject: [PATCH] network: Add new module openssl_privatekey

This module aims to allow a user to manage the lifecycle of OpenSSL
private keys. Internally it relies on the pyOpenSSL python library
to interact with openssl.

A user is able to specify :

  * key size (via `size` parameter)
  * key algorithm (via `type` parameter)
  * key location (via `path` parameter)

The most simple use case is:

```
- name: Generate ansible.com.pem SSL private key
  openssl_privatekey: name=ansible.com.pem
                      path=/etc/ssl/private
```

A user can speficy more settings:

```
- name: Generate ansible.com.pem SSL private key
  openssl_privatekey: name=ansible.com.pem
                      path=/etc/ssl/private
                      size=2048
                      type=DSA
```

A user can also force the regeneration of an SSL key:

```
- name: Generate ansible.com.pem SSL private key
  openssl_privatekey: name=ansible.com.pem
                      path=/etc/ssl/private
                      force=true
```
---
 lib/ansible/modules/extras/crypto/__init__.py |   0
 .../extras/crypto/openssl_privatekey.py       | 247 ++++++++++++++++++
 .../extras/crypto/openssl_publickey.py        | 224 ++++++++++++++++
 3 files changed, 471 insertions(+)
 create mode 100644 lib/ansible/modules/extras/crypto/__init__.py
 create mode 100644 lib/ansible/modules/extras/crypto/openssl_privatekey.py
 create mode 100644 lib/ansible/modules/extras/crypto/openssl_publickey.py

diff --git a/lib/ansible/modules/extras/crypto/__init__.py b/lib/ansible/modules/extras/crypto/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/lib/ansible/modules/extras/crypto/openssl_privatekey.py b/lib/ansible/modules/extras/crypto/openssl_privatekey.py
new file mode 100644
index 00000000000..5055a2826e6
--- /dev/null
+++ b/lib/ansible/modules/extras/crypto/openssl_privatekey.py
@@ -0,0 +1,247 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+
+from ansible.module_utils.basic import *
+
+try:
+    from OpenSSL import crypto
+except ImportError:
+    pyopenssl_found = False
+else:
+    pyopenssl_found = True
+
+
+import os
+
+DOCUMENTATION = '''
+---
+module: openssl_privatekey
+author: "Yanis Guenane (@Spredzy)"
+version_added: "2.3"
+short_description: Generate OpenSSL private keys.
+description:
+    - "This module allows one to (re)generate OpenSSL private keys. It uses
+       the pyOpenSSL python library to interact with openssl. One can generate
+       either RSA or DSA private keys. Keys are generated in PEM format."
+requirements:
+    - "python-pyOpenSSL"
+options:
+    state:
+        required: false
+        default: "present"
+        choices: [ present, absent ]
+        description:
+            - Whether the private key 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 TLS/SSL key to generate
+    type:
+        required: false
+        default: "RSA"
+        choices: [ RSA, DSA ]
+        description:
+            - The algorithm used to generate the TLS/SSL private key
+    force:
+        required: false
+        default: False
+        choices: [ True, False ]
+        description:
+            - Should the key be regenerated even it it already exists
+    path:
+        required: true
+        description:
+            - Name of the file in which the generated TLS/SSL private key will be written. It will have 0600 mode.
+'''
+
+EXAMPLES = '''
+# Generate an OpenSSL private key with the default values (4096 bits, RSA)
+# and no public key
+- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem
+
+# Generate an OpenSSL private key with a different size (2048 bits)
+- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem size=2048
+
+# Force regenerate an OpenSSL private key if it already exists
+- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem force=True
+
+# Generate an OpenSSL private key with a different algorithm (DSA)
+- openssl_privatekey: path=/etc/ssl/private/ansible.com.pem type=DSA
+'''
+
+RETURN = '''
+size:
+    description: Size (in bits) of the TLS/SSL private key
+    returned:
+        - changed
+        - success
+    type: integer
+    sample: 4096
+type:
+    description: Algorithm used to generate the TLS/SSL private key
+    returned:
+        - changed
+        - success
+    type: string
+    sample: RSA
+filename:
+    description: Path to the generated TLS/SSL private key file
+    returned:
+        - changed
+        - success
+    type: string
+    sample: /etc/ssl/private/ansible.com.pem
+'''
+
+class PrivateKeyError(Exception):
+    pass
+
+class PrivateKey(object):
+
+    def __init__(self, module):
+        self.size = module.params['size']
+        self.state = module.params['state']
+        self.name = os.path.basename(module.params['path'])
+        self.type = module.params['type']
+        self.force = module.params['force']
+        self.path = module.params['path']
+        self.mode = module.params['mode']
+        self.changed = True
+        self.check_mode = module.check_mode
+
+
+    def generate(self, module):
+        """Generate a keypair."""
+
+        if not os.path.exists(self.path) or self.force:
+            self.privatekey = crypto.PKey()
+
+            if self.type == 'RSA':
+                crypto_type = crypto.TYPE_RSA
+            else:
+                crypto_type = crypto.TYPE_DSA
+
+            try:
+                self.privatekey.generate_key(crypto_type, self.size)
+            except (TypeError, ValueError):
+                raise PrivateKeyError(get_exception())
+
+            try:
+                privatekey_file = os.open(self.path,
+                                          os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
+                                          self.mode)
+
+                os.write(privatekey_file, crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey))
+                os.close(privatekey_file)
+            except IOError:
+                self.remove()
+                raise PrivateKeyError(get_exception())
+        else:
+            self.changed = False
+
+        file_args = module.load_file_common_arguments(module.params)
+        if module.set_fs_attributes_if_different(file_args, False):
+            self.changed = True
+
+
+    def remove(self):
+        """Remove the private key from the filesystem."""
+
+        try:
+            os.remove(self.path)
+        except OSError:
+            e = get_exception()
+            if e.errno != errno.ENOENT:
+                raise PrivateKeyError(e)
+            else:
+                self.changed = False
+
+
+    def dump(self):
+        """Serialize the object into a dictionnary."""
+
+        result = {
+            'size': self.size,
+            'type': self.type,
+            'filename': self.path,
+            'changed': self.changed,
+        }
+
+        return result
+        
+
+def main():
+
+    module = AnsibleModule(
+        argument_spec = dict(
+            state=dict(default='present', choices=['present', 'absent'], type='str'),
+            size=dict(default=4096, type='int'),
+            type=dict(default='RSA', choices=['RSA', 'DSA'], type='str'),
+            force=dict(default=False, type='bool'),
+            path=dict(required=True, type='path'),
+        ),
+        supports_check_mode = True,
+        add_file_common_args = True,
+    )
+
+    if not pyopenssl_found:
+        module.fail_json(msg='the python pyOpenSSL module is required')
+
+    path = module.params['path']
+    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)
+
+    if not module.params['mode']:
+        module.params['mode'] = 0600
+
+    private_key = PrivateKey(module)
+    if private_key.state == 'present':
+
+        if module.check_mode:
+            result = private_key.dump()
+            result['changed'] = module.params['force'] or not os.path.exists(path)
+            module.exit_json(**result)
+
+        try:
+            private_key.generate(module)
+        except PrivateKeyError:
+            e = get_exception()
+            module.fail_json(msg=str(e))
+    else:
+
+        if module.check_mode:
+            result = private_key.dump()
+            result['changed'] = os.path.exists(path)
+            module.exit_json(**result)
+
+        try:
+            private_key.remove()
+        except PrivateKeyError:
+            e = get_exception()
+            module.fail_json(msg=str(e))
+
+    result = private_key.dump()
+
+    module.exit_json(**result)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/lib/ansible/modules/extras/crypto/openssl_publickey.py b/lib/ansible/modules/extras/crypto/openssl_publickey.py
new file mode 100644
index 00000000000..3fb2de85ae7
--- /dev/null
+++ b/lib/ansible/modules/extras/crypto/openssl_publickey.py
@@ -0,0 +1,224 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+
+from ansible.module_utils.basic import *
+
+try:
+    from OpenSSL import crypto
+except ImportError:
+    pyopenssl_found = False
+else:
+    pyopenssl_found = True
+
+
+import os
+
+DOCUMENTATION = '''
+---
+module: openssl_publickey
+author: "Yanis Guenane (@Spredzy)"
+version_added: "2.3"
+short_description: Generate an OpenSSL public key from its private key.
+description:
+    - "This module allows one to (re)generate OpenSSL public keys from their private keys.
+       It uses the pyOpenSSL python library to interact with openssl. Keys are generated
+       in PEM format. This module works only if the version of PyOpenSSL is recent enough (> 16.0.0)"
+requirements:
+    - "python-pyOpenSSL"
+options:
+    state:
+        required: false
+        default: "present"
+        choices: [ present, absent ]
+        description:
+            - Whether the public key should exist or not, taking action if the state is different from what is stated.
+    force:
+        required: false
+        default: False
+        choices: [ True, False ]
+        description:
+            - Should the key be regenerated even it it already exists
+    path:
+        required: true
+        description:
+            - Name of the file in which the generated TLS/SSL public key will be written.
+    privatekey_path:
+        required: true
+        description:
+            - Path to the TLS/SSL private key from which to genereate the public key.
+'''
+
+EXAMPLES = '''
+# Generate an OpenSSL public key.
+- openssl_publickey: path=/etc/ssl/public/ansible.com.pem
+                     privatekey_path=/etc/ssl/private/ansible.com.pem
+
+# Force regenerate an OpenSSL public key if it already exists
+- openssl_publickey: path=/etc/ssl/public/ansible.com.pem
+                     privatekey_path=/etc/ssl/private/ansible.com.pem
+                     force=True
+
+# Remove an OpenSSL public key
+- openssl_publickey: path=/etc/ssl/public/ansible.com.pem
+                     privatekey_path=/etc/ssl/private/ansible.com.pem
+                     state=absent
+'''
+
+RETURN = '''
+privatekey:
+    description: Path to the TLS/SSL private key the public key was generated from
+    returned:
+        - changed
+        - success
+    type: string
+    sample: /etc/ssl/private/ansible.com.pem
+filename:
+    description: Path to the generated TLS/SSL public key file
+    returned:
+        - changed
+        - success
+    type: string
+    sample: /etc/ssl/public/ansible.com.pem
+'''
+
+class PublicKeyError(Exception):
+    pass
+
+class PublicKey(object):
+
+    def __init__(self, module):
+        self.state = module.params['state']
+        self.force = module.params['force']
+        self.name = os.path.basename(module.params['path'])
+        self.path = module.params['path']
+        self.privatekey_path = module.params['privatekey_path']
+        self.changed = True
+        self.check_mode = module.check_mode
+
+
+    def generate(self, module):
+        """Generate the public key.."""
+
+        if not os.path.exists(self.path) or self.force:
+            try:
+                 privatekey_content = open(self.privatekey_path, 'r').read()
+                 privatekey = crypto.load_privatekey(crypto.FILETYPE_PEM, privatekey_content)
+                 publickey_file = open(self.path, 'w')
+                 publickey_file.write(crypto.dump_publickey(crypto.FILETYPE_PEM, privatekey))
+                 publickey_file.close()
+            except (IOError, OSError):
+                e = get_exception()
+                raise PublicKeyError(e)
+            except AttributeError:
+                self.remove()
+                raise PublicKeyError('You need to have PyOpenSSL>=16.0.0 to generate public keys')
+        else:
+            self.changed = False
+
+        file_args = module.load_file_common_arguments(module.params)
+        if module.set_fs_attributes_if_different(file_args, False):
+            self.changed = True
+
+    def remove(self):
+        """Remove the public key from the filesystem."""
+
+        try:
+            os.remove(self.path)
+        except OSError:
+            e = get_exception()
+            if e.errno != errno.ENOENT:
+                raise PublicKeyError(e)
+            else:
+                self.changed = False
+
+    def dump(self):
+        """Serialize the object into a dictionnary."""
+
+        result = {
+            'privatekey': self.privatekey_path,
+            'filename': self.path,
+            'changed': self.changed,
+        }
+
+        return result
+        
+
+def main():
+
+    module = AnsibleModule(
+        argument_spec = dict(
+            state=dict(default='present', choices=['present', 'absent'], type='str'),
+            force=dict(default=False, type='bool'),
+            path=dict(required=True, type='path'),
+            privatekey_path=dict(type='path'),
+        ),
+        supports_check_mode = True,
+        add_file_common_args = True,
+    )
+
+    if not pyopenssl_found:
+        module.fail_json(msg='the python pyOpenSSL module is required')
+
+    path = module.params['path']
+    privatekey_path = module.params['privatekey_path']
+    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)
+
+    public_key = PublicKey(module)
+    if public_key.state == 'present':
+
+        # This is only applicable when generating a new public key.
+        # When removing one the privatekey_path should not be required.
+        if not privatekey_path:
+            module.fail_json(msg='When generating a new public key you must specify a private key')
+
+        if not os.path.exists(privatekey_path):
+            module.fail_json(name=privatekey_path, msg='The private key %s does not exist' % privatekey_path)
+
+        if module.check_mode:
+            result = public_key.dump()
+            result['changed'] = module.params['force'] or not os.path.exists(path)
+            module.exit_json(**result)
+
+        try:
+            public_key.generate(module)
+        except PublicKeyError:
+            e = get_exception()
+            module.fail_json(msg=str(e))
+    else:
+
+        if module.check_mode:
+            result = public_key.dump()
+            result['changed'] = os.path.exists(path)
+            module.exit_json(**result)
+
+        try:
+            public_key.remove()
+        except PublicKeyError:
+            e = get_exception()
+            module.fail_json(msg=str(e))
+
+    result = public_key.dump()
+
+    module.exit_json(**result)
+
+
+if __name__ == '__main__':
+    main()