#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

DOCUMENTATION = '''
---
module: service
author: Michael DeHaan
version_added: 0.1
short_description:  Manage services.
description:
    - Controls services on remote hosts.
options:
    name:
        required: true
        description:
        - Name of the service.
    state:
        required: false
        choices: [ started, stopped, restarted, reloaded ]
        description:
        - I(started), I(stopped), I(reloaded), I(restarted).
          I(Started)/I(stopped) are idempotent actions that will not run
          commands unless necessary.  I(restarted) will always bounce the
          service.  I(reloaded) will always reload.
    pattern:
        required: false
        version_added: "0.7"
        description:
        - If the service does not respond to the status command, name a
          substring to look for as would be found in the output of the I(ps)
          command as a stand-in for a status result.  If the string is found,
          the service will be assumed to be running.
    enabled:
        required: false
        choices: [ "yes", "no" ]
        description:
        - Whether the service should start on boot.
examples:
    - code: "service: name=httpd state=started"
      description: Example action from Ansible Playbooks
    - code: "service: name=httpd state=stopped"
      description: Example action from Ansible Playbooks
    - code: "service: name=httpd state=restarted"
      description: Example action from Ansible Playbooks
    - code: "service: name=httpd state=reloaded"
      description: Example action from Ansible Playbooks
    - code: "service: name=foo pattern=/usr/bin/foo state=started"
      description: Example action from Ansible Playbooks
'''

import platform
import os
import re

SERVICE = None
CHKCONFIG = None
INITCTL = None
INITSCRIPT = None
RCCONF = None

PS_OPTIONS = 'auxww'

def _find_binaries(m,name):
    # list of possible paths for service/chkconfig binaries
    # with the most probable first
    global SERVICE
    global CHKCONFIG
    global INITCTL
    global INITSCRIPT
    global RCCONF
    paths = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
    binaries = [ 'service', 'chkconfig', 'update-rc.d', 'initctl', 'systemctl']
    initpaths = [ '/etc/init.d','/etc/rc.d','/usr/local/etc/rc.d' ]
    rcpaths = [ '/etc/rc.conf','/usr/local/etc/rc.conf' ]
    location = dict()

    for binary in binaries:
        location[binary] = None

    for binary in binaries:
        location[binary] = m.get_bin_path(binary)

    if location.get('systemctl', None):
        CHKCONFIG = location['systemctl']
    elif location.get('chkconfig', None):
        CHKCONFIG = location['chkconfig']
    elif location.get('update-rc.d', None):
        CHKCONFIG = location['update-rc.d']
    else:
        for rcfile in rcpaths:
            if os.path.isfile(rcfile):
                RCCONF = rcfile
    if not CHKCONFIG and not RCCONF:
        m.fail_json(msg='unable to find chkconfig or update-rc.d binary')
    if location.get('service', None):
        SERVICE = location['service']
    else:
        for rcdir in initpaths:
            initscript = "%s/%s" % (rcdir,name)
            if os.path.isfile(initscript):
                INITSCRIPT = initscript
    if not SERVICE and not INITSCRIPT:
        m.fail_json(msg='unable to find service binary nor initscript')
    if location.get('initctl', None):
        INITCTL = location['initctl']
    else:
        INITCTL = None

def _get_service_status(name, pattern):
    rc, status_stdout, status_stderr = _run("%s %s status" % (SERVICE, name))

    # set the running state to None because we don't know it yet
    running = None

    # If pattern is provided, search for that
    # before checking initctl, service output, and other tricks
    if pattern is not None:

        psbin = '/bin/ps'
        if not os.path.exists(psbin):
            if os.path.exists('/usr/bin/ps'):
                psbin = '/usr/bin/ps'
            else:
                psbin = None

        if psbin is not None:
            (rc, psout, pserr) = _run('%s %s' % (psbin, PS_OPTIONS))
            # If rc is 0, set running as appropriate
            # If ps command fails, fall back to other means.
            if rc == 0:
                running = False
                lines = psout.split("\n")
                for line in lines:
                    if pattern in line and not "pattern=" in line:
                        # so as to not confuse ./hacking/test-module
                        running = True
                        break

    # Check if we got upstart on the system and then the job state
    if INITCTL != None and running is None:
        # check the job status by upstart response
        initctl_rc, initctl_status_stdout, initctl_status_stderr = _run("%s status %s" % (INITCTL, name))
        if initctl_status_stdout.find("stop/waiting") != -1:
            running = False
        elif initctl_status_stdout.find("start/running") != -1:
            running = True

    # if the job status is still not known check it by response code
    if running == None:
        if rc == 3:
            running = False
        if rc == 2:
            running = False
        elif rc == 0:
            running = True

    # if the job status is still not known check it by status output keywords
    if running == None:
        # first tranform the status output that could irritate keyword matching
        cleanout = status_stdout.lower().replace(name.lower(), '')
        if "stop" in cleanout:
            running = False
        elif "run" in cleanout and "not" in cleanout:
            running = False
        elif "run" in cleanout and "not" not in cleanout:
            running = True
        elif "start" in cleanout and "not" not in cleanout:
            running = True
        elif 'could not access pid file' in cleanout:
            running = False
        elif 'is dead and pid file exists' in cleanout:
            running = False
        elif 'dead but subsys locked' in cleanout:
            running = False
        elif 'dead but pid file exists' in cleanout:
            running = False

    # if the job status is still not known check it by special conditions
    if running == None:
        if name == 'iptables' and status_stdout.find("ACCEPT") != -1:
            # iptables status command output is lame
            # TODO: lookup if we can use a return code for this instead?
            running = True

    return running

def _run(cmd):
    # returns (rc, stdout, stderr) from shell command
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
    stdout, stderr = process.communicate()
    return (process.returncode, stdout, stderr)


def _do_enable(name, enable):
    # we change argument depending on real binary used
    # update-rc.d wants enable/disable while
    # chkconfig wants on/off
    # also, systemctl needs the arguments reversed
    if enable:
        on_off = "on"
        enable_disable = "enable"
        rc = "YES"
    else:
        on_off = "off"
        enable_disable = "disable"
        rc = "NO"

    if RCCONF:
        entry = "%s_enable" % name
        full_entry = '%s="%s"' % (entry,rc)
        rc = open(RCCONF,"r+")
        rctext = rc.read()
        if re.search("^%s" % full_entry,rctext,re.M) is None:
            if re.search("^%s" % entry,rctext,re.M) is None:
                rctext += "\n%s" % full_entry
            else:
                rctext = re.sub("^%s.*" % entry,full_entry,rctext,1,re.M)
            rc.truncate(0)
            rc.seek(0)
            rc.write(rctext)
        rc.close()

        rc=0
        stderr=stdout=''
    else:
        if CHKCONFIG.endswith("update-rc.d"):
            args = (CHKCONFIG, name, enable_disable)
        elif CHKCONFIG.endswith("systemctl"):
            args = (CHKCONFIG, enable_disable, name + ".service")
        else:
            args = (CHKCONFIG, name, on_off)

        if enable is not None:
            rc, stdout, stderr = _run("%s %s %s" % args)

    return rc, stdout, stderr

def main():
    module = AnsibleModule(
        argument_spec = dict(
            name = dict(required=True),
            state = dict(choices=['running', 'started', 'stopped', 'restarted', 'reloaded']),
            pattern = dict(required=False, default=None),
            enabled = dict(choices=BOOLEANS)
        )
    )

    name = module.params['name']
    state = module.params['state']
    pattern = module.params['pattern']
    enable = module.boolean(module.params.get('enabled', None))

    # Set PS options here if 'ps auxww' will not work on
    # target platform
    if platform.system() == 'SunOS':
        global PS_OPTIONS
        PS_OPTIONS = '-ef'

    # ===========================================
    # find binaries locations on minion
    _find_binaries(module,name)

    # ===========================================
    # get service status
    running = _get_service_status(name, pattern)

    # ===========================================
    # Some common variables
    changed = False
    rc = 0
    err = ''
    out = ''

    # set command to run
    if SERVICE:
        svc_cmd = "%s %s" % (SERVICE, name)
    elif INITSCRIPT:
        svc_cmd = "%s" % INITSCRIPT

    if module.params['enabled']:
        rc_enable, out_enable, err_enable = _do_enable(name, enable)
        rc += rc_enable
        out += out_enable
        err += err_enable

    if state and running == None:
        module.fail_json(msg="failed determining the current service state => state stays unchanged", changed=False)

    elif state:
        # a state change command has been requested

        # ===========================================
        # determine if we are going to change anything
        if not running and state in ["started", "running"]:
            changed = True
        elif running and state in ["stopped","reloaded"]:
            changed = True
        elif state == "restarted":
            changed = True

        # ===========================================
        # run change commands if we need to
        if changed:

            if platform.system() == 'FreeBSD':
                start = "onestart"
                stop = "onestop"
                reload = "onereload"
            else:
                start = "start"
                stop = "stop"
                reload = "reload"

            if state in ['started', 'running']:
                rc_state, stdout, stderr = _run("%s %s" % (svc_cmd,start))
            elif state == 'stopped':
                rc_state, stdout, stderr = _run("%s %s" % (svc_cmd,stop))
            elif state == 'reloaded':
                rc_state, stdout, stderr = _run("%s %s" % (svc_cmd,reload))
            elif state == 'restarted':
                rc1, stdout1, stderr1 = _run("%s %s" % (svc_cmd,stop))
                rc2, stdout2, stderr2 = _run("%s %s" % (svc_cmd,start))
                if rc1 != 0 and rc2 == 0:
                    rc_state = rc + rc2
                    stdout = stdout2
                    stderr = stderr2
                else:
                    rc_state = rc + rc1 + rc2
                    stdout = stdout1 + stdout2
                    stderr = stderr1 + stderr2

            out += stdout
            err += stderr
            rc = rc + rc_state

    if rc != 0:
        module.fail_json(msg=err)

    result = {"changed": changed}
    if module.params['enabled']:
        result['enabled'] = module.params['enabled']
    if state:
        result['state'] = state

    rc, stdout, stderr = _run("%s status" % (svc_cmd))
    module.exit_json(**result)

# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>

main()