#!/opt/vmware/bin/python

"""
This script is called during update step.

"""

import argparse
import logging
import os
import sys
import subprocess
import shlex
import shutil
import datetime
import time
import re

try:
   # WIN: C:\Program Files\VMware\vCenter Server\python-modules
   # LIN: /usr/lib/vmware/site-packages
   sys.path.append(os.environ['VMWARE_PYTHON_PATH'])
except KeyError:
   print 'Error: %s is not set!' % 'VMWARE_PYTHON_PATH'
   sys.exit(1)

from cis.tools import processSvcDeps
from cis.svcscfg import loadServicesFile
from cis.utils import  run_command, setupLogging, get_deployment_nodetype
from cis.defaults import get_certool, get_openssl
######################################################################

# supported actions
ACTIONS = [ 'update', 'rollback' ]
AT_UPDATE, AT_ROLLBACK = range(2)

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


def isPythonExecutable(filename):
   if filename.endswith('.py'):
      return True
   else :
      try:
         with open(filename, "r") as fopen:
            firstLine = fopen.readline().rstrip()
            return (firstLine.startswith('#!/usr/bin/python') or
                    firstLine.startswith('#!/opt/vmware/bin/python'))
      except IOError:
         return False


class VmwServices(object):
   '''Handles VMware Service start/stop'''

   def __init__(self):
      self._svcCtlPath = os.path.join(os.environ['VMWARE_CIS_HOME'],
                                      'bin', 'service-control.bat')

   def startAll(self):
      '''Start all services

      @return  True/False
      '''
      logging.info('VmwServices.start: starting all services ...')

      t_start = time.time()
      rc, stdout, stderr = run_command([self._svcCtlPath, '--start', '--all'])
      t_end = time.time()

      logging.info("VmwServices.start: details:\n%s" % stdout)
      logging.info('VmwServices.start: elapsed time: %s',
                   datetime.timedelta(seconds = (t_end - t_start)))

      if rc != 0:
         logging.error("Failed to start all services. RC=%d. Details:\n%s" % (rc, stderr))
         sys.stderr.write("Failed to start all services\n")
         return False

      return True

   def stopAll(self):
      '''Stop all services

      @return  True/False
      '''
      logging.info('VmwServices.stop: stopping all services ...')

      t_start = time.time()
      rc, stdout, stderr = run_command([self._svcCtlPath, '--stop', '--all'])
      t_end = time.time()

      logging.info("VmwServices.stop: details:\n%s" % stdout)
      logging.info('VmwServices.stop: elapsed time: %s',
                   datetime.timedelta(seconds = (t_end - t_start)))

      if rc != 0:
         logging.error("Failed to stop all services. RC=%d. Details:\n%s" % (rc, stderr))
         sys.stderr.write("Failed to stop all services.\n")
         return False

      return True


class FailedSubProcess():
   '''
   nop class for bad executables
   '''
   def __init__(self, retcode=1):
      self.returncode = retcode
      return

   def poll(self):
      return

   def wait(self):
      return


class UpgradeBootScript():
   '''Handles execution of upgradeboot script'''

   def __init__(self, script, logDir):
      '''Create an UpgradeBootScript object

      @param script    full path to script
      @param logDir    log location
      '''
      self._proc = None
      self._script = script
      self._scriptName = os.path.splitext(os.path.basename(script))[0]
      self._logName = os.path.join(logDir, os.path.basename(script))
      self._openSSLTmpFile = os.path.join(logDir, self._scriptName + 'sslTmp.txt')
      self._stdoutName = '%s_%d_stdout.log' % (self._logName, os.getpid())
      self._outFile = None
      self._stderrName = '%s_%d_stderr.log' % (self._logName, os.getpid())
      self._errFile = None
      self._isDone = False
      self._skipped = False

   def run(self, arg):
      '''Executes the upgradeboot script

      @param arg  array of additional arguments
      '''
      self._outFile = open(self._stdoutName, 'w')
      self._errFile = open(self._stderrName, 'w')
      self._isDone = False

      '''
        The shlex.split() function has a bug and does not handle
        unicode strings correctly, it just returns garbage.
        str() is added to convert to ascii
      '''
      if sys.executable != None and isPythonExecutable(self._script) :
         args = shlex.split(str('"%s" "%s"' %(sys.executable, self._script)))
      else :
         args = shlex.split(str('"%s"' % self._script))
      if arg:
         args.extend(arg)
      logging.info('Running script: %s', args)
      try :
         # Popen on windows requires ASCII characters
         env = {}
         for k in os.environ:
            env[k] = str(os.environ[k])
         env['RANDFILE'] = str(self._openSSLTmpFile)
         env['PYTHONPATH'] = str(os.environ['VMWARE_PYTHON_PATH'])
         self._proc = subprocess.Popen(args,
                                       stdout=self._outFile,
                                       stderr=self._errFile,
                                       env=env)
      except OSError, e:
         logging.critical('Failed to run %s: %s', args, str(e))
         self._proc = FailedSubProcess()

   def skip(self):
      self._skipped = True

   def isDone(self):
      self._proc.poll()
      if self._proc.returncode != None:
         self._isDone = True
      return self._isDone

   def waitForDone(self):
      self._proc.wait()
      self._isDone = True

   def succeeded(self):
      return self._proc.returncode == 0

   def status(self):
      if self._skipped:
         return 'Skipped'
      elif not self._isDone:
         return 'Running'
      elif self.succeeded():
         return 'Finished'
      else:
         return 'Failed'

   def cleanUp(self):
      # close file handle
      if self._outFile:
         self._outFile.close()
         self._outFile = None
      if self._errFile:
         self._errFile.close()
         self._errFile = None
      # delete stdout/stderr file if zero size
      if (os.path.isfile(self._stdoutName) and
          not os.path.getsize(self._stdoutName)):
         logging.info('deleting empty STDOUT file: %s', self._stdoutName)
         os.remove(self._stdoutName)
      if (os.path.isfile(self._stderrName) and
          not os.path.getsize(self._stderrName)):
         logging.info('deleting empty STDERR file: %s', self._stderrName)
         os.remove(self._stderrName)
      return

   def __repr__(self):
      return "[%s] %s" % (self.status(), self._script)


#################### Special Handling ####################

# Component specific update code.

def runSSOUpdate():
   '''Performs SSO update

   @return  True if successful
   '''
   try:
      ssoUpdateScriptPath = os.path.join(os.environ['VMWARE_CIS_HOME'],
                                         'VMware Identity Services',
                                         'vmidentity-updateboot.py')
      if os.path.isfile(ssoUpdateScriptPath):
         updateScriptArgs = '"%s" "%s" --action=update' % \
                         (os.environ['VMWARE_PYTHON_BIN'],
                         ssoUpdateScriptPath)
         proc = subprocess.Popen(updateScriptArgs)
         proc.wait()
   except Exception as ex:
      logging.critical("Failed to run sso updateboot. Details %s" % str(ex))
      sys.stderr.write("Failed to run sso updateboot post update.\n")
      return

   return True

def runPSCUpdate():
   '''Performs PSC Client update

   @return  True if successful
   '''
   try:
      pscUpdateScriptPath = os.path.join(os.environ['VMWARE_CIS_HOME'],
                                         'vmware-psc-client', 'scripts',
                                         'psc_updateboot.py')
      if os.path.isfile(pscUpdateScriptPath):
         updateScriptArgs = '"%s" "%s" --action=firstboot' % \
                         (os.environ['VMWARE_PYTHON_BIN'],
                         pscUpdateScriptPath)
         proc = subprocess.Popen(updateScriptArgs)
         proc.wait()
   except Exception as ex:
      logging.critical("Failed to run psc updateboot. Details %s" % str(ex))
      sys.stderr.write("Failed to run psc updateboot post update.\n")
      return

   return True

def runH5ClientUpdate():
   '''Call update script for vsphere-ui (h5-client) service

   @return  True if successful
   '''
   try:
      h5clientUpdateScript = os.path.join(os.environ['VMWARE_CIS_HOME'],
                                          'vsphere-ui', 'vsphere_ui_update_win.py')
      if os.path.isfile(h5clientUpdateScript):
         updateScriptCmd = os.environ['VMWARE_PYTHON_BIN'], h5clientUpdateScript
         proc = subprocess.Popen(updateScriptCmd)
         proc.wait()

   except Exception as ex:
      logging.critical("Failed to run h5-client update script. Details %s" % str(ex))
      sys.stderr.write("Failed to run h5-client update script.\n")
      return

   return True

def runWebClientUpdate():
   '''Call updateboot script for vsphere-client (NGC) service

   @return  True if successful
   '''
   try:
      ngcUpdateScriptPath = os.path.join(os.environ['VMWARE_CIS_HOME'],
                                          'firstboot', 'ngc_updateboot.py')
      if os.path.isfile(ngcUpdateScriptPath):
         updateScriptCmd = os.environ['VMWARE_PYTHON_BIN'], ngcUpdateScriptPath
         proc = subprocess.Popen(updateScriptCmd)
         proc.wait()

   except Exception as ex:
      logging.critical("Failed to run Web Client updateboot script. Details %s" % str(ex))
      sys.stderr.write("Failed to run Web Client updateboot script.\n")
      return

   return True
# XXX: add additional component specific update logic here here, and
#      call them from runCustomUpdateCode

CLS_CONF_FILE = "cls.conf"
CL_TS_CONFIG_FILE = "ts-config.properties"
CL_REVERSE_PROXY_PROPERTIES = """
transfer.httpClient.http.proxyHost={no-proxy}
transfer.httpClient.http.proxyPort=0
transfer.httpClient.https.proxyHost={no-proxy}
transfer.httpClient.https.proxyPort=0
"""


def runContentLibraryUpdate():
    '''Performs update for content-library service.

    @return True if successful
    '''
    # Remove Pivotal tc Server directory
    cls_server_dir = os.path.join(os.environ['VMWARE_CIS_HOME'],
                                  "content-library", 'server')
    logging.info("Removing: %s", cls_server_dir)
    if os.path.exists(cls_server_dir):
        shutil.rmtree(cls_server_dir, True)
        logging.info("Removed: %s", cls_server_dir)

    # VMWARE_CFG_DIR = C:\ProgramData\VMware\vCenterServer\cfg
    # config file location = C:\ProgramData\VMware\vCenterServer\cfg\content-library\config
    ts_config_file_path = os.path.join(os.environ['VMWARE_CFG_DIR'],
                                       "content-library", 'config', CL_TS_CONFIG_FILE)

    logging.info("ts_config_file_path = %s" % ts_config_file_path);
    # Check if the transfer service properties file exists (it will not exist on a PSC node in
    # an MxN deployment), and if it has the proxy host related properties.
    if (os.path.exists(ts_config_file_path) and
              'transfer.httpClient.http.proxyHost' not in open(ts_config_file_path).read()):
        # Additional properties needed for transfer service are missing, add these
        logging.info("Adding CLS/Transfer Service proxy properties")
        with open(ts_config_file_path, "a") as ts_config_file_fp:
            ts_config_file_fp.write(CL_REVERSE_PROXY_PROPERTIES)

    # Patch cls.conf to force HTTPS
    cls_conf_file_path = os.path.join(os.environ['VMWARE_CFG_DIR'], 'vmware-rhttpproxy', 'endpoints.conf.d', CLS_CONF_FILE)
    if (os.path.exists(cls_conf_file_path)):
        logging.info("Patching cls.conf file")
        f = open(cls_conf_file_path, "r")
        lines = f.readlines()
        f.close()

        for index, line in enumerate(lines):
            lines[index] = re.sub(r'allow\s+allow', 'redirect allow', line)

        f = open(cls_conf_file_path, "w")
        f.writelines(lines)
        f.close()

    return True

def runCustomOpensslUpdate():
   '''This is needed to handle the files copied manually on vpxd_firstboot.py '''
   # The list of OpenSSL dlls in format of (src, dest) as generated on firstboot script
   # during upgrade will be replaced with the new versions
   OPENSSL_HOME = os.path.dirname(get_openssl())
   CERTOOL_HOME = os.path.dirname(get_certool())
   HOME_DIR = os.path.join(os.environ['VMWARE_CIS_HOME'],'vpxd') + os.sep

   dll_list = [
       (os.path.join(OPENSSL_HOME, "libeay32.dll"), os.path.join(HOME_DIR, "libeay32.dll")),
       (os.path.join(OPENSSL_HOME, "ssleay32.dll"), os.path.join(HOME_DIR, "ssleay32.dll")),
       (os.path.join(CERTOOL_HOME, "libvmcaclient.dll"), os.path.join(HOME_DIR, "libvmcaclient.dll")),
       (os.path.join(OPENSSL_HOME, "libeay32.dll"), os.path.join(HOME_DIR, "dbupgrade", "libeay32.dll")),
       (os.path.join(OPENSSL_HOME, "ssleay32.dll"), os.path.join(HOME_DIR, "dbupgrade", "ssleay32.dll"))
   ]

   for (src, dest) in dll_list:
       if os.path.isfile(src) and os.path.isfile(dest):
           shutil.copyfile(src, dest)

   return True

def runCustomUpdateCode():
   '''Runs the special code inherited from update

   @return  True if successful
   '''
   # Copy tcserver war files for sso
   logging.info('runCustomUpdateCode: calling runSSOUpdate() ...')
   if not runSSOUpdate():
      return

   # Run h5-client specific upgrade code
   logging.info('runCustomUpdateCode: calling runH5ClientUpdate() ...')
   runH5ClientUpdate()

   # Run vsphere-client specific upgrade code
   logging.info('runCustomUpdateCode: calling runWebClientUpdate() ...')
   runWebClientUpdate()

   # Run psc-client specific upgrade code
   logging.info('runCustomUpdateCode: calling runPSCUpdate() ...')
   runPSCUpdate()

   # add additional custom upgrade code hook here

   logging.info('runCustomUpdateCode: calling runContentLibraryUpdate() ...')
   runContentLibraryUpdate()

   logging.info('runCustomUpdateCode: calling runCustomOpensslUpdate() ...')
   runCustomOpensslUpdate()

   return True


#################### Update ####################


def updateSvcDeps():
   '''Re-inject svc dependencies after update.

   @return  True if successful
   '''
   svcPath = os.path.join(os.environ['VMWARE_CIS_HOME'], 'visl-integration',
                          'config', 'services.json')
   try:
      # Re-inject svc dependencies after update.
      logging.info('updateSvcDeps: calling processSvcDeps() ...')
      processSvcDeps(loadServicesFile(svcPath))
   except Exception as ex:
      logging.critical("Failed to inject svc dependency post update. Details %s" % str(ex))
      sys.stderr.write("Failed to inject svc dependency post update.\n")
      return

   return True


def runUpdate(args, svcsObj):
   '''Performs an update.

   This is called after the MSI files are updated.

   @param args     command line options
   @param svcsObj  VmwServices object
   @return         return code for OS
   '''
   # runs some custom component update code.
   runCustomUpdateCode()

   # update service dependencies
   if not updateSvcDeps():
      return

   # check the deployment type
   if get_deployment_nodetype() in ['management', 'embedded']:
      sys.path.append(os.path.join(os.environ['VMWARE_CIS_HOME'], 'vmware-sps', 'sps', 'scripts'))
      from sps_update import updateSpsProperties
      # upadate sps.properties
      if not updateSpsProperties():
        return 1

   # start all the services
   if not svcsObj.startAll():
      return 1

   logging.info('runUpdate: success')
   return 0


#################### Rollback ####################


def runRollback(args, svcsObj):
   '''Performs the rollback for Update

   This simply stops all services. No actual rollback will be
   performed. We simply try to return the system to a sane state as
   best as we can.

   @param args     command line options
   @param svcsObj  VmwServices object
   @return         return code for OS
   '''
   if not svcsObj.stopAll():
      return 1

   logging.info('runRollback: done')
   return 0


#################### Main ####################


def parseArgs():
   '''Parse command line arguments

   @return  namespace from ArgumentParser.parse_args()
   '''
   # setup the parser
   parser = argparse.ArgumentParser()
   parser.add_argument('--action', choices=ACTIONS, default=None,
                       help='Action to perform')
   parser.add_argument("--logDirectory", dest='logDir', default=None,
                       help="Override default log directory")

   # parse command line options
   args = parser.parse_args()
   # print args

   # any additional validation
   if not args.action:
      parser.print_help()
      return

   return args


def mainWin(args):
   '''Main function for Windows

   @param args  command line options
   @return      return code for OS
   '''
   # handles services start/stop
   servicesObj = VmwServices()

   # perform the requested actions
   rc = 0
   if args.action == ACTIONS[AT_UPDATE]:
      logging.info('mainWin: performing %s action ...', args.action)
      rc = runUpdate(args, servicesObj)
   elif args.action == ACTIONS[AT_ROLLBACK]:
      logging.info('mainWin: performing %s action ...', args.action)
      rc = runRollback(args, servicesObj)
   else:
      logging.warning("mainWin: ignoring unknown action: %s" % args.action)

   logging.info('mainWin: exiting')
   return rc


def mainLin(args):
   '''Main function for Linux

   @param args  command line options
   @return      return code for OS
   '''
   # XXX We should consolidate ciswin and cloudvm update script into one.
   # if os.name == 'posix':
   #    sys.exit(0)
   return 0


def main():
   '''Main entry function

   @return  return code for OS
   '''
   args = parseArgs()
   if not args:
      return 1  # cmd line error

   # setup logging
   if not args.logDir:
      # WIN: C:\ProgramData\VMware\vCenterServer\logs
      # LIN: /var/log
      args.logDir = os.path.join(os.environ['VMWARE_LOG_DIR'], 'firstboot')
   setupLogging("updateboot", logMechanism='file', logDir=args.logDir)
   # start market so we can clearily see one run from another
   logging.info('********** Start %s **********', sys.argv[1:])
   if os.name == 'posix':
      rc = mainLin(args)
   else:
      rc = mainWin(args)

   logging.info('final return code: %d', rc)
   return rc


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

# starts script execution
if __name__ == '__main__':
   sys.exit(main())
