#!/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):