# Copyright (c) 2016-2017 VMware, Inc.  All rights reserved. -- VMware Confidential

"""
vpxd component module following the B2B patching principles
"""

import errno
import os
from extensions import extend, Hook
from l10n import msgMetadata as _T, localizedString as _
import vcsa_utils
from os_utils import executeCommand
import logging

from patch_specs import Requirements, PatchInfo, RequirementsResult

from reporting import getProgressReporter

logger = logging.getLogger(__name__)


def _get_ssl_path():
   cfgPath = os.environ['VMWARE_CFG_DIR']
   if os.name == 'posix':
      return os.path.join('%s-vpx' % cfgPath, 'ssl')
   else:
      return os.path.join(cfgPath, 'vmware-vpx', 'ssl')


def _invoke_command(cmd):
   """
   execute a command and throw an exception for non-zero
   return code
   """
   stdout, stderr, ret = executeCommand(cmd)
   if ret != 0:
      logger.info("Invoked command: " + str(cmd))
      logger.info("RC = %s\nStdout = %s\nStderr = %s" % (ret, stdout, stderr))
      raise Exception('Failed command: %s\nStderr: %s' % (cmd, stderr))
   return stdout

def _patchAlarms():
   """
   Force the alarm manager to update expression_name fields in the vpx_alarm_runtime
   table using the actual vim version instead of the latest stable version that B2B uses
   """
   logger.info('Applying alarms fix')
   from . import simpleXml
   VPXD_CFG_FILE = '/etc/vmware-vpx/vpxd.cfg'
   with open(VPXD_CFG_FILE, 'r', encoding="utf-8-sig") as fp:
      root = simpleXml.Parse(fp.read())

   # Drop the alarms section
   simpleXml.SetText(root, 'alarms/vim/version', '')

   # Write back without alarms section, it will be regenerated by the service
   with open(VPXD_CFG_FILE, 'w', encoding="utf-8-sig") as fp:
      fp.write(simpleXml.Serialize(root))


class DataEncipherCert(object):
   def __init__(self, name, ssl_path):
      self._name = name
      self._ssl_path = ssl_path
      self._cert_path = os.path.join(ssl_path, "tmp-%s.crt" % name)
      self._private_key_path = os.path.join(ssl_path, "tmp-%s.key" % name)
      self._public_key_path = os.path.join(ssl_path, "tmp-%s.pub" % name)
      self._certool_path = self._get_certool_path()
      self._vecscli_path = self._get_vecscli_path()
      self._vmafdcli_path = self._get_vmafdcli_path()
      self._install_param_cli = self._get_install_param_cli()
      self._hostname = self._get_hostname()
      self._hostname_type = self._get_hostname_type()
      self._infra_node = self._get_infra_node()

   def _get_install_param_cli(self):
      '''Gets the install_parameter location'''
      if 'VMWARE_INSTALL_PARAMETER' not in os.environ:
         raise Exception('missing env var VMWARE_INSTALL_PARAMETER')
      return os.path.normpath(os.environ['VMWARE_INSTALL_PARAMETER'])

   def _get_install_param(self, key):
      cmd = [self._install_param_cli, key]
      return _invoke_command(cmd).rstrip()

   def _get_certool_path(self):
      '''Returns the path to certool'''
      if os.name == 'nt':
         if 'VMWARE_CIS_HOME' in os.environ:
            return os.path.join(
               os.environ['VMWARE_CIS_HOME'], 'vmcad', 'certool.exe')
         PROGRAM_FILES = os.environ['PROGRAMFILES']
         return os.path.normpath(PROGRAM_FILES + '/VMware/CIS/vmcad/certool.exe')
      elif os.name == 'posix':
         return '/usr/lib/vmware-vmca/bin/certool'

   def _get_vmafd_util(self, name):
      '''Returns the vmafd CLI specified'''
      if os.name == 'nt':
         if 'VMWARE_CIS_HOME' in os.environ:
            return os.path.join(
               os.environ['VMWARE_CIS_HOME'], 'vmafdd', '%s.exe' % name)
         PROGRAM_FILES = os.environ['PROGRAMFILES']
         return os.path.normpath(PROGRAM_FILES +
                                 '/VMware/CIS/vmafdd/%s.exe' % name)
      elif os.name == 'posix':
         return '/usr/lib/vmware-vmafd/bin/%s' % name

   def _get_vecscli_path(self):
      '''Returns the path to vecs-cli'''
      return self._get_vmafd_util('vecs-cli')

   def _get_vmafdcli_path(self):
      '''Returns the path to vmafd-cli'''
      return self._get_vmafd_util('vmafd-cli')

   def _get_hostname(self):
      return self._get_install_param('system.hostname')

   def _get_hostname_type(self):
      return self._get_install_param('system.hostname.type')

   def _get_infra_node(self):
      cmd = [self._vmafdcli_path, 'get-dc-name', '--server-name', 'localhost']
      return _invoke_command(cmd).rstrip()

   def _gen_cert(self):
      """
      Generates a certificate
      """
      logger.info('Generating certificate for "%s"' % self._name)
      try:
         if not os.path.exists(self._ssl_path):
            os.makedirs(self._ssl_path)
            logger.info('Created directory %s' % self._ssl_path)
      except OSError as e:
      # If file exists continue silently, else throw an error.
         if e.errno != errno.EEXIST:
            raise Exception('I/O error({0}): {1}'.format(e.errno, e.strerror))

      # Generate Private Key and Public Keys First
      cmd = [self._certool_path,
             '--genkey',
             '--privkey=' + self._private_key_path,
             '--pubkey=' + self._public_key_path]

      _invoke_command(cmd)

      # Generate Cert
      cmd = [self._certool_path,
             '--server=' + self._infra_node,
             '--genCIScert',
             '--dataencipherment',
             '--privkey=' + self._private_key_path,
             '--cert=' + self._cert_path,
             '--Name=' + self._name]

      # if we know the host name, put that into the certificate
      if self._hostname_type == 'fqdn':
         cmd.append('--FQDN=' + self._hostname)
      elif self._hostname_type == 'ipv4':
         cmd.append('--IPAddress=' + self._hostname)
      elif self._hostname_type == 'ipv6':
         cmd.append('--IPAddress=' + self._hostname)

      _invoke_command(cmd)

   def _cleanup(self):
      """
      Cleans up the generated certificate from the filesystem
      """
      logger.info('Removing certificate "%s" from the filesystem' % self._name)
      os.remove(self._private_key_path)
      os.remove(self._public_key_path)
      os.remove(self._cert_path)

   def create(self):
      """
      Creates the named data encipherment certificate
      and adds it to VECS
      """
      self._gen_cert()
      self._cleanup()

   def exists_in_vecs(self):
      """
      Check whether the named data encipherment certificate exists in VECS
      """
      logger.info('Checking whether certificate "%s" exists in VECS' %
                  self._name)
      # Check the store name
      cmd = [self._vecscli_path, 'store', 'list']
      result = _invoke_command(cmd)
      lines = result.splitlines()
      if self._name not in lines:
         return False
      # Check the entry
      cmd = [self._vecscli_path, 'entry', 'list', '--store', self._name]
      result = _invoke_command(cmd)
      # Get the aliases
      lines = [l for l in result.splitlines() if l.startswith('Alias')]
      aliases = [l.split('\t')[1] for l in lines]
      return self._name in aliases


@extend(Hook.Discovery)
def discover(ctx):
   '''DiscoveryResult discover(PatchContext sharedCtx) throw UserUpgradeError'''
   return vcsa_utils.getComponentDiscoveryResult("vpxd",
      displayName=_(_T("VPXD.displayName", "VMware vCenter Server")))

@extend(Hook.Requirements)
def collectRequirements(ctx):
   '''RequirementsResult collectRequirements(PatchContext ctx)'''
   mismatches = []
   requirements = Requirements()
   patchInfo = PatchInfo()
   return RequirementsResult(requirements, patchInfo, mismatches)

@extend(Hook.Validation)
def validate(ctx):
   '''ValidationResult validate(PatchContext ctx)'''
   pass

@extend(Hook.Prepatch)
def prePatch(ctx):
   '''void prePatch(PatchContext ctx) throw UserError'''
   pass

@extend(Hook.Patch)
def patch(ctx):
   '''void patch(PatchContext ctx) throw UserError'''

   progressReporter = getProgressReporter()
   progressReporter.updateProgress(0, _(_T("vpxd.patch.begin",
                                           'Start VPXD  patching')))

   # This needs to be called before patch_utils as otherwise patch_utils messes its imports
   _patchAlarms()

   # patch_utils require modules that may not be present in the source system.
   # Those can't be imported when running discovery on the source system.
   from .patch_utils import is_embedded, AuthzPatch, SSOPatch

   decert = DataEncipherCert(name='data-encipherment',
                             ssl_path=_get_ssl_path())
   if not decert.exists_in_vecs():
      decert.create()

   if is_embedded():
      # SSO groups
      progressReporter.updateProgress(50)
      sso_patch = SSOPatch()
      sso_patch.add_groups()
      sso_patch.assign_users_to_groups()

      # Authz
      progressReporter.updateProgress(75)
      authz_patch = AuthzPatch()
      authz_patch.reload_roles()
      authz_patch.assign_groups_to_roles()
      del authz_patch  # to ensure cleanup / close sockets sooner

   progressReporter.updateProgress(100, _(_T("vpxd.patch.complete",
                                             'patching VPXD completed')))
