#!/usr/bin/python
# -*- coding: utf-8 -*-
# pylint: disable=line-too-long,invalid-name

"""For details on this module see DOCUMENTATION (below)"""

import base64
import datetime
import io
import os
import subprocess
import yaml

# pylint import-error disabled because pylint cannot find the package
# when installed in a virtualenv
from ansible.module_utils.six.moves import configparser  # pylint: disable=import-error
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
    # HOST (or any host that just doesn't have PyOpenSSL
    # 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.
    HAS_OPENSSL = False

DOCUMENTATION = '''
---
module: openshift_cert_expiry
short_description: Check OpenShift Container Platform (OCP) and Kube certificate expirations on a cluster
description:
  - The M(openshift_cert_expiry) module has two basic functions: to flag certificates which will expire in a set window of time from now, and to notify you about certificates which have already expired.
  - When the module finishes, a summary of the examination is returned. Each certificate in the summary has a C(health) key with a value of one of the following:
  - C(ok) - not expired, and outside of the expiration C(warning_days) window.
  - C(warning) - not expired, but will expire between now and the C(warning_days) window.
  - C(expired) - an expired certificate.
  - Certificate flagging follow this logic:
  - If the expiration date is before now then the certificate is classified as C(expired).
  - The certificates time to live (expiration date - now) is calculated, if that time window is less than C(warning_days) the certificate is classified as C(warning).
  - All other conditions are classified as C(ok).
  - The following keys are ALSO present in the certificate summary:
  - C(cert_cn) - The common name of the certificate (additional CNs present in SAN extensions are omitted)
  - C(days_remaining) - The number of days until the certificate expires.
  - C(expiry) - The date the certificate expires on.
  - C(path) - The full path to the certificate on the examined host.
version_added: "1.0"
options:
  config_base:
    description:
      - Base path to OCP system settings.
    required: false
    default: /etc/origin
  warning_days:
    description:
      - Flag certificates which will expire in C(warning_days) days from now.
    required: false
    default: 30
  show_all:
    description:
      - Enable this option to show analysis of ALL certificates examined by this module.
      - By default only certificates which have expired, or will expire within the C(warning_days) window will be reported.
    required: false
    default: false

author: "Tim Bielawa (@tbielawa) <tbielawa@redhat.com>"
'''

EXAMPLES = '''
# Default invocation, only notify about expired certificates or certificates which will expire within 30 days from now
- openshift_cert_expiry:

# Expand the warning window to show certificates expiring within a year from now
- openshift_cert_expiry: warning_days=365

# Show expired, soon to expire (now + 30 days), and all other certificates examined
- openshift_cert_expiry: show_all=true
'''


class FakeOpenSSLCertificate(object):
    """This provides a rough mock of what you get from
`OpenSSL.crypto.load_certificate()`. This is a work-around for
platforms missing the Python OpenSSL library.
    """
    def __init__(self, cert_string):
        """`cert_string` is a certificate in the form you get from running a
.crt through 'openssl x509 -in CERT.cert -text'"""
        self.cert_string = cert_string
        self.serial = None
        self.subject = None
        self.extensions = []
        self.not_after = None
        self._parse_cert()

    def _parse_cert(self):
        """Manually parse the certificate line by line"""
        self.extensions = []

        PARSING_ALT_NAMES = False
        PARSING_HEX_SERIAL = False
        for line in self.cert_string.split('\n'):
            l = line.strip()
            if PARSING_ALT_NAMES:
                # We're parsing a 'Subject Alternative Name' line
                self.extensions.append(
                    FakeOpenSSLCertificateSANExtension(l))

                PARSING_ALT_NAMES = False
                continue

            if PARSING_HEX_SERIAL:
                # Hex serials arrive colon-delimited
                serial_raw = l.replace(':', '')
                # Convert to decimal
                self.serial = int('0x' + serial_raw, base=16)
                PARSING_HEX_SERIAL = False
                continue

            # parse out the bits that we can
            if l.startswith('Serial Number:'):
                # Decimal format:
                #   Serial Number: 11 (0xb)
                #   => 11
                # Hex Format (large serials):
                #   Serial Number:
                #       0a:de:eb:24:04:75:ab:56:39:14:e9:5a:22:e2:85:bf
                #   => 14449739080294792594019643629255165375
                if l.endswith(':'):
                    PARSING_HEX_SERIAL = True
                    continue
                self.serial = int(l.split()[-2])

            elif l.startswith('Not After :'):
                # Not After : Feb  7 18:19:35 2019 GMT
                # => strptime(str, '%b %d %H:%M:%S %Y %Z')
                # => strftime('%Y%m%d%H%M%SZ')
                # => 20190207181935Z
                not_after_raw = l.partition(' : ')[-1]
                # Last item: ('Not After', ' : ', 'Feb  7 18:19:35 2019 GMT')
                not_after_parsed = datetime.datetime.strptime(not_after_raw, '%b %d %H:%M:%S %Y %Z')
                self.not_after = not_after_parsed.strftime('%Y%m%d%H%M%SZ')

            elif l.startswith('X509v3 Subject Alternative Name:'):
                PARSING_ALT_NAMES = True
                continue

            elif l.startswith('Subject:'):
                # O = system:nodes, CN = system:node:m01.example.com
                self.subject = FakeOpenSSLCertificateSubjects(l.partition(': ')[-1])

    def get_serial_number(self):
        """Return the serial number of the cert"""
        return self.serial

    def get_subject(self):
        """Subjects must implement get_components() and return dicts or
tuples. An 'openssl x509 -in CERT.cert -text' with 'Subject':

    Subject: Subject: O=system:nodes, CN=system:node:m01.example.com

might return: [('O=system', 'nodes'), ('CN=system', 'node:m01.example.com')]
        """
        return self.subject

    def get_extension(self, i):
        """Extensions must implement get_short_name() and return the string
'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:
'%Y%m%d%H%M%SZ'."""
        return self.not_after


class FakeOpenSSLCertificateSANExtension(object):  # pylint: disable=too-few-public-methods
    """Mocks what happens when `get_extension` is called on a certificate
object"""

    def __init__(self, san_string):
        """With `san_string` as you get from:

    $ openssl x509 -in certificate.crt -text
        """
        self.san_string = san_string
        self.short_name = 'subjectAltName'

    def get_short_name(self):
        """Return the 'type' of this extension. It's always the same though
because we only care about subjectAltName's"""
        return self.short_name

    def __str__(self):
        """Return this extension and the value as a simple string"""
        return self.san_string


# pylint: disable=too-few-public-methods
class FakeOpenSSLCertificateSubjects(object):
    """Mocks what happens when `get_subject` is called on a certificate
object"""

    def __init__(self, subject_string):
        """With `subject_string` as you get from:

    $ openssl x509 -in certificate.crt -text
        """
        self.subjects = []
        for s in subject_string.split(', '):
            name, _, value = s.partition(' = ')
            self.subjects.append((name, value))

    def get_components(self):
        """Returns a list of tuples"""
        return self.subjects


######################################################################
def filter_paths(path_list):
    """`path_list` - A list of file paths to check. Only files which exist
will be returned
    """
    return [p for p in path_list if os.path.exists(os.path.realpath(p))]


# pylint: disable=too-many-locals,too-many-branches
#
# TODO: Break this function down into smaller chunks
def load_and_handle_cert(cert_string, now, base64decode=False, ans_module=None):
    """Load a certificate, split off the good parts, and return some
useful data

Params:

- `cert_string` (string) - a certificate loaded into a string object
- `now` (datetime) - a datetime object of the time to calculate the certificate 'time_remaining' against
- `base64decode` (bool) - run base64.b64decode() on the input
- `ans_module` (AnsibleModule) - The AnsibleModule object for this module (so we can raise errors)

Returns:
A tuple of the form:
    (cert_subject, cert_expiry_date, time_remaining, cert_serial_number)
    """
    if base64decode:
        _cert_string = base64.b64decode(cert_string).decode('utf-8')
    else:
        _cert_string = cert_string

    # Disable this. We 'redefine' the type because we are working
    # around a missing library on the target host.
    #
    # pylint: disable=redefined-variable-type
    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. Run the 'openssl'
        # command on it to decode it
        cmd = 'openssl x509 -text'
        try:
            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_proc.communicate(_cert_string.encode('utf-8'))[0].decode('utf-8')
            cert_loaded = FakeOpenSSLCertificate(openssl_decoded)

    ######################################################################
    # Read all possible names from the cert
    cert_subjects = []
    for name, value in cert_loaded.get_subject().get_components():
        if isinstance(name, bytes) or isinstance(value, bytes):
            name = name.decode('utf-8')
            value = value.decode('utf-8')
        cert_subjects.append('{}:{}'.format(name, value))

    # 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 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
        # string with the alt names separated by a comma and a
        # space. Split the string by ', ' and then add our new names
        # to the list of existing names
        cert_subjects.extend(str(san).split(', '))

    cert_subject = ', '.join(cert_subjects)
    ######################################################################

    # 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(
        not_after,
        '%Y%m%d%H%M%SZ')

    time_remaining = cert_expiry_date - now

    return (cert_subject, cert_expiry_date, time_remaining, cert_loaded.get_serial_number())


def classify_cert(cert_meta, now, time_remaining, expire_window, cert_list):
    """Given metadata about a certificate under examination, classify it
    into one of three categories, 'ok', 'warning', and 'expired'.

Params:

- `cert_meta` dict - A dict with certificate metadata. Required fields
  include: 'cert_cn', 'path', 'expiry', 'days_remaining', 'health'.
- `now` (datetime) - a datetime object of the time to calculate the certificate 'time_remaining' against
- `time_remaining` (datetime.timedelta) - a timedelta for how long until the cert expires
- `expire_window` (datetime.timedelta) - a timedelta for how long the warning window is
- `cert_list` list - A list to shove the classified cert into

Return:
- `cert_list` - The updated list of classified certificates
    """
    expiry_str = str(cert_meta['expiry'])
    # Categorization
    if cert_meta['expiry'] < now:
        # This already expired, must NOTIFY
        cert_meta['health'] = 'expired'
    elif time_remaining < expire_window:
        # WARN about this upcoming expirations
        cert_meta['health'] = 'warning'
    else:
        # Not expired or about to expire
        cert_meta['health'] = 'ok'

    cert_meta['expiry'] = expiry_str
    cert_meta['serial_hex'] = hex(int(cert_meta['serial']))
    cert_list.append(cert_meta)
    return cert_list


def tabulate_summary(certificates, kubeconfigs, etcd_certs, router_certs, registry_certs):
    """Calculate the summary text for when the module finishes
running. This includes counts of each classification and what have
you.

Params:

- `certificates` (list of dicts) - Processed `expire_check_result`
  dicts with filled in `health` keys for system certificates.
- `kubeconfigs` - as above for kubeconfigs
- `etcd_certs` - as above for etcd certs

Return:

- `summary_results` (dict) - Counts of each cert type classification
  and total items examined.
    """
    items = certificates + kubeconfigs + etcd_certs + router_certs + registry_certs

    summary_results = {
        'system_certificates': len(certificates),
        'kubeconfig_certificates': len(kubeconfigs),
        'etcd_certificates': len(etcd_certs),
        'router_certs': len(router_certs),
        'registry_certs': len(registry_certs),
        'total': len(items),
        'ok': 0,
        'warning': 0,
        'expired': 0
    }

    summary_results['expired'] = len([c for c in items if c['health'] == 'expired'])
    summary_results['warning'] = len([c for c in items if c['health'] == 'warning'])
    summary_results['ok'] = len([c for c in items if c['health'] == 'ok'])

    return summary_results


######################################################################
# This is our module MAIN function after all, so there's bound to be a
# lot of code bundled up into one block
#
# Reason: These checks are disabled because the issue was introduced
# during a period where the pylint checks weren't enabled for this file
# Status: temporarily disabled pending future refactoring
# pylint: disable=too-many-locals,too-many-statements,too-many-branches
def main():
    """This module examines certificates (in various forms) which compose
an OpenShift Container Platform cluster
    """

    module = AnsibleModule(
        argument_spec=dict(
            config_base=dict(
                required=False,
                default="/etc/origin",
                type='str'),
            warning_days=dict(
                required=False,
                default=30,
                type='int'),
            show_all=dict(
                required=False,
                default=False,
                type='bool')
        ),
        supports_check_mode=True,
    )

    # Basic scaffolding for OpenShift specific certs
    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,
    ]

    # Paths for Kubeconfigs. Additional kubeconfigs are conditionally
    # checked later in the code
    master_kube_configs = ['admin', 'openshift-master',
                           'openshift-node', 'openshift-router',
                           'openshift-registry']

    kubeconfig_paths = []
    for m_kube_config in master_kube_configs:
        kubeconfig_paths.append(
            os.path.join(openshift_base_config_path, "master", m_kube_config + ".kubeconfig")
        )

    # Validate some paths we have the ability to do ahead of time
    openshift_cert_check_paths = filter_paths(openshift_cert_check_paths)
    kubeconfig_paths = filter_paths(kubeconfig_paths)

    # etcd, where do you hide your certs? Used when parsing etcd.conf
    etcd_cert_params = [
        "ETCD_TRUSTED_CA_FILE",
        "ETCD_CERT_FILE",
        "ETCD_PEER_TRUSTED_CA_FILE",
        "ETCD_PEER_CERT_FILE",
    ]

    # Expiry checking stuff
    now = datetime.datetime.now()
    # todo, catch exception for invalid input and return a fail_json
    warning_days = int(module.params['warning_days'])
    expire_window = datetime.timedelta(days=warning_days)

    # Module stuff
    #
    # The results of our cert checking to return from the task call
    check_results = {}
    check_results['meta'] = {}
    check_results['meta']['warning_days'] = warning_days
    check_results['meta']['checked_at_time'] = str(now)
    check_results['meta']['warn_before_date'] = str(now + expire_window)
    check_results['meta']['show_all'] = str(module.params['show_all'])
    # All the analyzed certs accumulate here
    ocp_certs = []

    ######################################################################
    # Sure, why not? Let's enable check mode.
    if module.check_mode:
        check_results['ocp_certs'] = []
        module.exit_json(
            check_results=check_results,
            msg="Checked 0 total certificates. Expired/Warning/OK: 0/0/0. Warning window: %s days" % module.params['warning_days'],
            rc=0,
            changed=False
        )

    ######################################################################
    # Check for OpenShift Container Platform specific certs
    ######################################################################
    for os_cert in filter_paths(openshift_cert_check_paths):
        # Open up that config file and locate the cert and CA
        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
            # of the original config file. 'master-config.yaml' with certFile
            # = 'foo.crt' implies that 'foo.crt' is in the same
            # directory. certFile = '../foo.crt' is in the parent directory.
            cfg_path = os.path.dirname(fp.name)

            servingInfoFile = cfg.get('servingInfo', {}).get('certFile')
            if servingInfoFile:
                cert_meta['certFile'] = os.path.join(cfg_path, servingInfoFile)

            servingInfoCA = cfg.get('servingInfo', {}).get('clientCA')
            if servingInfoCA:
                cert_meta['clientCA'] = os.path.join(cfg_path, servingInfoCA)

            serviceSigner = cfg.get('controllerConfig', {}).get('serviceServingCert', {}).get('signer', {}).get('certFile')
            if serviceSigner:
                cert_meta['serviceSigner'] = os.path.join(cfg_path, serviceSigner)

            etcdClientCA = cfg.get('etcdClientInfo', {}).get('ca')
            if etcdClientCA:
                cert_meta['etcdClientCA'] = os.path.join(cfg_path, etcdClientCA)

            etcdClientCert = cfg.get('etcdClientInfo', {}).get('certFile')
            if etcdClientCert:
                cert_meta['etcdClientCert'] = os.path.join(cfg_path, etcdClientCert)

            kubeletCert = cfg.get('kubeletClientInfo', {}).get('certFile')
            if kubeletCert:
                cert_meta['kubeletCert'] = os.path.join(cfg_path, kubeletCert)

            proxyClient = cfg.get('kubernetesMasterConfig', {}).get('proxyClientInfo', {}).get('certFile')
            if proxyClient:
                cert_meta['proxyClient'] = os.path.join(cfg_path, proxyClient)

        ######################################################################
        # Load the certificate and the CA, parse their expiration dates into
        # datetime objects so we can manipulate them later
        for v in cert_meta.values():
            with io.open(v, 'r', encoding='utf-8') as fp:
                cert = fp.read()
                (cert_subject,
                 cert_expiry_date,
                 time_remaining,
                 cert_serial) = load_and_handle_cert(cert, now, ans_module=module)

                expire_check_result = {
                    'cert_cn': cert_subject,
                    'path': fp.name,
                    'expiry': cert_expiry_date,
                    'days_remaining': time_remaining.days,
                    'health': None,
                    'serial': cert_serial
                }

                classify_cert(expire_check_result, now, time_remaining, expire_window, ocp_certs)

    ######################################################################
    # /Check for OpenShift Container Platform specific certs
    ######################################################################

    ######################################################################
    # Check service Kubeconfigs
    ######################################################################
    kubeconfigs = []

    # There may be additional kubeconfigs to check, but their naming
    # is less predictable than the ones we've already assembled.

    try:
        # Try to read the standard 'node-config.yaml' file to check if
        # this host is a node.
        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
        # node. Nodes have their own kubeconfig files to
        # communicate with the master API. Let's read the relative
        # path to that file from the node config.
        node_masterKubeConfig = cfg['masterKubeConfig']
        # As before, the path to the 'masterKubeConfig' file is
        # relative to `fp`
        cfg_path = os.path.dirname(fp.name)
        node_kubeconfig = os.path.join(cfg_path, node_masterKubeConfig)

        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)

        c = cfg['users'][0]['user']['client-certificate-data']
        (cert_subject,
         cert_expiry_date,
         time_remaining,
         cert_serial) = load_and_handle_cert(c, now, base64decode=True, ans_module=module)

        expire_check_result = {
            'cert_cn': cert_subject,
            'path': fp.name,
            'expiry': cert_expiry_date,
            'days_remaining': time_remaining.days,
            'health': None,
            'serial': cert_serial
        }

        classify_cert(expire_check_result, now, time_remaining, expire_window, kubeconfigs)
    except IOError:
        # This is not a node
        pass

    for kube in filter_paths(kubeconfig_paths):
        with io.open(kube, 'r', encoding='utf-8') as fp:
            # TODO: Maybe consider catching exceptions here?
            cfg = yaml.load(fp)

        # Per conversation, "the kubeconfigs you care about:
        # admin, router, registry should all be single
        # value". Following that advice we only grab the data for
        # the user at index 0 in the 'users' list. There should
        # not be more than one user.
        c = cfg['users'][0]['user']['client-certificate-data']
        (cert_subject,
         cert_expiry_date,
         time_remaining,
         cert_serial) = load_and_handle_cert(c, now, base64decode=True, ans_module=module)

        expire_check_result = {
            'cert_cn': cert_subject,
            'path': fp.name,
            'expiry': cert_expiry_date,
            'days_remaining': time_remaining.days,
            'health': None,
            'serial': cert_serial
        }

        classify_cert(expire_check_result, now, time_remaining, expire_window, kubeconfigs)

    ######################################################################
    # /Check service Kubeconfigs
    ######################################################################

    ######################################################################
    # Check etcd certs
    #
    # Two things to check: 'external' etcd, and embedded etcd.
    ######################################################################
    # FIRST: The 'external' etcd
    #
    # Some values may be duplicated, make this a set for now so we
    # unique them all
    etcd_certs_to_check = set([])
    etcd_certs = []
    etcd_cert_params.append('dne')
    try:
        with io.open('/etc/etcd/etcd.conf', 'r', encoding='utf-8') as fp:
            # Add dummy header section.
            config = io.StringIO()
            config.write(u'[ETCD]\n')
            config.write(fp.read().replace('%', '%%'))
            config.seek(0, os.SEEK_SET)

            etcd_config = configparser.ConfigParser()
            etcd_config.readfp(config)

        for param in etcd_cert_params:
            try:
                etcd_certs_to_check.add(etcd_config.get('ETCD', param))
            except configparser.NoOptionError:
                # That parameter does not exist, oh well...
                pass
    except IOError:
        # No etcd to see here, move along
        pass

    for etcd_cert in filter_paths(etcd_certs_to_check):
        with io.open(etcd_cert, 'r', encoding='utf-8') as fp:
            c = fp.read()
            (cert_subject,
             cert_expiry_date,
             time_remaining,
             cert_serial) = load_and_handle_cert(c, now, ans_module=module)

            expire_check_result = {
                'cert_cn': cert_subject,
                'path': fp.name,
                'expiry': cert_expiry_date,
                'days_remaining': time_remaining.days,
                'health': None,
                'serial': cert_serial
            }

            classify_cert(expire_check_result, now, time_remaining, expire_window, etcd_certs)

    ######################################################################
    # Now the embedded etcd
    ######################################################################
    try:
        with io.open('/etc/origin/master/master-config.yaml', 'r', encoding='utf-8') as fp:
            cfg = yaml.load(fp)
    except IOError:
        # Not present
        pass
    else:
        if cfg.get('etcdConfig', {}).get('servingInfo', {}).get('certFile', None) is not None:
            # This is embedded
            etcd_crt_name = cfg['etcdConfig']['servingInfo']['certFile']
        else:
            # Not embedded
            etcd_crt_name = None

        if etcd_crt_name is not None:
            # etcd_crt_name is relative to the location of the
            # master-config.yaml file
            cfg_path = os.path.dirname(fp.name)
            etcd_cert = os.path.join(cfg_path, etcd_crt_name)
            with open(etcd_cert, 'r') as etcd_fp:
                (cert_subject,
                 cert_expiry_date,
                 time_remaining,
                 cert_serial) = load_and_handle_cert(etcd_fp.read(), now, ans_module=module)

                expire_check_result = {
                    'cert_cn': cert_subject,
                    'path': etcd_fp.name,
                    'expiry': cert_expiry_date,
                    'days_remaining': time_remaining.days,
                    'health': None,
                    'serial': cert_serial
                }

                classify_cert(expire_check_result, now, time_remaining, expire_window, etcd_certs)

    ######################################################################
    # /Check etcd certs
    ######################################################################

    ######################################################################
    # Check router/registry certs
    #
    # These are saved as secrets in etcd. That means that we can not
    # simply read a file to grab the data. Instead we're going to
    # subprocess out to the 'oc get' command. On non-masters this
    # command will fail, that is expected so we catch that exception.
    ######################################################################
    router_certs = []
    registry_certs = []

    ######################################################################
    # First the router certs
    try:
        router_secrets_raw = subprocess.Popen('oc get -n default secret router-certs -o yaml'.split(),
                                              stdout=subprocess.PIPE)
        router_ds = yaml.load(router_secrets_raw.communicate()[0])
        router_c = router_ds['data']['tls.crt']
        router_path = router_ds['metadata']['selfLink']
    except TypeError:
        # YAML couldn't load the result, this is not a master
        pass
    except OSError:
        # The OC command doesn't exist here. Move along.
        pass
    else:
        (cert_subject,
         cert_expiry_date,
         time_remaining,
         cert_serial) = load_and_handle_cert(router_c, now, base64decode=True, ans_module=module)

        expire_check_result = {
            'cert_cn': cert_subject,
            'path': router_path,
            'expiry': cert_expiry_date,
            'days_remaining': time_remaining.days,
            'health': None,
            'serial': cert_serial
        }

        classify_cert(expire_check_result, now, time_remaining, expire_window, router_certs)

    ######################################################################
    # Now for registry
    try:
        registry_secrets_raw = subprocess.Popen('oc get -n default secret registry-certificates -o yaml'.split(),
                                                stdout=subprocess.PIPE)
        registry_ds = yaml.load(registry_secrets_raw.communicate()[0])
        registry_c = registry_ds['data']['registry.crt']
        registry_path = registry_ds['metadata']['selfLink']
    except TypeError:
        # YAML couldn't load the result, this is not a master
        pass
    except OSError:
        # The OC command doesn't exist here. Move along.
        pass
    else:
        (cert_subject,
         cert_expiry_date,
         time_remaining,
         cert_serial) = load_and_handle_cert(registry_c, now, base64decode=True, ans_module=module)

        expire_check_result = {
            'cert_cn': cert_subject,
            'path': registry_path,
            'expiry': cert_expiry_date,
            'days_remaining': time_remaining.days,
            'health': None,
            'serial': cert_serial
        }

        classify_cert(expire_check_result, now, time_remaining, expire_window, registry_certs)

    ######################################################################
    # /Check router/registry certs
    ######################################################################

    res = tabulate_summary(ocp_certs, kubeconfigs, etcd_certs, router_certs, registry_certs)

    msg = "Checked {count} total certificates. Expired/Warning/OK: {exp}/{warn}/{ok}. Warning window: {window} days".format(
        count=res['total'],
        exp=res['expired'],
        warn=res['warning'],
        ok=res['ok'],
        window=int(module.params['warning_days']),
    )

    # By default we only return detailed information about expired or
    # warning certificates. If show_all is true then we will print all
    # the certificates examined.
    if not module.params['show_all']:
        check_results['ocp_certs'] = [crt for crt in ocp_certs if crt['health'] in ['expired', 'warning']]
        check_results['kubeconfigs'] = [crt for crt in kubeconfigs if crt['health'] in ['expired', 'warning']]
        check_results['etcd'] = [crt for crt in etcd_certs if crt['health'] in ['expired', 'warning']]
        check_results['registry'] = [crt for crt in registry_certs if crt['health'] in ['expired', 'warning']]
        check_results['router'] = [crt for crt in router_certs if crt['health'] in ['expired', 'warning']]
    else:
        check_results['ocp_certs'] = ocp_certs
        check_results['kubeconfigs'] = kubeconfigs
        check_results['etcd'] = etcd_certs
        check_results['registry'] = registry_certs
        check_results['router'] = router_certs

    # Sort the final results to report in order of ascending safety
    # time. That is to say, the certificates which will expire sooner
    # will be at the front of the list and certificates which will
    # expire later are at the end. Router and registry certs should be
    # limited to just 1 result, so don't bother sorting those.
    def cert_key(item):
        ''' return the days_remaining key '''
        return item['days_remaining']

    check_results['ocp_certs'] = sorted(check_results['ocp_certs'], key=cert_key)
    check_results['kubeconfigs'] = sorted(check_results['kubeconfigs'], key=cert_key)
    check_results['etcd'] = sorted(check_results['etcd'], key=cert_key)

    # This module will never change anything, but we might want to
    # change the return code parameter if there is some catastrophic
    # error we noticed earlier
    module.exit_json(
        check_results=check_results,
        summary=res,
        msg=msg,
        rc=0,
        changed=False
    )


if __name__ == '__main__':
    main()
