text: #!/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
  import dateutil.parser
  
  # 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 = dateutil.parser.parse(not_after_raw)
                  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_node_bootstrap_config_path = os.path.join(openshift_base_config_path,
                                                          "node", "bootstrap-node-config.yaml")
      openshift_cert_check_paths = [
          openshift_master_config_path,
          openshift_node_config_path,
          openshift_node_bootstrap_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.
  
      for node_config in [openshift_node_config_path, openshift_node_bootstrap_config_path]:
          try:
              # Try to read the standard 'node-config.yaml' file to check if
              # this host is a node.
              with io.open(node_config, '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'].get('client-certificate-data')
              if not c:
                  # This is not a node
                  raise IOError
              (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)
  
      ######################################################################
      # /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)
      warn_certs = bool(res['expired'] + res['warning'])
      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,
          warn_certs=warn_certs,
          summary=res,
          msg=msg,
          rc=0,
          changed=False
      )
  
  
  if __name__ == '__main__':
      main()
  