From a22e8daf1f7c93469023239f442f19608ffbc970 Mon Sep 17 00:00:00 2001 From: Tim Bielawa Date: Fri, 7 Oct 2016 09:55:13 -0700 Subject: Refactor into a role --- library/openshift_cert_expiry.py | 635 --------------------- .../byo/openshift-cluster/check-cert-expiry.yaml | 35 -- .../openshift-cluster/check-cert-expiry.yaml | 44 -- .../templates/cert-expiry-table.html.j2 | 124 ---- roles/openshift_certificate_expiry/README.md | 63 ++ .../openshift_certificate_expiry/defaults/main.yml | 6 + .../library/openshift_cert_expiry.py | 635 +++++++++++++++++++++ roles/openshift_certificate_expiry/meta/main.yml | 15 + roles/openshift_certificate_expiry/tasks/main.yml | 23 + .../templates/cert-expiry-table.html.j2 | 124 ++++ .../templates/save_json_results.j2 | 5 + 11 files changed, 871 insertions(+), 838 deletions(-) delete mode 100644 library/openshift_cert_expiry.py delete mode 100644 playbooks/byo/openshift-cluster/check-cert-expiry.yaml delete mode 100644 playbooks/common/openshift-cluster/check-cert-expiry.yaml delete mode 100644 playbooks/common/openshift-cluster/templates/cert-expiry-table.html.j2 create mode 100644 roles/openshift_certificate_expiry/README.md create mode 100644 roles/openshift_certificate_expiry/defaults/main.yml create mode 100644 roles/openshift_certificate_expiry/library/openshift_cert_expiry.py create mode 100644 roles/openshift_certificate_expiry/meta/main.yml create mode 100644 roles/openshift_certificate_expiry/tasks/main.yml create mode 100644 roles/openshift_certificate_expiry/templates/cert-expiry-table.html.j2 create mode 100644 roles/openshift_certificate_expiry/templates/save_json_results.j2 diff --git a/library/openshift_cert_expiry.py b/library/openshift_cert_expiry.py deleted file mode 100644 index f18ab75d0..000000000 --- a/library/openshift_cert_expiry.py +++ /dev/null @@ -1,635 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# pylint: disable=line-too-long,invalid-name - -"""For details on this module see DOCUMENTATION (below)""" - -# router/registry cert grabbing -import subprocess -# etcd config file -import ConfigParser -# Expiration parsing -import datetime -# File path stuff -import os -# Config file parsing -import yaml -# Certificate loading -import OpenSSL.crypto - -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: "0.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) " -''' - -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 -''' - - -# We only need this for one thing, we don't care if it doesn't have -# that many public methods -# -# pylint: disable=too-few-public-methods -class FakeSecHead(object): - """etcd does not begin their config file with an opening [section] as -required by the Python ConfigParser module. We hack around it by -slipping one in ourselves prior to parsing. - -Source: Alex Martelli - http://stackoverflow.com/a/2819788/6490583 - """ - def __init__(self, fp): - self.fp = fp - self.sechead = '[ETCD]\n' - - def readline(self): - """Make this look like a file-type object""" - if self.sechead: - try: - return self.sechead - finally: - self.sechead = None - else: - return self.fp.readline() - - -###################################################################### - -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))] - - -def load_and_handle_cert(cert_string, now, base64decode=False): - """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 .decode('base64') on the input? - -Returns: -A 3-tuple of the form: (certificate_common_name, certificate_expiry_date, certificate_time_remaining) - - """ - if base64decode: - _cert_string = cert_string.decode('base-64') - else: - _cert_string = cert_string - - cert_loaded = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, _cert_string) - - ###################################################################### - # Read just the first name from the cert - DISABLED while testing - # out the 'get all possible names' function (below) - # - # Strip the subject down to just the value of the first name - # cert_subject = cert_loaded.get_subject().get_components()[0][1] - - ###################################################################### - # Read all possible names from the cert - cert_subjects = [] - for name, value in cert_loaded.get_subject().get_components(): - 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 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 - - 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 - cert_expiry = cert_loaded.get_notAfter() - cert_expiry_date = datetime.datetime.strptime( - cert_expiry, - # example get_notAfter() => 20180922170439Z - '%Y%m%d%H%M%SZ') - - time_remaining = cert_expiry_date - now - - return (cert_subject, cert_expiry_date, time_remaining) - - -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_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 counds 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 -# -# pylint: disable=too-many-locals,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=int(30), - type='int'), - show_all=dict( - required=False, - default="False", - type='bool') - ), - supports_check_mode=True, - ) - - # Basic scaffolding for OpenShift spcific 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_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.normpath( - os.path.join(openshift_base_config_path, "master/%s.kubeconfig" % m_kube_config) - ) - ) - - # etcd, where do you hide your certs? Used when parsing etcd.conf - etcd_cert_params = [ - "ETCD_CA_FILE", - "ETCD_CERT_FILE", - "ETCD_PEER_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_after_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 open(os_cert, 'r') 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) - cert_meta['certFile'] = os.path.join(cfg_path, cfg['servingInfo']['certFile']) - cert_meta['clientCA'] = os.path.join(cfg_path, cfg['servingInfo']['clientCA']) - - ###################################################################### - # Load the certificate and the CA, parse their expiration dates into - # datetime objects so we can manipulate them later - for _, v in cert_meta.iteritems(): - with open(v, 'r') as fp: - cert = fp.read() - cert_subject, cert_expiry_date, time_remaining = load_and_handle_cert(cert, now) - - expire_check_result = { - 'cert_cn': cert_subject, - 'path': fp.name, - 'expiry': cert_expiry_date, - 'days_remaining': time_remaining.days, - 'health': None, - } - - 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 open(openshift_node_config_path, 'r') 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 open(node_kubeconfig, 'r') 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) = load_and_handle_cert(c, now, base64decode=True) - - expire_check_result = { - 'cert_cn': cert_subject, - 'path': fp.name, - 'expiry': cert_expiry_date, - 'days_remaining': time_remaining.days, - 'health': None, - } - - 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 open(kube, 'r') 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) = load_and_handle_cert(c, now, base64decode=True) - - expire_check_result = { - 'cert_cn': cert_subject, - 'path': fp.name, - 'expiry': cert_expiry_date, - 'days_remaining': time_remaining.days, - 'health': None, - } - - classify_cert(expire_check_result, now, time_remaining, expire_window, kubeconfigs) - - ###################################################################### - # /Check service Kubeconfigs - ###################################################################### - - ###################################################################### - # Check etcd certs - ###################################################################### - # 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 open('/etc/etcd/etcd.conf', 'r') as fp: - etcd_config = ConfigParser.ConfigParser() - etcd_config.readfp(FakeSecHead(fp)) - - 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 open(etcd_cert, 'r') as fp: - c = fp.read() - (cert_subject, - cert_expiry_date, - time_remaining) = load_and_handle_cert(c, now) - - expire_check_result = { - 'cert_cn': cert_subject, - 'path': fp.name, - 'expiry': cert_expiry_date, - 'days_remaining': time_remaining.days, - 'health': None, - } - - 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 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 - else: - (cert_subject, - cert_expiry_date, - time_remaining) = load_and_handle_cert(router_c, now, base64decode=True) - - expire_check_result = { - 'cert_cn': cert_subject, - 'path': router_path, - 'expiry': cert_expiry_date, - 'days_remaining': time_remaining.days, - 'health': None, - } - - classify_cert(expire_check_result, now, time_remaining, expire_window, router_certs) - - check_results['router'] = router_certs - - ###################################################################### - # Now for registry - # registry_secrets = subprocess.call('oc get secret registry-certificates -o yaml'.split()) - # out = subprocess.PIPE - try: - registry_secrets_raw = subprocess.Popen('oc get 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 - else: - (cert_subject, - cert_expiry_date, - time_remaining) = load_and_handle_cert(registry_c, now, base64decode=True) - - expire_check_result = { - 'cert_cn': cert_subject, - 'path': registry_path, - 'expiry': cert_expiry_date, - 'days_remaining': time_remaining.days, - 'health': None, - } - - classify_cert(expire_check_result, now, time_remaining, expire_window, registry_certs) - - check_results['registry'] = 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']] - else: - check_results['ocp_certs'] = ocp_certs - check_results['kubeconfigs'] = kubeconfigs - check_results['etcd'] = etcd_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. - check_results['ocp_certs'] = sorted(check_results['ocp_certs'], cmp=lambda x, y: cmp(x['days_remaining'], y['days_remaining'])) - check_results['kubeconfigs'] = sorted(check_results['kubeconfigs'], cmp=lambda x, y: cmp(x['days_remaining'], y['days_remaining'])) - check_results['etcd'] = sorted(check_results['etcd'], cmp=lambda x, y: cmp(x['days_remaining'], y['days_remaining'])) - - # 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 - ) - -###################################################################### -# 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 -if __name__ == '__main__': - main() diff --git a/playbooks/byo/openshift-cluster/check-cert-expiry.yaml b/playbooks/byo/openshift-cluster/check-cert-expiry.yaml deleted file mode 100644 index 39efdbd36..000000000 --- a/playbooks/byo/openshift-cluster/check-cert-expiry.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- -# check-cert-expiry.yaml - A utility for cluster ops to scan through -# (critical) certificates for the ongoing operations of a cluster. - -# We do not support all Ansible versions. This is our safety net. -- include: ../../common/openshift-cluster/verify_ansible_version.yml - -- name: Generate the l_oo_all_hosts group - hosts: localhost - connection: local - become: no - gather_facts: no - tasks: - - include_vars: cluster_hosts.yml - - add_host: - name: "{{ item }}" - groups: l_oo_all_hosts - with_items: "{{ g_all_hosts | default([]) }}" - -# This may seem redundant, running `include_vars` again on the list of -# hosts in the group 'l_oo_all_hosts' which we just created. But the -# fact of the matter is that if we don't re-run include_vars on the -# new host group we created, then they will not have access to those -# same group variables they were birthed from. -# -# Go ahead and try to 'debug: var=g_all_hosts' later on (without this -# play) and you'll find that the result is UNDEFINED VARIABLE. -- name: Inject cluster hosts variables into l_oo_all_hosts - hosts: l_oo_all_hosts - gather_facts: no - tasks: - - include_vars: cluster_hosts.yml - -# This is where the actual business gets started: -- include: ../../common/openshift-cluster/check-cert-expiry.yaml diff --git a/playbooks/common/openshift-cluster/check-cert-expiry.yaml b/playbooks/common/openshift-cluster/check-cert-expiry.yaml deleted file mode 100644 index b585fd849..000000000 --- a/playbooks/common/openshift-cluster/check-cert-expiry.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# --- -# # This must be evaluated after cluster_hosts.yml has been evaluated on -# # 'localhost' to generate l_oo_all_hosts and then subsequently -# # evaluated again on the generated hosts group (l_oo_all_hosts). -# - include: evaluate_groups.yml -# tags: -# - always - -# # Initialize cluster facts for oo_all_hosts using the openshift_facts -# # role followed by the 'common' role -# - include: initialize_facts.yml -# tags: -# - always - -# # Get the version to install from the first master, then synchronize -# # that variable across all in oo_all_hosts -# - include: initialize_openshift_version.yml -# tags: -# - always - -# # Earlier 'initialize_facts' included the openshift_facts role and -# # used the openshift_facts module with the 'role' parameter set to -# # 'common'. Now we're applying the openshift_facts role AGAIN but just -# # to the subset of oo_all_hosts which require configuring. -# - name: Load openshift_facts -# hosts: oo_etcd_to_config:oo_masters_to_config:oo_nodes_to_config -# roles: -# - openshift_facts - -- hosts: all - become: yes - gather_facts: no - tasks: - - name: Check cert expirys on host - openshift_cert_expiry: - warning_days: 1500 - register: check_results - - name: Generate html - become: no - run_once: yes - template: - src: templates/cert-expiry-table.html.j2 - dest: /tmp/cert-table.html - delegate_to: localhost diff --git a/playbooks/common/openshift-cluster/templates/cert-expiry-table.html.j2 b/playbooks/common/openshift-cluster/templates/cert-expiry-table.html.j2 deleted file mode 100644 index f74d7f1ce..000000000 --- a/playbooks/common/openshift-cluster/templates/cert-expiry-table.html.j2 +++ /dev/null @@ -1,124 +0,0 @@ - - - - - OCP Certificate Expiry Report - {# For fancy icons and a pleasing font #} - - - - - - - - {# Each host has a header and table to itself #} - {% for host in play_hosts %} -

{{ host }}

- -

- {{ hostvars[host].check_results.msg }} -

- - - - {# These are hard-coded right now, but should be grabbed dynamically from the registered results #} - {%- for kind in ['ocp_certs', 'etcd', 'kubeconfigs', 'router', 'registry'] -%} - - - - - - - - - - - - - - {# A row for each certificate examined #} - {%- for v in hostvars[host].check_results.check_results[kind] -%} - - {# Let's add some flair and show status visually with fancy icons #} - {% if v.health == 'ok' %} - {% set health_icon = 'glyphicon glyphicon-ok' %} - {% elif v.health == 'warning' %} - {% set health_icon = 'glyphicon glyphicon-alert' %} - {% else %} - {% set health_icon = 'glyphicon glyphicon-remove' %} - {% endif %} - - - - - - - - - - {% endfor %} - {# end row generation per cert of this type #} - {% endfor %} - {# end generation for each kind of cert block #} -

{{ kind }}

 Certificate Common/Alt Name(s)HealthDays RemainingExpiration DatePath
{{ v.cert_cn }}{{ v.health }}{{ v.days_remaining }}{{ v.expiry }}{{ v.path }}
-
- {% endfor %} - {# end section generation for each host #} - -
-

- Expiration report generated by openshift-ansible -

-

- Status icons from bootstrap/glyphicon -

-
- - diff --git a/roles/openshift_certificate_expiry/README.md b/roles/openshift_certificate_expiry/README.md new file mode 100644 index 000000000..75970c7a0 --- /dev/null +++ b/roles/openshift_certificate_expiry/README.md @@ -0,0 +1,63 @@ +OpenShift Certificate Expiration Checker +======================================== + +OpenShift certificate expiration checking. Be warned of certificates +expiring within a configurable window of days, and notified of +certificates which have already expired. Certificates examined +include: + +* Master/Node Service Certificates +* Router/Registry Service Certificates from etcd secrets +* Master/Node/Router/Registry/Admin `kubeconfig`s +* Etcd certificates + + + +Requirements +------------ + +* None + + +Role Variables +-------------- + +From this role: + +| Name | Default value | Description | +|--------------------------|---------------|-------------------------------------------------------------------------------------| +| `config_base` | `/etc/origin` | Base openshift config directory | +| `warning_days` | `30` | Flag certificates which will expire in this many days from now | +| `show_all` | `False` | Include healthy (non-expired and non-warning) certificates in results | +| `generate_report` | `False` | Generate an HTML report of the expiry check results | +| `save_json_results` | `False` | Save expiry check results as a json file | +| `result_dir` | `/tmp` | Directory in which to put check results and generated reports | + + +Dependencies +------------ + +* None + +Example Playbook +---------------- + +``` +- name: Check cert expirys + hosts: all + become: yes + gather_facts: no + roles: + - role: openshift_certificate_expiry +``` + + +License +------- + +Apache License, Version 2.0 + +Author Information +------------------ + +Tim Bielawa (tbielawa@redhat.com) diff --git a/roles/openshift_certificate_expiry/defaults/main.yml b/roles/openshift_certificate_expiry/defaults/main.yml new file mode 100644 index 000000000..5c077f450 --- /dev/null +++ b/roles/openshift_certificate_expiry/defaults/main.yml @@ -0,0 +1,6 @@ +--- +config_base: "/etc/origin" +warning_days: 30 +show_all: false +generate_report: false +result_dir: "/tmp" diff --git a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py new file mode 100644 index 000000000..f18ab75d0 --- /dev/null +++ b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py @@ -0,0 +1,635 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,invalid-name + +"""For details on this module see DOCUMENTATION (below)""" + +# router/registry cert grabbing +import subprocess +# etcd config file +import ConfigParser +# Expiration parsing +import datetime +# File path stuff +import os +# Config file parsing +import yaml +# Certificate loading +import OpenSSL.crypto + +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: "0.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) " +''' + +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 +''' + + +# We only need this for one thing, we don't care if it doesn't have +# that many public methods +# +# pylint: disable=too-few-public-methods +class FakeSecHead(object): + """etcd does not begin their config file with an opening [section] as +required by the Python ConfigParser module. We hack around it by +slipping one in ourselves prior to parsing. + +Source: Alex Martelli - http://stackoverflow.com/a/2819788/6490583 + """ + def __init__(self, fp): + self.fp = fp + self.sechead = '[ETCD]\n' + + def readline(self): + """Make this look like a file-type object""" + if self.sechead: + try: + return self.sechead + finally: + self.sechead = None + else: + return self.fp.readline() + + +###################################################################### + +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))] + + +def load_and_handle_cert(cert_string, now, base64decode=False): + """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 .decode('base64') on the input? + +Returns: +A 3-tuple of the form: (certificate_common_name, certificate_expiry_date, certificate_time_remaining) + + """ + if base64decode: + _cert_string = cert_string.decode('base-64') + else: + _cert_string = cert_string + + cert_loaded = OpenSSL.crypto.load_certificate( + OpenSSL.crypto.FILETYPE_PEM, _cert_string) + + ###################################################################### + # Read just the first name from the cert - DISABLED while testing + # out the 'get all possible names' function (below) + # + # Strip the subject down to just the value of the first name + # cert_subject = cert_loaded.get_subject().get_components()[0][1] + + ###################################################################### + # Read all possible names from the cert + cert_subjects = [] + for name, value in cert_loaded.get_subject().get_components(): + 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 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 + + 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 + cert_expiry = cert_loaded.get_notAfter() + cert_expiry_date = datetime.datetime.strptime( + cert_expiry, + # example get_notAfter() => 20180922170439Z + '%Y%m%d%H%M%SZ') + + time_remaining = cert_expiry_date - now + + return (cert_subject, cert_expiry_date, time_remaining) + + +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_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 counds 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 +# +# pylint: disable=too-many-locals,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=int(30), + type='int'), + show_all=dict( + required=False, + default="False", + type='bool') + ), + supports_check_mode=True, + ) + + # Basic scaffolding for OpenShift spcific 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_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.normpath( + os.path.join(openshift_base_config_path, "master/%s.kubeconfig" % m_kube_config) + ) + ) + + # etcd, where do you hide your certs? Used when parsing etcd.conf + etcd_cert_params = [ + "ETCD_CA_FILE", + "ETCD_CERT_FILE", + "ETCD_PEER_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_after_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 open(os_cert, 'r') 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) + cert_meta['certFile'] = os.path.join(cfg_path, cfg['servingInfo']['certFile']) + cert_meta['clientCA'] = os.path.join(cfg_path, cfg['servingInfo']['clientCA']) + + ###################################################################### + # Load the certificate and the CA, parse their expiration dates into + # datetime objects so we can manipulate them later + for _, v in cert_meta.iteritems(): + with open(v, 'r') as fp: + cert = fp.read() + cert_subject, cert_expiry_date, time_remaining = load_and_handle_cert(cert, now) + + expire_check_result = { + 'cert_cn': cert_subject, + 'path': fp.name, + 'expiry': cert_expiry_date, + 'days_remaining': time_remaining.days, + 'health': None, + } + + 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 open(openshift_node_config_path, 'r') 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 open(node_kubeconfig, 'r') 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) = load_and_handle_cert(c, now, base64decode=True) + + expire_check_result = { + 'cert_cn': cert_subject, + 'path': fp.name, + 'expiry': cert_expiry_date, + 'days_remaining': time_remaining.days, + 'health': None, + } + + 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 open(kube, 'r') 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) = load_and_handle_cert(c, now, base64decode=True) + + expire_check_result = { + 'cert_cn': cert_subject, + 'path': fp.name, + 'expiry': cert_expiry_date, + 'days_remaining': time_remaining.days, + 'health': None, + } + + classify_cert(expire_check_result, now, time_remaining, expire_window, kubeconfigs) + + ###################################################################### + # /Check service Kubeconfigs + ###################################################################### + + ###################################################################### + # Check etcd certs + ###################################################################### + # 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 open('/etc/etcd/etcd.conf', 'r') as fp: + etcd_config = ConfigParser.ConfigParser() + etcd_config.readfp(FakeSecHead(fp)) + + 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 open(etcd_cert, 'r') as fp: + c = fp.read() + (cert_subject, + cert_expiry_date, + time_remaining) = load_and_handle_cert(c, now) + + expire_check_result = { + 'cert_cn': cert_subject, + 'path': fp.name, + 'expiry': cert_expiry_date, + 'days_remaining': time_remaining.days, + 'health': None, + } + + 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 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 + else: + (cert_subject, + cert_expiry_date, + time_remaining) = load_and_handle_cert(router_c, now, base64decode=True) + + expire_check_result = { + 'cert_cn': cert_subject, + 'path': router_path, + 'expiry': cert_expiry_date, + 'days_remaining': time_remaining.days, + 'health': None, + } + + classify_cert(expire_check_result, now, time_remaining, expire_window, router_certs) + + check_results['router'] = router_certs + + ###################################################################### + # Now for registry + # registry_secrets = subprocess.call('oc get secret registry-certificates -o yaml'.split()) + # out = subprocess.PIPE + try: + registry_secrets_raw = subprocess.Popen('oc get 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 + else: + (cert_subject, + cert_expiry_date, + time_remaining) = load_and_handle_cert(registry_c, now, base64decode=True) + + expire_check_result = { + 'cert_cn': cert_subject, + 'path': registry_path, + 'expiry': cert_expiry_date, + 'days_remaining': time_remaining.days, + 'health': None, + } + + classify_cert(expire_check_result, now, time_remaining, expire_window, registry_certs) + + check_results['registry'] = 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']] + else: + check_results['ocp_certs'] = ocp_certs + check_results['kubeconfigs'] = kubeconfigs + check_results['etcd'] = etcd_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. + check_results['ocp_certs'] = sorted(check_results['ocp_certs'], cmp=lambda x, y: cmp(x['days_remaining'], y['days_remaining'])) + check_results['kubeconfigs'] = sorted(check_results['kubeconfigs'], cmp=lambda x, y: cmp(x['days_remaining'], y['days_remaining'])) + check_results['etcd'] = sorted(check_results['etcd'], cmp=lambda x, y: cmp(x['days_remaining'], y['days_remaining'])) + + # 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 + ) + +###################################################################### +# 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 +if __name__ == '__main__': + main() diff --git a/roles/openshift_certificate_expiry/meta/main.yml b/roles/openshift_certificate_expiry/meta/main.yml new file mode 100644 index 000000000..dd66299ea --- /dev/null +++ b/roles/openshift_certificate_expiry/meta/main.yml @@ -0,0 +1,15 @@ +--- +galaxy_info: + author: Tim Bielawa + description: OpenShift Certificate Expiry Checker + company: Red Hat, Inc. + license: Apache License, Version 2.0 + min_ansible_version: 2.1 + platforms: + - name: EL + versions: + - 7 + categories: + - cloud + - system +dependencies: [] diff --git a/roles/openshift_certificate_expiry/tasks/main.yml b/roles/openshift_certificate_expiry/tasks/main.yml new file mode 100644 index 000000000..def7d1284 --- /dev/null +++ b/roles/openshift_certificate_expiry/tasks/main.yml @@ -0,0 +1,23 @@ +--- +- name: Check cert expirys on host + openshift_cert_expiry: + warning_days: 1500 + register: check_results + +- name: Generate html + become: no + run_once: yes + template: + src: cert-expiry-table.html.j2 + dest: /tmp/cert-table.html + delegate_to: localhost + when: generate_report + +- name: Generate JSON + become: no + run_once: yes + template: + src: save_json_results.j2 + dest: /tmp/cert-expiry-results.json + delegate_to: localhost + when: save_json_results diff --git a/roles/openshift_certificate_expiry/templates/cert-expiry-table.html.j2 b/roles/openshift_certificate_expiry/templates/cert-expiry-table.html.j2 new file mode 100644 index 000000000..f74d7f1ce --- /dev/null +++ b/roles/openshift_certificate_expiry/templates/cert-expiry-table.html.j2 @@ -0,0 +1,124 @@ + + + + + OCP Certificate Expiry Report + {# For fancy icons and a pleasing font #} + + + + + + + + {# Each host has a header and table to itself #} + {% for host in play_hosts %} +

{{ host }}

+ +

+ {{ hostvars[host].check_results.msg }} +

+
    +
  • Expirations checked at: {{ hostvars[host].check_results.check_results.meta.checked_at_time }}
  • +
  • Warn after date: {{ hostvars[host].check_results.check_results.meta.warn_after_date }}
  • +
+ + + {# These are hard-coded right now, but should be grabbed dynamically from the registered results #} + {%- for kind in ['ocp_certs', 'etcd', 'kubeconfigs', 'router', 'registry'] -%} + + + + + + + + + + + + + + {# A row for each certificate examined #} + {%- for v in hostvars[host].check_results.check_results[kind] -%} + + {# Let's add some flair and show status visually with fancy icons #} + {% if v.health == 'ok' %} + {% set health_icon = 'glyphicon glyphicon-ok' %} + {% elif v.health == 'warning' %} + {% set health_icon = 'glyphicon glyphicon-alert' %} + {% else %} + {% set health_icon = 'glyphicon glyphicon-remove' %} + {% endif %} + + + + + + + + + + {% endfor %} + {# end row generation per cert of this type #} + {% endfor %} + {# end generation for each kind of cert block #} +

{{ kind }}

 Certificate Common/Alt Name(s)HealthDays RemainingExpiration DatePath
{{ v.cert_cn }}{{ v.health }}{{ v.days_remaining }}{{ v.expiry }}{{ v.path }}
+
+ {% endfor %} + {# end section generation for each host #} + + + + diff --git a/roles/openshift_certificate_expiry/templates/save_json_results.j2 b/roles/openshift_certificate_expiry/templates/save_json_results.j2 new file mode 100644 index 000000000..89602ac2b --- /dev/null +++ b/roles/openshift_certificate_expiry/templates/save_json_results.j2 @@ -0,0 +1,5 @@ +{ +{% for host in play_hosts %} +"{{host}}": {{ hostvars[host].check_results.check_results | to_nice_json(indent=2) }}{% if not loop.last %},{% endif %} +{% endfor %} +} -- cgit v1.2.1