diff options
9 files changed, 293 insertions, 352 deletions
diff --git a/.coveragerc b/.coveragerc index 1e819e157..6f33e0bfe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,11 +8,12 @@ omit =      # TODO(rhcarvalho): this is used to ignore test files from coverage report.      # We can make this less generic when we stick with a single test pattern in      # the repo. -    test_*.py -    *_tests.py +    */conftest.py +    */test_*.py +    */*_tests.py  [report] -fail_under = 28 +fail_under = 26  [html]  directory = cover diff --git a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py index b093d84fe..33a4faf3e 100644 --- a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py +++ b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py @@ -8,15 +8,12 @@ import datetime  import io  import os  import subprocess -import sys -import tempfile +import yaml -# File pointers from io.open require unicode inputs when using their -# `write` method -import six  from six.moves import configparser -import yaml +from ansible.module_utils.basic import AnsibleModule +  try:      # You can comment this import out and include a 'pass' in this      # block if you're manually testing this module on a NON-ATOMIC @@ -24,13 +21,14 @@ try:      # available). That will force the `load_and_handle_cert` function      # to use the Fake OpenSSL classes.      import OpenSSL.crypto +    HAS_OPENSSL = True  except ImportError:      # Some platforms (such as RHEL Atomic) may not have the Python      # OpenSSL library installed. In this case we will use a manual      # work-around to parse each certificate.      #      # Check for 'OpenSSL.crypto' in `sys.modules` later. -    pass +    HAS_OPENSSL = False  DOCUMENTATION = '''  --- @@ -158,6 +156,10 @@ might return: [('O=system', 'nodes'), ('CN=system', 'node:m01.example.com')]  'subjectAltName'"""          return self.extensions[i] +    def get_extension_count(self): +        """ get_extension_count """ +        return len(self.extensions) +      def get_notAfter(self):          """Returns a date stamp as a string in the form  '20180922170439Z'. strptime the result with format param: @@ -268,30 +270,23 @@ A tuple of the form:      # around a missing library on the target host.      #      # pylint: disable=redefined-variable-type -    if 'OpenSSL.crypto' in sys.modules: +    if HAS_OPENSSL:          # No work-around required          cert_loaded = OpenSSL.crypto.load_certificate(              OpenSSL.crypto.FILETYPE_PEM, _cert_string)      else: -        # Missing library, work-around required. We need to write the -        # cert out to disk temporarily so we can run the 'openssl' +        # Missing library, work-around required. Run the 'openssl'          # command on it to decode it -        _, path = tempfile.mkstemp() -        with io.open(path, 'w') as fp: -            fp.write(six.u(_cert_string)) -            fp.flush() - -        cmd = 'openssl x509 -in {} -text'.format(path) +        cmd = 'openssl x509 -text'          try: -            openssl_decoded = subprocess.Popen(cmd.split(), -                                               stdout=subprocess.PIPE) +            openssl_proc = subprocess.Popen(cmd.split(), +                                            stdout=subprocess.PIPE, +                                            stdin=subprocess.PIPE)          except OSError:              ans_module.fail_json(msg="Error: The 'OpenSSL' python library and CLI command were not found on the target host. Unable to parse any certificates. This host will not be included in generated reports.")          else: -            openssl_decoded = openssl_decoded.communicate()[0] +            openssl_decoded = openssl_proc.communicate(_cert_string.encode('utf-8'))[0].decode('utf-8')              cert_loaded = FakeOpenSSLCertificate(openssl_decoded) -        finally: -            os.remove(path)      ######################################################################      # Read all possible names from the cert @@ -301,34 +296,12 @@ A tuple of the form:      # To read SANs from a cert we must read the subjectAltName      # extension from the X509 Object. What makes this more difficult -    # is that pyOpenSSL does not give extensions as a list, nor does -    # it provide a count of all loaded extensions. -    # -    # Rather, extensions are REQUESTED by index. We must iterate over -    # all extensions until we find the one called 'subjectAltName'. If -    # we don't find that extension we'll eventually request an -    # extension at an index where no extension exists (IndexError is -    # raised). When that happens we know that the cert has no SANs so -    # we break out of the loop. -    i = 0 -    checked_all_extensions = False -    while not checked_all_extensions: -        try: -            # Read the extension at index 'i' -            ext = cert_loaded.get_extension(i) -        except IndexError: -            # We tried to read an extension but it isn't there, that -            # means we ran out of extensions to check. Abort -            san = None -            checked_all_extensions = True -        else: -            # We were able to load the extension at index 'i' -            if ext.get_short_name() == 'subjectAltName': -                san = ext -                checked_all_extensions = True -            else: -                # Try reading the next extension -                i += 1 +    # is that pyOpenSSL does not give extensions as an iterable +    san = None +    for i in range(cert_loaded.get_extension_count()): +        ext = cert_loaded.get_extension(i) +        if ext.get_short_name() == 'subjectAltName': +            san = ext      if san is not None:          # The X509Extension object for subjectAltName prints as a @@ -341,9 +314,13 @@ A tuple of the form:      ######################################################################      # Grab the expiration date +    not_after = cert_loaded.get_notAfter() +    # example get_notAfter() => 20180922170439Z +    if isinstance(not_after, bytes): +        not_after = not_after.decode('utf-8') +      cert_expiry_date = datetime.datetime.strptime( -        cert_loaded.get_notAfter(), -        # example get_notAfter() => 20180922170439Z +        not_after,          '%Y%m%d%H%M%SZ')      time_remaining = cert_expiry_date - now @@ -455,13 +432,11 @@ an OpenShift Container Platform cluster      )      # Basic scaffolding for OpenShift specific certs -    openshift_base_config_path = module.params['config_base'] -    openshift_master_config_path = os.path.normpath( -        os.path.join(openshift_base_config_path, "master/master-config.yaml") -    ) -    openshift_node_config_path = os.path.normpath( -        os.path.join(openshift_base_config_path, "node/node-config.yaml") -    ) +    openshift_base_config_path = os.path.realpath(module.params['config_base']) +    openshift_master_config_path = os.path.join(openshift_base_config_path, +                                                "master", "master-config.yaml") +    openshift_node_config_path = os.path.join(openshift_base_config_path, +                                              "node", "node-config.yaml")      openshift_cert_check_paths = [          openshift_master_config_path,          openshift_node_config_path, @@ -476,9 +451,7 @@ an OpenShift Container Platform cluster      kubeconfig_paths = []      for m_kube_config in master_kube_configs:          kubeconfig_paths.append( -            os.path.normpath( -                os.path.join(openshift_base_config_path, "master/%s.kubeconfig" % m_kube_config) -            ) +            os.path.join(openshift_base_config_path, "master", m_kube_config + ".kubeconfig")          )      # Validate some paths we have the ability to do ahead of time @@ -527,7 +500,7 @@ an OpenShift Container Platform cluster      ######################################################################      for os_cert in filter_paths(openshift_cert_check_paths):          # Open up that config file and locate the cert and CA -        with open(os_cert, 'r') as fp: +        with io.open(os_cert, 'r', encoding='utf-8') as fp:              cert_meta = {}              cfg = yaml.load(fp)              # cert files are specified in parsed `fp` as relative to the path @@ -542,7 +515,7 @@ an OpenShift Container Platform cluster          # Load the certificate and the CA, parse their expiration dates into          # datetime objects so we can manipulate them later          for _, v in cert_meta.items(): -            with open(v, 'r') as fp: +            with io.open(v, 'r', encoding='utf-8') as fp:                  cert = fp.read()                  (cert_subject,                   cert_expiry_date, @@ -575,7 +548,7 @@ an OpenShift Container Platform cluster      try:          # Try to read the standard 'node-config.yaml' file to check if          # this host is a node. -        with open(openshift_node_config_path, 'r') as fp: +        with io.open(openshift_node_config_path, 'r', encoding='utf-8') as fp:              cfg = yaml.load(fp)          # OK, the config file exists, therefore this is a @@ -588,7 +561,7 @@ an OpenShift Container Platform cluster          cfg_path = os.path.dirname(fp.name)          node_kubeconfig = os.path.join(cfg_path, node_masterKubeConfig) -        with open(node_kubeconfig, 'r') as fp: +        with io.open(node_kubeconfig, 'r', encoding='utf8') as fp:              # Read in the nodes kubeconfig file and grab the good stuff              cfg = yaml.load(fp) @@ -613,7 +586,7 @@ an OpenShift Container Platform cluster          pass      for kube in filter_paths(kubeconfig_paths): -        with open(kube, 'r') as fp: +        with io.open(kube, 'r', encoding='utf-8') as fp:              # TODO: Maybe consider catching exceptions here?              cfg = yaml.load(fp) @@ -656,7 +629,7 @@ an OpenShift Container Platform cluster      etcd_certs = []      etcd_cert_params.append('dne')      try: -        with open('/etc/etcd/etcd.conf', 'r') as fp: +        with io.open('/etc/etcd/etcd.conf', 'r', encoding='utf-8') as fp:              etcd_config = configparser.ConfigParser()              # Reason: This check is disabled because the issue was introduced              # during a period where the pylint checks weren't enabled for this file @@ -675,7 +648,7 @@ an OpenShift Container Platform cluster          pass      for etcd_cert in filter_paths(etcd_certs_to_check): -        with open(etcd_cert, 'r') as fp: +        with io.open(etcd_cert, 'r', encoding='utf-8') as fp:              c = fp.read()              (cert_subject,               cert_expiry_date, @@ -697,7 +670,7 @@ an OpenShift Container Platform cluster      # Now the embedded etcd      ######################################################################      try: -        with open('/etc/origin/master/master-config.yaml', 'r') as fp: +        with io.open('/etc/origin/master/master-config.yaml', 'r', encoding='utf-8') as fp:              cfg = yaml.load(fp)      except IOError:          # Not present @@ -864,10 +837,5 @@ an OpenShift Container Platform cluster      ) -###################################################################### -# It's just the way we do things in Ansible. So disable this warning -# -# pylint: disable=wrong-import-position,import-error -from ansible.module_utils.basic import AnsibleModule  # noqa: E402  if __name__ == '__main__':      main() diff --git a/roles/openshift_certificate_expiry/test/conftest.py b/roles/openshift_certificate_expiry/test/conftest.py new file mode 100644 index 000000000..4ca35ecbc --- /dev/null +++ b/roles/openshift_certificate_expiry/test/conftest.py @@ -0,0 +1,116 @@ +# pylint: disable=missing-docstring,invalid-name,redefined-outer-name +import pytest +from OpenSSL import crypto + +# Parameter list for valid_cert fixture +VALID_CERTIFICATE_PARAMS = [ +    { +        'short_name': 'client', +        'cn': 'client.example.com', +        'serial': 4, +        'uses': b'clientAuth', +        'dns': [], +        'ip': [], +    }, +    { +        'short_name': 'server', +        'cn': 'server.example.com', +        'serial': 5, +        'uses': b'serverAuth', +        'dns': ['kubernetes', 'openshift'], +        'ip': ['10.0.0.1', '192.168.0.1'] +    }, +    { +        'short_name': 'combined', +        'cn': 'combined.example.com', +        'serial': 6, +        'uses': b'clientAuth, serverAuth', +        'dns': ['etcd'], +        'ip': ['10.0.0.2', '192.168.0.2'] +    } +] + +# Extract the short_name from VALID_CERTIFICATE_PARAMS to provide +# friendly naming for the valid_cert fixture +VALID_CERTIFICATE_IDS = [param['short_name'] for param in VALID_CERTIFICATE_PARAMS] + + +@pytest.fixture(scope='session') +def ca(tmpdir_factory): +    ca_dir = tmpdir_factory.mktemp('ca') + +    key = crypto.PKey() +    key.generate_key(crypto.TYPE_RSA, 2048) + +    cert = crypto.X509() +    cert.set_version(3) +    cert.set_serial_number(1) +    cert.get_subject().commonName = 'test-signer' +    cert.gmtime_adj_notBefore(0) +    cert.gmtime_adj_notAfter(24 * 60 * 60) +    cert.set_issuer(cert.get_subject()) +    cert.set_pubkey(key) +    cert.add_extensions([ +        crypto.X509Extension(b'basicConstraints', True, b'CA:TRUE, pathlen:0'), +        crypto.X509Extension(b'keyUsage', True, +                             b'digitalSignature, keyEncipherment, keyCertSign, cRLSign'), +        crypto.X509Extension(b'subjectKeyIdentifier', False, b'hash', subject=cert) +    ]) +    cert.add_extensions([ +        crypto.X509Extension(b'authorityKeyIdentifier', False, b'keyid:always', issuer=cert) +    ]) +    cert.sign(key, 'sha256') + +    return { +        'dir': ca_dir, +        'key': key, +        'cert': cert, +    } + + +@pytest.fixture(scope='session', +                ids=VALID_CERTIFICATE_IDS, +                params=VALID_CERTIFICATE_PARAMS) +def valid_cert(request, ca): +    common_name = request.param['cn'] + +    key = crypto.PKey() +    key.generate_key(crypto.TYPE_RSA, 2048) + +    cert = crypto.X509() +    cert.set_serial_number(request.param['serial']) +    cert.gmtime_adj_notBefore(0) +    cert.gmtime_adj_notAfter(24 * 60 * 60) +    cert.set_issuer(ca['cert'].get_subject()) +    cert.set_pubkey(key) +    cert.set_version(3) +    cert.get_subject().commonName = common_name +    cert.add_extensions([ +        crypto.X509Extension(b'basicConstraints', True, b'CA:FALSE'), +        crypto.X509Extension(b'keyUsage', True, b'digitalSignature, keyEncipherment'), +        crypto.X509Extension(b'extendedKeyUsage', False, request.param['uses']), +    ]) + +    if request.param['dns'] or request.param['ip']: +        san_list = ['DNS:{}'.format(common_name)] +        san_list.extend(['DNS:{}'.format(x) for x in request.param['dns']]) +        san_list.extend(['IP:{}'.format(x) for x in request.param['ip']]) + +        cert.add_extensions([ +            crypto.X509Extension(b'subjectAltName', False, ', '.join(san_list).encode('utf8')) +        ]) +    cert.sign(ca['key'], 'sha256') + +    cert_contents = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) +    cert_file = ca['dir'].join('{}.crt'.format(common_name)) +    cert_file.write_binary(cert_contents) + +    return { +        'common_name': common_name, +        'serial': request.param['serial'], +        'dns': request.param['dns'], +        'ip': request.param['ip'], +        'uses': request.param['uses'], +        'cert_file': cert_file, +        'cert': cert +    } diff --git a/roles/openshift_certificate_expiry/test/master.server.crt b/roles/openshift_certificate_expiry/test/master.server.crt deleted file mode 100644 index 51aa85c8c..000000000 --- a/roles/openshift_certificate_expiry/test/master.server.crt +++ /dev/null @@ -1,42 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBBDANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu -c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxMjM5WhcNMTkwMjA3 -MTgxMjQwWjAVMRMwEQYDVQQDEwoxNzIuMzAuMC4xMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEA44n6kVlnRXSwgnKhXX7JrRvxZm+nCqEE/vpKRfNtrMDP -AuVtcLUWdEDdT0L7QdceLTCBFe7VugrfokPhVi0XavrC2xFpYJ6+wPpuo7HyBRhf -z/8rOxftAnMeFU5JhFDaeLwSbDjiRgjE1ZYYz8Hcq9YlPujptD6j6YaW1Inae+Vs -QKXc1uAobemhClLKazEzccVGu53CaSHe4kJoKUZwJ8Ujt/nRHUr+wekbkpx0NfmF -UEGgNRXN46cq7ZwkLHsjtuR2pttC6JhF+KHgXTRyWM9ssfvL2McmhTFxrToAlhsq -8MuHMn0y9DMzmAK6EntvlC5AscxTRljtwHZEicspFwIDAQABo4IBNzCCATMwDgYD -VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAw -gf0GA1UdEQSB9TCB8oIKa3ViZXJuZXRlc4ISa3ViZXJuZXRlcy5kZWZhdWx0ghZr -dWJlcm5ldGVzLmRlZmF1bHQuc3ZjgiRrdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNs -dXN0ZXIubG9jYWyCD20wMS5leGFtcGxlLmNvbYIJb3BlbnNoaWZ0ghFvcGVuc2hp -ZnQuZGVmYXVsdIIVb3BlbnNoaWZ0LmRlZmF1bHQuc3ZjgiNvcGVuc2hpZnQuZGVm -YXVsdC5zdmMuY2x1c3Rlci5sb2NhbIIKMTcyLjMwLjAuMYIPMTkyLjE2OC4xMjIu -MjQxhwSsHgABhwTAqHrxMA0GCSqGSIb3DQEBCwUAA4IBAQDSdKBpUVB5Sgn1JB// -bk804+zrUf01koaT83/17WMI+zG8IOwCZ9Be5+zDe4ThXH+PQC6obbwUi9dn8SN6 -rlihvrhNvAJaknY1YRjW07L7aus2RFKXpzsLuWoWLVlLXBTpmfWhQ2w40bCo4Kri -jQqvezBQ+u1otFzozWmF7nrI/JK+7o89hLvaobx+mDj5wCPQLO+cM/q11Jcz3htv -VOTFsMh2VnuKOxZqLGJz2CXkr6YXvAhJiFQWaRCnJEaA2ogTYbDahV5ixFKwqpGZ -o+yDEroPlCw54Bxs0P1ewUx4TRsqd+Qzhnr73xiFBQ0M7JjhKHF6EczHt87XPvsn -HEL2 ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu -c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxMjM3WhcNMjIwMjA2 -MTgxMjM4WjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE0ODY0OTExNTgw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdyU8AD7sTXHP5jk04i1HY -cmUXuSiXdIByeIAtTZqiHU46Od0qnZib7tY1NSbo9FtGRl5YEvrfNL+1ig0hZjDh -gKZK4fNbsanuKgj2SWx11bt4yJH0YSbm9+H45y0E15IY1h30jGHnHFNFZDdYwxtO -8u+GShb4MOqZL9aUEfvfaoGJIIpfR+eW5NaBZQr6tiM89Z1wJYqoqzgzI/XIyUXR -zWLOayP1M/eeSXPvBncwZfTPLzphZjB2rz3MdloPrdYMm2b5tfbEOjD7L2aYOJJU -nVSkgjoFXAazL8KuXSIGcdrdDecyJ4ta8ijD4VIZRN9PnBlYiKaz0DsagkGjUVRd -AgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG -SIb3DQEBCwUAA4IBAQAZ/Kerb5eJXbByQ29fq60V+MQgLIJ1iTo3+zfaXxGlmV9v -fTp3S1xQhdGyhww7UC0Ze940eRq6BQ5/I6qPcgSGNpUB064pnjSf0CexCY4qoGqK -4VSvHRrG9TP5V+YIlX9UR1zuPy//a+wuCwKaqiWedTMb4jpvj5jsEOGIrciSmurg -/9nKvvJXRbgqRYQeJGLT5QW5clHywsyTrE7oYytYSEcAvEs3UZT37H74wj2RFxk6 -KcEzsxUB3W+iYst0QdOPByt64OCwAaUJ96VJstaOYMmyWSShAxGAKDSjcrr4JJnF -KtqOC1K56x0ONuBsY4MB15TNGPp8SbOhVV6OfIWj ------END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/master.server.crt.txt b/roles/openshift_certificate_expiry/test/master.server.crt.txt deleted file mode 100644 index 6b3c8fb03..000000000 --- a/roles/openshift_certificate_expiry/test/master.server.crt.txt +++ /dev/null @@ -1,82 +0,0 @@ -Certificate: -    Data: -        Version: 3 (0x2) -        Serial Number: 4 (0x4) -    Signature Algorithm: sha256WithRSAEncryption -        Issuer: CN=openshift-signer@1486491158 -        Validity -            Not Before: Feb  7 18:12:39 2017 GMT -            Not After : Feb  7 18:12:40 2019 GMT -        Subject: CN=172.30.0.1 -        Subject Public Key Info: -            Public Key Algorithm: rsaEncryption -                Public-Key: (2048 bit) -                Modulus: -                    00:e3:89:fa:91:59:67:45:74:b0:82:72:a1:5d:7e: -                    c9:ad:1b:f1:66:6f:a7:0a:a1:04:fe:fa:4a:45:f3: -                    6d:ac:c0:cf:02:e5:6d:70:b5:16:74:40:dd:4f:42: -                    fb:41:d7:1e:2d:30:81:15:ee:d5:ba:0a:df:a2:43: -                    e1:56:2d:17:6a:fa:c2:db:11:69:60:9e:be:c0:fa: -                    6e:a3:b1:f2:05:18:5f:cf:ff:2b:3b:17:ed:02:73: -                    1e:15:4e:49:84:50:da:78:bc:12:6c:38:e2:46:08: -                    c4:d5:96:18:cf:c1:dc:ab:d6:25:3e:e8:e9:b4:3e: -                    a3:e9:86:96:d4:89:da:7b:e5:6c:40:a5:dc:d6:e0: -                    28:6d:e9:a1:0a:52:ca:6b:31:33:71:c5:46:bb:9d: -                    c2:69:21:de:e2:42:68:29:46:70:27:c5:23:b7:f9: -                    d1:1d:4a:fe:c1:e9:1b:92:9c:74:35:f9:85:50:41: -                    a0:35:15:cd:e3:a7:2a:ed:9c:24:2c:7b:23:b6:e4: -                    76:a6:db:42:e8:98:45:f8:a1:e0:5d:34:72:58:cf: -                    6c:b1:fb:cb:d8:c7:26:85:31:71:ad:3a:00:96:1b: -                    2a:f0:cb:87:32:7d:32:f4:33:33:98:02:ba:12:7b: -                    6f:94:2e:40:b1:cc:53:46:58:ed:c0:76:44:89:cb: -                    29:17 -                Exponent: 65537 (0x10001) -        X509v3 extensions: -            X509v3 Key Usage: critical -                Digital Signature, Key Encipherment -            X509v3 Extended Key Usage:  -                TLS Web Server Authentication -            X509v3 Basic Constraints: critical -                CA:FALSE -            X509v3 Subject Alternative Name:  -                DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:m01.example.com, DNS:openshift, DNS:openshift.default, DNS:openshift.default.svc, DNS:openshift.default.svc.cluster.local, DNS:172.30.0.1, DNS:192.168.122.241, IP Address:172.30.0.1, IP Address:192.168.122.241 -    Signature Algorithm: sha256WithRSAEncryption -         d2:74:a0:69:51:50:79:4a:09:f5:24:1f:ff:6e:4f:34:e3:ec: -         eb:51:fd:35:92:86:93:f3:7f:f5:ed:63:08:fb:31:bc:20:ec: -         02:67:d0:5e:e7:ec:c3:7b:84:e1:5c:7f:8f:40:2e:a8:6d:bc: -         14:8b:d7:67:f1:23:7a:ae:58:a1:be:b8:4d:bc:02:5a:92:76: -         35:61:18:d6:d3:b2:fb:6a:eb:36:44:52:97:a7:3b:0b:b9:6a: -         16:2d:59:4b:5c:14:e9:99:f5:a1:43:6c:38:d1:b0:a8:e0:aa: -         e2:8d:0a:af:7b:30:50:fa:ed:68:b4:5c:e8:cd:69:85:ee:7a: -         c8:fc:92:be:ee:8f:3d:84:bb:da:a1:bc:7e:98:38:f9:c0:23: -         d0:2c:ef:9c:33:fa:b5:d4:97:33:de:1b:6f:54:e4:c5:b0:c8: -         76:56:7b:8a:3b:16:6a:2c:62:73:d8:25:e4:af:a6:17:bc:08: -         49:88:54:16:69:10:a7:24:46:80:da:88:13:61:b0:da:85:5e: -         62:c4:52:b0:aa:91:99:a3:ec:83:12:ba:0f:94:2c:39:e0:1c: -         6c:d0:fd:5e:c1:4c:78:4d:1b:2a:77:e4:33:86:7a:fb:df:18: -         85:05:0d:0c:ec:98:e1:28:71:7a:11:cc:c7:b7:ce:d7:3e:fb: -         27:1c:42:f6 ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBBDANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu -c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxMjM5WhcNMTkwMjA3 -MTgxMjQwWjAVMRMwEQYDVQQDEwoxNzIuMzAuMC4xMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEA44n6kVlnRXSwgnKhXX7JrRvxZm+nCqEE/vpKRfNtrMDP -AuVtcLUWdEDdT0L7QdceLTCBFe7VugrfokPhVi0XavrC2xFpYJ6+wPpuo7HyBRhf -z/8rOxftAnMeFU5JhFDaeLwSbDjiRgjE1ZYYz8Hcq9YlPujptD6j6YaW1Inae+Vs -QKXc1uAobemhClLKazEzccVGu53CaSHe4kJoKUZwJ8Ujt/nRHUr+wekbkpx0NfmF -UEGgNRXN46cq7ZwkLHsjtuR2pttC6JhF+KHgXTRyWM9ssfvL2McmhTFxrToAlhsq -8MuHMn0y9DMzmAK6EntvlC5AscxTRljtwHZEicspFwIDAQABo4IBNzCCATMwDgYD -VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAw -gf0GA1UdEQSB9TCB8oIKa3ViZXJuZXRlc4ISa3ViZXJuZXRlcy5kZWZhdWx0ghZr -dWJlcm5ldGVzLmRlZmF1bHQuc3ZjgiRrdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNs -dXN0ZXIubG9jYWyCD20wMS5leGFtcGxlLmNvbYIJb3BlbnNoaWZ0ghFvcGVuc2hp -ZnQuZGVmYXVsdIIVb3BlbnNoaWZ0LmRlZmF1bHQuc3ZjgiNvcGVuc2hpZnQuZGVm -YXVsdC5zdmMuY2x1c3Rlci5sb2NhbIIKMTcyLjMwLjAuMYIPMTkyLjE2OC4xMjIu -MjQxhwSsHgABhwTAqHrxMA0GCSqGSIb3DQEBCwUAA4IBAQDSdKBpUVB5Sgn1JB// -bk804+zrUf01koaT83/17WMI+zG8IOwCZ9Be5+zDe4ThXH+PQC6obbwUi9dn8SN6 -rlihvrhNvAJaknY1YRjW07L7aus2RFKXpzsLuWoWLVlLXBTpmfWhQ2w40bCo4Kri -jQqvezBQ+u1otFzozWmF7nrI/JK+7o89hLvaobx+mDj5wCPQLO+cM/q11Jcz3htv -VOTFsMh2VnuKOxZqLGJz2CXkr6YXvAhJiFQWaRCnJEaA2ogTYbDahV5ixFKwqpGZ -o+yDEroPlCw54Bxs0P1ewUx4TRsqd+Qzhnr73xiFBQ0M7JjhKHF6EczHt87XPvsn -HEL2 ------END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt b/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt deleted file mode 100644 index cd13ddc38..000000000 --- a/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIBCzANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu -c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxOTM0WhcNMTkwMjA3 -MTgxOTM1WjA9MRUwEwYDVQQKEwxzeXN0ZW06bm9kZXMxJDAiBgNVBAMTG3N5c3Rl -bTpub2RlOm0wMS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAOcVdDaSmeXuSp+7VCHUjEDeTP3j9aH0nreBj3079sEzethlLoQmwAqf -CZp23qXGYm0R89+CC55buaH1FN/ltQ8QDGUzi4tdow9Af/0OcD0EINO2ukmyG5/9 -N+X905mo+y923wppvrchAA6AcxxeDyA63zouGS4exI98iuZlcdS48zbsGERkRPGg -hoGCo7HoiKtUNL5X8MYibnFYnA4EUngdHZsRKuKte4t8GY4PYq4cxIOYXsJsNmT5 -mkFy4ThGFfR9IGg/VfyeWIkRe2VWyaUgzL0gHytAhlRJ9l54ynx96YEWrjCtp/kh -d3KeVj0IUcMzvoXX5hipYUPkoezcxI8CAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgWg -MBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL -BQADggEBAM1jexLtuOOTsOPfEal/ICzfP9aX3m0R/yGPjwQv43jOc81NcL5d+CeD -MX36tKAxFIe+wvXo0kUQOzTK3D7ol4x2YTtB4uDzNE5tVh5dWi2LrKYSqZDIrhKO -MOmJRWR3AFEopaaGQpxsD/FSfZ5Mg0OMMBPHABxMrsiserHO1nh4ax3+SI0i7Jen -gVsB4B/Xxg9Lw9JDX3/XMcI+fyLVw5ctO62BaluljpT+HkdbRWnH8ar7TmcJjzTo -/TyXOeOLkbozx0BQK16d/CbtLomJ+PO4cdwCNs2Z6HGSPTL7S9y0pct52N0kfJfx -ZGXMsW+N62S2vVSXEekMR0GJgJnLNSo= ------END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt b/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt deleted file mode 100644 index 67a3eb81c..000000000 --- a/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt +++ /dev/null @@ -1,75 +0,0 @@ -Certificate: -    Data: -        Version: 3 (0x2) -        Serial Number: 11 (0xb) -    Signature Algorithm: sha256WithRSAEncryption -        Issuer: CN=openshift-signer@1486491158 -        Validity -            Not Before: Feb  7 18:19:34 2017 GMT -            Not After : Feb  7 18:19:35 2019 GMT -        Subject: O=system:nodes, CN=system:node:m01.example.com -        Subject Public Key Info: -            Public Key Algorithm: rsaEncryption -                Public-Key: (2048 bit) -                Modulus: -                    00:e7:15:74:36:92:99:e5:ee:4a:9f:bb:54:21:d4: -                    8c:40:de:4c:fd:e3:f5:a1:f4:9e:b7:81:8f:7d:3b: -                    f6:c1:33:7a:d8:65:2e:84:26:c0:0a:9f:09:9a:76: -                    de:a5:c6:62:6d:11:f3:df:82:0b:9e:5b:b9:a1:f5: -                    14:df:e5:b5:0f:10:0c:65:33:8b:8b:5d:a3:0f:40: -                    7f:fd:0e:70:3d:04:20:d3:b6:ba:49:b2:1b:9f:fd: -                    37:e5:fd:d3:99:a8:fb:2f:76:df:0a:69:be:b7:21: -                    00:0e:80:73:1c:5e:0f:20:3a:df:3a:2e:19:2e:1e: -                    c4:8f:7c:8a:e6:65:71:d4:b8:f3:36:ec:18:44:64: -                    44:f1:a0:86:81:82:a3:b1:e8:88:ab:54:34:be:57: -                    f0:c6:22:6e:71:58:9c:0e:04:52:78:1d:1d:9b:11: -                    2a:e2:ad:7b:8b:7c:19:8e:0f:62:ae:1c:c4:83:98: -                    5e:c2:6c:36:64:f9:9a:41:72:e1:38:46:15:f4:7d: -                    20:68:3f:55:fc:9e:58:89:11:7b:65:56:c9:a5:20: -                    cc:bd:20:1f:2b:40:86:54:49:f6:5e:78:ca:7c:7d: -                    e9:81:16:ae:30:ad:a7:f9:21:77:72:9e:56:3d:08: -                    51:c3:33:be:85:d7:e6:18:a9:61:43:e4:a1:ec:dc: -                    c4:8f -                Exponent: 65537 (0x10001) -        X509v3 extensions: -            X509v3 Key Usage: critical -                Digital Signature, Key Encipherment -            X509v3 Extended Key Usage:  -                TLS Web Client Authentication -            X509v3 Basic Constraints: critical -                CA:FALSE -    Signature Algorithm: sha256WithRSAEncryption -         cd:63:7b:12:ed:b8:e3:93:b0:e3:df:11:a9:7f:20:2c:df:3f: -         d6:97:de:6d:11:ff:21:8f:8f:04:2f:e3:78:ce:73:cd:4d:70: -         be:5d:f8:27:83:31:7d:fa:b4:a0:31:14:87:be:c2:f5:e8:d2: -         45:10:3b:34:ca:dc:3e:e8:97:8c:76:61:3b:41:e2:e0:f3:34: -         4e:6d:56:1e:5d:5a:2d:8b:ac:a6:12:a9:90:c8:ae:12:8e:30: -         e9:89:45:64:77:00:51:28:a5:a6:86:42:9c:6c:0f:f1:52:7d: -         9e:4c:83:43:8c:30:13:c7:00:1c:4c:ae:c8:ac:7a:b1:ce:d6: -         78:78:6b:1d:fe:48:8d:22:ec:97:a7:81:5b:01:e0:1f:d7:c6: -         0f:4b:c3:d2:43:5f:7f:d7:31:c2:3e:7f:22:d5:c3:97:2d:3b: -         ad:81:6a:5b:a5:8e:94:fe:1e:47:5b:45:69:c7:f1:aa:fb:4e: -         67:09:8f:34:e8:fd:3c:97:39:e3:8b:91:ba:33:c7:40:50:2b: -         5e:9d:fc:26:ed:2e:89:89:f8:f3:b8:71:dc:02:36:cd:99:e8: -         71:92:3d:32:fb:4b:dc:b4:a5:cb:79:d8:dd:24:7c:97:f1:64: -         65:cc:b1:6f:8d:eb:64:b6:bd:54:97:11:e9:0c:47:41:89:80: -         99:cb:35:2a ------BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIBCzANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu -c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxOTM0WhcNMTkwMjA3 -MTgxOTM1WjA9MRUwEwYDVQQKEwxzeXN0ZW06bm9kZXMxJDAiBgNVBAMTG3N5c3Rl -bTpub2RlOm0wMS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAOcVdDaSmeXuSp+7VCHUjEDeTP3j9aH0nreBj3079sEzethlLoQmwAqf -CZp23qXGYm0R89+CC55buaH1FN/ltQ8QDGUzi4tdow9Af/0OcD0EINO2ukmyG5/9 -N+X905mo+y923wppvrchAA6AcxxeDyA63zouGS4exI98iuZlcdS48zbsGERkRPGg -hoGCo7HoiKtUNL5X8MYibnFYnA4EUngdHZsRKuKte4t8GY4PYq4cxIOYXsJsNmT5 -mkFy4ThGFfR9IGg/VfyeWIkRe2VWyaUgzL0gHytAhlRJ9l54ynx96YEWrjCtp/kh -d3KeVj0IUcMzvoXX5hipYUPkoezcxI8CAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgWg -MBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL -BQADggEBAM1jexLtuOOTsOPfEal/ICzfP9aX3m0R/yGPjwQv43jOc81NcL5d+CeD -MX36tKAxFIe+wvXo0kUQOzTK3D7ol4x2YTtB4uDzNE5tVh5dWi2LrKYSqZDIrhKO -MOmJRWR3AFEopaaGQpxsD/FSfZ5Mg0OMMBPHABxMrsiserHO1nh4ax3+SI0i7Jen -gVsB4B/Xxg9Lw9JDX3/XMcI+fyLVw5ctO62BaluljpT+HkdbRWnH8ar7TmcJjzTo -/TyXOeOLkbozx0BQK16d/CbtLomJ+PO4cdwCNs2Z6HGSPTL7S9y0pct52N0kfJfx -ZGXMsW+N62S2vVSXEekMR0GJgJnLNSo= ------END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py b/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py index 2e245191f..ccdd48fa8 100644 --- a/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py +++ b/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py @@ -1,82 +1,89 @@ -#!/usr/bin/env python  '''   Unit tests for the FakeOpenSSL classes  ''' -  import os +import subprocess  import sys -import unittest +  import pytest -# Disable import-error b/c our libraries aren't loaded in jenkins -# pylint: disable=import-error,wrong-import-position -# place class in our python path -module_path = os.path.join('/'.join(os.path.realpath(__file__).split(os.path.sep)[:-1]), 'library') -sys.path.insert(0, module_path) -openshift_cert_expiry = pytest.importorskip("openshift_cert_expiry") +MODULE_PATH = os.path.realpath(os.path.join(__file__, os.pardir, os.pardir, 'library')) +sys.path.insert(1, MODULE_PATH) + +# pylint: disable=import-error,wrong-import-position,missing-docstring +# pylint: disable=invalid-name,redefined-outer-name +from openshift_cert_expiry import FakeOpenSSLCertificate  # noqa: E402 + + +@pytest.fixture(scope='module') +def fake_valid_cert(valid_cert): +    cmd = ['openssl', 'x509', '-in', str(valid_cert['cert_file']), '-text'] +    cert = subprocess.check_output(cmd) +    return FakeOpenSSLCertificate(cert.decode('utf8')) + +def test_not_after(valid_cert, fake_valid_cert): +    ''' Validate value returned back from get_notAfter() ''' +    real_cert = valid_cert['cert'] -@pytest.mark.skip('Skipping all tests because of unresolved import errors') -class TestFakeOpenSSLClasses(unittest.TestCase): -    ''' -     Test class for FakeOpenSSL classes -    ''' +    # Internal representation of pyOpenSSL is bytes, while FakeOpenSSLCertificate +    # is text, so decode the result from pyOpenSSL prior to comparing +    assert real_cert.get_notAfter().decode('utf8') == fake_valid_cert.get_notAfter() -    def setUp(self): -        ''' setup method for other tests ''' -        with open('test/system-node-m01.example.com.crt.txt', 'r') as fp: -            self.cert_string = fp.read() -        self.fake_cert = openshift_cert_expiry.FakeOpenSSLCertificate(self.cert_string) +def test_serial(valid_cert, fake_valid_cert): +    ''' Validate value returned back form get_serialnumber() ''' +    real_cert = valid_cert['cert'] +    assert real_cert.get_serial_number() == fake_valid_cert.get_serial_number() -        with open('test/master.server.crt.txt', 'r') as fp: -            self.cert_san_string = fp.read() -        self.fake_san_cert = openshift_cert_expiry.FakeOpenSSLCertificate(self.cert_san_string) +def test_get_subject(valid_cert, fake_valid_cert): +    ''' Validate the certificate subject ''' -    def test_FakeOpenSSLCertificate_get_serial_number(self): -        """We can read the serial number from the cert""" -        self.assertEqual(11, self.fake_cert.get_serial_number()) +    # Gather the subject components and create a list of colon separated strings. +    # Since the internal representation of pyOpenSSL uses bytes, we need to decode +    # the results before comparing. +    c_subjects = valid_cert['cert'].get_subject().get_components() +    c_subj = ', '.join(['{}:{}'.format(x.decode('utf8'), y.decode('utf8')) for x, y in c_subjects]) +    f_subjects = fake_valid_cert.get_subject().get_components() +    f_subj = ', '.join(['{}:{}'.format(x, y) for x, y in f_subjects]) +    assert c_subj == f_subj -    def test_FakeOpenSSLCertificate_get_notAfter(self): -        """We can read the cert expiry date""" -        expiry = self.fake_cert.get_notAfter() -        self.assertEqual('20190207181935Z', expiry) -    def test_FakeOpenSSLCertificate_get_sans(self): -        """We can read Subject Alt Names from a cert""" -        ext = self.fake_san_cert.get_extension(0) +def get_san_extension(cert): +    # Internal representation of pyOpenSSL is bytes, while FakeOpenSSLCertificate +    # is text, so we need to set the value to search for accordingly. +    if isinstance(cert, FakeOpenSSLCertificate): +        san_short_name = 'subjectAltName' +    else: +        san_short_name = b'subjectAltName' -        if ext.get_short_name() == 'subjectAltName': -            sans = str(ext) +    for i in range(cert.get_extension_count()): +        ext = cert.get_extension(i) +        if ext.get_short_name() == san_short_name: +            # return the string representation to compare the actual SAN +            # values instead of the data types +            return str(ext) -        self.assertEqual('DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:m01.example.com, DNS:openshift, DNS:openshift.default, DNS:openshift.default.svc, DNS:openshift.default.svc.cluster.local, DNS:172.30.0.1, DNS:192.168.122.241, IP Address:172.30.0.1, IP Address:192.168.122.241', sans) +    return None -    def test_FakeOpenSSLCertificate_get_sans_no_sans(self): -        """We can tell when there are no Subject Alt Names in a cert""" -        with self.assertRaises(IndexError): -            self.fake_cert.get_extension(0) -    def test_FakeOpenSSLCertificate_get_subject(self): -        """We can read the Subject from a cert""" -        # Subject: O=system:nodes, CN=system:node:m01.example.com -        subject = self.fake_cert.get_subject() -        subjects = [] -        for name, value in subject.get_components(): -            subjects.append('{}={}'.format(name, value)) +def test_subject_alt_names(valid_cert, fake_valid_cert): +    real_cert = valid_cert['cert'] -        self.assertEqual('O=system:nodes, CN=system:node:m01.example.com', ', '.join(subjects)) +    san = get_san_extension(real_cert) +    f_san = get_san_extension(fake_valid_cert) -    def test_FakeOpenSSLCertificate_get_subject_san_cert(self): -        """We can read the Subject from a cert with sans""" -        # Subject: O=system:nodes, CN=system:node:m01.example.com -        subject = self.fake_san_cert.get_subject() -        subjects = [] -        for name, value in subject.get_components(): -            subjects.append('{}={}'.format(name, value)) +    assert san == f_san -        self.assertEqual('CN=172.30.0.1', ', '.join(subjects)) +    # If there are either dns or ip sans defined, verify common_name present +    if valid_cert['ip'] or valid_cert['dns']: +        assert 'DNS:' + valid_cert['common_name'] in f_san +    # Verify all ip sans are present +    for ip in valid_cert['ip']: +        assert 'IP Address:' + ip in f_san -if __name__ == "__main__": -    unittest.main() +    # Verify all dns sans are present +    for name in valid_cert['dns']: +        assert 'DNS:' + name in f_san diff --git a/roles/openshift_certificate_expiry/test/test_load_and_handle_cert.py b/roles/openshift_certificate_expiry/test/test_load_and_handle_cert.py new file mode 100644 index 000000000..98792e2ee --- /dev/null +++ b/roles/openshift_certificate_expiry/test/test_load_and_handle_cert.py @@ -0,0 +1,67 @@ +''' + Unit tests for the load_and_handle_cert method +''' +import datetime +import os +import sys + +import pytest + +MODULE_PATH = os.path.realpath(os.path.join(__file__, os.pardir, os.pardir, 'library')) +sys.path.insert(1, MODULE_PATH) + +# pylint: disable=import-error,wrong-import-position,missing-docstring +# pylint: disable=invalid-name,redefined-outer-name +import openshift_cert_expiry  # noqa: E402 + +# TODO: More testing on the results of the load_and_handle_cert function +# could be implemented here as well, such as verifying subjects +# match up. + + +@pytest.fixture(params=['OpenSSLCertificate', 'FakeOpenSSLCertificate']) +def loaded_cert(request, valid_cert): +    """ parameterized fixture to provide load_and_handle_cert results +        for both OpenSSL and FakeOpenSSL parsed certificates +    """ +    now = datetime.datetime.now() + +    openshift_cert_expiry.HAS_OPENSSL = request.param == 'OpenSSLCertificate' + +    # valid_cert['cert_file'] is a `py.path.LocalPath` object and +    # provides a read_text() method for reading the file contents. +    cert_string = valid_cert['cert_file'].read_text('utf8') + +    (subject, +     expiry_date, +     time_remaining, +     serial) = openshift_cert_expiry.load_and_handle_cert(cert_string, now) + +    return { +        'now': now, +        'subject': subject, +        'expiry_date': expiry_date, +        'time_remaining': time_remaining, +        'serial': serial, +    } + + +def test_serial(loaded_cert, valid_cert): +    """Params: + +    * `loaded_cert` comes from the `loaded_cert` fixture in this file +    * `valid_cert` comes from the 'valid_cert' fixture in conftest.py +    """ +    valid_cert_serial = valid_cert['cert'].get_serial_number() +    assert loaded_cert['serial'] == valid_cert_serial + + +def test_expiry(loaded_cert): +    """Params: + +    * `loaded_cert` comes from the `loaded_cert` fixture in this file +    """ +    expiry_date = loaded_cert['expiry_date'] +    time_remaining = loaded_cert['time_remaining'] +    now = loaded_cert['now'] +    assert expiry_date == now + time_remaining  | 
