#!/usr/bin/env python

"""
* This file is used to register VDC to CIS which includes
* 1. Register solution user and its privileges.
* 2. Register solution group and assign registered solution user to the group
* 3. Register to reverse proxy
"""

from __future__ import absolute_import
import base64
import json
import logging
import os
import sys
from xml.etree import ElementTree

if not os.environ['VMWARE_PYTHON_PATH'] in sys.path:
    sys.path.append(os.environ['VMWARE_PYTHON_PATH'])
# Prefer the files in the directory of this file when importing. This is important during patching
# as the VC can have older copies of other files in /usr/lib/vmware-content-library
sys.path.insert(0, os.path.dirname(__file__))
from cis.defaults import (get_cis_config_dir,
                          get_cis_install_dir)
from cis.utils import def_by_os, log, gen_random_pwd
from cis.tools import wait_for_install_parameter

# The following path should be involved in VMWARE_PYTHON_PATH
sys.path.append(def_by_os("%s/vmware-%s/bin" % (get_cis_install_dir(), "cm"),
                          os.path.join(get_cis_install_dir(), "cm", "bin")))
from cloudvmcisreg import VecsKeyStore, cloudvm_sso_cm_register

from add_new_cls_privileges import add_new_privileges
from install_lib.install_constants import CONTENT_LIBRARY_SERVICE_SPEC
from install_lib.install_common import (get_service_cisreg_spec_path, SCA_VMON_SCRIPT,
                                        createFileOperateException)
from install_lib.pre_install import CisInfo

from cis.cisreglib import (
    _get_syscfg_info, LookupServiceClient, SsoClient, AuthzClient)
from cis.vecs import SsoGroup
import vmafd


__author__ = 'VMware, Inc.'
__copyright__ = 'Copyright (c) 2013-2017,2019 VMware, Inc. All rights reserved.'

logger = logging.getLogger(__name__)

SOLUTION_USER_SPEC_TEMPLATE = \
    """
 solutionUser.name={USER_NAME}
 solutionUser.ownerId={USER_ID}
 # Optional, use to assign an SSO WS-Trust role to the solution user if needed
 solutionUser.ssoWsTrustRole=ActAsUser
 solutionUser.permission=Admin
"""

CISREG_SERVICE_SPEC_TEMPLATE = \
    """
 serviceVersion = 1.0
 # Service type should be published to the service publication wiki. Service
 # type must be globally unique. Consult the service publication wiki above
 # for naming convention.
 serviceType.product = com.vmware.cis
 serviceType.type = cis.{SERVICE_NAME}
 serviceGroupInternalId = com.vmware.cis.{SERVICE_NAME}
 serviceNameResourceKey = cis.content-library.ServiceName
 serviceGroupResourceKey = com.vmware.vdcs.{SERVICE_NAME}-main.service_group_resource_key
 serviceDescriptionResourceKey = cis.content-library.ServiceDescription

 hostId = {SERVICE_HOSTID}

 endpoint0.url = {SERVICE_URL}
 endpoint0.type.protocol = vapi.json.https
 endpoint0.type.id = com.vmware.cis.{SERVICE_NAME}.vapi.https

 endpoint1.url = {SERVICE_CM_HEALTH_URL}
 endpoint1.type.protocol = rest
 endpoint1.type.id = com.vmware.cis.common.healthstatus

 endpoint2.url = {SERVICE_URL_HTTP}
 endpoint2.type.protocol = vapi.json.http
 endpoint2.type.id = com.vmware.cis.{SERVICE_NAME}.vapi.http

 endpoint3.url = {SERVICE_URL}
 endpoint3.type.protocol = vapi.json.https
 endpoint3.type.id = com.vmware.cis.data.provider

 endpoint4.url = {SERVICE_URL}
 endpoint4.type.protocol = vapi.json.https
 endpoint4.type.id = com.vmware.cdc.provider

 resourcebundle.url = {SERVICE_URL}resourcebundle
 resourcebundle.data0.key = com.vmware.cis.common.resourcebundle.basename
 resourcebundle.data0.value = {RESOURCE_BUNDLE_BASENAME}
"""
CISREREG_SERVICE_SPEC_TEMPLATE = """
 cmreg.serviceid = {SERVICE_ID}
"""
REVERSE_PROXY_SPEC_TEMPLATE = """
rhttpproxy.file={SERVICE_NAME}.conf
rhttpproxy.endpoint0.namespace={ENDPOINT}
rhttpproxy.endpoint0.connectionType={CONNECTION_TYPE}
rhttpproxy.endpoint0.address={EP_ADDRESS}
rhttpproxy.endpoint0.httpAccessMode={ACCESS_MODE_HTTP}
rhttpproxy.endpoint0.httpsAccessMode={ACCESS_MODE_HTTPS}
"""

##########################################################################


class RegCIS():

    """
     Register product to CIS.
      1. Register solution groups.
      2. Register solution users.
      3. Register product services
      4. Register to reverse proxy.
    """

    SERVICE_INFO_SERVICE_ID_KEY = "service_id"
    SERVICE_INFO_ENDPOINT_KEY = "endpoint"
    SERVICE_INFO_GROUP_NAME_KEY = "group_name"

    def __init__(self, prod_home, services_spec=None, vdc_cfg_dir=None, service_host=None,
                 service_http_port=None, service_jmx_port=None, is_patch=False,
                 key_store_name=None):

        """
         @param prod_home: The home directory of running vCD-E.
         @param services_spec: a dict which has format of ./cmSpec/vdc_service_spec.json
         @param vdc_cfg_dir: the file directory to store vcd-e configuration / properties
         @param service_host: The service url (ip). Only set it if the service is on remote. It is normally used for dev env.
         @param service_http_port: The service http port. It is normally used for dev env.
         @param service_jmx_port: The service jmx port. It is normally used for dev env.
         @param is_patch: Whether or not this is a patch install, by default it is false
         @param key_store_name: key store name for service registration, only used for patch
        """
        file_path = os.path.dirname(os.path.realpath(__file__))
        log("Initialize CIS registration")
        log("vDC home directory is %s" % prod_home)

        if not services_spec:
            services_spec = os.path.join(file_path, 'regSpecs', 'vdc_services_spec.json')
        try:
            self._service_specs = json.load(open(services_spec))
        except Exception as ex:
            raise createFileOperateException(services_spec, ex)

        if not vdc_cfg_dir:
            vdc_cfg_dir = os.path.join(prod_home, "etc")
        if not os.path.exists(vdc_cfg_dir):
            os.makedirs(vdc_cfg_dir)
        log("vDC config directory is %s" % vdc_cfg_dir)
        self.vdc_cfg_dir = os.path.realpath(vdc_cfg_dir)
        self.prod_home = prod_home

        self.is_patch = is_patch
        if self.is_patch:
            # remote service should be false for patch since firstboot always has it off
            # NOTE: self.service_host is not used if remote_service is false
            # NOTE: self.service_url is not actually used
            # NOTE: self.jmx_port is no longer used and should be removed
            self._remote_service = False
            self.http_port = service_http_port
            self.key_store_name = key_store_name
        else:
            self.cis_info = CisInfo()
            if not service_host:
                self._remote_service = False
                self.service_host = self.cis_info.ip_url_addr
            else:
                self._remote_service = True
                self.service_host = service_host

            # set expected port
            if not service_http_port:
                _, self.http_port = self.cis_info.get_available_port(16666)
            else:
                self.http_port = service_http_port

            self.component_url = "http://%s:%s" % (self.service_host, self.http_port)
            if self._remote_service:
                self.service_url = self.component_url
            else:
                self.service_url = "https://%s" % self.service_host

            # set the jmx port
            if not service_jmx_port:
                _, self.jmx_port = self.cis_info.get_available_port(16667)
            else:
                self.jmx_port = service_jmx_port

        # a dictionary {service-name:{user_name:<string>, user_owner_id:<string>,
        # endpoint:<string>}, ...}
        self.services_info = self._get_all_services_info_with_endpoint()

    def _get_all_services_info_with_endpoint(self):
        services_info = {}
        for service in self._service_specs:
            vapi_endpoint = service["endpoint"]
            services_info[
                service["service-name"]] = {RegCIS.SERVICE_INFO_ENDPOINT_KEY: vapi_endpoint}

        return services_info

    def _get_cm_truststore(self):
        s_path = os.path.join(self.vdc_cfg_dir, "ssl")
        if not os.path.exists(s_path):
            os.makedirs(s_path)

        return os.path.join(s_path, "cm-trustore")

    def _get_solution_user_spec_path(self):
        su_spec_path = os.path.join(self.vdc_cfg_dir, "solution-users")
        if not os.path.exists(su_spec_path):
            os.makedirs(su_spec_path)
        return su_spec_path

    def get_solution_user_spec(self, user_name, user_id):
        """
        @return: the solution_user_spec.
        """
        return SOLUTION_USER_SPEC_TEMPLATE.format(
            USER_NAME=user_name,
            USER_ID=user_id
        )

    # TODO: Refer bug https://bugzilla.eng.vmware.com/show_bug.cgi?id=1240521
    def __get_sca_file_name(self):
        return SCA_VMON_SCRIPT

    def generate_cisreg_service_spec(self, service_spec, cisreg_spec_template):
        """
        @param service_spec: is defined as in regSpec/vdc_services.spec.json
        @return: the content of service spec file for cisreg.
        """
        service_name = service_spec["service-name"]
        # using reverse proxy here!
        service_url = 'https://${system.urlhostname}:${rhttpproxy.ext.port2}/%s/' % service_spec[
            "endpoint"].strip("/")
        service_url_http = 'http://${system.urlhostname}:${rhttpproxy.ext.port1}/%s/' % service_spec[
            "endpoint"].strip("/")
        service_cm_health_url = service_url + service_spec["cm-health-endpoint-path"].strip("/")

        # SCA should be able to perform start/stop/restart/status action without
        # relying on some scripts provided by service itself -- ref bug 1078119.
        if not self._remote_service:
            # SCA just control the service running inside of cloudvm / ciswin.
            cisreg_spec_template += " controlScriptPath = %s" % self.__get_sca_file_name() + \
                os.linesep

        service_cisreg = cisreg_spec_template.format(
            SERVICE_HOSTID=wait_for_install_parameter('sca.hostid'),
            SERVICE_NAME=service_name,
            SERVICE_URL=service_url,
            SERVICE_URL_HTTP=service_url_http,
            SERVICE_CM_HEALTH_URL=service_cm_health_url,
            RESOURCE_BUNDLE_BASENAME=service_spec['resource-bundle-basename'])

        endpoint_id = 3

        if "subservices" in service_spec:
            for subservice in service_spec["subservices"]:
                endpoint_prefix = 'endpoint%d' % endpoint_id
                subservice_url = "%s://${system.urlhostname}:%d/%s/%s" % (subservice["url_protocol"],
                                                                          subservice["url_port"],
                                                                          service_spec[
                                                                              "endpoint"].strip("/"),
                                                                          subservice["path"].strip("/"))

                service_cisreg += " %s.url = %s%s" % (endpoint_prefix,
                                                      subservice_url,
                                                      os.linesep)
                service_cisreg += " %s.type.protocol = %s%s" % (
                    endpoint_prefix, subservice["protocol"], os.linesep)
                service_cisreg += " %s.type.id = %s%s" % (
                    endpoint_prefix, subservice["id"], os.linesep)
                endpoint_id += 1

        if not service_spec["cm-files"]:
            return service_cisreg

        # define vapi meta types, if its json file name postfix is different with
        # it meta name, the postfix is given.
        meta_types = [['cli'], ['metamodel'], ['authentication'],
                      ['privilege', 'authorization'], ['routing']]
        cm_file_dir = os.path.join(self.vdc_cfg_dir, service_spec["cm-files"]["root"])

        # Add per component metadata type key-value pairs to the vdcs endpoint (https/http)
        # com.vmware.vapi.metadata.{metadata_kind}.{ remote or file }[.{metadata_set}]
        for endpoint_id in [0, 2]:
            cm_file_spec = service_spec["cm-files"]
            dataIndex = 0
            for metadata_path in cm_file_spec["metadata-path"]:
                # Get all the files for this service that need to be transferred to the cloudvm
                # It includes all the metadata files and cisreg files.
                metadata_dir = os.path.join(cm_file_dir, metadata_path)

                endpoint_prefix = 'endpoint%d' % endpoint_id

                metadata_files = self._get_metadata_files(metadata_dir)
                for metadata_file in metadata_files:
                    # eg. metadata_file on Windows -
                    # C:\ProgramData\VMware\CIS\cfg\vdcs\metadata\vdcs\vdcs_authorization.json
                    # metadata file on Linux -
                    # /etc/vmware-vdcs/metadata/vdcs/vdcs_authorization.json
                    if metadata_file.endswith(".json"):
                        product_name = metadata_file.replace(
                            "\\", "/").split('_')[0].split('/')[-1]
                    for meta_type in meta_types:
                        if metadata_file.endswith("_%s.json" % meta_type[len(meta_type) - 1]):
                            service_cisreg += " %s.data%d.key = com.vmware.vapi.metadata.%s.file.%s%s" % (
                                endpoint_prefix, dataIndex,
                                meta_type[0], product_name, os.linesep)
                            service_cisreg += " %s.data%d.value = %s%s" % (endpoint_prefix, dataIndex,
                                                                           metadata_file.replace(
                                                                               "\\", "/"),
                                                                           os.linesep)
                            dataIndex += 1

        # Add privileges if specified. Merge them if necessary
        privilege_files = service_spec["cm-files"]["privilege-files"]
        if privilege_files:
            # Not empty.
            privilege_file_name = service_name + '_privilege.xml'
            privilege_file_path = os.path.join(cm_file_dir, privilege_file_name)

            add_privilege = self._merge_xml_files(
                cm_file_dir, privilege_files, privilege_file_path)
            if add_privilege:
                service_cisreg += "permission.newPrivilege = %s%s" % (
                    self._escape_space_in_path(privilege_file_path), os.linesep)

        # Add role files if specified. Merge them if necessary
        role_files = service_spec["cm-files"]["role-files"]
        if role_files:
            role_file_name = service_name + '_role.xml'
            role_file_path = os.path.join(cm_file_dir, role_file_name)

            add_role = self._merge_xml_files(cm_file_dir, role_files, role_file_path)
            if add_role:
                service_cisreg += "permission.newRole = %s%s" % (
                    self._escape_space_in_path(role_file_path), os.linesep)

        log("vDC cisreg spec: %s" % service_cisreg)
        return service_cisreg

    def generate_reverse_proxy_spec(self, service_spec, rp_spec_template):
        """
        Generate reverse proxy spec content using by cis register.
        @param service_spec: the service object defined in vdc_services_spec.json
        @param rp_spec_template: The template of reverse proxy spec.
        """
        rp_settings = {}
        rp_settings['SERVICE_NAME'] = service_spec['service-name']
        rp_settings['ACCESS_MODE_HTTP'] = 'redirect'
        rp_settings['ACCESS_MODE_HTTPS'] = 'allow'
        rp_settings['ENDPOINT'] = service_spec['endpoint']
        if self._remote_service:
            rp_settings['CONNECTION_TYPE'] = 'remote'
            rp_settings['EP_ADDRESS'] = '%s:%s' % (self.service_host, self.http_port)
        else:
            rp_settings['CONNECTION_TYPE'] = 'local'
            rp_settings['EP_ADDRESS'] = self.http_port

        return rp_spec_template.format(**rp_settings)

    def registerAll(self, user_name, user_id, service_id=None, reset=False):
        """
        Register all services to LS / SSo / Reverse proxy by using same solution user name and
        owner id. Reregistration happens if service id is specified.

        Args:
            user_name (str): name of user to be registered
            user_id (str): id of user to be registered
            service_id (str): ID of service to be reregistered
            reset (bool): indicates if old reverse proxy needs to be cleaned
        """
        log("Start to register all services with user %s and owner id %s" % (user_name, user_id))
        if reset:
            self.__cleanOldReverseProxy()

        for service in self._service_specs:
            self.registerUserAndService(user_name, user_id, service, service_id=service_id)

    def registerUserAndService(self, user_name, user_id, service_spec, service_id=None):
        """
        Register/Reregister user and service (include reverse proxy) through cisreg.

        Args:
            user_name (str): name of user to be registered
            user_id (str): id of user to be registered
            service_spec (dict): the service object defined in vdc_services_spec.json
            service_id (str): ID of service to be reregistered
        """
        service_name = service_spec["service-name"]

        sso_user_spec = self.get_solution_user_spec(user_name, user_id)
        cisreg_spec = self.generate_cisreg_service_spec(service_spec, CISREG_SERVICE_SPEC_TEMPLATE)
        rp_spec = self.generate_reverse_proxy_spec(service_spec, REVERSE_PROXY_SPEC_TEMPLATE)
        if service_id:
            logger.info("Reregistering CL service (ID: %s) with lookup service", service_id)
            rereg_spec = CISREREG_SERVICE_SPEC_TEMPLATE.format(SERVICE_ID=service_id)
            service_cisreg = sso_user_spec + cisreg_spec + rp_spec + rereg_spec
        else:
            logger.info("Registering CL service with lookup service")
            service_cisreg = sso_user_spec + cisreg_spec + rp_spec

        if self.is_patch:
            key_store_name = self.key_store_name
        else:
            key_store_name = service_name
        keystore = VecsKeyStore(key_store_name)
        logger.info("Using key store name: %s", key_store_name)
        svcinfo = cloudvm_sso_cm_register(
            keystore, self._generate_cisreg_service_spec_file(service_name, service_cisreg),
            key_store_name, isPatch=self.is_patch)

        create_sso_groups(service_spec)

        self.services_info[service_name][RegCIS.SERVICE_INFO_SERVICE_ID_KEY] = svcinfo['serviceId']

        # Check if any new privileges are needed, add if necessary
        add_new_privileges(self.vdc_cfg_dir)

    def __cleanOldReverseProxy(self):
        # TODO: Remove this function once the cloudVm / CisWin build does not
        # contains old vdc.conf in reverse proxy configure directory.
        rp_comp_name = "rhttpproxy"
        rp_conf = def_by_os(os.path.join("%s-%s" % (get_cis_config_dir(), rp_comp_name), "endpoints.conf.d", "vdc.conf"),
                            os.path.join(get_cis_config_dir(), "vmware-%s" % rp_comp_name, "endpoints.conf.d", "vdc.conf"))
        if os.path.exists(rp_conf):
            os.remove(rp_conf)

    def _generate_cisreg_service_spec_file(self, service_name, content):
        service_cisreg_file = get_service_cisreg_spec_path(service_name, self.vdc_cfg_dir)

        try:
            with open(service_cisreg_file, "w") as f:
                f.write(content)
        except Exception as ex:
            raise createFileOperateException(service_cisreg_file, ex)

        return service_cisreg_file

    def _get_metadata_files(self, metadata_path):
        metadata_files = []

        metadata_path = os.path.abspath(metadata_path)
        fileList = os.listdir(metadata_path)
        for f in fileList:
            f = os.path.join(metadata_path, f)
            if os.path.isfile(f):
                metadata_files.append(f)

        return metadata_files

    def _merge_xml_files(self, cm_files_dir, files, dest_file):
        first = None

        for f in files:
            file_path = os.path.join(cm_files_dir, f)
            try:
                tree = ElementTree.parse(file_path)
            except Exception as ex:
                raise createFileOperateException(file_path, ex)

            if first is None:
                first = tree
            else:
                first.getroot().extend(tree.getroot().getchildren())

        if first is not None:
            first.write(dest_file)
            return True

        return False

    def _escape_space_in_path(self, path):
        return path.replace(" ", "%20").replace("\\", "/")

    def get_security_key(self):
        """
        Generates and returns a cryptographically strong 128-bit key.
        """
        if hasattr(self, '_security_key'):
            return self._security_key

        random_pwd = gen_random_pwd(16)
        logger.info("gen_random_pwd(16) type=%s" % type(random_pwd))
        if type(random_pwd) is str:
            random_pwd = random_pwd.encode()
        self._security_key = base64.b64encode(random_pwd)
        if type(self._security_key) is not str:
            self._security_key = self._security_key.decode()
        return self._security_key

    @staticmethod
    def unregisterUserAndService(cfg_dir, service_names):
        """
        # Check and unregister user and service through cisreg if service register spec exists.
        @param service_names: A list of registered service names
        """

        logger.info("Unregistering Content Library services from Component Manager: %s",
                    service_names)

        for service_name in service_names:
            reg_spec_file = get_service_cisreg_spec_path(service_name, cfg_dir)
            if os.path.exists(reg_spec_file):
                logger.info("Service %s: Unregistering service with spec file: %s" %
                            (service_name, reg_spec_file))
                keystore = VecsKeyStore(service_name)
                svc_info = cloudvm_sso_cm_register(
                    keystore, reg_spec_file, service_name, regOp='unregister')
                logger.info("Service %s: Finished unregistering service: %s" %
                            (service_name, svc_info))
                os.remove(reg_spec_file)
            else:
                logger.info("Service %s: Skipping unregistration. Spec file does not exist: %s" %
                            (service_name, reg_spec_file))

        logger.info("Finished unregistering Content Library services from Component Manager: %s",
                    service_names)


class SsoPatch:
    def __init__(self):
        vmafdClient = vmafd.client('localhost')
        username = vmafdClient.GetMachineName()
        password = vmafdClient.GetMachinePassword()
        self.sso_group = SsoGroup(username, password)

    def ensure_groups_exist(self, groups):
        """
        Tries to create all the groups specified if they do not exist.

        :param groups: list of groups that should exist.
        :type groups: list
        :rtype: list
        """
        created = []
        for name, description in groups.items():
            logger.debug('Attempting to create group "%s"', name)
            if self.sso_group.create(name, description):
                logger.info('Added "%s" group', name)
                created.append(name)
            else:
                # SsoGroup.create returns False when group already exists
                logger.debug('Group "%s" already exists', name)
        return created


class AuthzPatch:
    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 assign_groups_to_roles(self, group_role_map):
        """
        Assign groups to roles as specified in group_role_map.

        :param group_role_map: map of global privileges linking group to role.
        :type group_role_map: dict
        :rtype: None
        """
        for group, role in group_role_map.items():
            logger.debug("Creating global permission for group %s and role %s",
                         group, role)
            # Last boolean argument in set_permission is "isGroup".
            self.authz_client.set_permission(
                self.domain_name, role, group, True)


def create_sso_groups_from_service_spec():
    """
    Creates the SSO groups managed by CLS and assigns roles as specified in the
    service spec. This function is used during SSO domain repointing and is invoked
    from the script /usr/lib/repoint/cls_repoint_script.py in VC to recreate the
    SSO users and groups appropriately in the new domain.
    """
    try:
        service_specs = json.load(open(CONTENT_LIBRARY_SERVICE_SPEC))
    except Exception as ex:
        raise createFileOperateException(CONTENT_LIBRARY_SERVICE_SPEC, ex)

    for service_spec in service_specs:
        create_sso_groups(service_spec)


def create_sso_groups(service_spec):
    """
    Creates the SSO groups and assigns roles as specified in the service spec.

    Args:
        service_spec (dict): the service object defined in vdc_services_spec.json
    """
    # Create SSO groups.
    sso_patch = SsoPatch()
    sso_patch.ensure_groups_exist(service_spec['groups'])

    # Bind SSO groups and roles.
    authz_patch = AuthzPatch()
    authz_patch.assign_groups_to_roles(service_spec['group-role'])
