# Copyright 2017-2021 VMware, Inc. All rights reserved. -- VMware Confidential
"""patch_utils.py

Utilities that are required during any upgrade and should be only imported on the
destination system (ie. after the dependant components were installed).
This is to ensure that we won't get ImportErrors when loading the patch on the
pre-patched/source system.

Some of this code is unused but left as is in case needed for future use.
"""
import logging
import os
import sys
from shutil import move
import xml.etree.ElementTree as ET

sys.path.append(os.environ['VMWARE_PYTHON_PATH'])
sys.path.append(os.path.dirname("/usr/lib/vmware-cm/bin/"))
sys.path.append(os.path.dirname("/usr/lib/vmware/cis_upgrade_runner/libs/"))
sys.path.append(os.path.dirname("/usr/lib/vmware/cis_upgrade_runner/libs/sdk/"))

from cis.cisreglib import (
    _get_syscfg_info, LookupServiceClient, SsoClient, AuthzClient,
    CisregOptionParser)
from cis.defaults import get_component_home_dir
from cis.tools import get_install_parameter
from cis.vecs import SsoGroup, vmafd_machine_id
from cis.utils import get_deployment_nodetype
from cloudvmcisreg import cloudvm_sso_cm_register, VecsKeyStore
from .vcdb_properties_utils import update_vcdb_properties
import vmafd
from pyVmomi import lookup
from identity.vmkeystore import VmKeyStore
from upgrade_errors import InternalError
sys.path.append("/usr/lib/vmware-vpx/py/")

logger = logging.getLogger(__name__)


def _get_node_type():
    """
    Returns node type for the current deployment. It'll be one of "embedded",
    "management" or "infrastructure" or False when it could not determine
    deployment type.
    """
    try:
        node_type = get_deployment_nodetype()
        logger.info('Node type is %s' % node_type)
        return node_type
    except Exception as e:
        logger.warning('Failed to get_deployment_nodetype: %s' % str(e))
        return False


def is_embedded():
    """
    Returns True when current machine's deployment type is "embedded".
    """
    return _get_node_type() == 'embedded'


def is_dbtype_embedded():
    """
    Returns true if the database is embedded in VCSA.
    """
    return get_install_parameter('db.type', 'embedded') == 'embedded'


def is_management():
    """
    Returns True when current machine's deployment type is "management".
    """
    return _get_node_type() == 'management'


class AuthzPatch(object):
    """
    Patches authz by reloading roles in defRoles.xml.
    Roles to groups mapping present in vpxd-service-spec.prop need to be copied
    to GROUP_ROLE_MAP.
    All methods are indempotent.
    """

    ROLES_TO_RELOAD = ['AutoUpdateUser']
    GROUP_ROLE_MAP = {
        "AutoUpdate": "AutoUpdateUser",
        "TrustedAdmins": "TrustedAdmin"}

    def __init__(self):
        """
        Initializes AuthzClient needed for patching roles and permissions
        """
        ls_url, self.domain_name = _get_syscfg_info()
        logger.debug("Connecting to Lookup Service, url=%s", ls_url)
        ls_obj = LookupServiceClient(ls_url, retry_count=1)
        sts_url, sts_cert_data = ls_obj.get_sts_endpoint_data()

        logger.debug("Logging into SSO AdminClient as machine solution user, "
                     "url=%s", sts_url)
        MACHINE_NAME = 'machine'
        self.sso_client = SsoClient.init_with_vecs(sts_url, sts_cert_data,
                                                   MACHINE_NAME, MACHINE_NAME)
        ls_obj.set_sso_client(self.sso_client)
        authz_url, authz_cert_data = ls_obj.get_authz_endpoint_data()
        logger.debug("Connecting to Authz, url=%s", authz_url)
        self.authz_client = AuthzClient(authz_url, self.sso_client)
        logger.debug("Succesfully initialized AuthzClient")

    def reload_roles(self):
        """
        Reloads roles defined in defRoles.xml (new roles will be added,
        existing will not be affected) according to ROLES_TO_RELOAD list.
        This is to be consistent with the logic in upgrade which won't recreate
        deleted roles.
        """
        vpx_dir = get_component_home_dir('vpx')
        cisreg_options = {
            'permission.newRole':
                os.path.join(vpx_dir, 'permission/defRoles.xml')
        }
        cisreg_optparser = CisregOptionParser(cisreg_options)
        # Filter roles from defRoles.xml
        roles = [role for role in cisreg_optparser.get_roles()
                 if role['name'] in AuthzPatch.ROLES_TO_RELOAD]
        added = self.authz_client.load_roles(roles)
        logger.info(
            "Succesfully loaded Authz roles, added %d new roles", added)

    def assign_groups_to_roles(self):
        """
        Assign groups to roles as defined in vpxd-service-spec.prop.
        """
        for group, role in AuthzPatch.GROUP_ROLE_MAP.items():
            self.authz_client.set_permission(
                self.domain_name, role, group, True)


class SSOPatch(object):
    """
    Patches SSO groups and solution users.
    All methods are indempotent so just add more users/groups in the future
    patches.
    """

    # groups to add stored as name -> description
    GROUPS_EMBEDDED = {
        'AutoUpdate': "Users allowed to perform update related operations",
        "TrustedAdmins": "Administrators who manage the trusted hosts"}

    GROUPS = {
        'ActAsUsers': "Act-As Users",
        'ServiceProviderUsers': "Users allowed to manage WCP and VMC infrastructure."}

    # user-group relations stored as username -> list of groups
    USER_GROUPS_EMBEDDED = {
        "machine-{machine_id}": ['AutoUpdate'],
        "vpxd-extension-{machine_id}": ['ServiceProviderUsers']}

    USER_GROUPS_MGMT = {
        "vpxd-extension-{machine_id}": ['ServiceProviderUsers']}

    USER_GROUPS = {"vpxd-{machine_id}": ['ActAsUsers']}

    def __init__(self):
        """
        Initializes vecs.SsoGroup using machine credentials from vmafd.
        """
        vmafdClient = vmafd.client('localhost')
        username = vmafdClient.GetMachineName()
        password = vmafdClient.GetMachinePassword()
        self.sso_group = SsoGroup(username, password)

    def _add_groups(self, groups):
        """
        Ensures that given groups are present. Ignores already exsiting groups.
        """
        logger.debug('Adding SSO groups')
        for name, description in groups.items():
            if self.sso_group.create(name, description):
                logger.info('Added "%s" group', name)
            else:
                # SsoGroup.create returns False when group already exists
                logger.debug('Group "%s" already exists', name)

    def _assign_users_to_groups(self, user_groups):
        """
        Ensures that solutions users are assigned to given groups.
        """
        logger.debug('Assigning solution users to SSO groups')
        machine_id = vmafd_machine_id()
        for username, groups in user_groups.items():
            username = username.replace('{machine_id}', machine_id)
            for group in groups:
                self.sso_group.add_user(group, username)
                logger.info('User %s added to %s group', username, group)

    def add_groups(self):
        if is_embedded():
            self._add_groups(SSOPatch.GROUPS_EMBEDDED)
        self._add_groups(SSOPatch.GROUPS)

    def assign_users_to_groups(self):
        if is_embedded():
           self._assign_users_to_groups(SSOPatch.USER_GROUPS_EMBEDDED)
        if is_management():
           self._assign_users_to_groups(SSOPatch.USER_GROUPS_MGMT)
        self._assign_users_to_groups(SSOPatch.USER_GROUPS)


class VCDBPropPatch:
    """
    Patches the vcdb.properties file to make jdbc url to use sslmode=disable
    property in case of embedded Postgres DB.
    """

    def __init__(self, vcdb_prop_path="/etc/vmware-vpx/vcdb.properties"):
        self.vcdb_prop_path = vcdb_prop_path

    def doPatching(self):
        if is_dbtype_embedded():
            return update_vcdb_properties(self.vcdb_prop_path)
        return False


class VPXDCfgPatch:
    """
    Patches the vpxd.cfg file to make clear/remove XML property entries.
    Uses elementtree XML lib to modify vpxd.cfg which
    allows modifying/adding XML nodes so this can be extended in future.
    """
    # <config> is the root and paths need not include config.
    PATH_ALARMS_VERSION_ENTRY = "alarms/vim/version"
    PATH_SSL_ENTRY = "vmacore/ssl"
    TAG_SSL_PROTOCOLS_ENTRY = "protocols"

    def __init__(self, vpxd_cfg_path="/etc/vmware-vpx/vpxd.cfg"):
        self.vpxd_cfg_path = vpxd_cfg_path

    def initCfgTree(self):
        self.vpxdcfg = ET.parse(self.vpxd_cfg_path)
        self.vpxdcfgroot = self.vpxdcfg.getroot()

    def writeCfgTree(self):
        self.vpxdcfg.write(self.vpxd_cfg_path)

    def clearCfgEntry(self, str_path):
        """
        Clear the config entry by clearing the text of the xml entry.
        :return: True if entry exists and cleared. False if none exists.
        """
        clearedCount = 0
        for entry in self.vpxdcfgroot.findall(str_path):
            entry.text = ''
            clearedCount += 1
        return clearedCount

    def removeCfgEntry(self, str_parent_path, str_child_tag):
        """
        Clear the config entry by removing the child element from parent.
        VPXD config in vmacore is not XML compliant and cannot handle <tag/>
        in some cases so the element must be removed instead.
        :str_parent_path: path of the parent element.
        :str_child_tag: One tag of the child element and no grand-child.
        :return: True if entry exists and cleared. False if none exists.
        """
        clearedCount = 0
        if not str_parent_path or not str_child_tag:
            return clearedCount
        # Assume Path contains one child tag.
        str_child_path = str_parent_path + '/' + str_child_tag
        for parent_entry in self.vpxdcfgroot.findall(str_parent_path):
            for entry in self.vpxdcfgroot.findall(str_child_path):
                parent_entry.remove(entry)
                parent_entry.text = ''
                clearedCount += 1
        return clearedCount

    def clearAlarmsVersionEntry(self):
        """
        Clear the version entry in the alarms section of vpxd.cfg.
        :return: True if entry exists and cleared. False if none exists.
        """
        return self.clearCfgEntry(self.PATH_ALARMS_VERSION_ENTRY)

    def clearSslProtocolsEntry(self):
        """
        Clear the protocols entry in the ssl section of vpxd.cfg.
        :return: True if entry exists and cleared. False if none exists.
        """
        return self.removeCfgEntry(self.PATH_SSL_ENTRY,
                                   self.TAG_SSL_PROTOCOLS_ENTRY)

    def doPatching(self):
        patchEntryCount = 0
        try:
            self.initCfgTree()
            # additional property modify/clear follow this pattern.
            # add a function to modify/clear property entry.
            # increment patchCount only if change was done.
            patchEntryCount += self.clearAlarmsVersionEntry()
            patchEntryCount += self.clearSslProtocolsEntry()
        except Exception as ex:
            raise ex
        finally:
            if patchEntryCount > 0:
                try:
                    # write modified file only if any changes made.
                    self.writeCfgTree()
                except Exception as iex:
                    # Failed to write file. no patching done. re-raise.
                    raise iex
        return patchEntryCount > 0

