#!/opt/vmware/bin/python

import logging
import os
import sys
import atexit
import signal
import argparse
sys.path.append(os.environ['VMWARE_PYTHON_PATH'])

from cis.defaults import (get_cis_log_dir, get_cis_data_dir,
                          is_vmon_enabled, get_vmon_startup_profile_file,
                          init_feature_switch)
from cis.svcscfg import (platform, isCorePnidService, loadServicesFile,
                         genDepTracker, isInstalledService)
from cis.utils import (getSrvStartType, service_start, service_stop,
                       service_query_status, get_deployment_nodetype,
                       get_db_type, setupLogging)
from cis.exceptions import ServiceStartException, ServiceStopException
from featureState import getApplianceMonitoring
from cis.svcsController import (get_services_status, list_services_details,
                                update_services_runstate, vmon_startupprofile)

isLinux = os.name == 'posix'

watchdogSubDirName = 'iiad'
watchdogMaintenanceModeName = 'iiad.maintenance-mode'

vmonSvc = 'vmware-vmon'
vmonStartupProfile = None
vmonStartupFile = get_vmon_startup_profile_file()

# These services integrated with vmon with a different name
diffSvcs = { 'vsphere-client-servicemarker' : 'vsphere-client',
             'vpostgres': 'vmware-vpostgres',
           }


def sigHandler(sigNum, frame):
    # Exit by invoking the exit handler
    exit(1)


def exitHandler():
    # Gets called when the script exits for any reason
    # other than calling os._exit
    if not is_vmon_enabled():
        return
    if vmonStartupProfile is not None:
        # Restore original contents of the file
        # TODO: Do this atomically
        with open(vmonStartupFile, 'wb') as fh:
            fh.write(vmonStartupProfile)


def getWatchdogDataDir():
    dataDir = os.path.join(get_cis_data_dir(), watchdogSubDirName)
    # Create the directory to hold the state, if it doesn't already exist
    try:
        os.mkdir(dataDir, 0755)
    except OSError:
        pass
    return dataDir


def notifyWatchdog(cfg, name):
    """
    Notifies iiad to enter or leave maintenance mode.
    If all services are stopped, then put iiad into maintenance mode otherwise
    make iiad come out of maintenance mode.
    """
    op = cfg['operation']
    isAll = cfg['opSvcs'] == 'all'
    maintenanceModeName = watchdogMaintenanceModeName
    if not isAll:
        maintenanceModeName = maintenanceModeName + '.' + name
        # Currently, we don't support per-service maintenance mode.
        return
    sentinelFileName = os.path.join(getWatchdogDataDir(), maintenanceModeName)
    try:
        if op == 'start':
            if os.path.isfile(sentinelFileName):
                os.remove(sentinelFileName)
        else:
            with open(sentinelFileName, 'w') as sentinel:
                sentinel.write('stopped')
    except OSError:
        # If this fails, there ain't much we can do about it.
        verb = 'create' if op == 'start' else 'delete'
        msg = 'Unable to %s %s.' % (verb, sentinelFileName)
        logging.error(msg)
        print msg

def checkRestrictSvc(cfg, name):
    """
    Check if the service name was explicitly given by user for start operation
    """
    for svc in cfg['svcs']:
        if cfg['restrict'] and svc in cfg['restrict']:
            # XXX: PR# 1611063 - explicitly check if the name matches either the
            # service name or the node name. if its a vmon enabled service, name
            # will match the node name, else it will match the servicename field
            
            return name == svc or \
                   name == cfg['svcs'][svc][platform()]['serviceName']

def startService(cfg, name):
    """
    On ciswin if the  service startMode in SCM is configred as
    1. Disabled: skip to start
    2. Manual: skip to start service if --all flag is set.
    """
    ret = 0
    manualStart = False

    try:
        # checking service's start mode
        startType = getSrvStartType(name)
        isExplicitStart = checkRestrictSvc(cfg, name)
    except Exception as e:
        msg = 'Unable to start service %s, ERROR: %s' % (name, e)
        logging.error(msg)
        print msg
        return 1
    # Checking for disabled and manual start type during starting all services
    if (startType == 'Disabled'):
        msg = "'%s' startMode is %s, skipping to start:" % (name, startType)
        print msg
        logging.info(msg)
        return ret
    elif (startType == 'Manual' and (not isExplicitStart)):
        msg = "'%s' startMode is %s, skipping to start:" % (name, startType)
        print msg
        logging.info(msg)
        return ret
    # Single service start for Manual start mode
    elif startType == 'Manual':
        manualStart = True

    try:
        service_start(name, manual_start=manualStart, check_state=True)
        notifyWatchdog(cfg, name)
    except ServiceStartException as e:
        msg = 'Unable to start service %s, Exception: %s' % (name, e)
        logging.error(msg)
        print msg
        return 1

    return ret


def stopService(cfg, name):
    ret = 0
    try:
        notifyWatchdog(cfg, name)
        service_stop(name, check_state=True)
    except ServiceStopException as e:
        ret = 1
        msg = 'Unable to stop service %s, ERROR: %s' % (name, e)
        print msg
    return ret


def getServiceStatus(name, desc, stateAccumulator):
    ret = 0
    s = '%s (%s)' % (name, desc)
    (ret, state) = service_query_status(name)
    if state == 'STOPPED':
        stateAccumulator['Stopped'].append(s)
    elif state == 'STOP_PENDING':
        stateAccumulator['StopPending'].append(s)
    elif state == 'START_PENDING':
        stateAccumulator['StartPending'].append(s)
    elif state == 'RUNNING':
        stateAccumulator['Running'].append(s)
    elif state == 'PAUSED':
        stateAccumulator['Paused'].append(s)
    elif state == 'PAUSED_PENDING':
        stateAccumulator['PausePending'].append(s)
    elif state == 'SERVICE_CONTINUE_PENDING':
        stateAccumulator['ContinuePending'].append(s)

    return ret


def serviceControl(cfg, service, stateAccumulator):
    """
    Start, stop, or check status of a service.
    """
    name = vmon_enabled_svcname(cfg, service)
    op = cfg['operation']
    ignoreErr = cfg['ignore']
    if not name or name == 'xxx' or not cfg['svcs'][service]['enabled']:
        return
    msg = 'Service: %s, Action: %s' % (name, op)
    logging.info(msg)
    print msg
    if cfg['dryrun']:
        pass
    else:
        ret = 0
        if op == 'start':
            ret = startService(cfg, name)
        elif op == 'stop':
            ret = stopService(cfg, name)
        elif op == 'status':
            ignoreErr = True
            desc = cfg['svcs'][service].get('serviceDisplayName', '')
            ret = getServiceStatus(name, desc, stateAccumulator)
        if ret != 0 and not ignoreErr:
            exit(1)


def isSelectedSvs(cfg, svcName):
    """
    Is given service selected for doing start/stop/status operation. This is
    based what category of services user wants to operate on.
    There are 2 categories defined viz. non-core and all.
    """
    return (svcName in cfg['restrict'] or
            (cfg['opSvcs'] ==
             'non-core' and not isCorePnidService(cfg, svcName))
            or cfg['opSvcs'] == 'all')

def is_feature_state_enabled_svc(svcName):
    """
    If a feature state associated with the *entire* service (e.g. VCHA feature
    state for vcha, vmware-vmon services) is not enabled, that service will be
    skipped for start/stop/status operation.
    """
    # Initialize feature states (if not already initialized).
    init_feature_switch()
    if svcName in ('vcha', 'vmware-vmon', 'vmonapi'):
        return True if is_vmon_enabled() else False
    if svcName in ('vmware-statsmonitor'):
        return True if getApplianceMonitoring() else False
    else:
        return True

def vmon_enabled_svcname(cfg, service):
    """
    Returns service name as registered with vmon. If service is not
    registered with vmon, returns serviceName.
    """
    # serviceName entry in services.json
    serviceName = cfg['svcs'][service][platform()]['serviceName']
    if not is_vmon_enabled() or not serviceName:
        return serviceName

    if service in diffSvcs:
        return diffSvcs[service]

    if vmonSvc in cfg['svcs'][service]['dependsOn']:
        # service is integrated with vmon
        return service

    return serviceName


def processServices(cfg, depTracker):
    """
    Start, stop, or get the status of all services encoded in depTracker.
    """
    op = cfg['operation']
    if op == 'status':
        stateAccumulator = {'ContinuePending': [],
                            'PausePending': [],
                            'Paused': [],
                            'Running': [],
                            'StartPending': [],
                            'StopPending': [],
                            'Stopped': []}
    else:
        stateAccumulator = None
    if is_vmon_enabled() and op == 'start' and cfg['opSvcs'] == 'all':
        # When vmon is enabled, bring up vmon with a NONE profile.
        # @prereq - vmon is not running
        # TODO: Move this to use vmon-cli --all after all services
        # complete integration

        # Exit handler to restore contents of defaultStarupProfile file
        atexit.register(exitHandler)

        # Easier to replace the profile name by opening the files in
        # different modes rather than dealing with platform dependent
        # seek, truncate and flush calls
        global vmonStartupProfile
        try:
            with open(vmonStartupFile, 'r') as fh:
                vmonStartupProfile = fh.readline().strip()
        except IOError as e:
            if e.errno == 2:
                # file did not exist
                vmonStartupProfile = 'ALL'
            else:
                logging.error(e)
                exit(1)
        # TODO: use atomic re-write here
        with open(vmonStartupFile, 'wb') as fh:
            fh.write('NONE')

    while len(depTracker.ready()):
        nextSvc = sorted(depTracker.ready())[0]
        if not (isInstalledService(cfg, nextSvc) and isSelectedSvs(cfg, nextSvc)
                and is_feature_state_enabled_svc(nextSvc)):
            # Not an installed service or is a core service and --all flag
            # is not set. Remove from depTracker.
            depTracker.rm(nextSvc)
        else:
            serviceControl(cfg, nextSvc, stateAccumulator)
            depTracker.rm(nextSvc)
    if not depTracker.empty():
        msg = 'There appears to be a cycle in the service dependence graph.'
        logging.error(msg)
        sys.stderr.write(msg + os.linesep)
        exit(1)
    if op == 'status':
        for state, svcs in stateAccumulator.iteritems():
            if svcs:
                msg = '%s:\n %s' % (state, ' '.join(sorted(svcs)))
                logging.info(msg)
                print msg


def parseArguments():
    """
    Parse command line arguments and place results in a config dictionary.
    """
    parser = argparse.ArgumentParser()
    parser.add_argument('--start',
                        help='Start all VMware services (except core)',
                        action='store_true')
    parser.add_argument('--stop',
                        help='Stop all VMware services (except core)',
                        action='store_true')
    parser.add_argument('--all',
                        help='Start/stop all VMware services (including core)',
                        action='store_true')
    parser.add_argument('--status',
                        help='Get status of all VMware services (except core) '
                        '(default)', action='store_true')
    parser.add_argument('--list',
                        help='List all controllable services',
                        action='store_true')
    parser.add_argument('--ignore',
                        help='Ignore errors.  Continue starting or stopping '
                             'services even if errors occur.',
                        action='store_true')
    parser.add_argument('--file', help='Service config file')
    parser.add_argument('--dry-run',
                        help='Print actions to be performed without actually '
                        'doing them', action='store_true')
    parser.add_argument('services',
                        help='Services on which to operate',
                        nargs=argparse.REMAINDER)
    parsedArgs = parser.parse_args()
    start = parsedArgs.start
    stop = parsedArgs.stop
    selectAll = parsedArgs.all
    status = parsedArgs.status
    listAll = parsedArgs.list
    ignore = parsedArgs.ignore
    dryrun = parsedArgs.dry_run
    svcCfgPath = parsedArgs.file
    services = parsedArgs.services

    if not start and not stop and not status and not listAll:
        listAll = True

    if start and stop or \
       start and status or \
       start and listAll or \
       stop and status or \
       stop and listAll or \
       status and listAll:
        print 'No more than one of --start, --stop, --status, or '\
              '--list can be specified.'
        exit(1)

    # if we have --all specified in the options or its a start operation with
    # no services specified, we start all services. If its only a '--stop'
    # operation, we stop only non-core services. This behavior of stop is needed
    # for cert update use-case.
    if selectAll or (start and not services):
        opSvcs = 'all'
    else:
        opSvcs = 'non-core'

    if start:
        operation = 'start'
    elif stop:
        operation = 'stop'
    elif status:
        operation = 'status'
    elif listAll:
        operation = 'list'
    else:
        print 'Unexpected error'
        exit(1)

    if not svcCfgPath:
        if isLinux:
            svcCfgPath = '/usr/lib/vmware-visl-integration/config/services.json'
        else:
            svcCfgPath = os.path.join(os.environ['VMWARE_CIS_HOME'],
                                      'visl-integration', 'config',
                                      'services.json')
    logging.info('Operation: %s, dry-run: %s, ignore: %s, svcCfgPath: %s' %
                 (operation, str(dryrun), str(ignore), svcCfgPath))
    svcs = loadServicesFile(svcCfgPath)

    serviceNames = {}
    for service, info in svcs.iteritems():
        try:
            serviceNames[info[platform()]['serviceName']] = service
        except KeyError:
            pass

    badSvcs = [s for s in services if s not in serviceNames]
    if badSvcs:
        print 'Unknown services: %s' % ' '.join(badSvcs)
        exit(1)

    intServices = []
    for svc in services:
        intServices.append(serviceNames[svc])

    return dict(operation=operation, svcs=svcs, dryrun=dryrun, ignore=ignore,
                restrict=intServices, deploymentType=get_deployment_nodetype(),
                dbType=get_db_type(), opSvcs=opSvcs)


#
# The work starts here.
#


def main():
    signal.signal(signal.SIGTERM, sigHandler)
    signal.signal(signal.SIGINT, sigHandler)

    cfg = parseArguments()

    depTracker = genDepTracker(cfg)
    if depTracker is None:
        exit(1)
    for svc in cfg['svcs']:
        if cfg['restrict'] and svc not in cfg['restrict']:
            depTracker.rm(svc)

    if cfg['operation'] == 'list':
        for service, info in sorted(cfg['svcs'].iteritems(), key=lambda k: k[0]):
            if isInstalledService(cfg, service):
                name = info[platform()]['serviceName']
                enabled = info['enabled']
                if name and name != 'xxx':
                    desc = cfg['svcs'][service].get('serviceDisplayName', '')
                    print '%s (%s)%s' % (name, desc,
                                         ' [disabled]' if not enabled else '')
    else:
        processServices(cfg, depTracker)
    return 0


###############################################################################
# service-control when vmon is enabled. Although some of above code can be
# reused, it quickly starts to get very messy. Therefore I am creating this
# seperate section for vmon based service control. If and when we decide to
# permenantly enable vMon, we can remove above code.
###############################################################################

def vmon_parse_arguments():
    parser = argparse.ArgumentParser(
        description='Perform operation on VMware services. By default the '
                    'services selected are based on current startup profile. '
                    'This can be overridden by using --all and --vmon-profile '
                    'options.')
    parser.add_argument('--start',
                        help='Perform start operation on VMware services.',
                        action='store_true')
    parser.add_argument('--stop',
                        help='Perform stop operation VMware services.',
                        action='store_true')
    parser.add_argument('--status',
                        help='Get running status of VMware services.',
                        action='store_true')
    parser.add_argument('--list',
                        help='List all controllable VMware services. This '
                        'option will soon be deprecated. Please use '
                        '--list-services.',
                        action='store_true')
    parser.add_argument('--list-services',
                        help='Lists all controllable VMware services.',
                        action='store_true')
    parser.add_argument('--vmon-profile', help='Start/Stop services associated'
                        ' with given profile name.')
    parser.add_argument('--all', help='Start/Stop all VMware services i.e. '
                        'core and the default mgmt services).',
                        action='store_true')
    parser.add_argument('--ignore', help='Ignore errors. Continue with given '
                        'operation even if errors occur.', action='store_true')
    parser.add_argument('services',
                        help='Services on which to operate',
                        nargs=argparse.REMAINDER)

    return parser.parse_args()


def process_arguments(parsed_args):
    """
    Does the main work based on the arguments given to service control.
    """
    operations = [parsed_args.start, parsed_args.stop, parsed_args.status,
                  parsed_args.list, parsed_args.list_services]
    if (operations.count(True) != 1):
        raise Exception('Exactly one operation must be specified.')

    svc_names = parsed_args.services if parsed_args.services else None
    ignore_err = parsed_args.ignore

    if svc_names:
        vmon_profile = None
    else:
        # Use the default vmon startup profile if vmon profile is not specified
        if parsed_args.vmon_profile is None:
            vmon_profile = vmon_startupprofile()
        else:
            vmon_profile = parsed_args.vmon_profile.strip()

    if parsed_args.start:
        update_services_runstate('start', vmon_profile,
                                 parsed_args.all, vmon_profile == 'ALL',
                                 svc_names=svc_names, ignore_err=ignore_err)
    elif parsed_args.stop:
        # XXX In which case should we stop the leaf os services is a little
        # unclear. Stopping them only when stopping all vmon services for now.
        update_services_runstate('stop', vmon_profile,
                                 parsed_args.all, vmon_profile == 'ALL',
                                 svc_names=svc_names, ignore_err=ignore_err)
    elif parsed_args.status:
        svcs_status = get_services_status(svc_names, ignore_err=ignore_err)
        state_accumulator = {'ContinuePending': [],
                             'PausePending': [],
                             'Paused': [],
                             'Running': [],
                             'StartPending': [],
                             'StopPending': [],
                             'Stopped': []}
        for s, state in svcs_status.iteritems():
            if state == 'STOPPED':
                state_accumulator['Stopped'].append(s)
            elif state == 'STOP_PENDING':
                state_accumulator['StopPending'].append(s)
            elif state == 'START_PENDING':
                state_accumulator['StartPending'].append(s)
            elif state == 'RUNNING':
                state_accumulator['Running'].append(s)
            elif state == 'PAUSED':
                state_accumulator['Paused'].append(s)
            elif state == 'PAUSED_PENDING':
                state_accumulator['PausePending'].append(s)
            elif state == 'SERVICE_CONTINUE_PENDING':
                state_accumulator['ContinuePending'].append(s)

        for state, svcs in state_accumulator.iteritems():
            if svcs:
                msg = '%s:\n %s' % (state, ' '.join(sorted(svcs)))
                logging.info(msg)
                print msg
    elif parsed_args.list or parsed_args.list_services:
        for svc_id, cfg in list_services_details().iteritems():
            svc = svc_id if parsed_args.list_services else cfg['serviceName']
            print('%s (%s)' % (svc, cfg['desc']))


def vmon_main():
    """
    For vmon enabled deployment, work starts here.
    """
    parsed_args = vmon_parse_arguments()

    try:
        process_arguments(parsed_args)
    except Exception as ex:
        msg = 'Service-control failed. Error %s\n' % str(ex)
        logging.error(msg)
        sys.stderr.write(msg)
        return 1


if __name__ == '__main__':
    # Setup up logging to file/syslog based on operation
    logDir = os.path.join(get_cis_log_dir(), 'cloudvm')
    setupLogging('service-control', logMechanism='file', logDir=logDir)

    # start marker so we can clearily see one run from another
    logging.info('********** Start %s **********', sys.argv[1:])

    if is_vmon_enabled():
        exit(vmon_main())
    exit(main())
