#!/usr/bin/python
#
# **********************************************************
# Copyright 2022 VMware, Inc.  All rights reserved.
# **********************************************************
"""
Python program to detect all connected hosts installed with both
i40en and i40enu vibs in vCenter Server inventory.
"""

import logging
import os
import ssl
import sys
from datetime import datetime
from optparse import OptionParser, make_option

sys.path.append('/usr/lib/vmware-updatemgr/python/hcl')
sys.path.append(os.environ['VMWARE_PYTHON_PATH'])

from hardware_discovery.services.utils.authentication import CertificateStore
from hardware_discovery.services.vc_service import getVCServiceFromCertificate

from pyVim.connect import Disconnect
from pyVmomi import vim, vmodl, SoapStubAdapter, VmomiSupport

OPTION_SKIP_VLCM_PRECHECK = 'config.SDDC.VCUpgradeVLCMPrecheck.Skip'
B2B_LOG_DIR = "/var/log/vmware/applmgmt"


def GetVCSi(certStore):
    vcService = getVCServiceFromCertificate(certStore)
    vcService.login()

    context = ssl.create_default_context()
    context.check_hostname = False
    context.verify_mode = ssl.CERT_NONE

    vcStub = vcService.stub.soapStub
    hostname = vcStub.host.split(':')[0]

    vcLatestVimStub = SoapStubAdapter(
        host=hostname,
        port=vcService.port,
        version=VmomiSupport.newestVersions.GetName('vim'))
    vcLatestVimStub.cookie = vcStub.cookie

    si = vim.ServiceInstance("ServiceInstance", vcLatestVimStub)
    if si is None:
        raise Exception("Failed to connect to vCenter Server with the latest "
                        "vmodl version {}".format(vcService.host))
    return si


def GetOptions():
    """
   Supports the command-line arguments listed below.
   """

    _CMD_OPTIONS_LIST = [
        make_option("--inB2B", action="store_true"),
        make_option("-t", "--timestamp",
                    help="time stamp - will be used to create output files"),
        make_option("-f", "--inputHostsFile",
                    help="File containing list of hosts to be validated"),
        make_option("-?", "--help", action="store_true", help="Help"),
    ]
    _STR_USAGE = "%prog [options]"

    parser = OptionParser(option_list=_CMD_OPTIONS_LIST,
                          usage=_STR_USAGE,
                          add_help_option=False)
    (options, _) = parser.parse_args()

    return options


def GetHostsForValidation(si, inputHosts=None):
    # Get the list of hosts in inventory.
    container = si.content.viewManager.CreateContainerView(
        si.content.rootFolder,
        [vim.HostSystem],
        True)
    # Printing list of hosts
    logging.debug("List of ESXi Hosts to be processed :")
    hosts = {}
    # Filter the hosts based on the input list and add to the hosts dict.
    for managed_object_ref in container.view:
        if inputHosts is None or managed_object_ref.name in inputHosts:
            logging.debug("  %s", managed_object_ref.name)
            hosts.update({managed_object_ref: managed_object_ref.name})
    container.Destroy()
    return hosts


def ValidateESXiHosts(hosts, faultyHostsFile,
                      skippedHostsFile, erroredHostsFile):
    faulty = 0
    skipped = 0
    errored = 0

    faultyHeaderWritten = skippedHeaderWritten = errorHeaderWritten = False
    # Set the output file paths for faulty, skipped and error hosts.
    with open(faultyHostsFile, "w") as faultyHosts, open(skippedHostsFile,"w") as skippedHosts, open(erroredHostsFile, "w") as erroredHosts:
        logging.info(
            "Dual driver checking - script execution status : SCANNING HOSTS")
        logging.info("Total numbers of hosts to be scanned: %d", len(hosts))
        faultyHosts.write("This file lists the hosts that have driver conflicts.\n")
        faultyHosts.write(
            "These hosts must first be upgraded to ESXi 7.0 U3c or higher version.\n")
        faultyHosts.write(
            "These hosts can be upgraded either with a baseline created from an ISO;\n")
        faultyHosts.write(
            "or using an image based upgrade, if they are in Image managed cluster.\n")
        faultyHosts.write(
            "This host upgrade needs to be completed before proceeding with the upgrade of vCenter Server.\n")
        faultyHosts.write(
            "Do not use Rollup based patch baselines. Refer to KB 86447 for details\n\n")
        skippedHosts.write(
            "This file lists the hosts that could not be scanned for driver conflicts \n")
        skippedHosts.write(
            "either because they were disconnected OR not responding. Refer to KB 86447 for details.\n\n")
        erroredHosts.write(
            "This file lists the hosts that could not be scanned for driver conflicts due to an error.\n\n")

        # Iterate over list of hosts to validate
        for host in hosts:
            try:
                logging.info(" Scanning host - %s", host.name)
                version = "unknown"
                build = "unknown"
                if host.config is not None:
                    version = host.config.product.version
                    build = host.config.product.build
                status = host.runtime.connectionState
                if status != "connected":
                    logging.warning(" Host not in connected state. Skipping "
                                    "validation; host: %s", host.name)
                    if not skippedHeaderWritten:
                        skippedHosts.write(
                            "----------------------------------------\n")
                        skippedHosts.write(
                            "Name of host, ESXi version, build number\n")
                        skippedHosts.write(
                            "----------------------------------------\n")
                        skippedHeaderWritten = True
                    skippedHosts.write(host.name + ", " + version + ", " + build + "\n")
                    skipped += 1
                    continue

                # Validate only connected Hosts
                softwarePkgs = host.configManager.imageConfigManager. \
                    fetchSoftwarePackages()
                foundi40en = next((pkg for pkg in softwarePkgs
                                   if pkg.name == "i40en"), None)
                foundi40enu = next((pkg for pkg in softwarePkgs
                                    if pkg.name == "i40enu"), None)
                if foundi40en is not None and foundi40enu is not None:
                    logging.info(
                        "  Dual Drivers i40en and i40enu detected on the host; "
                        "host: %s", host.name)
                    if not faultyHeaderWritten:
                        faultyHosts.write(
                            "----------------------------------------\n")
                        faultyHosts.write(
                            "Name of host, ESXi version, build number\n")
                        faultyHosts.write(
                            "----------------------------------------\n")
                        faultyHeaderWritten = True
                    faultyHosts.write(
                        host.name + ", " + version + ", " + build + "\n")
                    faulty += 1
            except Exception as e:
                logging.warning(" Caught unexpected exception while validating "
                                "the host; host: %s, exception: %s", host.name, str(e))
                if not errorHeaderWritten:
                    erroredHosts.write(
                        "-----------------------------------------------\n")
                    erroredHosts.write(
                        "Name of host, ESXi version, build number, error\n")
                    erroredHosts.write(
                        "-----------------------------------------------\n")
                    errorHeaderWritten = True
                erroredHosts.write(host.name + ", " + version + ", "
                                   + build + ", " + str(e) + "\n")
                errored += 1
    return faulty, skipped, errored


def UpdateVCOption(si, optionKey, optionVal):
    option = [vim.Option.OptionValue(key=optionKey, value=optionVal)]
    si.content.setting.UpdateOptions(changedValue=option)
    logging.info("Updated option key: %s to %s", optionKey, optionVal)


def GetInputHostNames(inputHostsFile):
    # Expects the file to contain one hostname per line.
    with open(inputHostsFile) as f:
        return f.read().splitlines()


def main():
    """
   Simple command-line program for finding the faulty hosts on VC
   """

    # Process the script options
    beginTime = datetime.now()
    options = GetOptions()
    if options.inB2B:
        outputDir = B2B_LOG_DIR
        if options.timestamp is not None:
            timestamp = options.timestamp
        else:
            logging.error(
                " Timestamp is required when argument inB2B is set")
            sys.exit(1)

        if options.inputHostsFile is None or not os.path.exists(
                options.inputHostsFile):
            logging.error("Provided list of input hosts file is not valid")
            sys.exit(1)
    else:
        curtime = datetime.now()
        timestamp = curtime.strftime("%Y-%m-%d_%H-%M-%S")
        outputDir = os.getcwd()

    logFileName = "dual_driver_check_" + timestamp + ".log"
    logFile = os.path.join(outputDir, logFileName)
    print("Script logs are written to file: " + logFile)
    logging.basicConfig(filename=logFile, filemode='w',
                        format='%(asctime)s  %(levelname)s: %(message)s',
                        level=logging.DEBUG)
    logging.info("Dual driver checking - script execution status : STARTED ")

    try:
        certStore = None
        certStore = CertificateStore()
        si = None
        si = GetVCSi(certStore)
        if options.inB2B:
            inputHosts = GetInputHostNames(options.inputHostsFile)
            esxiHosts = GetHostsForValidation(si, inputHosts)
        else:
            esxiHosts = GetHostsForValidation(si)

        # Validate the ESXi Hosts
        faultyHostsFile = outputDir + "/dual_driver_check_faulty_hosts_" + \
                          timestamp + ".txt"

        skippedHostsFile = outputDir + \
                           "/dual_driver_check_skipped_hosts_" + timestamp + \
                           ".txt"
        erroredHostsFile = outputDir + "/dual_driver_check_errored_hosts_" + \
                           timestamp + ".txt"

        faulty, skipped, errored = \
            ValidateESXiHosts(esxiHosts, faultyHostsFile, skippedHostsFile,
                              erroredHostsFile)

        if options.inB2B and not faulty and not errored:
            # Update the VLCM precheck skip option to skip prechecks during the next B2b upgrade.
            UpdateVCOption(si, OPTION_SKIP_VLCM_PRECHECK, str(True))
    except vmodl.MethodFault as e:
        logging.exception("Caught vmodl fault : %s", e.msg)
        sys.exit(1)
    except Exception as e:
        logging.exception("Caught exception : %s", str(e))
        sys.exit(1)
    finally:
        if si:
            Disconnect(si)
        if certStore:
            certStore.cleanup()

    scriptExeTime = datetime.now() - beginTime

    logging.info("Dual driver checking - script execution status : DONE")
    logging.info(" ")
    logging.info("######## Summary of script execution ######### ")
    logging.info(" ")
    logging.info("  Script execution time = %s", scriptExeTime)
    logging.info("  Total number of hosts scanned : %d", len(esxiHosts))
    logging.info(
        "  Number of hosts with dual drivers: %d, See file \'dual_driver_check_faulty_hosts_%s.txt\' for more details",
        faulty, timestamp)
    logging.info(
        "  Number of skipped hosts since they are not connected: %d, See file \'dual_driver_check_skipped_hosts_%s.txt\' for more details",
        skipped, timestamp)
    logging.info(
        "  Number of hosts that encountered an error: %d, See file \'dual_driver_check_errored_hosts_%s.txt\' for more details",
        errored, timestamp)
    logging.info(" ")
    logging.info("  Total 3 output files got generated. ")
    logging.info("   1. %s", faultyHostsFile)
    logging.info(
        "    - This is the list of hosts that have been scanned and found to have driver conflicts ")
    logging.info("   2. %s", skippedHostsFile)
    logging.info(
        "    - This is the list of hosts that could not be reached for scanning")
    logging.info("   3. %s", erroredHostsFile)
    logging.info(
        "    - This is the list of hosts that could not have the scan completed")
    logging.info(" ")

    sys.exit(0)


# Start program
if __name__ == "__main__":
    main()
