#!/opt/vmware/bin/python

import argparse
import json
import os
import sys
import glob
import time
import logging
import platform
import subprocess
import shlex
import shutil
import random
import traceback
import re
sys.path.append(os.environ['VMWARE_PYTHON_PATH'])

from cis.svcscfg import genDepTracker, isInstalledService, loadServicesFile
from cis.tools import (processSvcDeps, getSvcName, svcPlatformName,
                       initSvcsDepLogger, get_install_parameter)
from appliance.installparamutil import InstallParameters
from cis.statusAggregator import StatusAggregator
from cis.progressReporter import ProgressReporter
from cis.defaults import (
   getFBStatusReportInternalFile, get_cis_rereg_dir, get_cis_tmp_dir,
   get_cis_config_dir, get_cloudvm_ram_size_bin, get_cis_log_dir,
   get_locale_dir, get_cis_data_dir, is_vmon_enabled,
   get_vmon_startup_profile_file, init_feature_switch
   )
from cis.utils import (
   create_dir, setupLogging, get_deployment_nodetype,
   get_db_type, run_command, invoke_command, enable_svc_cgroup_accounting)
from cis.exceptions import composeFBIntErr, InvokeCommandException, format_exc
from cis.baseCISException import BaseInstallException
from cis.l10n import localizedString, configure
from cis.msgL10n import MessageMetadata as _T
from cis.componentStatus import ComponentsExecutionStatusInfo
from cis.componentStatus import ProgressData
from cis.componentStatus import ErrorInfo
from cis.filelock import FileLock
from cis.json_utils import JsonSerializer
from cis.defaultStatusFunctor import SimpleComponentStatusReader
from cis.defaultStatusFunctor import SimpleComponentQuestionHandler
from cis.execution_settings import *
from cis.security import get_service_user
from cis.exceptions import InstallParameterException

osIsLinux = platform.system() == 'Linux'
osIsWindows = platform.system() == 'Windows'
osIsSles12 = osIsLinux and platform.linux_distribution()[1].startswith('12')

try:
   osIsPhoton = os.path.isfile('/etc/photon-release')
except AttributeError:
   osIsPhoton = False

if osIsLinux and osIsSles12:
   sys.path.append('/usr/lib64/python2.7/site-packages/')
watchdogSubDirName = 'iiad'
watchdogMaintenanceModeName = 'iiad.maintenance-mode'

def isLinux():
   return osIsLinux

def isWindows():
   return osIsWindows

def getWatchdogSentinelFile():
   return os.path.join(get_cis_data_dir(), watchdogSubDirName,
                       watchdogMaintenanceModeName)

def removeWatchdogSentinelFile():
   name = getWatchdogSentinelFile()
   if os.path.isfile(name):
      os.remove(name)

actions = dict(firstboot='First boot', prefreeze='Pre-freeze',
               postthaw='Post-thaw')
actionNames = ', '.join(actions.keys())

fbSubActions = dict(firstboot='First boot', start='Start', stop='Stop',
                    uninstall='Uninstall')
fbSubActionNames = ', '.join(fbSubActions.keys())

parser = argparse.ArgumentParser()
parser.add_argument('--action', help='Action (%s)' % actionNames)
parser.add_argument('--subaction', help='Subaction for firstboot action (%s)' %
                    fbSubActionNames)
parser.add_argument('--stress', action='store_true',
                    help='Run firstboot with stress options on')
parser.add_argument('--maxDelay', default=4, type=int,
                    help='Maximum delay (secs) before launching firstboot scripts')
parser.add_argument('--logDirectory', default=None,
                    help='Override default log directory')
parser.add_argument('--statusFile', default=None,
                    help='Override default status file (must be absolute path)')
parser.add_argument('--fbWhiteList', default=None,
                    help='Comma-separated list of firstboot scriptnames to execute.')
parser.add_argument('--interactive', default=False, action='store_true',
                    help='Run in interactive mode')
parsedArgs = parser.parse_args()
stressOption = parsedArgs.stress
maxDelay = parsedArgs.maxDelay
logDir = parsedArgs.logDirectory
statusFile = parsedArgs.statusFile
fbWhiteList = parsedArgs.fbWhiteList.split(',') if parsedArgs.fbWhiteList else None
interactive = parsedArgs.interactive

if parsedArgs.action and parsedArgs.action not in actions:
    print 'Unknown action ("%s")' % parsedArgs.action
    parser.print_help()
    sys.exit(1)
elif parsedArgs.action:
    action = parsedArgs.action
else:
    action = 'firstboot'

subaction = parsedArgs.subaction
if action != 'firstboot' and subaction:
    print 'Subactions are only supported for firstboot action'
    parser.print_help()
    sys.exit(1)
elif action == 'firstboot' and subaction and subaction not in fbSubActions:
    print 'Unknown subaction ("%s")' % parsedArgs.subaction
    parser.print_help()
    sys.exit(1)
elif action == 'firstboot' and not subaction:
    subaction = 'firstboot'

# Create directories necessary for firstboots.
create_dir(get_cis_tmp_dir())

if logDir is None:
    logDir = os.path.join(os.environ['VMWARE_LOG_DIR'], action)

# Install param utility
installParams = InstallParameters()

#Configure locale specific logging file
configure(get_locale_dir(),
          installParams.getParameter('clientlocale', 'en')[1])

if not statusFile:
   statusFile = os.path.join(logDir, 'fbInstall.json')
else:
   assert(os.path.isabs(statusFile))

inFile = getFBStatusReportInternalFile()
try:
   os.remove(inFile)
except Exception:
   pass

# Ensure that files created during firstboot are readable by default
os.umask(022)

if isLinux():
   firstBootPathPrefix = '/usr/lib/'
   clearCommand = '/usr/bin/clear'
else:
   firstBootPathPrefix = os.path.join(os.environ['VMWARE_CIS_HOME'], action)
   clearCommand = None
   import win32api
   icacls_bin_path = os.path.join(win32api.GetSystemDirectory(), 'icacls.exe')

# Set up file logging using CIS common lib util
setupLogging('%sInfrastructure' % action, logMechanism='file', logDir=logDir)
# Initialize service dependency injection logger.
initSvcsDepLogger()

if isLinux():
    svcPath = '/usr/lib/vmware-visl-integration/config/services.json'
else:
    svcPath = os.path.join(os.environ['VMWARE_CIS_HOME'],
                           'visl-integration', 'config',
                           'services.json')

svcConfig = {'svcs' : loadServicesFile(svcPath) }

# Persist information require post firstboot
if action == 'firstboot' and subaction == 'firstboot':
   (source, svcConfig['deploymentType']) = \
      installParams.getParameter('deployment.node.type', 'embedded')

   if isWindows():
      # For ciswin write deployment node type install param value to a config
      # file so as to avoid reading install-params after deployment.
      # Note:- For cloudvm we create this file before firstboot
      # during common installs.
      deloymentTypeCfgPath = os.path.join(get_cis_config_dir(),
                                    'deployment.node.type')
      with open(deloymentTypeCfgPath, 'w') as fp:
         fp.write(svcConfig['deploymentType'])

   # If vSphere is configured with external DB, then embedded DB is not
   # installed. Hence persists this information to not start/stop
   # embedded DB in this case.
   # Note 'dbType' key in svcConfig dict must be same as in service-control
   (source, svcConfig['dbType']) = installParams.getParameter('db.type', 'embedded')
   # XXX: Should we assert here
   assert svcConfig['dbType'], 'DB Type must be set'

   # Note for cloudvm we create  db.type config file during common install.
   # If you change dbTypeCfgPath, please update cis.util.get_db_type()
   if isWindows():
      dbTypeCfgPath = os.path.join(get_cis_config_dir(), 'db.type')
      with open(dbTypeCfgPath, 'w') as fp:
         fp.write(svcConfig['dbType'])

else: # Uninstall
   # Read deployment.node.type from cfg file.
   svcConfig['deploymentType'] = get_deployment_nodetype()
   svcConfig['dbType'] = get_db_type()

# Deptracker currently requires below operation key to be set to decide
# whether to do reverse or same dependency ordering.
# XXX This key should go away once we better abstract Deptracker in svcscfg.py
if action == 'firstboot' and subaction in ['uninstall', 'stop']:
   # Reverse dep ordering.
   svcConfig['operation'] = 'stop'
else:
   svcConfig['operation'] = 'start'

def add_vmon_vaos_dependency():
   VMON_UNIT_FILE = '/usr/lib/systemd/system/vmware-vmon.service'
   TEMP_VMON_UNIT_FILE = os.path.join(os.environ['VMWARE_DATA_DIR'],
                                      'vmware-vmon', 'vmware-vmon.service.tmp')

   if  os.name != 'posix':
      return

   with open(VMON_UNIT_FILE, 'r') as f:
      text = f.read()

   lines = text.splitlines(True)
   depsPresent = 'After=' in text

   with open(TEMP_VMON_UNIT_FILE, 'w') as f:
      for line in lines:
         if line.startswith('[Unit]') and not depsPresent:
            f.write(line)
            f.write('After=vaos.service\n')
         elif line.startswith('After='):
            line = line.strip() + ' vaos.service\n'
            f.write(line)
         else:
            f.write(line)

   os.rename(TEMP_VMON_UNIT_FILE, VMON_UNIT_FILE)

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

def lockdown_path_win(file_path, account_ids, admin_access=True):
    '''
    Given a file_path explicitly gives full access to only the given account
    ids (account name or sid). By default system and administrators group also
    are given full control to the file_path.
    '''
    # Return if the file path does not exist
    if not os.path.exists(file_path):
        return

    cmd_list = [icacls_bin_path, file_path]
    if admin_access:
        admin_grp_sid = '*S-1-5-32-544'
        system_sid = '*S-1-5-18'
        cmd_list.extend(['/grant:r', '%s:(OI)(CI)(F)' % system_sid])
        cmd_list.extend(['/grant:r', '%s:(OI)(CI)(F)' % admin_grp_sid])

    for account_id in account_ids:
        if is_vmon_enabled():
            # Read account id from lockdownfiles.json
            cmd_list.extend(['/grant:r', '%s:(OI)(CI)(F)' % account_id])
        else:
            # Parse services.json and get the service name
            service_name = getSvcName(account_id, svcConfig['svcs'])
            if service_name:
                service_user = get_service_user(service_name)
                cmd_list.extend(['/grant:r', '%s:(OI)(CI)(F)' % service_user])
            else:
                logging.warn('Service corresponding to account id %s does not \
                              exist' % account_id)

    cmd_list.extend(['/inheritance:r', '/L', '/Q'])
    invoke_command(cmd_list)
    if os.path.isdir(file_path):
        invoke_command([icacls_bin_path, os.path.join(file_path, '*'), '/reset',
                        '/T', '/L', '/Q'])


def lockDownCISDir():
   '''
   Locks down parent of cis config directory. Only System and Administrators
   will get access to this directory. All subdirs are reset to inherit
   permissions from this parent directory.
   Based on PR 1335392, Update #20.
   '''
   if not isWindows():
      return

   cisDirPath = os.path.dirname(get_cis_config_dir())
   try:
      lockdown_path_win(cisDirPath, [], admin_access=True)

      adminGrpSid = '*S-1-5-32-544'
      invoke_command([icacls_bin_path, cisDirPath, '/setowner', adminGrpSid,
                      '/T', '/L', '/Q'])
   except InvokeCommandException as ex:
      err = _T('install.ciscommon.fbrun.cislockdown',
               'Failed to set permissions on %s')
      err_lmsg = localizedString(err, [cisDirPath])
      res = _T('install.ciscommon.fbrun.cislockdown.res',
               'Make sure that %s does not already exist and re-run '
               'the installer.')
      res_lmsg = localizedString(res, [cisDirPath])
      ex.appendErrorStack(err_lmsg)
      ex.getErrorInfo().resolution = res_lmsg
      raise


def lockdown_sensitive_files():
    '''
    Post firstboots, locks down sensitive file/dirs listed in
    lockdownfiles.json file. This is done only for Windows.
    '''
    if not isWindows():
        return

    lockdown_metafile = os.path.join(os.environ['VMWARE_CIS_HOME'],
                                     'visl-integration', 'config',
                                     'lockdownfiles.json')
    try:
        with open(lockdown_metafile, 'r') as fp:
            lockdown_list = json.load(fp)
            for lockdown_node in lockdown_list:
                lockdown_path_win(os.path.expandvars(lockdown_node['path']),
                                  lockdown_node['accounts'], admin_access=True)
    except InvokeCommandException as ex:
        err = _T('install.ciscommon.fbrun.sensitivelockdown',
                 'Failed to lockdown sensitive files')
        ex.appendErrorStack(localizedString(err))
        raise


# helper function to parse the component stderr output and extract the
# exception information.
def parseErrorInfo(errfile):
   with open(errfile, 'r') as fp:
      errlog = fp.read()

   errPattern = re.compile(r'({.*?"resolution":.*?"problemId":.*?})',
                           re.S | re.M)
   errMatch = re.search(errPattern, errlog)
   if not errMatch:
      logging.info('No localized error detail found in %s, '
                   'assuming internal error' % errfile)
      return None

   errInfo = None

   try:
      errDict = json.loads(errMatch.group(1))
      # convert keys to ascii due to http://bugs.python.org/issue2646
      # this is fixed in 2.7
      errInfoArg =  dict(map(lambda (k, v): (str(k), v), errDict.items()))
      errInfo = ErrorInfo(**errInfoArg)
   except:
      logging.error('Failed to parse %s, assuming internal error' % errfile)

   return errInfo

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

   def poll(self):
      return

   def wait(self):
      return

class FirstBootScript():
   def __init__(self, script, compName):
      self._proc = None
      script = script
      self._script = script
      self._compName = compName
      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._errFileName = '%s_%d_stderr.log' % (self._logName, os.getpid())
      self._isDone = False
      self._skipped = False

   def run(self, arg):
      self._outFile = open('%s_%d_stdout.log' % (self._logName, os.getpid()), 'w')
      self._errFile = open(self._errFileName, 'w')
      close_fds = True if isLinux() else False
      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.append('--action')
          args.append(arg)
          if self._compName != 'vmafdd':
             args.append('--compkey')
             args.append(self._compName)
             args.append('--errlog')
             args.append(self._errFileName)
      logging.info('Running %s script: %s' % (action, args))
      try :
         import tempfile
         # Popen on windows requires ASCII characters
         env = {}
         for k in os.environ:
            env[k] = str(os.environ[k])
         env['RANDFILE'] = str(self._openSSLTmpFile)
         if osIsSles12:
            current_python_path = ':'.join(sys.path)
            vmware_python_path = os.environ.get('VMWARE_PYTHON_PATH','')
            env['PYTHONPATH'] = ':'.join([current_python_path,
                                          vmware_python_path])
         else:
             env['PYTHONPATH'] = str(os.environ['VMWARE_PYTHON_PATH'])
         self._proc = subprocess.Popen(args,
                                stdout=self._outFile,
                                stderr=self._errFile,
                                env=env, close_fds=close_fds)
      except OSError, ex:
         logging.critical('Failed to run %s' % args)
         self._proc = FailedSubProcess()

   def skip(self):
      self._skipped = True

   def isServiceEnabled(self):
      if fbWhiteList and self._scriptName not in fbWhiteList:
         logging.info('Firstboot script %s not in fb whitelist.'
                      % self._scriptName)
         return False

      try:
         (source,value) = installParams.getParameter(
                            '%s.enabled' % self._scriptName, 'true')
         enabled = str(value).lower()
         if enabled in ('true', 'false'):
            logging.info('Firstboot script %s: enabled=%s' % (
                          self._scriptName, enabled))
            return (enabled == 'true')
         else:
            logging.error('install-parameter returned invalid data when '
                          'querying for service enabled flag -- "%r", '
                          'source -- "%r"', value, source)
            return True
      except OSError,  ValueError:
         logging.warn('Failed to run %s' %args)
      return 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):
      # nothing to cleanup
      return

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

def touch(fileName, destDir=logDir):
   '''
   Create a file
   '''
   try:
      f = open(os.path.join(destDir, fileName), mode='w')
      f.close()
   except:
      # Don't care about errors
      pass

def isFirstbootStressEnabled():
   if stressOption:
      return True
   try:
      (source,value) = installParams.getParameter('firstboot.stress', 'false')
      enabled = str(value).lower()
      if enabled in ('true', 'false'):
         logging.info('Install-parameter firstboot.stress=%s' % (enabled))
         return (enabled == 'true')
      else:
         return False
   except OSError,  ValueError:
      logging.warn('Failed to read firstboot.stress install-parameter')
   return False

def isExternalDBColocated():
   '''
   On ciswin, returns True if external DB is installed on the same
   machine. For appliance return False.
   '''
   if isWindows():
      (source, db_dsn) = installParams.getParameter('db.dsn')
      from cis import dsnanalyzer
      from appliance.hostname import IPResolver
      try:
         dbHost = dsnanalyzer.GetDsnAnalyzerObj(db_dsn).GetDbParams()['host']
         logging.info('Hostname for external DB: %s', dbHost)
         if 'localhost' in dbHost.lower() or \
            IPResolver.getIPResolver().isValidNetworkID(dbHost):
            return True
      except Exception as e:
         logging.warn('Unable to determine if external DB is colocated.'
             ' Exception: %s', e)
         return True

   return False

class FBStatus(object):
    '''
    This class manages status of firstboot process and generates status files.

    The status file (VMWARE_LOG_DIR/<action>/<action>Status.json) looks
    something like this.

    {
      "totalSteps": 25,
      "stepsStarted": 25,
      "stepsCompleted": 11,
      "finalStatus": "in-progress"
    }

    If finalStatus is "failed", there will also be a failedSteps key,
    naming the services (space separated) that failed.

    There is no locking on this file.
    '''
    def __init__(self, totalSteps):
        self._totalSteps = totalSteps
        self._stepsStarted = 0
        self._stepsCompleted = 0
        self._finalStatus = 'in-progress'
        self._failedSteps = []
        self._runningSteps = set()
        self._completedSteps = []
        self._statusFile = os.path.join(logDir, '%sStatus.json' % action)
        self._tmpFile = os.path.join(logDir, '%sStatus.tmp' % action)
        self._staleFile = os.path.join(logDir, '%sStatus.old' % action)
        self.genFile()

    def genFile(self):
        status = { 'totalSteps': self._totalSteps,
                   'stepsStarted': self._stepsStarted,
                   'stepsCompleted': self._stepsCompleted,
                   'finalStatus': self._finalStatus
                   }
        renameSuccessful = False

        if self._runningSteps:
            status['runningSteps'] = ','.join(self._runningSteps)
        if self._failedSteps:
            status['failedSteps'] = ','.join(self._failedSteps)
        if self._completedSteps:
            status['stepsCompletedList'] = ','.join(self._completedSteps)

        with open(self._tmpFile, 'w') as fp:
            json.dump(status, fp, indent=4)
        try:
            os.rename(self._tmpFile, self._statusFile)
            renameSuccessful = True
        except:
            pass
        if not renameSuccessful:
            try:
                os.remove(self._staleFile)
            except:
                pass
            try:
                os.rename(self._statusFile, self._staleFile)
            except:
                pass
            try:
                os.remove(self._statusFile)
            except:
                pass
            try:
                os.rename(self._tmpFile, self._statusFile)
            except:
                logging.warn('Failed to rename new status file to current (ignored)')

    def startStep(self, service, compName):
        # temporary kludge to report component progress as individual
        # component may not have been updated to use the status
        # framework
        compSetting = CoreExecutionSettings()
        compSetting.componentName = compName
        self.catchAllReporter = ProgressReporter(statusFile = inFile,
                                                 setting = compSetting)
        svcOperation = svcConfig['operation']
        if svcOperation == 'start':
           comp_starting = _T('install.ciscommon.component.starting',
                              'Starting %s...')
        elif svcOperation == 'stop':
           comp_starting = _T('install.ciscommon.component.stopping',
                              'Stopping %s...')
        else:
           raise ValueError('svsConfig[operation]: %s' % svcOperation)

        try:
           displayName = svcConfig['svcs'][compName]['serviceDisplayName']
        except:
           displayName = compName
        # XXX compname should be localizable
        start_msg = localizedString(comp_starting, displayName)
        self.catchAllReporter.updateProgress(0, start_msg)

        self._stepsStarted += 1
        self._runningSteps.add(service)
        self.genFile()

    def completeStep(self, service):
        self._stepsCompleted += 1
        self._runningSteps.remove(service)
        self._completedSteps.append(service)
        self.genFile()

    def _setFinalStatus(self, status):
        self._finalStatus = status
        self.genFile()

    def succeed(self):
        self._setFinalStatus('success')

    def fail(self, services=None):
        if services:
            self._failedSteps = services;
        self._setFinalStatus('failure')

    def intermediateSuccess(self):
       '''
       Update the intermediate success.
       '''
       self.catchAllReporter.success()

    def intermediateFail(self, service, errFileName):
        '''
        Update the failed services with setting the finalStatus.  This
        is useful if one wants to bail out before learning all the
        services that have failed.  When finalStatus changes to
        'success' or 'failure' we can be certain the firstboot process
        is fully completed.
        '''
        self._failedSteps.append(service)
        self.genFile()
        # if the component execution failed but hasn't mark its
        # status so, here it will just get all stderr from component
        # and present it to end user unlocalized -- this should not
        # happen in release
        if not SimpleComponentStatusReader(inFile)().error:
           ex = parseErrorInfo(errFileName)
           if not ex:
              outerr = 'see %s' % errFileName
              ex = composeFBIntErr(outerr)
           self.catchAllReporter.failure(ex)

def getFirstbootScripts():
   '''
   Looks up firstboot script paths in service config file, checks for if they
   are installed on the system and whether they are enabled.

   Returns a list of corresponding FirstBootScript objects in the order
   dictated by the dependencies encoded in service config file.
   '''
   depTracker = genDepTracker(svcConfig)

   outFBScripts = []
   readyList = depTracker.ready()
   while len(readyList):
      for next in sorted(readyList):
         if isInstalledService(svcConfig, next) and svcConfig['svcs'][next]['enabled']:
            # next entry is installed/valid service for current deployment type
            # XXX: We should get fbPaths based on given action value.
            fbPaths = svcConfig['svcs'][next][svcPlatformName]['fbScripts']
            for fbPath in fbPaths:
               fbFullPath = os.path.join(firstBootPathPrefix, fbPath)
               # Check if fbFullPath points to a valid file
               if os.path.isfile(fbFullPath):
                  fbScript = FirstBootScript(fbFullPath, next)
                  if fbScript.isServiceEnabled():
                     # fbScript is enabled. Add it to the final fbScript list.
                     outFBScripts.append(fbScript)
         elif action == 'firstboot' and subaction == 'firstboot':
            # Reset ram sizing for not installed services.
            service_name = getSvcName(next, svcConfig['svcs'])
            # If external DB is installed here then do not reset memory reserved
            # for vPostgres. It get consumed by the external DB.
            if service_name and 'vpostgres' in service_name.lower() and \
               not isExternalDBColocated():
               cmd = [get_cloudvm_ram_size_bin(), '-C', '1', service_name]
               (rc, stdout, stderr) = run_command(cmd)
               if rc:
                  logging.error('Unable to clear memory allotment for %s service.'
                      ' Error: %s', service_name, stderr)
               else:
                  logging.info('Cleared memory allotment for %s service', service_name)

         depTracker.rm(next)

      readyList = depTracker.ready()
   if not depTracker.empty():
      msg = ('There appears to be a cycle in the service dependence graph.\n'
             'Following are the services with unsatisfied dependencies.\n')

      for src,targets in depTracker.getRemainingDependencies().iteritems():
         msg += 'Service %s, Unsatisfied dependencies %s\n' % (src, targets)
      logging.error(msg)
      raise ValueError('Invalid svc config file.');

   return outFBScripts

def getActionScripts():
   '''
   Based on the given action, return list of FirstBootScript objects.
   '''
   if action == 'firstboot':
      return getFirstbootScripts()
   else:
      raise NotImplementedError('action %s is not yet implemented.'%action)

def enableSvcsCgroupAccounting():
   '''
   Applicable only to SLES12. By default the cgroup accounting for services
   is disabled. The method enables it.
   '''
   if not (osIsSles12 or osIsPhoton):
      return

   for svc in svcConfig['svcs']:
      if isInstalledService(svcConfig, svc):
         svcName = getSvcName(svc, svcConfig['svcs'])
         if not svcName:
            continue
         enable_svc_cgroup_accounting(svcName)

def logBugCompInfo(serviceName):
   '''
   Adding Bugzilla info for the failed service
   '''
   if isLinux():
      bugComPath = ('/usr/lib/vmware-visl-integration/config/'
                   'components-vsphere2015.json')
   else:
      bugComPath = os.path.join(os.environ['VMWARE_CIS_HOME'],
                                'visl-integration', 'config',
                                'components-vsphere2015.json')

   if not os.path.exists(bugComPath):
      logging.warn('Bug component info file does not exist')
      return
   try:
      with open(bugComPath, 'r') as bugCompFile:
         jsonOutput = json.load(bugCompFile)
         for key, value in jsonOutput.iteritems():
            if key.startswith(serviceName):
               logging.info('%s' % ('*' * 80))
               msg = ('File Bugzilla PR in Product: %s, Category: %s, '
                      'Component: %s against %s'
                      % (value['Product'], value['Category'],
                         value['Component'], serviceName))
               logging.info('%s' % msg)
               logging.info('%s' % ('*' * 80))

   except Exception as e:
      logging.warn('Could not log the bug component info %s'% e)

def serialFBRun(fbScripts, fbStatus, fbInteractive):
   '''
   Executes firstboot scripts in a serial order and updates status.
   The order is dictated by the fbScripts list ordering.
   Note:- If any firstboot fails, we stop execution.

   Returns if firstboots have failed or not.
      True: on Pass
      False: on Fail
   '''
   for fbScript in fbScripts:
      if fbInteractive:
         run = None
         while not run or run[0] not in 'yncq':
            run = raw_input('Run %s (yes/no/continue/quit)? ' % fbScript._script).lower()
         if run[0] == 'n':
            fbScript.skip()
            print fbScript
            continue
         elif run[0] == 'c':
            print 'Running remaining firstboot scripts...'
            fbInteractive = False
         elif run[0] == 'q':
            print 'Exiting firstboot...'
            touch('failed')
            return False
      print fbScript

      fbScript.run(subaction)
      fbStatus.startStep(fbScript._scriptName, fbScript._compName)
      fbScript.waitForDone()
      logging.info('%s is complete' % fbScript)
      fbScript.cleanUp()
      fbStatus.completeStep(fbScript._scriptName)
      if not fbScript.succeeded():
         fbStatus.intermediateFail(fbScript._scriptName, fbScript._errFileName)
         touch('failed')
         logBugCompInfo(fbScript._scriptName)
         return False
      fbStatus.intermediateSuccess()

   return True

aggregator = None
def fbRun(interactive):
   fbScripts = getActionScripts()
   serviceCount = len(fbScripts)
   fbStatus = FBStatus(serviceCount)
   # set up status aggregator to run every 5 seconds
   global aggregator
   aggregator = StatusAggregator(5, statusFile, OverallProgress(fbStatus),
                                 SimpleComponentStatusReader(inFile),
                                 SimpleComponentQuestionHandler(inFile,
                                                                None).setReply)
   aggregator.start()
   logging.info('Starting %d services.' % serviceCount)
   fbstartTime = time.time()
   passed = serialFBRun(fbScripts, fbStatus, interactive)

   duration = time.time() - fbstartTime
   logging.info('Firstboot duration: %d sec' % duration)
   print '\n\nResult:'
   print '--------------------------'

   if not passed:
      print 'Failure'
      touch('failed')
      logging.info('%s is a failure' % actions[action])
      fbStatus.fail()
      return False
   else:
      print 'Success'
      touch('succeeded')
      logging.info('%s is a success' % actions[action])
      finalStatus = 'success'
      fbStatus.succeed()
      processSvcDeps(svcConfig['svcs'])
      # Enable cgroup accounting for services.
      enableSvcsCgroupAccounting()
      return True

logging.info('PID:%s' % (os.getpid()))

class OverallProgress(object):
   '''Aggregate the overall progress of first boots from internal raw
   status
   '''
   def __init__(self, fbStatus):
      '''Initialization
      '''
      self._fbStatus = fbStatus

   def __call__(self, rawStatus):
      '''
      @param rawStatus: the raw status from components
      @type rawStatus: ComponentsExecutionStatusInfo
      '''
      totalProgress = 0
      for k, v in rawStatus.allProgress.iteritems():
         totalProgress += v.percentage

      totalProgress /= self._fbStatus._totalSteps

      overallState = ProgressData.State.RUNNING
      if self._fbStatus._finalStatus == 'failure':
         overallState = ProgressData.State.ERROR
      elif self._fbStatus._finalStatus == 'success':

         overallState = ProgressData.State.SUCCESS
      lastmsg = rawStatus.lastProgressMessage
      return ProgressData(percentage = totalProgress,
                          status = overallState,
                          progress_message=lastmsg)

def fixCISDirLongPath():
   '''
   On Windwos delete folders with very long path that are causing
   "icacls.exe" tool to fail.
   '''
   if not isWindows():
      return

   prefix = '\\\\?\\'

   # Delete C:\ProgramData\VMware\vCenterServer\data\vSphere Web Client\SerenityDB folder
   cisDataDir = get_cis_data_dir()
   webClientDataDir = os.path.join(cisDataDir, 'vSphere Web Client')
   serenityDbDir = os.path.join(webClientDataDir, 'SerenityDB')
   if not os.path.exists(serenityDbDir):
      logging.info('%s folder does not exist' % serenityDbDir)
   else:
      logging.info('About to delete folder: %s' % serenityDbDir)
      # workaround for excessively long paths in windows
      shutil.rmtree(unicode(prefix + serenityDbDir))
      logging.info('Successfully deleted the folder')

   # Delete C:\ProgramData\VMware\vCenterServer\runtime\vsphere-client\server\work folder
   cisDirPath = os.path.dirname(get_cis_config_dir())
   virgoWorkDir = os.path.join(cisDirPath, 'runtime', 'vsphere-client', 'server', 'work')
   if not os.path.exists(virgoWorkDir):
      logging.info('%s folder does not exist' % virgoWorkDir)
   else:
      logging.info('About to delete folder: %s' % virgoWorkDir)
      # workaround for excessively long paths in windows
      shutil.rmtree(unicode(prefix + virgoWorkDir))
      logging.info('Successfully deleted the folder')


def runH5ClientUpdate():
   '''Call the update script of H5-client

   @return  True if successful
   '''
   if not isWindows():
      return
   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

# for the fb infrastructure errors other than from component scripts
getCoreExecutionSettings().componentName = 'fb-infra'

# used to post a infrastructure progress to indicate error comes from
# fb-infra
fbInfraReporter = ProgressReporter(statusFile = inFile)
fbInfraReporter.updateProgress(0)

success = False
try:
   if action == 'firstboot' and subaction == 'firstboot':
      # Fix long path problem on Windows before invoking icacls
      fixCISDirLongPath()
      # Hook for upgrade script for h5-client on windows
      runH5ClientUpdate()
      # Lockdown CIS directory on Windows.
      lockDownCISDir()

   # Running of firstboot scripts happens here.
   success = fbRun(interactive)
   removeWatchdogSentinelFile()

   # Lock sensitive files only during installation. Skip this during
   # uninstallation step.
   if action == 'firstboot' and subaction == 'firstboot':
       lockdown_sensitive_files()

   # If vMon is enabled, after all firstboot finish, we change the default start
   # profile file, so that starting vMon again will start vMon with profile ALL.
   if is_vmon_enabled():
      logging.info('Changing vMon default start profile to ALL')
      with open(get_vmon_startup_profile_file(), 'wb') as vmonDefaultProfileFile:
         vmonDefaultProfileFile.write('ALL')
      add_vmon_vaos_dependency()

except BaseInstallException as ex:
   # if the infra gives good errors
   logging.error('Installation of vCenter Server failed with error:\n%s' % ex)
   fbInfraReporter.failure(ex.getErrorInfo())
except Exception:
   # catch all for all internal errors
   exc_type, exc_value, exc_traceback = sys.exc_info()
   errtb = traceback.format_exception(exc_type, exc_value, exc_traceback)
   logging.error('Installation of vCenter Server failed with error:\n%s' % errtb)
   fbInfraReporter.failure(composeFBIntErr(format_exc()))
finally:
   # Do cleanup.
   # stop the aggregator, which will do one last aggregation before return
   if aggregator:
      aggregator.stop()

exitCode = 0 if success else 1
sys.exit(exitCode)
