From 7e36f4f302cdbd2da7f3137fba93791e7c16a214 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 7 Sep 2016 07:34:00 -0700 Subject: [PATCH] Adds bigip_ssl_certificate module (#2831) Adds bigip_ssl_certificate module This module is another in the ongoing "bootstrapping saga" that is being undertaken. With this module you can manage the lifecycle of the SSL certificates on a BIG-IP. This includes those used for SSL offloading. Tests for this module can be found here https://github.com/F5Networks/f5-ansible/blob/master/roles/__bigip_ssl_certificate/tasks/main.yaml Platforms this was tested on are 12.0.0 12.1.0 --- .../network/f5/bigip_ssl_certificate.py | 516 ++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 lib/ansible/modules/extras/network/f5/bigip_ssl_certificate.py diff --git a/lib/ansible/modules/extras/network/f5/bigip_ssl_certificate.py b/lib/ansible/modules/extras/network/f5/bigip_ssl_certificate.py new file mode 100644 index 00000000000..076caba9f54 --- /dev/null +++ b/lib/ansible/modules/extras/network/f5/bigip_ssl_certificate.py @@ -0,0 +1,516 @@ +#!/usr/bin/python +# +# (c) 2016, Kevin Coming (@waffie1) +# +# This file is part of Ansible +# +# 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 . + +DOCUMENTATION = ''' +module: bigip_ssl_certificate +short_description: Import/Delete certificates from BIG-IP +description: + - This module will import/delete SSL certificates on BIG-IP LTM. + Certificates can be imported from certificate and key files on the local + disk, in PEM format. +version_added: 2.2 +options: + cert_content: + description: + - When used instead of 'cert_src', sets the contents of a certificate directly + to the specified value. This is used with lookup plugins or for anything + with formatting or templating. Either one of C(key_src), + C(key_content), C(cert_src) or C(cert_content) must be provided when + C(state) is C(present). + required: false + key_content: + description: + - When used instead of 'key_src', sets the contents of a certificate key + directly to the specified value. This is used with lookup plugins or for + anything with formatting or templating. Either one of C(key_src), + C(key_content), C(cert_src) or C(cert_content) must be provided when + C(state) is C(present). + required: false + state: + description: + - Certificate and key state. This determines if the provided certificate + and key is to be made C(present) on the device or C(absent). + required: true + default: present + choices: + - present + - absent + partition: + description: + - BIG-IP partition to use when adding/deleting certificate. + required: false + default: Common + name: + description: + - SSL Certificate Name. This is the cert/key pair name used + when importing a certificate/key into the F5. It also + determines the filenames of the objects on the LTM + (:Partition:name.cer_11111_1 and :Partition_name.key_11111_1). + required: true + cert_src: + description: + - This is the local filename of the certificate. Either one of C(key_src), + C(key_content), C(cert_src) or C(cert_content) must be provided when + C(state) is C(present). + required: false + key_src: + description: + - This is the local filename of the private key. Either one of C(key_src), + C(key_content), C(cert_src) or C(cert_content) must be provided when + C(state) is C(present). + required: false + passphrase: + description: + - Passphrase on certificate private key + required: false +notes: + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. + - Requires the netaddr Python package on the host. + - If you use this module, you will not be able to remove the certificates + and keys that are managed, via the web UI. You can only remove them via + tmsh or these modules. +extends_documentation_fragment: f5 +requirements: + - f5-sdk >= 1.3.1 + - BigIP >= v12 +author: + - Kevin Coming (@waffie1) + - Tim Rupp (@caphrim007) +''' + +EXAMPLES = ''' +- name: Import PEM Certificate from local disk + bigip_ssl_certificate: + name: "certificate-name" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + cert_src: "/path/to/cert.crt" + key_src: "/path/to/key.key" + delegate_to: localhost + +- name: Use a file lookup to import PEM Certificate + bigip_ssl_certificate: + name: "certificate-name" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "present" + cert_content: "{{ lookup('file', '/path/to/cert.crt') }}" + key_content: "{{ lookup('file', '/path/to/key.key') }}" + delegate_to: localhost + +- name: "Delete Certificate" + bigip_ssl_certificate: + name: "certificate-name" + server: "lb.mydomain.com" + user: "admin" + password: "secret" + state: "absent" + delegate_to: localhost +''' + +RETURN = ''' +cert_name: + description: > + The name of the SSL certificate. The C(cert_name) and + C(key_name) will be equal to each other. + returned: + - created + - changed + - deleted + type: string + sample: "cert1" +key_name: + description: > + The name of the SSL certificate key. The C(key_name) and + C(cert_name) will be equal to each other. + returned: + - created + - changed + - deleted + type: string + sample: "key1" +partition: + description: Partition in which the cert/key was created + returned: + - changed + - created + - deleted + type: string + sample: "Common" +key_checksum: + description: SHA1 checksum of the key that was provided + return: + - changed + - created + type: string + sample: "cf23df2207d99a74fbe169e3eba035e633b65d94" +cert_checksum: + description: SHA1 checksum of the cert that was provided + return: + - changed + - created + type: string + sample: "f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0" +''' + + +try: + from f5.bigip.contexts import TransactionContextManager + from f5.bigip import ManagementRoot + from icontrol.session import iControlUnexpectedHTTPError + HAS_F5SDK = True +except ImportError: + HAS_F5SDK = False + + +import hashlib +import StringIO + + +class BigIpSslCertificate(object): + def __init__(self, *args, **kwargs): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + required_args = ['key_content', 'key_src', 'cert_content', 'cert_src'] + + ksource = kwargs['key_src'] + if ksource: + with open(ksource) as f: + kwargs['key_content'] = f.read() + + csource = kwargs['cert_src'] + if csource: + with open(csource) as f: + kwargs['cert_content'] = f.read() + + if kwargs['state'] == 'present': + if not any(kwargs[k] is not None for k in required_args): + raise F5ModuleError( + "Either 'key_content', 'key_src', 'cert_content' or " + "'cert_src' must be provided" + ) + + # This is the remote BIG-IP path from where it will look for certs + # to install. + self.dlpath = '/var/config/rest/downloads' + + # The params that change in the module + self.cparams = dict() + + # Stores the params that are sent to the module + self.params = kwargs + self.api = ManagementRoot(kwargs['server'], + kwargs['user'], + kwargs['password'], + port=kwargs['server_port']) + + def exists(self): + cert = self.cert_exists() + key = self.key_exists() + + if cert and key: + return True + else: + return False + + def get_hash(self, content): + k = hashlib.sha1() + s = StringIO.StringIO(content) + while True: + data = s.read(1024) + if not data: + break + k.update(data) + return k.hexdigest() + + def present(self): + current = self.read() + changed = False + do_key = False + do_cert = False + chash = None + khash = None + + check_mode = self.params['check_mode'] + name = self.params['name'] + partition = self.params['partition'] + cert_content = self.params['cert_content'] + key_content = self.params['key_content'] + passphrase = self.params['passphrase'] + + # Technically you dont need to provide us with anything in the form + # of content for your cert, but that's kind of illogical, so we just + # return saying you didn't "do" anything if you left the cert and keys + # empty. + if not cert_content and not key_content: + return False + + if key_content is not None: + if 'key_checksum' in current: + khash = self.get_hash(key_content) + if khash not in current['key_checksum']: + do_key = "update" + else: + do_key = "create" + + if cert_content is not None: + if 'cert_checksum' in current: + chash = self.get_hash(cert_content) + if chash not in current['cert_checksum']: + do_cert = "update" + else: + do_cert = "create" + + if do_cert or do_key: + changed = True + params = dict() + params['cert_name'] = name + params['key_name'] = name + params['partition'] = partition + if khash: + params['key_checksum'] = khash + if chash: + params['cert_checksum'] = chash + self.cparams = params + + if check_mode: + return changed + + if not do_cert and not do_key: + return False + + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + if do_cert: + # Upload the content of a certificate as a StringIO object + cstring = StringIO.StringIO(cert_content) + filename = "%s.crt" % (name) + filepath = os.path.join(self.dlpath, filename) + api.shared.file_transfer.uploads.upload_stringio( + cstring, + filename + ) + + if do_cert == "update": + # Install the certificate + params = { + 'name': name, + 'partition': partition + } + cert = api.tm.sys.file.ssl_certs.ssl_cert.load(**params) + + # This works because, while the source path is the same, + # calling update causes the file to be re-read + cert.update() + changed = True + elif do_cert == "create": + # Install the certificate + params = { + 'sourcePath': "file://" + filepath, + 'name': name, + 'partition': partition + } + api.tm.sys.file.ssl_certs.ssl_cert.create(**params) + changed = True + + if do_key: + # Upload the content of a certificate key as a StringIO object + kstring = StringIO.StringIO(key_content) + filename = "%s.key" % (name) + filepath = os.path.join(self.dlpath, filename) + api.shared.file_transfer.uploads.upload_stringio( + kstring, + filename + ) + + if do_key == "update": + # Install the key + params = { + 'name': name, + 'partition': partition + } + key = api.tm.sys.file.ssl_keys.ssl_key.load(**params) + + params = dict() + + if passphrase: + params['passphrase'] = passphrase + else: + params['passphrase'] = None + + key.update(**params) + changed = True + elif do_key == "create": + # Install the key + params = { + 'sourcePath': "file://" + filepath, + 'name': name, + 'partition': partition + } + if passphrase: + params['passphrase'] = self.params['passphrase'] + else: + params['passphrase'] = None + + api.tm.sys.file.ssl_keys.ssl_key.create(**params) + changed = True + return changed + + def key_exists(self): + return self.api.tm.sys.file.ssl_keys.ssl_key.exists( + name=self.params['name'], + partition=self.params['partition'] + ) + + def cert_exists(self): + return self.api.tm.sys.file.ssl_certs.ssl_cert.exists( + name=self.params['name'], + partition=self.params['partition'] + ) + + def read(self): + p = dict() + name = self.params['name'] + partition = self.params['partition'] + + if self.key_exists(): + key = self.api.tm.sys.file.ssl_keys.ssl_key.load( + name=name, + partition=partition + ) + if hasattr(key, 'checksum'): + p['key_checksum'] = str(key.checksum) + + if self.cert_exists(): + cert = self.api.tm.sys.file.ssl_certs.ssl_cert.load( + name=name, + partition=partition + ) + if hasattr(cert, 'checksum'): + p['cert_checksum'] = str(cert.checksum) + + p['name'] = name + return p + + def flush(self): + result = dict() + state = self.params['state'] + + try: + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + result.update(**self.cparams) + result.update(dict(changed=changed)) + return result + + def absent(self): + changed = False + + if self.exists(): + changed = self.delete() + + return changed + + def delete(self): + changed = False + + check_mode = self.params['check_mode'] + + delete_cert = self.cert_exists() + delete_key = self.key_exists() + + if not delete_cert and not delete_key: + return changed + + if check_mode: + params = dict() + params['cert_name'] = name + params['key_name'] = name + params['partition'] = partition + self.cparams = params + return True + + tx = self.api.tm.transactions.transaction + with TransactionContextManager(tx) as api: + if delete_cert: + # Delete the certificate + c = api.tm.sys.file.ssl_certs.ssl_cert.load( + name=self.params['name'], + partition=self.params['partition'] + ) + c.delete() + changed = True + + if delete_key: + # Delete the certificate key + k = self.api.tm.sys.file.ssl_keys.ssl_key.load( + name=self.params['name'], + partition=self.params['partition'] + ) + k.delete() + changed = True + return changed + + +def main(): + argument_spec = f5_argument_spec() + + meta_args = dict( + name=dict(type='str', required=True), + cert_content=dict(type='str', default=None), + cert_src=dict(type='path', default=None), + key_content=dict(type='str', default=None), + key_src=dict(type='path', default=None), + passphrase=dict(type='str', default=None, no_log=True) + ) + + argument_spec.update(meta_args) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[ + ['key_content', 'key_src'], + ['cert_content', 'cert_src'] + ] + ) + + try: + obj = BigIpSslCertificate(check_mode=module.check_mode, + **module.params) + result = obj.flush() + module.exit_json(**result) + except F5ModuleError as e: + module.fail_json(msg=str(e)) + +from ansible.module_utils.basic import * +from ansible.module_utils.f5 import * + +if __name__ == '__main__': + main()