# -*- coding: utf-8 -*-
'''
Service support for RHEL-based systems, including support for both upstart and sysvinit

.. important::
    If you feel that Salt should be using this module to manage services on a
    minion, and it is using a different module (or gives an error similar to
    *'service.start' is not available*), see :ref:`here
    <module-provider-override>`.
'''
from __future__ import absolute_import, unicode_literals, print_function

# Import python libs
import glob
import logging
import os
import stat
import fnmatch
import re

# Import salt libs
import salt.utils.path
from salt.ext import six

log = logging.getLogger(__name__)

__func_alias__ = {
    'reload_': 'reload'
}

# Define the module's virtual name
__virtualname__ = 'service'

# Import upstart module if needed
HAS_UPSTART = False
if salt.utils.path.which('initctl'):
    try:
        # Don't re-invent the wheel, import the helper functions from the
        # upstart module.
        from salt.modules.upstart_service import _upstart_enable, _upstart_disable, _upstart_is_enabled
    except Exception as exc:  # pylint: disable=broad-except
        log.error('Unable to import helper functions from '
                  'salt.modules.upstart: %s', exc)
    else:
        HAS_UPSTART = True


def __virtual__():
    '''
    Only work on select distros which still use Red Hat's /usr/bin/service for
    management of either sysvinit or a hybrid sysvinit/upstart init system.
    '''
    # Disable when booted with systemd
    if __utils__['systemd.booted'](__context__):
        return (False, 'The rh_service execution module failed to load: this system was booted with systemd.')

    # Enable on these platforms only.
    enable = set((
        'XenServer',
        'XCP-ng',
        'RedHat',
        'CentOS',
        'ScientificLinux',
        'CloudLinux',
        'Amazon',
        'Fedora',
        'ALT',
        'OEL',
        'SUSE  Enterprise Server',
        'SUSE',
        'McAfee  OS Server',
        'VirtuozzoLinux'
    ))
    if __grains__['os'] in enable:

        if __grains__['os'] == 'SUSE':
            if six.text_type(__grains__['osrelease']).startswith('11'):
                return __virtualname__
            else:
                return (False, 'Cannot load rh_service module on SUSE > 11')

        osrelease_major = __grains__.get('osrelease_info', [0])[0]

        if __grains__['os'] in ('XenServer', 'XCP-ng'):
            if osrelease_major >= 7:
                return (
                    False,
                    'XenServer and XCP-ng >= 7 use systemd, will not load rh_service.py '
                    'as virtual \'service\''
                )
            return __virtualname__

        if __grains__['os'] == 'Fedora':
            if osrelease_major >= 15:
                return (
                    False,
                    'Fedora >= 15 uses systemd, will not load rh_service.py '
                    'as virtual \'service\''
                )
        if __grains__['os'] in ('RedHat', 'CentOS', 'ScientificLinux', 'OEL', 'CloudLinux'):
            if osrelease_major >= 7:
                return (
                    False,
                    'RedHat-based distros >= version 7 use systemd, will not '
                    'load rh_service.py as virtual \'service\''
                )
        return __virtualname__
    return (False, 'Cannot load rh_service module: OS not in {0}'.format(enable))


def _runlevel():
    '''
    Return the current runlevel
    '''
    out = __salt__['cmd.run']('/sbin/runlevel')
    # unknown will be returned while inside a kickstart environment, since
    # this is usually a server deployment it should be safe to assume runlevel
    # 3.  If not all service related states will throw an out of range
    # exception here which will cause other functions to fail.
    if 'unknown' in out:
        return '3'
    else:
        return out.split()[1]


def _chkconfig_add(name):
    '''
    Run 'chkconfig --add' for a service whose script is installed in
    /etc/init.d.  The service is initially configured to be disabled at all
    run-levels.
    '''
    cmd = '/sbin/chkconfig --add {0}'.format(name)
    if __salt__['cmd.retcode'](cmd, python_shell=False) == 0:
        log.info('Added initscript "%s" to chkconfig', name)
        return True
    else:
        log.error('Unable to add initscript "%s" to chkconfig', name)
        return False


def _service_is_upstart(name):
    '''
    Return True if the service is an upstart service, otherwise return False.
    '''
    return HAS_UPSTART and os.path.exists('/etc/init/{0}.conf'.format(name))


def _service_is_sysv(name):
    '''
    Return True if the service is a System V service (includes those managed by
    chkconfig); otherwise return False.
    '''
    try:
        # Look for user-execute bit in file mode.
        return bool(os.stat(
            os.path.join('/etc/init.d', name)).st_mode & stat.S_IXUSR)
    except OSError:
        return False


def _service_is_chkconfig(name):
    '''
    Return True if the service is managed by chkconfig.
    '''
    cmdline = '/sbin/chkconfig --list {0}'.format(name)
    return __salt__['cmd.retcode'](cmdline, python_shell=False, ignore_retcode=True) == 0


def _sysv_is_enabled(name, runlevel=None):
    '''
    Return True if the sysv (or chkconfig) service is enabled for the specified
    runlevel; otherwise return False.  If `runlevel` is None, then use the
    current runlevel.
    '''
    # Try chkconfig first.
    result = _chkconfig_is_enabled(name, runlevel)
    if result:
        return True

    if runlevel is None:
        runlevel = _runlevel()
    return (
        len(glob.glob('/etc/rc.d/rc{0}.d/S??{1}'.format(runlevel, name))) > 0)


def _chkconfig_is_enabled(name, runlevel=None):
    '''
    Return ``True`` if the service is enabled according to chkconfig; otherwise
    return ``False``.  If ``runlevel`` is ``None``, then use the current
    runlevel.
    '''
    cmdline = '/sbin/chkconfig --list {0}'.format(name)
    result = __salt__['cmd.run_all'](cmdline, python_shell=False)

    if runlevel is None:
        runlevel = _runlevel()
    if result['retcode'] == 0:
        for row in result['stdout'].splitlines():
            if '{0}:on'.format(runlevel) in row:
                if row.split()[0] == name:
                    return True
            elif row.split() == [name, 'on']:
                return True
    return False


def _sysv_enable(name):
    '''
    Enable the named sysv service to start at boot.  The service will be enabled
    using chkconfig with default run-levels if the service is chkconfig
    compatible.  If chkconfig is not available, then this will fail.
    '''
    if not _service_is_chkconfig(name) and not _chkconfig_add(name):
        return False
    cmd = '/sbin/chkconfig {0} on'.format(name)
    return not __salt__['cmd.retcode'](cmd, python_shell=False)


def _sysv_disable(name):
    '''
    Disable the named sysv service from starting at boot.  The service will be
    disabled using chkconfig with default run-levels if the service is chkconfig
    compatible; otherwise, the service will be disabled for the current
    run-level only.
    '''
    if not _service_is_chkconfig(name) and not _chkconfig_add(name):
        return False
    cmd = '/sbin/chkconfig {0} off'.format(name)
    return not __salt__['cmd.retcode'](cmd, python_shell=False)


def _sysv_delete(name):
    '''
    Delete the named sysv service from the system. The service will be
    deleted using chkconfig.
    '''
    if not _service_is_chkconfig(name):
        return False
    cmd = '/sbin/chkconfig --del {0}'.format(name)
    return not __salt__['cmd.retcode'](cmd)


def _upstart_delete(name):
    '''
    Delete an upstart service. This will only rename the .conf file
    '''
    if HAS_UPSTART:
        if os.path.exists('/etc/init/{0}.conf'.format(name)):
            os.rename('/etc/init/{0}.conf'.format(name),
                      '/etc/init/{0}.conf.removed'.format(name))
    return True


def _upstart_services():
    '''
    Return list of upstart services.
    '''
    if HAS_UPSTART:
        return [os.path.basename(name)[:-5]
            for name in glob.glob('/etc/init/*.conf')]
    else:
        return []


def _sysv_services():
    '''
    Return list of sysv services.
    '''
    _services = []
    output = __salt__['cmd.run'](['chkconfig', '--list'], python_shell=False)
    for line in output.splitlines():
        comps = line.split()
        try:
            if comps[1].startswith('0:'):
                _services.append(comps[0])
        except IndexError:
            continue
    # Return only the services that have an initscript present
    return [x for x in _services if _service_is_sysv(x)]


def get_enabled(limit=''):
    '''
    Return the enabled services. Use the ``limit`` param to restrict results
    to services of that type.

    CLI Examples:

    .. code-block:: bash

        salt '*' service.get_enabled
        salt '*' service.get_enabled limit=upstart
        salt '*' service.get_enabled limit=sysvinit
    '''
    limit = limit.lower()
    if limit == 'upstart':
        return sorted(name for name in _upstart_services()
            if _upstart_is_enabled(name))
    elif limit == 'sysvinit':
        runlevel = _runlevel()
        return sorted(name for name in _sysv_services()
            if _sysv_is_enabled(name, runlevel))
    else:
        runlevel = _runlevel()
        return sorted(
            [name for name in _upstart_services()
                if _upstart_is_enabled(name)]
            + [name for name in _sysv_services()
            if _sysv_is_enabled(name, runlevel)])


def get_disabled(limit=''):
    '''
    Return the disabled services. Use the ``limit`` param to restrict results
    to services of that type.

    CLI Example:

    .. code-block:: bash

        salt '*' service.get_disabled
        salt '*' service.get_disabled limit=upstart
        salt '*' service.get_disabled limit=sysvinit
    '''
    limit = limit.lower()
    if limit == 'upstart':
        return sorted(name for name in _upstart_services()
            if not _upstart_is_enabled(name))
    elif limit == 'sysvinit':
        runlevel = _runlevel()
        return sorted(name for name in _sysv_services()
            if not _sysv_is_enabled(name, runlevel))
    else:
        runlevel = _runlevel()
        return sorted(
            [name for name in _upstart_services()
                if not _upstart_is_enabled(name)]
            + [name for name in _sysv_services()
            if not _sysv_is_enabled(name, runlevel)])


def get_all(limit=''):
    '''
    Return all installed services. Use the ``limit`` param to restrict results
    to services of that type.

    CLI Example:

    .. code-block:: bash

        salt '*' service.get_all
        salt '*' service.get_all limit=upstart
        salt '*' service.get_all limit=sysvinit
    '''
    limit = limit.lower()
    if limit == 'upstart':
        return sorted(_upstart_services())
    elif limit == 'sysvinit':
        return sorted(_sysv_services())
    else:
        return sorted(_sysv_services() + _upstart_services())


def available(name, limit=''):
    '''
    Return True if the named service is available.  Use the ``limit`` param to
    restrict results to services of that type.

    CLI Examples:

    .. code-block:: bash

        salt '*' service.available sshd
        salt '*' service.available sshd limit=upstart
        salt '*' service.available sshd limit=sysvinit
    '''
    if limit == 'upstart':
        return _service_is_upstart(name)
    elif limit == 'sysvinit':
        return _service_is_sysv(name)
    else:
        return _service_is_upstart(name) or _service_is_sysv(name) or _service_is_chkconfig(name)


def missing(name, limit=''):
    '''
    The inverse of service.available.
    Return True if the named service is not available.  Use the ``limit`` param to
    restrict results to services of that type.

    CLI Examples:

    .. code-block:: bash

        salt '*' service.missing sshd
        salt '*' service.missing sshd limit=upstart
        salt '*' service.missing sshd limit=sysvinit
    '''
    if limit == 'upstart':
        return not _service_is_upstart(name)
    elif limit == 'sysvinit':
        return not _service_is_sysv(name)
    else:
        if _service_is_upstart(name) or _service_is_sysv(name):
            return False
        else:
            return True


def start(name):
    '''
    Start the specified service

    CLI Example:

    .. code-block:: bash

        salt '*' service.start <service name>
    '''
    if _service_is_upstart(name):
        cmd = 'start {0}'.format(name)
    else:
        cmd = '/sbin/service {0} start'.format(name)
    return not __salt__['cmd.retcode'](cmd, python_shell=False)


def stop(name):
    '''
    Stop the specified service

    CLI Example:

    .. code-block:: bash

        salt '*' service.stop <service name>
    '''
    if _service_is_upstart(name):
        cmd = 'stop {0}'.format(name)
    else:
        cmd = '/sbin/service {0} stop'.format(name)
    return not __salt__['cmd.retcode'](cmd, python_shell=False)


def restart(name):
    '''
    Restart the named service

    CLI Example:

    .. code-block:: bash

        salt '*' service.restart <service name>
    '''
    if _service_is_upstart(name):
        cmd = 'restart {0}'.format(name)
    else:
        cmd = '/sbin/service {0} restart'.format(name)
    return not __salt__['cmd.retcode'](cmd, python_shell=False)


def reload_(name):
    '''
    Reload the named service

    CLI Example:

    .. code-block:: bash

        salt '*' service.reload <service name>
    '''
    if _service_is_upstart(name):
        cmd = 'reload {0}'.format(name)
    else:
        cmd = '/sbin/service {0} reload'.format(name)
    return not __salt__['cmd.retcode'](cmd, python_shell=False)


def status(name, sig=None):
    '''
    Return the status for a service.
    If the name contains globbing, a dict mapping service name to True/False
    values is returned.

    .. versionchanged:: 2018.3.0
        The service name can now be a glob (e.g. ``salt*``)

    Args:
        name (str): The name of the service to check
        sig (str): Signature to use to find the service via ps

    Returns:
        bool: True if running, False otherwise
        dict: Maps service name to True if running, False otherwise

    CLI Example:

    .. code-block:: bash

        salt '*' service.status <service name> [service signature]
    '''
    if sig:
        return bool(__salt__['status.pid'](sig))

    contains_globbing = bool(re.search(r'\*|\?|\[.+\]', name))
    if contains_globbing:
        services = fnmatch.filter(get_all(), name)
    else:
        services = [name]
    results = {}
    for service in services:
        if _service_is_upstart(service):
            cmd = 'status {0}'.format(service)
            results[service] = 'start/running' in __salt__['cmd.run'](cmd, python_shell=False)
        else:
            cmd = '/sbin/service {0} status'.format(service)
            results[service] = __salt__['cmd.retcode'](cmd, python_shell=False, ignore_retcode=True) == 0
    if contains_globbing:
        return results
    return results[name]


def delete(name, **kwargs):
    '''
    Delete the named service

    .. versionadded:: 2016.3

    CLI Example:

    .. code-block:: bash

        salt '*' service.delete <service name>
    '''
    if _service_is_upstart(name):
        return _upstart_delete(name)
    else:
        return _sysv_delete(name)


def enable(name, **kwargs):
    '''
    Enable the named service to start at boot

    CLI Example:

    .. code-block:: bash

        salt '*' service.enable <service name>
    '''
    if _service_is_upstart(name):
        return _upstart_enable(name)
    else:
        return _sysv_enable(name)


def disable(name, **kwargs):
    '''
    Disable the named service to start at boot

    CLI Example:

    .. code-block:: bash

        salt '*' service.disable <service name>
    '''
    if _service_is_upstart(name):
        return _upstart_disable(name)
    else:
        return _sysv_disable(name)


def enabled(name, **kwargs):
    '''
    Check to see if the named service is enabled to start on boot

    CLI Example:

    .. code-block:: bash

        salt '*' service.enabled <service name>
    '''
    if _service_is_upstart(name):
        return _upstart_is_enabled(name)
    else:
        return _sysv_is_enabled(name)


def disabled(name):
    '''
    Check to see if the named service is disabled to start on boot

    CLI Example:

    .. code-block:: bash

        salt '*' service.disabled <service name>
    '''
    if _service_is_upstart(name):
        return not _upstart_is_enabled(name)
    else:
        return not _sysv_is_enabled(name)
