#!/bin/env bash

import argparse
import logging
import os
import re
import subprocess
import time
import sys
#import urllib.request
eus = 0

######################################################
# logger the output of the script into /var/log/rhuicheck.log file
######################################################
class CustomFormatter(logging.Formatter):

    black =    "\x1b[30;1m"
    grey =     "\x1b[30;0m"
    red =      "\x1b[31;20m"
    green =    "\x1b[32;20m"
    yellow =   "\x1b[33;20m"
    bright_red = "\x1b[91;1m"
    bold_red = "\x1b[31;1m"
    bold_green =    "\x1b[32;1m"
    bold_yellow =   "\x1b[33;1m"
    reset =    "\x1b[0m"


    # format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
    format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"

    FORMATS = {
        logging.DEBUG: black + format + reset,
        logging.INFO: bold_green + format + reset,
        logging.WARNING: bold_yellow + format + reset,
        logging.ERROR: bright_red + format + reset,
        logging.CRITICAL: bold_red + format + reset
    }

    def format(self, record):
        log_fmt = self.FORMATS.get(record.levelno)
        formatter = logging.Formatter(log_fmt)
        return formatter.format(record)



if os.geteuid() != 0:
   logger.critical('This script needs to execute with root privileges')
   logger.critical('You could leverage the sudo tool to gain administrative privileges')
   exit(1)

def start_logging(debug_level = False):
    """This function sets up the logging configuration for the script and writes the log to /var/log/rhuicheck.log"""

    logger = logging.getLogger(__name__)

    console_handler = logging.StreamHandler()
    color_formatter = CustomFormatter()
    console_handler.setFormatter(color_formatter)    
    logger.addHandler(console_handler)
    console_handler.setLevel(logging.INFO)

    if debug_level:
        console_handler.setLevel(logging.DEBUG)

    try:
        log_filename = '/var/log/rhuicheck.log'
        file_handler = logging.FileHandler(filename=log_filename)
        plain_formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
        file_handler.setFormatter(plain_formatter)    
    except:
        logger.critical("Unable to create log file in /var/log/rhuicheck.log, make sure the script is running with root privileges, the filesystem has enough space and it's not in Read-Only mode")
        exit(1)
    else:
        file_handler.setLevel(logging.DEBUG)
        logger.addHandler(file_handler)

    logger.setLevel(logging.DEBUG)
    return logger
       
parser = argparse.ArgumentParser()
parser.add_argument(  '--debug','-d',
                      action='store_true',
                      help='Use DEBUG level')
args = parser.parse_args()
logger = start_logging(args.debug)

try:
    import requests
except ImportError:
    logger.critical("'requests' python module not found, but it's required for this test script, review your python installation.")
    exit(1) 

rhui3 = ['13.91.47.76', '40.85.190.91', '52.187.75.218']
rhui4 = ['52.136.197.163', '20.225.226.182', '52.142.4.99', '20.248.180.252', '20.24.186.80']
rhuius = ['13.72.186.193', '13.72.14.155', '52.224.249.194']
system_proxy = dict()
bad_hosts = list()
 
pattern = dict()
pattern['clientcert'] = r'^/[/a-zA-Z0-9_\-]+\.(crt)$'
pattern['clientkey']  = r'^/[/a-zA-Z0-9_\-]+\.(pem)$'
pattern['repofile']    = r'^/[/a-zA-Z0-9_\-\.]+\.(repo)$'


try:
    import configparser
except ImportError:
    import ConfigParser as configparser

class localParser(configparser.ConfigParser):
    def as_dict(self):
        d = dict(self.sections)
        for k in d:
            d[k] = dict(self._defaults, **d[k])
            d[k].pop('__name__', None)
        return d

def get_host(url):
    urlregex = '[^:]*://([^/]*)/.*'
    host_match = re.match(urlregex, url)
    return host_match.group(1)

def validate_ca_certificates():
    """
    Used to verify whether the default certificate database has been modified or not
    """
    logger.debug('Entering validate_ca-certificates()')
    reinstall_ca_bundle_link = 'https://learn.microsoft.com/troubleshoot/azure/virtual-machines/linux/troubleshoot-linux-rhui-certificate-issues?tabs=rhel7-eus%2Crhel7-noneus%2Crhel7-rhel-sap-apps%2Crhel8-rhel-sap-apps%2Crhel9-rhel-sap-apps#solution-4-update-or-reinstall-the-ca-certificates-package'

    try:
        result = subprocess.call('/usr/bin/rpm -V ca-certificates', shell=True)
    except:
        logger.error('Unable to check server side certificates installed in the server')
        logger.error('Use {} to reinstall the ca-certificates'.format(reinstall_ca_bundle_link))
        
        exit(1)
   
    if result:
        logger.error('The ca-certificate package is invalid, you can reinstall it. Follow {} to reinstall it manually'.format(reinstall_ca_bundle_link))
        exit(1)
    else:
        return True

def connect_to_host(url, selection, mysection):

    try:
        uname = os.uname()
    except:
        logger.critical('Unable to identify OS version.')
        exit(1)    

    try:
        basearch = uname.machine
    except AttributeError:
        basearch = uname[-1]

    try:
        baserelease = uname.release
    except AttributeError:
        baserelease = uname[2]

    if eus:
        fd = open('/etc/yum/vars/releasever')
        releasever = fd.readline().strip()
    else:
        releasever  = re.sub(r'^.*el([0-9][0-9]*).*',r'\1',baserelease)
        if releasever == '7':
            releasever = '7Server'

    url = url+"/repodata/repomd.xml"
    url = url.replace('$releasever',releasever)
    url = url.replace('$basearch',basearch)
    logger.debug('baseurl for repo {} is {}'.format(mysection, url))

    headers = {'content-type': 'application/json'}
    s = requests.Session()
    local_proxy = get_proxies(selection, mysection)

    cert = ()
    try:
        cert=(selection.get(mysection, 'sslclientcert'), selection.get(mysection, 'sslclientkey'))
    except:
        logger.warning('Client certificate and/or client key attribute not found for {}, testing connectivity w/o certificates'.format(mysection))
        cert=()

    try:
        r = s.get(url, cert=cert, headers=headers, timeout=5, proxies=local_proxy)
    except requests.exceptions.Timeout:
        logger.warning('TIMEOUT: Unable to reach RHUI URI {}'.format(url))
        return False
    except requests.exceptions.SSLError:
        validate_ca_certificates()
        logger.warning('PROBLEM: MITM proxy misconfiguration. Proxy cannot intercept certs for {}'.format(url))
        return 1
    except requests.exceptions.ProxyError:
        logger.warning('PROBLEM: Unable to use the proxy gateway when connecting to RHUI server {}'.format(url))
        return False
    except requests.exceptions.ConnectionError as e:
        logger.warning('PROBLEM: Unable to establish connectivity to RHUI server {}'.format(url))
        logger.error('{}'.format(e))
        return False
    except OSError:
        validate_ca_certificates()
        raise()
    except Exception as e:
        logger.warning('PROBLEM: Unknown error, unable to connect to the RHUI server {}'.format(url))
        raise(e)
        return False
    else:
        if r.status_code == 200:
            logger.debug('The RC for this {} link is {}'.format(url, r.status_code))
            return True
        elif r.status_code == 404:
            logger.error("Unable to find the contents for repo {}, make sure to use the correct version lock if you're using EUS repositories".format(mysection))
            logger.error("For more detailed information and valid levels consult: https://access.redhat.com/support/policy/updates/errata#RHEL8_and_9_Life_Cycle")
            return False
        else:
            logger.warning('The RC for this {} link is {}'.format(url, r.status_code))
            return False

def rpm_names():
    """
    Identifies the RHUI repositories installed in the server and returns a list of RHUI rpms installed in the server.
    """
    logger.debug('Entering repo_name()')
    result = subprocess.Popen("rpm -qa 'rhui-*'", shell=True, stdout=subprocess.PIPE)
    rpm_names = result.stdout.readlines()
    rpm_names = [ rpm.decode('utf-8').strip() for rpm in rpm_names ]
    if rpm_names:
        for rpm in rpm_names:
            logger.debug('Server has this RHUI pkg: {}'.format(rpm))
        return(rpm_names)
    else:
        logger.critical('Could not find a specific RHUI package installed, please refer to the documentation and install the apropriate one. ')
        logger.critical('Consider using the following document to install RHUI support https://learn.microsoft.com/troubleshoot/azure/virtual-machines/troubleshoot-linux-rhui-certificate-issues#cause-3-rhui-package-is-missing')
        exit(1) 

def get_pkg_info(package_name):
    ''' Identifies rhui package name(s)'''
    logger.debug('Entering get_pkg_info()')

    logger.debug('Entering pkg_info function')
    try:
        result = subprocess.Popen(['rpm', '-q', '--list', package_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        info = result.stdout.read().decode('utf-8').strip().split('\n')
        
        hash_info = {}
        for key in pattern.keys():
            logger.debug('checking key {}'.format(key))
            for data in info:
                logger.debug('checking key {} and data {}'.format(key, data))
                if re.match(pattern[key], data):
                    hash_info[key] = data
                    break
    except:
        logger.critical('Failed to grab RHUI RPM details, rebuild RPM database.')
        exit(1)
    else:
        return hash_info


def verify_pkg_info(package_name, rpm_info):
    ''' verifies basic elements of the RHUI package are present on the server '''

    errors = 0
    for keyname in pattern.keys():
        if keyname not in rpm_info.keys():
            logger.critical('{} file definition not found in RPM metadata, {} rpm needs to be reinstalled.'.format(keyname, package_name))
            errors += 1
        else: 
            if not os.path.exists(rpm_info[keyname]):
                logger.critical('{} file not found in server, {} rpm needs to be reinstalled.'.format(keyname, package_name))
                errors += 1

    if errors:
        data_link = "https://learn.microsoft.com/troubleshoot/azure/virtual-machines/troubleshoot-linux-rhui-certificate-issues#cause-2-rhui-certificate-is-missing"
        logger.critical('follow {} for information to install the RHUI package'.format(data_link))
        exit(1)

    return True

def default_policy():
    """"Returns a boolean whether the default encryption policies are set to default via the /etc/crypto-policies/config file, if it can't test it, the result will be set to true."""
    try:
        uname = os.uname()
    except:
        logger.critical('Unable to identify OS version.')
        exit(1)    

    try:
        baserelease = uname.release
    except AttributeError:
        baserelease = uname[2]

    policy_releasever  = re.sub(r'^.*el([0-9][0-9]*).*',r'\1',baserelease)

    # return true for EL7
    if policy_releasever == '7':
        return True

    try:
        policy = subprocess.check_output('/bin/update-crypto-policies --show', shell=True)
        policy = policy.decode('utf-8').strip()
        if policy != 'DEFAULT':
            return False
    except:
        return True

    return True

def expiration_time(cert_path):
    """ 
    Checks whether client certificate stored at cert_path has expired or not.
    """
    logger.debug('Entering expiration_time()')
    logger.debug('Checking certificate expiration time.')
    try:
        result = subprocess.check_call('openssl x509 -in {} -checkend 0 > /dev/null 2>&1 '.format(cert_path),shell=True)

    except subprocess.CalledProcessError:
        logger.critical('Client RHUI Certificate has expired, please update the RHUI rpm.')
        logger.critical('Refer to: https://learn.microsoft.com/troubleshoot/azure/virtual-machines/troubleshoot-linux-rhui-certificate-issues#cause-1-rhui-client-certificate-is-expired')
        exit(1)

    if not default_policy():
        logger.critical('Client crypto policies not set to DEFAULT.')
        logger.critical('Refer to: https://learn.microsoft.com/troubleshoot/azure/virtual-machines/linux/troubleshoot-linux-rhui-certificate-issues?tabs=rhel7-eus%2Crhel7-noneus%2Crhel7-rhel-sap-apps%2Crhel8-rhel-sap-apps%2Crhel9-rhel-sap-apps#cause-5-verification-error-in-rhel-version-8-or-9-ca-certificate-key-too-weak')
        exit(1) 

def read_yum_dnf_conf():
    """Read /etc/yum.conf or /etc/dnf/dnf.conf searching for proxy information"""
    logger.debug('Entering read_yum_dnf_conf()')

    try:
        yumdnfdotconf = localParser(allow_no_value=True, strict=False)
    except TypeError:
        yumdnfdotconf = localParser(allow_no_value=True)

    try:
        file = '/etc/yum.conf'
        with open(file) as stream:
            yumdnfdotconf.read_string('[default]\n' + stream.read())
    except AttributeError:
        yumdnfdotconf.add_section('[default]')
        yumdnfdotconf.read(file)
    except Exception as e:
        logger.error('Problems reading /etc/yum.conf, on RHEL8+ it is a symbolic link to /etc/dnf/dnf.conf.')
        raise

    return yumdnfdotconf


def get_proxies(parser_object, mysection):
    ''' gets the proxy from a configparser section object pointd by the proxy variable if defined in the configuration file '''
    proxy_info = dict()

    # proxy_regex = '(^[^:]*)(:(//)(([^:]*)(:([^@]*)){0,1}@){0,1}.*)?'
    proxy_regex = '(^[^:]*):(//)(([^:]*)(:([^@]*)){0,1}@){0,1}.*'

    for key in ['proxy', 'proxy_user', 'proxy_password']:
        try:
            value = parser_object.get(mysection, key)

        except configparser.NoOptionError: 
            continue
        except Exception as e:
            logger.error('Problems handling the parser object.')
            raise
        else:
            proxy_info[key] = value

    try:
        myproxy = proxy_info['proxy']
        if myproxy:
            ''' Get the scheme used in a proxy for example http from http://proxy.com/.
                Have to remove the last : as it is not part of the scheme.            '''
            proxy_match = re.match(proxy_regex, myproxy)
            if proxy_match:
                scheme = proxy_match.group(1)
                # proxy_info['scheme'] = scheme
                proxy_info['scheme'] = 'https'
            else:
                logger.critical('Invalid proxy configuration, please make sure you are using a valid proxy in your settings.')
                exit(1)
        else:
            return system_proxy
    except KeyError:
        return system_proxy

    if proxy_match.group(4) and ('proxy_user' in proxy_info.keys()):
        logger.warning('proxy definition already has a username defined and proxy_user is also defined, there might be conflicts using the proxy, repair.')

    if proxy_match.group(6) and ('proxy_password' in proxy_info.keys()):
        logger.warning('proxy definition already has a username and password defined and proxy_password is also defined, there might be conflicts using the proxy, repair.')

    if ('proxy_password' in proxy_info.keys() and proxy_info['proxy_password'])  and ('proxy_user' not in proxy_info.keys()):
        logger.warning('proxy_password defined, but there is no proxy user, this could be causing problems.')
        logger.warning('ignoring proxy_password')

    if ('proxy_user' in proxy_info.keys() and proxy_info['proxy_user']) and not proxy_match.group(4):
        ####### need to insert proxy user and passwod in proxy link
        proxy_prefix = myproxy[:proxy_match.end(2)]
        proxy_suffix = myproxy[proxy_match.end(2):]

        if ('proxy_password' in proxy_info.keys()) and proxy_info['proxy_password'] and not proxy_match.group(6):
            myproxy = '{}{}:{}@{}'.format(proxy_prefix, proxy_info['proxy_user'], proxy_info['proxy_password'], proxy_suffix)
        else:
            myproxy = '{}{}@{}'.format(proxy_prefix, proxy_info['proxy_user'], proxy_suffix)

    logger.critical('Found proxy information in the config files, make sure connectivity works through the proxy.')
    return {proxy_info['scheme']: myproxy}
    

def check_rhui_repo_file(path):
    """ 
    Handling the consistency of the Red Hat repositories
    path:    Indicates where the rhui repo is stored.
    returns: A RHUI repository configuration stored in a configparser structure, each repository is a section.
    """   
    logger.debug('Entering check_rhui_repo_file()')

    logger.debug('RHUI repo file is {}'.format(path))
    try:
        reposconfig = localParser()
        try:
            with open(path) as stream:
                reposconfig.read_string('[default]\n' + stream.read())
        except AttributeError:
            reposconfig.add_section('[default]')
            reposconfig.read(path)

        logger.debug('{}'.format(str(reposconfig.sections())))
        return reposconfig

    except configparser.ParsingError:
        logger.critical('{} does not follow standard REPO config format, reinstall the RHUI rpm and try again.'.format(path))
        exit(1)


def check_repos(reposconfig):
    """ Checks whether the rhui-microsoft-azure-* repository exists and tests whether it's enabled or not."""
    global eus 

    logger.debug('Entering microsoft_repo()')
    rhuirepo = '^(rhui-)?microsoft.*'
    eusrepo  = '.*-(eus|e4s)-.*'
    microsoft_reponame = ''
    enabled_repos = list()

    for repo_name in reposconfig.sections():
        if re.match('\[*default\]*', repo_name):
            continue

        try:
            enabled =  int(reposconfig.get(repo_name, 'enabled').strip())
        except configparser.NoOptionError:
            enabled = 1
        
    
        if re.match(rhuirepo, repo_name):
            microsoft_reponame = repo_name
            if enabled:
                logger.info('Using Microsoft RHUI repository {}'.format(repo_name))
            else:
                logger.critical('Microsoft RHUI repository not enabled, please enable it with the following command.')
                logger.critical('yum-config-manager --enable {}'.format(repo_name))
                exit(1)

        if enabled:
           # only check enabled repositories 
           enabled_repos.append(repo_name)
        else:
           continue

        if re.match(eusrepo, repo_name):
            eus = 1

    if not microsoft_reponame:
        reinstall_link = 'https://learn.microsoft.com/troubleshoot/azure/virtual-machines/linux/troubleshoot-linux-rhui-certificate-issues?source=recommendations&tabs=rhel7-eus%2Crhel7-noneus%2Crhel7-rhel-sap-apps%2Crhel8-rhel-sap-apps%2Crhel9-rhel-sap-apps#solution-2-reinstall-the-eus-non-eus-or-sap-rhui-package'
        logger.critical('Microsoft RHUI repository not found, reinstall the RHUI package following{}'.format(reinstall_link))
        exit(1)
       
    if eus:
        if not os.path.exists('/etc/yum/vars/releasever'):
            logger.critical('Server is using EUS repostories but /etc/yum/vars/releasever file not found, please correct and test again.')
            logger.critical('Refer to: https://learn.microsoft.com/azure/virtual-machines/workloads/redhat/redhat-rhui?tabs=rhel7#rhel-eus-and-version-locking-rhel-vms, to select the appropriate RHUI repo')
            exit(1)

    if not eus:
        if os.path.exists('/etc/yum/vars/releasever'):
            logger.critical('Server is using non-EUS repos and /etc/yum/vars/releasever file found, correct and try again')
            logger.critical('Refer to: https://learn.microsoft.com/azure/virtual-machines/workloads/redhat/redhat-rhui?tabs=rhel7#rhel-eus-and-version-locking-rhel-vms, to select the appropriate RHUI repo')
            exit(1)

    return enabled_repos


def ip_address_check(host):
    ''' Checks whether the parameter is within the RHUI4 infrastructure '''

    try:
        import socket
    except ImportError:
        logger.critical("'socket' python module not found, but it is required for this test script, review your python installation.")
        exit(1) 

    try:
        rhui_ip_address = socket.gethostbyname(host)

        if rhui_ip_address  in rhui4:
            logger.debug('RHUI host {} points to RHUI4 infrastructure.'.format(host))
            return True
        elif rhui_ip_address in rhui3 + rhuius:
            reinstall_link = 'https://learn.microsoft.com/troubleshoot/azure/virtual-machines/linux/troubleshoot-linux-rhui-certificate-issues?tabs=rhel7-eus%2Crhel7-noneus%2Crhel7-rhel-sap-apps%2Crhel8-rhel-sap-apps%2Crhel9-rhel-sap-apps#solution-2-reinstall-the-eus-non-eus-or-sap-rhui-package'
            logger.error('RHUI server {} points to decommissioned infrastructure, reinstall the RHUI package'.format(host))
            logger.error('for more detailed information, use: {}'.format(reinstall_link))
            bad_hosts.append(host)
            warnings = warnings + 1
            return False
        else:
            logger.critical('RHUI server {} points to an invalid destination, validate /etc/hosts file for any invalid static RHUI IPs or reinstall the RHUI package.'.format(host))
            logger.warning('Please make sure your server is able to resolve {} to one of the ip addresses'.format(host))
            rhui_link = 'https://learn.microsoft.com/azure/virtual-machines/workloads/redhat/redhat-rhui?tabs=rhel7#the-ips-for-the-rhui-content-delivery-servers'
            logger.warning('listed in this document {}'.format(rhui_link))
            return False
    except Exception as e:
         logger.warning('Unable to resolve IP address for host {}.'.format(host))
         logger.warning('Please make sure your server is able to resolve {} to one of the IP addresses.'.format(host))
         rhui_link = 'https://learn.microsoft.com/azure/virtual-machines/workloads/redhat/redhat-rhui?tabs=rhel7#the-ips-for-the-rhui-content-delivery-servers'
         logger.warning('listed in this document {}'.format(rhui_link ))
         logger.warning(e)
         return False

def connect_to_repos(reposconfig, check_repos):
    """Downloads repomd.xml from each enabled repository."""

    logger.debug('Entering connect_to_repos()')
    rhuirepo = '^rhui-microsoft.*'
    warnings = 0

    for repo_name in check_repos:

        if re.match('\[*default\]*', repo_name):
            continue

        try:
            baseurl_info = reposconfig.get(repo_name, 'baseurl').strip().split('\n')
        except configparser.NoOptionError:
            reinstall_link = 'https://learn.microsoft.com/troubleshoot/azure/virtual-machines/linux/troubleshoot-linux-rhui-certificate-issues?source=recommendations&tabs=rhel7-eus%2Crhel7-noneus%2Crhel7-rhel-sap-apps%2Crhel8-rhel-sap-apps%2Crhel9-rhel-sap-apps#solution-2-reinstall-the-eus-non-eus-or-sap-rhui-package'
            logger.critical('The baseurl is a critical component of the repository stanza, and it is not found for repo {}'.format(repo_name))
            logger.critical('Follow this link to reinstall the Microsoft RHUI repo {}'.format(reinstall_link))
            exit(1)

        successes = 0
        for url in baseurl_info:
            url_host = get_host(url)
            if not ip_address_check(url_host):
                bad_hosts.append(url_host)
                continue

            if connect_to_host(url, reposconfig, repo_name):
                successes += 1

        if successes == 0:
            error_link = 'https://learn.microsoft.com/azure/virtual-machines/workloads/redhat/redhat-rhui?tabs=rhel9#the-ips-for-the-rhui-content-delivery-servers'
            logger.critical('PROBLEM: Unable to successfully download repository metadata from the any of the configured RHUI server(s).')
            logger.critical('         Ensure the server is able to resolve to a valid IP address, the communication is allowed to the IP addresses listed in the public document {}'.format(error_link))
            logger.critical('         and if you are using EUS repositories, make sure you have a valid EUS version value in /etc/dnf/vars/releasever file.')
            sys.exit(1)



logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)

yum_dnf_conf = read_yum_dnf_conf()
system_proxy = get_proxies(yum_dnf_conf,'main')

for package_name in rpm_names():
    data = get_pkg_info(package_name)
    if verify_pkg_info(package_name, data):
        expiration_time(data['clientcert'])

        reposconfig = check_rhui_repo_file(data['repofile'])
        check_repos = check_repos(reposconfig)
        connect_to_repos(reposconfig, check_repos)


logger.info('All communication tests to the RHUI infrastructure have passed, if problems persist, remove third party repositories and test again.')
logger.info('The RHUI repository configuration file is {}, move any other configuration file to a temporary location and test again.'.format(data['repofile']))
