diff --git a/lib/ansible/modules/network/f5/bigip_ssl_certificate.py b/lib/ansible/modules/network/f5/bigip_ssl_certificate.py index b1536ae9eeb..4e808002bb5 100644 --- a/lib/ansible/modules/network/f5/bigip_ssl_certificate.py +++ b/lib/ansible/modules/network/f5/bigip_ssl_certificate.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -# (c) 2016, Kevin Coming (@waffie1) +# Copyright 2017 F5 Networks Inc. # # This file is part of Ansible # @@ -17,14 +17,15 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -ANSIBLE_METADATA = {'metadata_version': '1.0', - 'status': ['preview'], - 'supported_by': 'community'} - +ANSIBLE_METADATA = { + 'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.0' +} DOCUMENTATION = ''' module: bigip_ssl_certificate -short_description: Import/Delete certificates from BIG-IP +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 @@ -38,7 +39,6 @@ options: 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 @@ -46,57 +46,47 @@ options: 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 + 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. + - This module does not behave like other modules that you might include in + roles where referencing files or templates first looks in the role's + files or templates directory. To have it behave that way, use the Ansible + file or template lookup (see Examples). The lookups behave as expected in + a role context. extends_documentation_fragment: f5 requirements: - f5-sdk >= 1.5.0 - - BigIP >= v12 + - BIG-IP >= v12 author: - - Kevin Coming (@waffie1) - Tim Rupp (@caphrim007) ''' @@ -135,279 +125,263 @@ EXAMPLES = ''' 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 or deleted + description: The name of the certificate that the user provided + returned: created 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 or deleted +key_filename: + description: + - The name of the SSL certificate key. The C(key_filename) and + C(cert_filename) will be similar to each other, however the + C(key_filename) will have a C(.key) extension. + returned: created type: string - sample: "key1" -partition: - description: Partition in which the cert/key was created - returned: created, changed or deleted - type: string - sample: "Common" + sample: "cert1.key" key_checksum: - description: SHA1 checksum of the key that was provided - returned: created or changed + description: SHA1 checksum of the key that was provided. + returned: changed and created type: string sample: "cf23df2207d99a74fbe169e3eba035e633b65d94" +key_source_path: + description: Path on BIG-IP where the source of the key is stored + returned: created + type: string + sample: "/var/config/rest/downloads/cert1.key" +cert_filename: + description: + - The name of the SSL certificate. The C(cert_filename) and + C(key_filename) will be similar to each other, however the + C(cert_filename) will have a C(.crt) extension. + returned: created + type: string + sample: "cert1.crt" cert_checksum: - description: SHA1 checksum of the cert that was provided - returned: created or changed + description: SHA1 checksum of the cert that was provided. + returned: changed and created type: string sample: "f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0" +cert_source_path: + description: Path on BIG-IP where the source of the certificate is stored. + returned: created + type: string + sample: "/var/config/rest/downloads/cert1.crt" ''' -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 +import os +import re + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +from ansible.module_utils.f5_utils import ( + AnsibleF5Client, + AnsibleF5Parameters, + HAS_F5SDK, + F5ModuleError, + iControlUnexpectedHTTPError, + iteritems +) -class BigIpSslCertificate(object): - def __init__(self, *args, **kwargs): - if not HAS_F5SDK: - raise F5ModuleError("The python f5-sdk module is required") +class Parameters(AnsibleF5Parameters): + def __init__(self, params=None): + super(Parameters, self).__init__(params) + self._values['__warnings'] = [] - required_args = ['key_content', 'key_src', 'cert_content', 'cert_src'] + def to_return(self): + result = {} + try: + for returnable in self.returnables: + result[returnable] = getattr(self, returnable) + result = self._filter_params(result) + except Exception: + pass + return result - ksource = kwargs['key_src'] - if ksource: - with open(ksource) as f: - kwargs['key_content'] = f.read() + def api_params(self): + result = {} + for api_attribute in self.api_attributes: + if self.api_map is not None and api_attribute in self.api_map: + result[api_attribute] = getattr(self, self.api_map[api_attribute]) + else: + result[api_attribute] = getattr(self, api_attribute) + result = self._filter_params(result) + return result - 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): + def _get_hash(self, content): k = hashlib.sha1() - s = StringIO.StringIO(content) + s = StringIO(content) while True: data = s.read(1024) if not data: break - k.update(data) + k.update(data.encode('utf-8')) return k.hexdigest() - def present(self): - current = self.read() + @property + def checksum(self): + if self._values['checksum'] is None: + return None + pattern = r'SHA1:\d+:(?P[\w+]{40})' + matches = re.match(pattern, self._values['checksum']) + if matches: + return matches.group('value') + else: + return None + + +class KeyParameters(Parameters): + api_map = { + 'sourcePath': 'key_source_path' + } + + updatables = ['key_source_path'] + + returnables = ['key_filename', 'key_checksum', 'key_source_path'] + + api_attributes = ['passphrase', 'sourcePath'] + + @property + def key_filename(self): + fname, fext = os.path.splitext(self.name) + if fext == '': + return fname + '.key' + else: + return self.name + + @property + def key_checksum(self): + if self.key_content is None: + return None + return self._get_hash(self.key_content) + + @property + def key_src(self): + if self._values['key_src'] is None: + return None + + self._values['__warnings'].append( + dict( + msg="The key_src param is deprecated", + version='2.4' + ) + ) + + try: + with open(self._values['key_src']) as fh: + self.key_content = fh.read() + except IOError: + raise F5ModuleError( + "The specified 'key_src' does not exist" + ) + + @property + def key_source_path(self): + result = 'file://' + os.path.join( + BaseManager.download_path, + self.key_filename + ) + return result + + +class CertParameters(Parameters): + api_map = { + 'sourcePath': 'cert_source_path' + } + + updatables = ['cert_source_path'] + + returnables = ['cert_filename', 'cert_checksum', 'cert_source_path'] + + api_attributes = ['sourcePath'] + + @property + def cert_checksum(self): + if self.cert_content is None: + return None + return self._get_hash(self.cert_content) + + @property + def cert_filename(self): + fname, fext = os.path.splitext(self.name) + if fext == '': + return fname + '.crt' + else: + return self.name + + @property + def cert_src(self): + if self._values['cert_src'] is None: + return None + + self._values['__warnings'].append( + dict( + msg="The cert_src param is deprecated", + version='2.4' + ) + ) + + try: + with open(self._value['cert_src']) as fh: + self.cert_content = fh.read() + except IOError: + raise F5ModuleError( + "The specified 'cert_src' does not exist" + ) + + @property + def cert_source_path(self): + result = 'file://' + os.path.join( + BaseManager.download_path, + self.cert_filename + ) + return result + + +class ModuleManager(object): + def __init__(self, client): + self.client = client + + def exec_module(self): + manager1 = self.get_manager('certificate') + manager2 = self.get_manager('key') + result = self.execute_managers([manager1, manager2]) + return result + + def execute_managers(self, managers): + results = dict(changed=False) + for manager in managers: + result = manager.exec_module() + for k, v in iteritems(result): + if k == 'changed': + if v is True: + results['changed'] = True + else: + results[k] = v + return results + + def get_manager(self, type): + if type == 'certificate': + return CertificateManager(self.client) + elif type == 'key': + return KeyManager(self.client) + + +class BaseManager(object): + download_path = '/var/config/rest/downloads' + + def __init__(self, client): + self.client = client + self.have = None + + def exec_module(self): 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 don't 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'] + state = self.want.state try: if state == "present": @@ -417,94 +391,311 @@ class BigIpSslCertificate(object): except iControlUnexpectedHTTPError as e: raise F5ModuleError(str(e)) - result.update(**self.cparams) + changes = self.changes.to_return() + result.update(**changes) result.update(dict(changed=changed)) + self._announce_deprecations() return result - def absent(self): - changed = False + def _announce_deprecations(self): + warnings = [] + if self.want: + warnings += self.want._values.get('__warnings', []) + if self.have: + warnings += self.have._values.get('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + def present(self): if self.exists(): - changed = self.delete() + return self.update() + else: + return self.create() - return changed - - def delete(self): - changed = False - name = self.params['name'] - partition = self.params['partition'] - - 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 + def create(self): + self._set_changed_options() + if self.client.check_mode: return True + self.create_on_device() + 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 + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False - 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 update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.client.check_mode: + return True + self.update_on_device() + return True + + def absent(self): + if self.exists(): + return self.remove() + return False + + def remove(self): + if self.client.check_mode: + return True + self.remove_from_device() + return True -def main(): - argument_spec = f5_argument_spec() +class CertificateManager(BaseManager): + def __init__(self, client): + super(CertificateManager, self).__init__(client) + self.want = CertParameters(self.client.module.params) + self.changes = CertParameters() - 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) - ) + def _set_changed_options(self): + changed = {} + try: + for key in CertParameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = CertParameters(changed) + except Exception: + pass - argument_spec.update(meta_args) + def _update_changed_options(self): + changed = {} + try: + for key in CertParameters.updatables: + if getattr(self.want, key) is not None: + attr1 = getattr(self.want, key) + attr2 = getattr(self.have, key) + if attr1 != attr2: + changed[key] = attr1 + if self.want.cert_checksum != self.have.checksum: + changed['cert_checksum'] = self.want.cert_checksum + if changed: + self.changes = CertParameters(changed) + return True + except Exception: + pass + return False - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[ + def exists(self): + result = self.client.api.tm.sys.file.ssl_certs.ssl_cert.exists( + name=self.want.cert_filename, + partition=self.want.partition + ) + return result + + def present(self): + if self.want.cert_content is None: + return False + return super(CertificateManager, self).present() + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update_on_device(self): + content = StringIO(self.want.cert_content) + self.client.api.shared.file_transfer.uploads.upload_stringio( + content, self.want.cert_filename + ) + resource = self.client.api.tm.sys.file.ssl_certs.ssl_cert.load( + name=self.want.cert_filename, + partition=self.want.partition + ) + resource.update() + + def create_on_device(self): + content = StringIO(self.want.cert_content) + self.client.api.shared.file_transfer.uploads.upload_stringio( + content, self.want.cert_filename + ) + self.client.api.tm.sys.file.ssl_certs.ssl_cert.create( + sourcePath=self.want.cert_source_path, + name=self.want.cert_filename, + partition=self.want.partition + ) + + def read_current_from_device(self): + resource = self.client.api.tm.sys.file.ssl_certs.ssl_cert.load( + name=self.want.cert_filename, + partition=self.want.partition + ) + result = resource.attrs + return CertParameters(result) + + def remove_from_device(self): + resource = self.client.api.tm.sys.file.ssl_certs.ssl_cert.load( + name=self.want.cert_filename, + partition=self.want.partition + ) + resource.delete() + + def remove(self): + result = super(CertificateManager, self).remove() + if self.exists() and not self.client.check_mode: + raise F5ModuleError("Failed to delete the certificate") + return result + + +class KeyManager(BaseManager): + def __init__(self, client): + super(KeyManager, self).__init__(client) + self.want = KeyParameters(self.client.module.params) + self.changes = KeyParameters() + + def _set_changed_options(self): + changed = {} + try: + for key in KeyParameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = Parameters(changed) + except Exception: + pass + + def _update_changed_options(self): + changed = {} + try: + for key in CertParameters.updatables: + if getattr(self.want, key) is not None: + attr1 = getattr(self.want, key) + attr2 = getattr(self.have, key) + if attr1 != attr2: + changed[key] = attr1 + if self.want.key_checksum != self.have.checksum: + changed['key_checksum'] = self.want.key_checksum + if changed: + self.changes = CertParameters(changed) + return True + except Exception: + pass + return False + + def should_update(self): + result = self._update_changed_options() + if result: + return True + return False + + def update_on_device(self): + content = StringIO(self.want.key_content) + self.client.api.shared.file_transfer.uploads.upload_stringio( + content, self.want.key_filename + ) + resource = self.client.api.tm.sys.file.ssl_keys.ssl_key.load( + name=self.want.key_filename, + partition=self.want.partition + ) + resource.update() + + def exists(self): + result = self.client.api.tm.sys.file.ssl_keys.ssl_key.exists( + name=self.want.key_filename, + partition=self.want.partition + ) + return result + + def present(self): + if self.want.key_content is None: + return False + return super(KeyManager, self).present() + + def read_current_from_device(self): + resource = self.client.api.tm.sys.file.ssl_keys.ssl_key.load( + name=self.want.key_filename, + partition=self.want.partition + ) + result = resource.attrs + return KeyParameters(result) + + def create_on_device(self): + content = StringIO(self.want.key_content) + self.client.api.shared.file_transfer.uploads.upload_stringio( + content, self.want.key_filename + ) + self.client.api.tm.sys.file.ssl_keys.ssl_key.create( + sourcePath=self.want.key_source_path, + name=self.want.key_filename, + partition=self.want.partition + ) + + def remove_from_device(self): + resource = self.client.api.tm.sys.file.ssl_keys.ssl_key.load( + name=self.want.key_filename, + partition=self.want.partition + ) + resource.delete() + + def remove(self): + result = super(KeyManager, self).remove() + if self.exists() and not self.client.check_mode: + raise F5ModuleError("Failed to delete the key") + return result + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.argument_spec = dict( + name=dict( + required=True + ), + cert_content=dict(), + cert_src=dict( + type='path', + removed_in_version='2.4' + ), + key_content=dict(), + key_src=dict( + type='path', + removed_in_version='2.4' + ), + passphrase=dict( + no_log=True + ), + state=dict( + required=False, + default='present', + choices=['absent', 'present'] + ) + ) + self.mutually_exclusive = [ ['key_content', 'key_src'], ['cert_content', 'cert_src'] ] + self.f5_product_name = 'bigip' + + +def main(): + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") + + spec = ArgumentSpec() + + client = AnsibleF5Client( + argument_spec=spec.argument_spec, + mutually_exclusive=spec.mutually_exclusive, + supports_check_mode=spec.supports_check_mode, + f5_product_name=spec.f5_product_name ) try: - obj = BigIpSslCertificate(check_mode=module.check_mode, - **module.params) - result = obj.flush() - module.exit_json(**result) + mm = ModuleManager(client) + results = mm.exec_module() + client.module.exit_json(**results) except F5ModuleError as e: - module.fail_json(msg=str(e)) + client.module.fail_json(msg=str(e)) -from ansible.module_utils.basic import * -from ansible.module_utils.f5_utils import * if __name__ == '__main__': main() diff --git a/test/units/modules/network/f5/fixtures/cert1.crt b/test/units/modules/network/f5/fixtures/cert1.crt new file mode 100644 index 00000000000..1d22f30289f --- /dev/null +++ b/test/units/modules/network/f5/fixtures/cert1.crt @@ -0,0 +1,101 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=New York, L=New York, O=ACME CA, OU=Coyote, CN=ourca.domain.local + Validity + Not Before: Jun 30 16:46:09 2016 GMT + Not After : Jun 25 16:46:09 2036 GMT + Subject: C=US, ST=New York, O=ACME, OU=Coyote, CN=cert1.domain.local + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:d6:0f:bd:26:ef:14:4d:09:f6:db:8b:01:f5:4e: + 6c:03:b1:35:20:16:b8:1b:7c:e6:b6:8d:97:1b:b0: + 4f:8a:b6:cb:54:7e:7a:ff:fd:af:02:db:bf:9d:cf: + 9a:4c:0d:87:93:8b:cc:61:f3:23:a9:6f:8e:d4:82: + 2c:93:b6:e2:fa:37:ed:8a:d3:23:8f:6d:b5:78:4a: + 38:ba:93:f9:4a:1c:40:06:33:d7:c0:98:20:d4:16: + ac:a4:a5:6b:41:20:4c:3a:55:7e:c7:50:e7:95:07: + 4e:86:15:86:7a:0f:6c:57:d2:07:1c:97:24:51:5b: + 4e:f5:52:3a:f8:4f:95:6b:6c:83:1f:34:4e:ee:b0: + ae:fe:46:90:38:f1:4d:85:72:8b:46:bc:d1:62:37: + 65:5a:de:bb:16:51:1e:f5:cb:a0:ef:d6:7b:11:6f: + 3b:0c:49:17:bc:4d:8c:f5:d9:f0:35:6b:f7:b6:4d: + 50:eb:47:81:e3:06:f2:bd:ec:67:4f:ab:2b:03:aa: + e2:1e:42:22:a9:c9:59:dc:0d:19:fb:c5:02:1d:d7: + 58:e4:04:53:0a:1d:79:bb:c1:33:f1:cd:b7:10:2e: + b4:6e:9b:dc:60:66:05:50:9f:20:66:a1:71:00:51: + 54:cf:0a:70:f4:7c:45:c6:f0:a7:1c:11:2f:3e:a3: + 1f:bf + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 2D:FB:27:C7:B4:32:FF:F7:87:DB:2D:A7:76:AE:F0:96:7E:DA:DC:17 + X509v3 Authority Key Identifier: + keyid:4F:2A:15:49:E6:CC:05:2F:2B:F4:0E:CC:BA:2E:4C:DF:13:90:F0:78 + + Signature Algorithm: sha256WithRSAEncryption + 3f:46:1c:3b:58:b4:99:f3:75:00:47:d2:fe:ba:ba:9a:04:46: + 62:b6:2d:a0:0f:8f:c0:95:2a:58:8b:61:f5:14:90:30:26:37: + 94:a1:a6:29:20:c9:b5:08:d7:f9:15:cb:9d:9c:19:ed:2f:a4: + e6:91:48:85:1a:f7:ab:17:5e:79:23:69:b8:3c:0c:48:ae:c8: + ba:90:d0:05:fb:33:7e:86:fd:12:f8:2d:0f:ff:16:15:9a:dc: + 76:48:7d:65:5b:4e:93:14:e8:be:37:d1:13:f7:a7:b1:cd:ad: + ae:4f:e1:72:b9:53:2d:cd:e6:42:76:44:93:21:28:58:c0:44: + ab:3c:da:5b:e5:55:ab:04:86:4d:9c:4c:33:f4:4e:13:98:e9: + 0f:d1:a3:70:2b:1d:11:20:47:26:f6:d8:45:7f:88:ad:f2:c1: + 81:0f:be:cd:6c:79:80:94:30:eb:8d:cc:f3:7d:a1:3e:6c:6f: + fa:8f:f3:1f:2e:76:97:3f:8a:1b:67:3b:e0:f9:b1:3c:6b:dc: + 64:1b:00:73:e9:89:81:f6:7f:51:f3:51:c8:b9:96:5f:fd:55: + f8:77:6f:88:bc:65:b3:e2:30:a4:00:7a:79:68:e0:36:8b:a9: + 1b:06:9b:20:fe:fe:98:aa:56:58:c8:08:a4:7b:12:59:ff:3d: + bd:5e:13:3b:c6:c7:8a:00:5b:cb:27:18:02:ee:cb:38:c2:b7: + a9:51:04:ef:31:ca:49:09:48:14:13:eb:91:e2:26:8c:88:5f: + 1c:78:e1:0d:90:29:d7:c1:fc:c8:89:fd:4d:53:0b:99:58:c2: + 1a:24:3d:c0:a2:4c:a3:d9:c7:95:c5:bc:72:fa:02:f1:ab:dd: + aa:2b:9e:a0:bb:1a:68:2d:09:8c:a2:99:0d:26:ec:9e:30:19: + 01:5a:41:45:63:b3:c5:db:24:32:4c:fe:7f:f3:ce:e9:4d:00: + 64:cf:bb:15:34:2d:31:6e:4f:c0:96:40:9b:32:35:65:92:01: + 29:7e:74:02:50:fd:3b:3b:3a:a3:9f:6a:c0:a5:be:3f:c3:07: + d6:8c:2a:c6:f4:0f:32:bd:3b:fc:45:90:d2:46:ee:6f:c3:2f: + 26:8c:97:0c:e8:da:9a:97:03:0b:86:17:45:a6:62:69:4e:8d: + cf:f8:bf:ea:2f:dc:ff:95:14:15:bd:92:2d:8a:08:cf:ce:8a: + b0:f6:34:0a:a2:0e:49:31:44:e1:47:fb:37:52:53:59:93:25: + 40:cc:ac:67:2d:a2:b6:9b:75:fd:13:a5:a7:93:4f:72:05:75: + cd:b1:37:f6:3b:69:3b:24:a1:1f:23:f0:cd:bb:ae:18:b3:aa: + eb:9f:d7:97:06:ba:fd:44 +-----BEGIN CERTIFICATE----- +MIIExjCCAq6gAwIBAgIBATANBgkqhkiG9w0BAQsFADBzMQswCQYDVQQGEwJVUzER +MA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMRAwDgYDVQQKDAdB +Q01FIENBMQ8wDQYDVQQLDAZDb3lvdGUxGzAZBgNVBAMMEm91cmNhLmRvbWFpbi5s +b2NhbDAeFw0xNjA2MzAxNjQ2MDlaFw0zNjA2MjUxNjQ2MDlaMF0xCzAJBgNVBAYT +AlVTMREwDwYDVQQIDAhOZXcgWW9yazENMAsGA1UECgwEQUNNRTEPMA0GA1UECwwG +Q295b3RlMRswGQYDVQQDDBJjZXJ0MS5kb21haW4ubG9jYWwwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDWD70m7xRNCfbbiwH1TmwDsTUgFrgbfOa2jZcb +sE+KtstUfnr//a8C27+dz5pMDYeTi8xh8yOpb47UgiyTtuL6N+2K0yOPbbV4Sji6 +k/lKHEAGM9fAmCDUFqykpWtBIEw6VX7HUOeVB06GFYZ6D2xX0gcclyRRW071Ujr4 +T5VrbIMfNE7usK7+RpA48U2FcotGvNFiN2Va3rsWUR71y6Dv1nsRbzsMSRe8TYz1 +2fA1a/e2TVDrR4HjBvK97GdPqysDquIeQiKpyVncDRn7xQId11jkBFMKHXm7wTPx +zbcQLrRum9xgZgVQnyBmoXEAUVTPCnD0fEXG8KccES8+ox+/AgMBAAGjezB5MAkG +A1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRp +ZmljYXRlMB0GA1UdDgQWBBQt+yfHtDL/94fbLad2rvCWftrcFzAfBgNVHSMEGDAW +gBRPKhVJ5swFLyv0Dsy6LkzfE5DweDANBgkqhkiG9w0BAQsFAAOCAgEAP0YcO1i0 +mfN1AEfS/rq6mgRGYrYtoA+PwJUqWIth9RSQMCY3lKGmKSDJtQjX+RXLnZwZ7S+k +5pFIhRr3qxdeeSNpuDwMSK7IupDQBfszfob9EvgtD/8WFZrcdkh9ZVtOkxTovjfR +E/ensc2trk/hcrlTLc3mQnZEkyEoWMBEqzzaW+VVqwSGTZxMM/ROE5jpD9GjcCsd +ESBHJvbYRX+IrfLBgQ++zWx5gJQw643M832hPmxv+o/zHy52lz+KG2c74PmxPGvc +ZBsAc+mJgfZ/UfNRyLmWX/1V+HdviLxls+IwpAB6eWjgNoupGwabIP7+mKpWWMgI +pHsSWf89vV4TO8bHigBbyycYAu7LOMK3qVEE7zHKSQlIFBPrkeImjIhfHHjhDZAp +18H8yIn9TVMLmVjCGiQ9wKJMo9nHlcW8cvoC8avdqiueoLsaaC0JjKKZDSbsnjAZ +AVpBRWOzxdskMkz+f/PO6U0AZM+7FTQtMW5PwJZAmzI1ZZIBKX50AlD9Ozs6o59q +wKW+P8MH1owqxvQPMr07/EWQ0kbub8MvJoyXDOjampcDC4YXRaZiaU6Nz/i/6i/c +/5UUFb2SLYoIz86KsPY0CqIOSTFE4Uf7N1JTWZMlQMysZy2itpt1/ROlp5NPcgV1 +zbE39jtpOyShHyPwzbuuGLOq65/Xlwa6/UQ= +-----END CERTIFICATE----- diff --git a/test/units/modules/network/f5/fixtures/cert1.key b/test/units/modules/network/f5/fixtures/cert1.key new file mode 100644 index 00000000000..a89a29161c8 --- /dev/null +++ b/test/units/modules/network/f5/fixtures/cert1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA1g+9Ju8UTQn224sB9U5sA7E1IBa4G3zmto2XG7BPirbLVH56 +//2vAtu/nc+aTA2Hk4vMYfMjqW+O1IIsk7bi+jftitMjj221eEo4upP5ShxABjPX +wJgg1BaspKVrQSBMOlV+x1DnlQdOhhWGeg9sV9IHHJckUVtO9VI6+E+Va2yDHzRO +7rCu/kaQOPFNhXKLRrzRYjdlWt67FlEe9cug79Z7EW87DEkXvE2M9dnwNWv3tk1Q +60eB4wbyvexnT6srA6riHkIiqclZ3A0Z+8UCHddY5ARTCh15u8Ez8c23EC60bpvc +YGYFUJ8gZqFxAFFUzwpw9HxFxvCnHBEvPqMfvwIDAQABAoIBAQCjQ7PP+y8vpvbp +8bbXoy2ND15mkA1xoazR9WIYEzxHny2rzx//GTyfYH1gXtPfR75tEYYb+vbrJxP4 +DyTysN2jXH7HkEwh+9oZ2fo0i+Hp3WwTjvzyftUjDfw1Q5lvPbQGFekxGgrXRpBk +ggxkEllfDeiwrLJdftfVEhe6BfD/0YibwQeHN7VoC4V8wOanKtDmx74W/1f7WhwQ +nKQnCrbYqNJa2nGvWiKU5Suvfb0v7tCnQYlfnCpUfj+wcnxlgmGkcyq1L+qC1qC8 +PO5i3T3LM5Yg8CSeGhO/q6gw/fUowuBN1cluTqN97oLHiEM5tLdjeVWwa1Vp0liv +1WXGT4eBAoGBAPtumMmyVTIorvV6KGNI/Eo6jfE0HOXVdXtm4iToDDuiYwto7/Ge +/kV+11Fpu0lV+eYPfZn175Of8FnQPwczQF1OOH/aQ/ViY8j87bZUbCy25mWrfNkh +2rRlyI3/OsSfL5SkyWpYB0yhSJZV9mSQJTZolB4GQRNPKtqi7NpB4WxBAoGBANnz +VS4JBJO75yeSG5BzPp5VVKm+nu0Betlva8GsHdEic8OM9bGpVozGysAW3Xdxp7q6 +gLJGyyuzpsxldCc/IdIlF5fz7gkLl4NoYanz9PSEr2XZLh9+2yXGkPFlC3IeHAUB +E+2UO9MFpWrmfKoAnYZCR6vJDxtQBpAlTUvJEYv/AoGBAPha62K32327P+7MJl7D +9ijgI9rwjebcbbpiCtlHuOWi5lCb6/7v/NvqiYcqeEvdOAXuoTNWAbsBTel5UPis +wFQp8pcfouccs9IRPEFQrLWSSIx+0sirrxtoOq1AQe18DAS4rRd1MmiYG1ocOVBm +LcvLixsJNHh9R6hFLM3+K0vBAoGANkmJ+gF9Bl9TYGPgQcay3jVa9Tzp0RcBRo+e +Q4tfkewG8bp2qF4JlN8fOWF4oHvKz5QM4lsH2EbTUS4kFHKBNhrPGaZEsDQW9UBW +s0J0zUMPfUrvViD+7RXcnIQSqcYeLJDsKc02aYWKgmoOuzmUAxEXUQ6vmJoCSH1C +F5JpsHkCgYEArwTSzb1+/ThQhK1JN8hJ4jMjQ8E7PzLTMILrdDALn2g1T4VzL7N7 +UG6oUieMlo/UH6cv6330dwaGVklXZbyDKSDROIafFcOpVfcvDUgJCjp3CaY9A2zG ++EPkRpeHKXAIgG+QuOwVOtYWcWltnBf61slTqiY2vKX1+ZGmrMrw1Zw= +-----END RSA PRIVATE KEY----- diff --git a/test/units/modules/network/f5/fixtures/cert2.crt b/test/units/modules/network/f5/fixtures/cert2.crt new file mode 100644 index 00000000000..30674bc8e7c --- /dev/null +++ b/test/units/modules/network/f5/fixtures/cert2.crt @@ -0,0 +1,101 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 2 (0x2) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=New York, L=New York, O=ACME CA, OU=Coyote, CN=ourca.domain.local + Validity + Not Before: Jun 30 16:49:00 2016 GMT + Not After : Jun 25 16:49:00 2036 GMT + Subject: C=US, ST=New York, O=ACME, OU=Coyote, CN=cert2.domain.local + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:c6:9e:84:99:4d:69:98:c2:42:95:ed:43:ca:24: + 05:64:9d:67:81:1c:ff:56:7b:ad:d1:cb:09:39:28: + 4f:ac:aa:1b:34:61:3a:b1:e3:57:d4:9e:15:40:77: + 91:20:2b:e8:7e:d3:91:1e:46:50:6c:2f:4b:00:c2: + f2:3a:43:89:d9:81:73:84:5f:02:db:49:ac:3b:9e: + fe:c0:77:2e:53:ea:ce:da:ff:49:98:21:1d:31:4d: + 0f:14:20:30:36:9a:23:b4:28:08:06:59:81:30:03: + 86:09:0b:5b:e1:72:63:5e:54:ac:90:b1:82:55:b8: + 12:00:d5:01:26:be:6a:eb:fc:58:5b:8a:7a:fe:46: + 23:a3:eb:5d:6c:e0:f6:79:00:5d:5b:49:82:42:62: + e2:58:e8:65:54:14:be:99:25:8b:b7:df:cf:53:26: + f2:7a:fd:b9:f9:f3:d5:af:06:d6:1e:ba:66:4d:41: + 8c:5d:aa:23:41:7f:f4:27:21:a0:30:09:86:13:c4: + 57:1b:13:45:63:6b:3b:a3:7f:d1:1a:cd:fd:07:51: + 0f:1a:e1:d9:25:3e:d2:77:e1:c7:60:db:12:df:ef: + 71:65:c8:c7:1a:42:94:6f:57:2a:d7:67:30:0f:33: + 31:ba:90:4d:d1:80:38:08:e7:90:7a:04:0e:8f:b0: + 2a:73 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 43:71:A9:16:2B:DA:DC:5F:FD:82:87:78:26:48:4E:77:21:47:44:D6 + X509v3 Authority Key Identifier: + keyid:4F:2A:15:49:E6:CC:05:2F:2B:F4:0E:CC:BA:2E:4C:DF:13:90:F0:78 + + Signature Algorithm: sha256WithRSAEncryption + 15:ac:f9:cb:bc:88:c0:d3:74:83:88:cd:19:94:bb:87:7e:fd: + 4d:25:09:9b:08:84:64:c5:37:c7:99:b3:25:ee:6e:82:46:b0: + 13:f9:05:ad:4d:4a:b2:e3:29:5d:0d:55:9e:9c:62:1d:95:f2: + 19:49:5e:d3:5b:58:98:ce:e8:f5:e5:c1:ce:b5:a8:7a:b1:f8: + 14:fe:25:10:12:5b:41:53:d2:47:ab:20:e5:50:da:b6:ba:00: + 21:94:6b:dd:0b:24:15:dc:c0:4e:b8:1d:cc:9e:5f:10:5e:46: + 3f:96:c9:f8:28:bb:13:31:d6:d2:6c:48:41:bb:23:ab:23:64: + 73:d6:2b:2e:9a:77:d4:08:fb:e0:e8:50:2c:49:7f:98:9e:f6: + 37:30:2b:7c:97:c6:a7:1e:5b:dc:ce:bb:1e:58:e4:bd:05:4c: + ad:07:d6:03:c5:a9:57:a4:26:e2:10:f7:f9:63:1a:2a:6a:9c: + 52:98:33:bf:ea:70:cd:c0:86:32:80:6e:70:54:87:74:3c:41: + 53:a1:c6:53:44:c7:74:a6:11:b6:48:66:86:f9:04:ca:ec:5d: + 4f:ce:7f:64:51:34:52:53:98:a8:70:62:f7:3b:fb:39:11:9a: + e1:e2:d3:00:0b:6b:d2:33:3c:44:de:c3:6b:e1:6f:c9:be:d2: + 2c:8a:f0:b3:d3:4c:12:2f:ad:9d:6b:40:89:23:94:93:6d:12: + 6c:38:89:fa:fe:ad:02:55:55:8b:c3:86:7f:15:c4:3a:a9:70: + e9:06:6c:26:09:28:9f:6e:94:f2:a1:27:5c:89:4c:42:ac:65: + 90:92:d2:6d:09:7c:d8:a1:bf:5b:25:e4:db:ed:71:41:d7:e2: + 61:47:89:9e:46:29:9d:f9:f4:94:cf:f5:b3:e8:df:6a:47:34: + d1:ed:fc:a4:58:fe:82:e1:6e:e9:05:65:f5:d2:57:9a:d1:42: + 64:ae:0c:bb:07:14:39:a2:c0:85:e4:25:a5:c4:e6:3f:e6:da: + d0:18:4f:e0:01:ba:99:2e:1f:75:35:c3:fa:a3:e7:e1:75:1b: + 1c:19:93:cc:96:eb:3f:ce:8b:10:40:36:63:f5:66:dc:6d:75: + 31:ba:db:27:21:b4:15:00:e9:ce:d0:08:e3:b0:1c:e3:29:c9: + 63:5a:c8:5c:ca:db:ce:51:b7:87:22:c6:ba:42:d7:ab:29:b4: + 87:fa:27:9a:18:22:90:9f:da:c0:90:c4:49:64:38:38:2e:a2: + ea:87:c1:8b:4e:8b:ff:a7:53:45:4f:d8:8b:86:69:ea:87:1d: + f6:e6:44:14:1f:69:ee:2c:de:5a:a1:df:a8:57:13:65:4d:5b: + ce:6e:f2:15:2a:c5:32:08 +-----BEGIN CERTIFICATE----- +MIIExjCCAq6gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBzMQswCQYDVQQGEwJVUzER +MA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMRAwDgYDVQQKDAdB +Q01FIENBMQ8wDQYDVQQLDAZDb3lvdGUxGzAZBgNVBAMMEm91cmNhLmRvbWFpbi5s +b2NhbDAeFw0xNjA2MzAxNjQ5MDBaFw0zNjA2MjUxNjQ5MDBaMF0xCzAJBgNVBAYT +AlVTMREwDwYDVQQIDAhOZXcgWW9yazENMAsGA1UECgwEQUNNRTEPMA0GA1UECwwG +Q295b3RlMRswGQYDVQQDDBJjZXJ0Mi5kb21haW4ubG9jYWwwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDGnoSZTWmYwkKV7UPKJAVknWeBHP9We63Rywk5 +KE+sqhs0YTqx41fUnhVAd5EgK+h+05EeRlBsL0sAwvI6Q4nZgXOEXwLbSaw7nv7A +dy5T6s7a/0mYIR0xTQ8UIDA2miO0KAgGWYEwA4YJC1vhcmNeVKyQsYJVuBIA1QEm +vmrr/Fhbinr+RiOj611s4PZ5AF1bSYJCYuJY6GVUFL6ZJYu3389TJvJ6/bn589Wv +BtYeumZNQYxdqiNBf/QnIaAwCYYTxFcbE0Vjazujf9Eazf0HUQ8a4dklPtJ34cdg +2xLf73FlyMcaQpRvVyrXZzAPMzG6kE3RgDgI55B6BA6PsCpzAgMBAAGjezB5MAkG +A1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRp +ZmljYXRlMB0GA1UdDgQWBBRDcakWK9rcX/2Ch3gmSE53IUdE1jAfBgNVHSMEGDAW +gBRPKhVJ5swFLyv0Dsy6LkzfE5DweDANBgkqhkiG9w0BAQsFAAOCAgEAFaz5y7yI +wNN0g4jNGZS7h379TSUJmwiEZMU3x5mzJe5ugkawE/kFrU1KsuMpXQ1VnpxiHZXy +GUle01tYmM7o9eXBzrWoerH4FP4lEBJbQVPSR6sg5VDatroAIZRr3QskFdzATrgd +zJ5fEF5GP5bJ+Ci7EzHW0mxIQbsjqyNkc9YrLpp31Aj74OhQLEl/mJ72NzArfJfG +px5b3M67HljkvQVMrQfWA8WpV6Qm4hD3+WMaKmqcUpgzv+pwzcCGMoBucFSHdDxB +U6HGU0THdKYRtkhmhvkEyuxdT85/ZFE0UlOYqHBi9zv7ORGa4eLTAAtr0jM8RN7D +a+Fvyb7SLIrws9NMEi+tnWtAiSOUk20SbDiJ+v6tAlVVi8OGfxXEOqlw6QZsJgko +n26U8qEnXIlMQqxlkJLSbQl82KG/WyXk2+1xQdfiYUeJnkYpnfn0lM/1s+jfakc0 +0e38pFj+guFu6QVl9dJXmtFCZK4MuwcUOaLAheQlpcTmP+ba0BhP4AG6mS4fdTXD ++qPn4XUbHBmTzJbrP86LEEA2Y/Vm3G11MbrbJyG0FQDpztAI47Ac4ynJY1rIXMrb +zlG3hyLGukLXqym0h/onmhgikJ/awJDESWQ4OC6i6ofBi06L/6dTRU/Yi4Zp6ocd +9uZEFB9p7izeWqHfqFcTZU1bzm7yFSrFMgg= +-----END CERTIFICATE----- diff --git a/test/units/modules/network/f5/fixtures/cert2.key b/test/units/modules/network/f5/fixtures/cert2.key new file mode 100644 index 00000000000..6d4bdf15263 --- /dev/null +++ b/test/units/modules/network/f5/fixtures/cert2.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,C56C8101A9C8D6B9AD0975807D4793BB + +rhV1cJee3XAlY83zIXytJOlvXFrsHmzyTVoOn26eOwgza8CuE5gQQzLXiT12zK0q +YAbYyyyEXJJogVU61s1vuCQRNiezUCwT4SBj7ni4rXqu5BYUxKh0wD0gjE519yNP +nqnPUYKdLkFY7I6RJjqCqkk8xnJm64g5zCqN58aR98Mkqr1898+lZ2OHqAsAYBNH +dM/SE7B7E4Mr1sAjpsn6L4PJ93WSwmEtH3nZTnPF9qtFuJwjUcHCN/r/s3QXki95 +eFX+7qW460lBfDeRUKXKqz4gO017AXu1kccrlHhdQoJGf3D+x9zwofG/uFeAH3iN +f9IRaiR2IN6SS67QFmOkI9S95tsFb4N8bmmfGV4w8wfxvDzGJuxzIb4gByX5xov4 +S22pDpkfn5YqxgC5ItSiFYpg01HEi2l79HwZqAn1kowLsuF1JJKAYL5IMS3DlrdH +AyA9CN28G6pYEjwFBbFgpOg64UNmrkxRncHxC4FuH7iGZNJL9+HQve/J5nlrnx6M +IU2myiZZhgbsl/V45ddXBDSlEdWFLHtEhcG+ICJP3EZAXHR0e9vyrWDk7T5zKhLP +ch9PNmIw+5zzpRuPu5NYw7V0ax8UOf2AydyBHeIQWuY52bai+QMDyQauomqpPXRY +tpCcW85P9jstY/F6TV32XQu/cHWolziJXI/QzWF5+uvnLMAsb3p5mriCG4DOTWF3 +KFSytTGnDQUUCLgaYSSKXL5Z52PVYmTjoqX8M6cvqSEdjK84wILQE0JMItQjGSIM +y5qHD7Mthf9YOJy1D86qtVumbaOBLw/rGPQS5QlK/m256xZ10LUslYczMpw1orN3 +3Uv8zHKk790XduHTllR0LwQXMJXG59hgiWAu3V3rsAkVSRpC3MI6IUZ2cfJvZ0Ds +FmUhCJ34JQxD4E/sT9uGAk6VIq/fAmM7/gq0oF4oqOFg4Zy1r3rc1Kvdoy1yKUi6 +JCI5bKCkgIthx4XUKQVtFMkHBDZAHr6i5Lzy4nM6I4S4/qL3JH4Q+739D1rjGVlq +OWcaeOzkkbJrE8h+A94UQao4R50LavKgq/o2n56tHG0RhXXyV5MC/X9rbSVipihR +rwNKnogdhAjY96IrOzdiHTArg8qZBGvHPoGUl3zjWFqNbHEs4NLSrEl6oEs6F/vC +zEZmi8gxqraw4u1GJnpoMuLO45PuhcxcXgJSvTh/OKDaR1u0ggEn7TxfAygm0ahP +i6NBgoZ/upTHAWqWht2JjSmQHQW7doVkp/BgNJq13oYF7FEUEg/ZtBTPKPR3CjM0 +ZKDGvKqWRVRyrw9FSwXn6WlSFfT3vhPMoW2jq1Kq5o/ZyhcquCVE8i+xq6hilcb5 +sNiV1tPWsZOFHx4T5hBVK+QnC8t7pCj38YpyEoY4/gffMtY85jsrLMlPYd5bmJ6O +x1tKiQauK+aX6IMu38YnHjCGnCkw1fF2OMSohbG2QfaKsmfkt8YLRuf2PTtjLtke +xGt0Irjac/sEZPc4SEIqnehNfXadiuMV3+4v6ey9vf782r76KH8gInY2gDsQ4X6d +1LVNCNAd/AGlitopL4hYomaeTjTzqIy5fMlGmTrpZjokenu/ILXsljZVAX2iyOAs +-----END RSA PRIVATE KEY----- diff --git a/test/units/modules/network/f5/fixtures/create_insecure_cert1.crt b/test/units/modules/network/f5/fixtures/create_insecure_cert1.crt new file mode 100644 index 00000000000..1d22f30289f --- /dev/null +++ b/test/units/modules/network/f5/fixtures/create_insecure_cert1.crt @@ -0,0 +1,101 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=New York, L=New York, O=ACME CA, OU=Coyote, CN=ourca.domain.local + Validity + Not Before: Jun 30 16:46:09 2016 GMT + Not After : Jun 25 16:46:09 2036 GMT + Subject: C=US, ST=New York, O=ACME, OU=Coyote, CN=cert1.domain.local + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:d6:0f:bd:26:ef:14:4d:09:f6:db:8b:01:f5:4e: + 6c:03:b1:35:20:16:b8:1b:7c:e6:b6:8d:97:1b:b0: + 4f:8a:b6:cb:54:7e:7a:ff:fd:af:02:db:bf:9d:cf: + 9a:4c:0d:87:93:8b:cc:61:f3:23:a9:6f:8e:d4:82: + 2c:93:b6:e2:fa:37:ed:8a:d3:23:8f:6d:b5:78:4a: + 38:ba:93:f9:4a:1c:40:06:33:d7:c0:98:20:d4:16: + ac:a4:a5:6b:41:20:4c:3a:55:7e:c7:50:e7:95:07: + 4e:86:15:86:7a:0f:6c:57:d2:07:1c:97:24:51:5b: + 4e:f5:52:3a:f8:4f:95:6b:6c:83:1f:34:4e:ee:b0: + ae:fe:46:90:38:f1:4d:85:72:8b:46:bc:d1:62:37: + 65:5a:de:bb:16:51:1e:f5:cb:a0:ef:d6:7b:11:6f: + 3b:0c:49:17:bc:4d:8c:f5:d9:f0:35:6b:f7:b6:4d: + 50:eb:47:81:e3:06:f2:bd:ec:67:4f:ab:2b:03:aa: + e2:1e:42:22:a9:c9:59:dc:0d:19:fb:c5:02:1d:d7: + 58:e4:04:53:0a:1d:79:bb:c1:33:f1:cd:b7:10:2e: + b4:6e:9b:dc:60:66:05:50:9f:20:66:a1:71:00:51: + 54:cf:0a:70:f4:7c:45:c6:f0:a7:1c:11:2f:3e:a3: + 1f:bf + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 2D:FB:27:C7:B4:32:FF:F7:87:DB:2D:A7:76:AE:F0:96:7E:DA:DC:17 + X509v3 Authority Key Identifier: + keyid:4F:2A:15:49:E6:CC:05:2F:2B:F4:0E:CC:BA:2E:4C:DF:13:90:F0:78 + + Signature Algorithm: sha256WithRSAEncryption + 3f:46:1c:3b:58:b4:99:f3:75:00:47:d2:fe:ba:ba:9a:04:46: + 62:b6:2d:a0:0f:8f:c0:95:2a:58:8b:61:f5:14:90:30:26:37: + 94:a1:a6:29:20:c9:b5:08:d7:f9:15:cb:9d:9c:19:ed:2f:a4: + e6:91:48:85:1a:f7:ab:17:5e:79:23:69:b8:3c:0c:48:ae:c8: + ba:90:d0:05:fb:33:7e:86:fd:12:f8:2d:0f:ff:16:15:9a:dc: + 76:48:7d:65:5b:4e:93:14:e8:be:37:d1:13:f7:a7:b1:cd:ad: + ae:4f:e1:72:b9:53:2d:cd:e6:42:76:44:93:21:28:58:c0:44: + ab:3c:da:5b:e5:55:ab:04:86:4d:9c:4c:33:f4:4e:13:98:e9: + 0f:d1:a3:70:2b:1d:11:20:47:26:f6:d8:45:7f:88:ad:f2:c1: + 81:0f:be:cd:6c:79:80:94:30:eb:8d:cc:f3:7d:a1:3e:6c:6f: + fa:8f:f3:1f:2e:76:97:3f:8a:1b:67:3b:e0:f9:b1:3c:6b:dc: + 64:1b:00:73:e9:89:81:f6:7f:51:f3:51:c8:b9:96:5f:fd:55: + f8:77:6f:88:bc:65:b3:e2:30:a4:00:7a:79:68:e0:36:8b:a9: + 1b:06:9b:20:fe:fe:98:aa:56:58:c8:08:a4:7b:12:59:ff:3d: + bd:5e:13:3b:c6:c7:8a:00:5b:cb:27:18:02:ee:cb:38:c2:b7: + a9:51:04:ef:31:ca:49:09:48:14:13:eb:91:e2:26:8c:88:5f: + 1c:78:e1:0d:90:29:d7:c1:fc:c8:89:fd:4d:53:0b:99:58:c2: + 1a:24:3d:c0:a2:4c:a3:d9:c7:95:c5:bc:72:fa:02:f1:ab:dd: + aa:2b:9e:a0:bb:1a:68:2d:09:8c:a2:99:0d:26:ec:9e:30:19: + 01:5a:41:45:63:b3:c5:db:24:32:4c:fe:7f:f3:ce:e9:4d:00: + 64:cf:bb:15:34:2d:31:6e:4f:c0:96:40:9b:32:35:65:92:01: + 29:7e:74:02:50:fd:3b:3b:3a:a3:9f:6a:c0:a5:be:3f:c3:07: + d6:8c:2a:c6:f4:0f:32:bd:3b:fc:45:90:d2:46:ee:6f:c3:2f: + 26:8c:97:0c:e8:da:9a:97:03:0b:86:17:45:a6:62:69:4e:8d: + cf:f8:bf:ea:2f:dc:ff:95:14:15:bd:92:2d:8a:08:cf:ce:8a: + b0:f6:34:0a:a2:0e:49:31:44:e1:47:fb:37:52:53:59:93:25: + 40:cc:ac:67:2d:a2:b6:9b:75:fd:13:a5:a7:93:4f:72:05:75: + cd:b1:37:f6:3b:69:3b:24:a1:1f:23:f0:cd:bb:ae:18:b3:aa: + eb:9f:d7:97:06:ba:fd:44 +-----BEGIN CERTIFICATE----- +MIIExjCCAq6gAwIBAgIBATANBgkqhkiG9w0BAQsFADBzMQswCQYDVQQGEwJVUzER +MA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMRAwDgYDVQQKDAdB +Q01FIENBMQ8wDQYDVQQLDAZDb3lvdGUxGzAZBgNVBAMMEm91cmNhLmRvbWFpbi5s +b2NhbDAeFw0xNjA2MzAxNjQ2MDlaFw0zNjA2MjUxNjQ2MDlaMF0xCzAJBgNVBAYT +AlVTMREwDwYDVQQIDAhOZXcgWW9yazENMAsGA1UECgwEQUNNRTEPMA0GA1UECwwG +Q295b3RlMRswGQYDVQQDDBJjZXJ0MS5kb21haW4ubG9jYWwwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDWD70m7xRNCfbbiwH1TmwDsTUgFrgbfOa2jZcb +sE+KtstUfnr//a8C27+dz5pMDYeTi8xh8yOpb47UgiyTtuL6N+2K0yOPbbV4Sji6 +k/lKHEAGM9fAmCDUFqykpWtBIEw6VX7HUOeVB06GFYZ6D2xX0gcclyRRW071Ujr4 +T5VrbIMfNE7usK7+RpA48U2FcotGvNFiN2Va3rsWUR71y6Dv1nsRbzsMSRe8TYz1 +2fA1a/e2TVDrR4HjBvK97GdPqysDquIeQiKpyVncDRn7xQId11jkBFMKHXm7wTPx +zbcQLrRum9xgZgVQnyBmoXEAUVTPCnD0fEXG8KccES8+ox+/AgMBAAGjezB5MAkG +A1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRp +ZmljYXRlMB0GA1UdDgQWBBQt+yfHtDL/94fbLad2rvCWftrcFzAfBgNVHSMEGDAW +gBRPKhVJ5swFLyv0Dsy6LkzfE5DweDANBgkqhkiG9w0BAQsFAAOCAgEAP0YcO1i0 +mfN1AEfS/rq6mgRGYrYtoA+PwJUqWIth9RSQMCY3lKGmKSDJtQjX+RXLnZwZ7S+k +5pFIhRr3qxdeeSNpuDwMSK7IupDQBfszfob9EvgtD/8WFZrcdkh9ZVtOkxTovjfR +E/ensc2trk/hcrlTLc3mQnZEkyEoWMBEqzzaW+VVqwSGTZxMM/ROE5jpD9GjcCsd +ESBHJvbYRX+IrfLBgQ++zWx5gJQw643M832hPmxv+o/zHy52lz+KG2c74PmxPGvc +ZBsAc+mJgfZ/UfNRyLmWX/1V+HdviLxls+IwpAB6eWjgNoupGwabIP7+mKpWWMgI +pHsSWf89vV4TO8bHigBbyycYAu7LOMK3qVEE7zHKSQlIFBPrkeImjIhfHHjhDZAp +18H8yIn9TVMLmVjCGiQ9wKJMo9nHlcW8cvoC8avdqiueoLsaaC0JjKKZDSbsnjAZ +AVpBRWOzxdskMkz+f/PO6U0AZM+7FTQtMW5PwJZAmzI1ZZIBKX50AlD9Ozs6o59q +wKW+P8MH1owqxvQPMr07/EWQ0kbub8MvJoyXDOjampcDC4YXRaZiaU6Nz/i/6i/c +/5UUFb2SLYoIz86KsPY0CqIOSTFE4Uf7N1JTWZMlQMysZy2itpt1/ROlp5NPcgV1 +zbE39jtpOyShHyPwzbuuGLOq65/Xlwa6/UQ= +-----END CERTIFICATE----- diff --git a/test/units/modules/network/f5/fixtures/create_insecure_key1.key b/test/units/modules/network/f5/fixtures/create_insecure_key1.key new file mode 100644 index 00000000000..a89a29161c8 --- /dev/null +++ b/test/units/modules/network/f5/fixtures/create_insecure_key1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA1g+9Ju8UTQn224sB9U5sA7E1IBa4G3zmto2XG7BPirbLVH56 +//2vAtu/nc+aTA2Hk4vMYfMjqW+O1IIsk7bi+jftitMjj221eEo4upP5ShxABjPX +wJgg1BaspKVrQSBMOlV+x1DnlQdOhhWGeg9sV9IHHJckUVtO9VI6+E+Va2yDHzRO +7rCu/kaQOPFNhXKLRrzRYjdlWt67FlEe9cug79Z7EW87DEkXvE2M9dnwNWv3tk1Q +60eB4wbyvexnT6srA6riHkIiqclZ3A0Z+8UCHddY5ARTCh15u8Ez8c23EC60bpvc +YGYFUJ8gZqFxAFFUzwpw9HxFxvCnHBEvPqMfvwIDAQABAoIBAQCjQ7PP+y8vpvbp +8bbXoy2ND15mkA1xoazR9WIYEzxHny2rzx//GTyfYH1gXtPfR75tEYYb+vbrJxP4 +DyTysN2jXH7HkEwh+9oZ2fo0i+Hp3WwTjvzyftUjDfw1Q5lvPbQGFekxGgrXRpBk +ggxkEllfDeiwrLJdftfVEhe6BfD/0YibwQeHN7VoC4V8wOanKtDmx74W/1f7WhwQ +nKQnCrbYqNJa2nGvWiKU5Suvfb0v7tCnQYlfnCpUfj+wcnxlgmGkcyq1L+qC1qC8 +PO5i3T3LM5Yg8CSeGhO/q6gw/fUowuBN1cluTqN97oLHiEM5tLdjeVWwa1Vp0liv +1WXGT4eBAoGBAPtumMmyVTIorvV6KGNI/Eo6jfE0HOXVdXtm4iToDDuiYwto7/Ge +/kV+11Fpu0lV+eYPfZn175Of8FnQPwczQF1OOH/aQ/ViY8j87bZUbCy25mWrfNkh +2rRlyI3/OsSfL5SkyWpYB0yhSJZV9mSQJTZolB4GQRNPKtqi7NpB4WxBAoGBANnz +VS4JBJO75yeSG5BzPp5VVKm+nu0Betlva8GsHdEic8OM9bGpVozGysAW3Xdxp7q6 +gLJGyyuzpsxldCc/IdIlF5fz7gkLl4NoYanz9PSEr2XZLh9+2yXGkPFlC3IeHAUB +E+2UO9MFpWrmfKoAnYZCR6vJDxtQBpAlTUvJEYv/AoGBAPha62K32327P+7MJl7D +9ijgI9rwjebcbbpiCtlHuOWi5lCb6/7v/NvqiYcqeEvdOAXuoTNWAbsBTel5UPis +wFQp8pcfouccs9IRPEFQrLWSSIx+0sirrxtoOq1AQe18DAS4rRd1MmiYG1ocOVBm +LcvLixsJNHh9R6hFLM3+K0vBAoGANkmJ+gF9Bl9TYGPgQcay3jVa9Tzp0RcBRo+e +Q4tfkewG8bp2qF4JlN8fOWF4oHvKz5QM4lsH2EbTUS4kFHKBNhrPGaZEsDQW9UBW +s0J0zUMPfUrvViD+7RXcnIQSqcYeLJDsKc02aYWKgmoOuzmUAxEXUQ6vmJoCSH1C +F5JpsHkCgYEArwTSzb1+/ThQhK1JN8hJ4jMjQ8E7PzLTMILrdDALn2g1T4VzL7N7 +UG6oUieMlo/UH6cv6330dwaGVklXZbyDKSDROIafFcOpVfcvDUgJCjp3CaY9A2zG ++EPkRpeHKXAIgG+QuOwVOtYWcWltnBf61slTqiY2vKX1+ZGmrMrw1Zw= +-----END RSA PRIVATE KEY----- diff --git a/test/units/modules/network/f5/test_bigip_ssl_certificate.py b/test/units/modules/network/f5/test_bigip_ssl_certificate.py new file mode 100644 index 00000000000..5a34ae2cef4 --- /dev/null +++ b/test/units/modules/network/f5/test_bigip_ssl_certificate.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2017 F5 Networks Inc. +# +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json +import sys + +from nose.plugins.skip import SkipTest +if sys.version_info < (2, 7): + raise SkipTest("F5 Ansible modules require Python >= 2.7") + +from ansible.compat.tests import unittest +from ansible.module_utils import basic +from ansible.compat.tests.mock import patch, Mock +from ansible.module_utils._text import to_bytes +from ansible.module_utils.f5_utils import AnsibleF5Client + +try: + from library.bigip_ssl_certificate import ArgumentSpec + from library.bigip_ssl_certificate import KeyParameters + from library.bigip_ssl_certificate import CertParameters + from library.bigip_ssl_certificate import CertificateManager + from library.bigip_ssl_certificate import KeyManager +except ImportError: + try: + from ansible.modules.network.f5.bigip_ssl_certificate import ArgumentSpec + from ansible.modules.network.f5.bigip_ssl_certificate import KeyParameters + from ansible.modules.network.f5.bigip_ssl_certificate import CertParameters + from ansible.modules.network.f5.bigip_ssl_certificate import CertificateManager + from ansible.modules.network.f5.bigip_ssl_certificate import KeyManager + except ImportError: + raise SkipTest("F5 Ansible modules require the f5-sdk Python library") + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def set_module_args(args): + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +def load_fixture(name): + path = os.path.join(fixture_path, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except Exception: + pass + + fixture_data[path] = data + return data + + +class TestParameters(unittest.TestCase): + def test_module_parameters_key(self): + key_content = load_fixture('create_insecure_key1.key') + args = dict( + key_content=key_content, + name="cert1", + partition="Common", + state="present", + password='password', + server='localhost', + user='admin' + ) + p = KeyParameters(args) + assert p.name == 'cert1' + assert p.key_filename == 'cert1.key' + assert '-----BEGIN RSA PRIVATE KEY-----' in p.key_content + assert '-----END RSA PRIVATE KEY-----' in p.key_content + assert p.key_checksum == '91bdddcf0077e2bb2a0258aae2ae3117be392e83' + assert p.state == 'present' + assert p.user == 'admin' + assert p.server == 'localhost' + assert p.password == 'password' + assert p.partition == 'Common' + + def test_module_parameters_cert(self): + cert_content = load_fixture('create_insecure_cert1.crt') + args = dict( + cert_content=cert_content, + name="cert1", + partition="Common", + state="present", + password='password', + server='localhost', + user='admin' + ) + p = CertParameters(args) + assert p.name == 'cert1' + assert p.cert_filename == 'cert1.crt' + assert 'Signature Algorithm' in p.cert_content + assert '-----BEGIN CERTIFICATE-----' in p.cert_content + assert '-----END CERTIFICATE-----' in p.cert_content + assert p.cert_checksum == '1e55aa57ee166a380e756b5aa4a835c5849490fe' + assert p.state == 'present' + assert p.user == 'admin' + assert p.server == 'localhost' + assert p.password == 'password' + assert p.partition == 'Common' + + +@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root', + return_value=True) +class TestCertificateManager(unittest.TestCase): + + def setUp(self): + self.spec = ArgumentSpec() + + def test_import_certificate_and_key_no_key_passphrase(self, *args): + set_module_args(dict( + name='foo', + cert_content=load_fixture('cert1.crt'), + key_content=load_fixture('cert1.key'), + state='present', + password='passsword', + server='localhost', + user='admin' + )) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods in the specific type of manager + cm = CertificateManager(client) + cm.exists = Mock(side_effect=[False, True]) + cm.create_on_device = Mock(return_value=True) + + results = cm.exec_module() + + assert results['changed'] is True + + def test_update_certificate_new_certificate_and_key_password_protected_key(self, *args): + set_module_args(dict( + name='foo', + cert_content=load_fixture('cert2.crt'), + key_content=load_fixture('cert2.key'), + state='present', + passphrase='keypass', + password='passsword', + server='localhost', + user='admin' + )) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods in the specific type of manager + cm = CertificateManager(client) + cm.exists = Mock(side_effect=[False, True]) + cm.create_on_device = Mock(return_value=True) + + results = cm.exec_module() + + assert results['changed'] is True + + +@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root', + return_value=True) +class TestKeyManager(unittest.TestCase): + + def setUp(self): + self.spec = ArgumentSpec() + + def test_import_certificate_and_key_no_key_passphrase(self, *args): + set_module_args(dict( + name='foo', + cert_content=load_fixture('cert1.crt'), + key_content=load_fixture('cert1.key'), + state='present', + password='passsword', + server='localhost', + user='admin' + )) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods in the specific type of manager + cm = KeyManager(client) + cm.exists = Mock(side_effect=[False, True]) + cm.create_on_device = Mock(return_value=True) + + results = cm.exec_module() + + assert results['changed'] is True + + def test_update_certificate_new_certificate_and_key_password_protected_key(self, *args): + set_module_args(dict( + name='foo', + cert_content=load_fixture('cert2.crt'), + key_content=load_fixture('cert2.key'), + state='present', + passphrase='keypass', + password='passsword', + server='localhost', + user='admin' + )) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods in the specific type of manager + cm = KeyManager(client) + cm.exists = Mock(side_effect=[False, True]) + cm.create_on_device = Mock(return_value=True) + + results = cm.exec_module() + + assert results['changed'] is True