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 +# +# 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 . + +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 +# +# 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 . + +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()