#!/usr/bin/env python3
"""
Copyright 2015-2025 Dell Inc. or its subsidiaries.
All Rights Reserved.

Used to deploy OME programmatically using ovftool
"""

import hashlib
import argparse
import sys
import ipaddress
import os


configKeys = {
    "ACCEPT_EULA": "bEULATxt",
    "ADMIN_PASSWORD": "adminPassword",
    "ENABLE_DHCP": "bEnableDHCP",
    "ENABLE_IPV6_AUTO_CONFIG": "bEnableIpv6AutoConfig",
    "IP_ADDRESS": "staticIP",
    "GATEWAY": "gateway"
}


class FileMgr:

    filenames = {
        "ovf": "openmanage_enterprise.x86_64-0.0.1.ovf",
        "manifest": "openmanage_enterprise.x86_64-0.0.1.mf",
        "config": "ovf_properties.config"
    }

    def __init__(self, path):
        missing = False
        self.path = path
        for name in self.filenames:
            if not os.path.isfile(self.getpath(name)):
                missing = True
                print("File " + self.getpath(name) + " not found.")
        if missing:
            raise Exception("Files are missing from provided path.")

    def getpath(self, name):
        if self.path:
            return os.path.join(self.path, self.filenames.get(name))
        else:
            return self.filenames.get(name)


def get_config_params(configfile):
    config_params = {}
    duplicates = {}

    with open(configfile) as file:
        for line in file:
            line = line.rstrip()
            if line.startswith("#"):
                continue
            if "=" not in line:
                continue
            key, val = line.split("=", 1)
            if key and val:
                if key.strip() not in config_params.keys():
                    config_params[key.strip()] = val.strip()
                else:
                    if key.strip() not in duplicates.keys():
                        duplicates[key.strip()] = [config_params[key.strip()], val.strip()]
                    else:
                        duplicates[key.strip()].append(val.strip())
    file.close()
    return config_params, duplicates


def validate_password(password):
    rval = True
    specials = "+&?>-}|.!(',_[@#)*;$]/%=<:`{^~"
    if len(password) < 8 or len(password) > 20:
        print("Password must be 8 to 20 characters in length")
        rval = False
    if " " in password:
        print("Whitespace characters are not allowed in password")
        rval = False
    if not any(x.isupper() for x in password):
        print("Must have at least one UPPER case character in password")
        rval = False
    if not any(x.islower() for x in password):
        print("Must have at least one lower case character in password")
        rval = False
    if not any(x.isdigit() for x in password):
        print("Must have at least one digit in password")
        rval = False
    if not any((x in specials) for x in password):
        print("Must have at least one special character " + specials + " in password")
        rval = False
    return rval


def validate_ip(ip):
    try:
        return ipaddress.ip_address(ip.split('/', 1)[0])
    except ValueError as error:
        print(error)


def validate_config_params(configs):
    """
    Validate configuration parameters and return final dictionary.
    If DHCP or AutoConfig is set, do not add static IP or Gateway settings to returned dictionary

    :param configs: dictionary of configuration parameters
    :return: dictionary of all valid and applicable parameters
    """

    do_dhcp = False
    do_ipv6_auto = False
    network_config = False
    params = {}

    if not configs:
        print("No config parameters found.")
        return False, params
    for key in configs.keys():
        if key not in configKeys.values():
            print("Invalid key value for OVF parameter: " + key)
            return False, params

    if configKeys.get('ACCEPT_EULA') not in configs or "true" not in configs[configKeys.get('ACCEPT_EULA')].lower():
        print("EULA not accepted.")
        return False, params
    params[configKeys.get('ACCEPT_EULA')] = "true"

    if configKeys.get('ADMIN_PASSWORD') not in configs or not validate_password(configs[configKeys.get('ADMIN_PASSWORD')]):
        print("Password validation failed.")
        return False, params
    params[configKeys.get('ADMIN_PASSWORD')] = configs[configKeys.get('ADMIN_PASSWORD')]

    if configKeys.get('ENABLE_DHCP') in configs:
        if "true" in configs[configKeys.get('ENABLE_DHCP')].lower():
            print("Enable IPv4 DHCP")
            do_dhcp = True
            params[configKeys.get('ENABLE_DHCP')] = "true"
        else:
            params[configKeys.get('ENABLE_DHCP')] = "false"

    if configKeys.get('ENABLE_IPV6_AUTO_CONFIG') in configs:
        if "true" in configs[configKeys.get('ENABLE_IPV6_AUTO_CONFIG')].lower():
            print("Enable IPv6 Auto config")
            do_ipv6_auto = True
            params[configKeys.get('ENABLE_IPV6_AUTO_CONFIG')] = "true"
        else:
            params[configKeys.get('ENABLE_IPV6_AUTO_CONFIG')] = "false"

    network_config = do_dhcp or do_ipv6_auto
    if configKeys.get('IP_ADDRESS') in configs:
        addr = validate_ip(configs[configKeys.get('IP_ADDRESS')])
        if not addr:
            print("Networking configuration failed.")
            return False, params
        if addr.version == 4:
            if do_dhcp:
                print("WARNING: IPv4 DHCP enabled.  Static IPv4 settings will be ignored.")
            else:
                print("Setting static IPv4 address: " + configs[configKeys.get('IP_ADDRESS')])
                params[configKeys.get('IP_ADDRESS')] = configs[configKeys.get('IP_ADDRESS')]
                network_config = True
                if configKeys.get('GATEWAY') in configs:
                    gw = validate_ip(configs[configKeys.get('GATEWAY')])
                    if gw and gw.version == 4:
                        print("Setting Gateway to: " + configs[configKeys.get('GATEWAY')])
                        params[configKeys.get('GATEWAY')] = configs[configKeys.get('GATEWAY')]
                    else:
                        print("Invalid Gateway IP: " + str(gw))
                        return False, params
        if addr.version == 6:
            if do_ipv6_auto:
                print("WARNING: IPv6 auto config enabled.  Static IPv6 settings will be ignored.")
            else:
                print("Setting static IPv6 address: " + configs[configKeys.get('IP_ADDRESS')])
                params[configKeys.get('IP_ADDRESS')] = configs[configKeys.get('IP_ADDRESS')]
                network_config = True
                if configKeys.get('GATEWAY') in configs:
                    gw = validate_ip(configs[configKeys.get('GATEWAY')])
                    if gw and gw.version == 6:
                        print("Setting Gateway to: " + configs[configKeys.get('GATEWAY')])
                        params[configKeys.get('GATEWAY')] = configs[configKeys.get('GATEWAY')]
                    else:
                        print("Invalid Gateway IP: " + str(gw))
                        return False, params
    if not network_config:
        print("No network configuration found.")
        return False, params
    return True, params



def parse_cmd_line():
    """
    Function to parse the command line arguments
    :return: cmd line args
    """
    parser = argparse.ArgumentParser(description='Preparing OVF file for auto Deployment of OpenManage Enterprise appliance')
    parser.add_argument('-p', '--path', help="Path to the extracted appliance files.  If not provided, assume current directory")
    args = parser.parse_args()
    return args



def get_sha256(ovf_file_path):
    """
    Function to create a new sha256 checksum after ovf file update/create product section
    :param ovf_file_path:
    :return: return the hex representation of digest
    """
    sha256hash = hashlib.sha256()

    with open(ovf_file_path, 'rb') as file:
        chunk = 0
        while chunk != b'':
            chunk = file.read(1024)  # read only 1024 bytes at a time
            sha256hash.update(chunk)
    file.close()
    return sha256hash.hexdigest()


def set_sha256(manifest_file_path, checksum):
    """
    Function to update sha256 in manifest file after adding custom tags to ovf file.
    :param manifest_file_path:
    :param checksum: checksum of OVF file
    :return:
    """
    with open(manifest_file_path, 'r+') as file:
        data = file.readlines()
        file.seek(0)
        file.truncate()
        for line in data:
            if "SHA256(openmanage_enterprise.x86_64-0.0.1.ovf)=" in line:
                line = 'SHA256(openmanage_enterprise.x86_64-0.0.1.ovf)= ' + checksum + "\n"

            file.write(line)
    file.close()


def create_ovf_product_section(ovffile, params):
    """
    Function to create the product section in the OVF file
    :param ovffile: path to the OVF file to modify
    :param params: parameters to add to the product section in the OVF file
    :return:
    """
    global current_indent, insert_index, append_index
    input_file = open(ovffile, "r")
    lines = input_file.readlines()
    input_file.close()

    indent = None
    for line in lines:
        if not indent and line.startswith(' '):
            indent = " " * (len(line) - len(line.lstrip(' ')))
        if "<VirtualHardwareSection>" in line:
            insert_index = lines.index(line)
            current_indent = " " * (len(line) - len(line.lstrip(' ')))
            lines[insert_index] = current_indent + "<VirtualHardwareSection ovf:transport=\"com.vmware.guestInfo\">\n"
        if "</VirtualHardwareSection>" in line:
            insert_index = lines.index(line) + 1
            current_indent = " " * (len(line) - len(line.lstrip(' ')))
        if '</VirtualSystem>' in line:
            append_index = lines.index(line)

    output = lines[:insert_index]
    output.append(current_indent + '<ovf:ProductSection required="true">\n')
    output.append(current_indent + indent + '<ovf:Info>Product Information</ovf:Info>\n')
    output.append(current_indent + indent + '<ovf:Property ovf:key="'+configKeys.get('ACCEPT_EULA')+'" ovf:type="string" ovf:value="'+params[configKeys.get('ACCEPT_EULA')]+'" />\n')
    output.append(current_indent + indent + '<ovf:Property ovf:key="'+configKeys.get('ADMIN_PASSWORD')+'" ovf:type="string" ovf:value="'+params[configKeys.get('ADMIN_PASSWORD')]+'" />\n')
    if configKeys.get('ENABLE_DHCP') in params:
        output.append(current_indent + indent + '<ovf:Property ovf:key="'+configKeys.get('ENABLE_DHCP')+'" ovf:type="string" ovf:value="'+params[configKeys.get('ENABLE_DHCP')]+'" />\n')
    if configKeys.get('ENABLE_IPV6_AUTO_CONFIG') in params:
        output.append(current_indent + indent + '<ovf:Property ovf:key="'+configKeys.get('ENABLE_IPV6_AUTO_CONFIG')+'" ovf:type="string" ovf:value="'+params[configKeys.get('ENABLE_IPV6_AUTO_CONFIG')]+'" />\n')
    if configKeys.get('IP_ADDRESS') in params:
        output.append(current_indent + indent + '<ovf:Property ovf:key="'+configKeys.get('IP_ADDRESS')+'" ovf:type="string" ovf:value="'+params[configKeys.get('IP_ADDRESS')]+'" />\n')
    if configKeys.get('GATEWAY') in params:
        output.append(current_indent + indent + '<ovf:Property ovf:key="'+configKeys.get('GATEWAY')+'" ovf:type="string" ovf:value="'+params[configKeys.get('GATEWAY')]+'" />\n')
    output.append(current_indent + '</ovf:ProductSection>\n')

    for line in lines[append_index:]:
        output.append(line)

    with open(ovffile, "w") as f:
        f.write(''.join(output))
    f.close()


def main():
    if sys.version_info < (3, 0):
        print("Python 3 required... exiting.")
        sys.exit(1)

    cmdline_args = parse_cmd_line()
    try:
        file_mgr = FileMgr(cmdline_args.path)
    except Exception as error:
        print(error)
        sys.exit(1)

    ovfprops, duplicates = get_config_params(file_mgr.getpath('config'))
    if duplicates:
        print("WARNING: duplicate key settings found in " + file_mgr.getpath('config'))
        for key in duplicates.keys():
            print("  Key: [" + key + "] has more than one value: " + str(duplicates.get(key)))
        print("Re-run the script with only one value set per key.")
        sys.exit(1)

    valid, params = validate_config_params(ovfprops)
    if not valid:
        print("OVF parameter validation failed.")
        sys.exit(1)

    create_ovf_product_section(file_mgr.getpath('ovf'), params)

    set_sha256(file_mgr.getpath('manifest'), get_sha256(file_mgr.getpath('ovf')))

    print('Auto deployment configuration completed.' + os.linesep)
    print(' *** You MUST run ovftool with the --X:injectOvfEnv and --powerOn flags set to auto deploy your appliance ***' + os.linesep)
    print('Below is a sample ovftool command that you can modify to deploy OpenManage Enterprise to an ESXi server.')
    print('All <UPPERCASE> tags need to be replaced with your deployment specific settings.' + os.linesep)
    print('ovftool --datastore=<DATASTORE1> -dm=thin --name=<"OPENMANAGE ENTERPRISE"> --noSSLVerify --acceptAllEulas --X:logToConsole=true --X:logLevel="info" --X:injectOvfEnv --powerOn openmanage_enterprise.x86_64-0.0.1.ovf vi://<USER>:<PASSWORD>@<IP ADDRESS>')
    print(os.linesep)


if __name__ == '__main__':
    main()
