service 45.3 KB
Newer Older
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3

4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# (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/>.

21 22 23 24
DOCUMENTATION = '''
---
module: service
author: Michael DeHaan
25
version_added: "0.1"
26 27 28 29 30 31
short_description:  Manage services.
description:
    - Controls services on remote hosts.
options:
    name:
        required: true
32
        description:
33
        - Name of the service.
34 35
    state:
        required: false
36
        choices: [ started, stopped, restarted, reloaded ]
37
        description:
38 39
          - C(started)/C(stopped) are idempotent actions that will not run
            commands unless necessary.  C(restarted) will always bounce the
40 41
            service.  C(reloaded) will always reload. At least one of state
            and enabled are required.
42 43 44 45 46 47 48 49
    sleep:
        required: false
        version_added: "1.3"
        description:
        - If the service is being C(restarted) then sleep this many seconds
          between the stop and start command. This helps to workaround badly
          behaving init scripts that exit immediately after signaling a process
          to stop.
50 51
    pattern:
        required: false
52
        version_added: "0.7"
53
        description:
54 55 56
        - 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,
57
          the service will be assumed to be running.
58 59
    enabled:
        required: false
60
        choices: [ "yes", "no" ]
61
        description:
62 63 64
        - Whether the service should start on boot. At least one of state and
          enabled are required.

65 66
    runlevel:
        required: false
Michael DeHaan committed
67
        default: 'default'
68
        description:
69
        - "For OpenRC init scripts (ex: Gentoo) only.  The runlevel that this service belongs to."
70 71 72 73
    arguments:
        description:
        - Additional arguments provided on the command line
        aliases: [ 'args' ]
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
'''

EXAMPLES = '''
# Example action to start service httpd, if not running
- service: name=httpd state=started

# Example action to stop service httpd, if running
- service: name=httpd state=stopped

# Example action to restart service httpd, in all cases
- service: name=httpd state=restarted

# Example action to reload service httpd, in all cases
- service: name=httpd state=reloaded

# Example action to enable service httpd, and not touch the running state
- service: name=httpd enabled=yes

# Example action to start service foo, based on running process /usr/bin/foo
- service: name=foo pattern=/usr/bin/foo state=started

# Example action to restart network service for interface eth0
- service: name=network state=restarted args=eth0
97 98
'''

99
import platform
100
import os
101
import re
102
import tempfile
103
import shlex
104
import select
105
import time
106
import string
107

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
class Service(object):
    """
    This is the generic Service manipulation class that is subclassed
    based on platform.

    A subclass should override the following action methods:-
      - get_service_tools
      - service_enable
      - get_service_status
      - service_control

    All subclasses MUST define platform and distribution (which may be None).
    """

    platform = 'Generic'
    distribution = None

    def __new__(cls, *args, **kwargs):
        return load_platform_subclass(Service, args, kwargs)

    def __init__(self, module):
        self.module         = module
        self.name           = module.params['name']
        self.state          = module.params['state']
132
        self.sleep          = module.params['sleep']
133
        self.pattern        = module.params['pattern']
134
        self.enable         = module.params['enabled']
135
        self.runlevel       = module.params['runlevel']
136 137
        self.changed        = False
        self.running        = None
138
        self.crashed        = None
139 140 141 142 143 144
        self.action         = None
        self.svc_cmd        = None
        self.svc_initscript = None
        self.svc_initctl    = None
        self.enable_cmd     = None
        self.arguments      = module.params.get('arguments', '')
145 146 147
        self.rcconf_file    = None
        self.rcconf_key     = None
        self.rcconf_value   = None
148
        self.svc_change     = False
149 150 151 152

        # select whether we dump additional debug info through syslog
        self.syslogging = False

153 154
    # ===========================================
    # Platform specific methods (must be replaced by subclass).
155 156 157 158 159 160 161 162 163 164 165 166 167

    def get_service_tools(self):
        self.module.fail_json(msg="get_service_tools not implemented on target platform")

    def service_enable(self):
        self.module.fail_json(msg="service_enable not implemented on target platform")

    def get_service_status(self):
        self.module.fail_json(msg="get_service_status not implemented on target platform")

    def service_control(self):
        self.module.fail_json(msg="service_control not implemented on target platform")

168 169
    # ===========================================
    # Generic methods that should be used on all platforms.
170

171
    def execute_command(self, cmd, daemonize=False):
172 173
        if self.syslogging:
            syslog.openlog('ansible-%s' % os.path.basename(__file__))
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
            syslog.syslog(syslog.LOG_NOTICE, 'Command %s, daemonize %r' % (cmd, daemonize))

        # Most things don't need to be daemonized
        if not daemonize:
            return self.module.run_command(cmd)

        # This is complex because daemonization is hard for people.
        # What we do is daemonize a part of this module, the daemon runs the
        # command, picks up the return code and output, and returns it to the
        # main process.
        pipe = os.pipe()
        pid = os.fork()
        if pid == 0:
            os.close(pipe[0])
            # Set stdin/stdout/stderr to /dev/null
            fd = os.open(os.devnull, os.O_RDWR)
            if fd != 0:
                os.dup2(fd, 0)
            if fd != 1:
                os.dup2(fd, 1)
            if fd != 2:
                os.dup2(fd, 2)
            if fd not in (0, 1, 2):
                os.close(fd)

            # Make us a daemon. Yes, that's all it takes.
            pid = os.fork()
            if pid > 0:
                os._exit(0)
            os.setsid()
            os.chdir("/")
            pid = os.fork()
            if pid > 0:
                os._exit(0)

            # Start the command
            p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=lambda: os.close(pipe[1]))
            stdout = ""
            stderr = ""
            fds = [p.stdout, p.stderr]
            # Wait for all output, or until the main process is dead and its output is done.
            while fds:
                rfd, wfd, efd = select.select(fds, [], fds, 1)
                if not (rfd + wfd + efd) and p.poll() is not None:
                    break
                if p.stdout in rfd:
                    dat = os.read(p.stdout.fileno(), 4096)
                    if not dat:
                        fds.remove(p.stdout)
223
                    stdout += dat
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
                if p.stderr in rfd:
                    dat = os.read(p.stderr.fileno(), 4096)
                    if not dat:
                        fds.remove(p.stderr)
                    stderr += dat
            p.wait()
            # Return a JSON blob to parent
            os.write(pipe[1], json.dumps([p.returncode, stdout, stderr]))
            os.close(pipe[1])
            os._exit(0)
        elif pid == -1:
            self.module.fail_json(msg="unable to fork")
        else:
            os.close(pipe[1])
            os.waitpid(pid, 0)
            # Wait for data from daemon process and process it.
            data = ""
            while True:
                rfd, wfd, efd = select.select([pipe[0]], [], [pipe[0]])
                if pipe[0] in rfd:
                    dat = os.read(pipe[0], 4096)
                    if not dat:
                        break
                    data += dat
            return json.loads(data)
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271

    def check_ps(self):
        # Set ps flags
        if platform.system() == 'SunOS':
            psflags = '-ef'
        else:
            psflags = 'auxww'

        # Find ps binary
        psbin = self.module.get_bin_path('ps', True)

        (rc, psout, pserr) = self.execute_command('%s %s' % (psbin, psflags))
        # If rc is 0, set running as appropriate
        if rc == 0:
            self.running = False
            lines = psout.split("\n")
            for line in lines:
                if self.pattern in line and not "pattern=" in line:
                    # so as to not confuse ./hacking/test-module
                    self.running = True
                    break

    def check_service_changed(self):
272
        if self.state and self.running is None:
273
            self.module.fail_json(msg="failed determining service state, possible typo of service name?")
274
        # Find out if state has changed
275
        if not self.running and self.state in ["started", "running", "reloaded"]:
276
            self.svc_change = True
277
        elif self.running and self.state in ["stopped","reloaded"]:
278
            self.svc_change = True
279
        elif self.state == "restarted":
280 281
            self.svc_change = True
        if self.module.check_mode and self.svc_change:
282
            self.module.exit_json(changed=True, msg='service state changed')
283 284

    def modify_service_state(self):
285

286
        # Only do something if state will change
287
        if self.svc_change:
288 289 290
            # Control service
            if self.state in ['started', 'running']:
                self.action = "start"
291 292
            elif not self.running and self.state == 'reloaded':
                self.action = "start"
293 294 295 296 297 298 299
            elif self.state == 'stopped':
                self.action = "stop"
            elif self.state == 'reloaded':
                self.action = "reload"
            elif self.state == 'restarted':
                self.action = "restart"

300 301 302
            if self.module.check_mode:
                self.module.exit_json(changed=True, msg='changing service state')

303 304 305 306 307 308 309 310 311
            return self.service_control()

        else:
            # If nothing needs to change just say all is well
            rc = 0
            err = ''
            out = ''
            return rc, out, err

312 313 314 315 316 317 318 319 320
    def service_enable_rcconf(self):
        if self.rcconf_file is None or self.rcconf_key is None or self.rcconf_value is None:
            self.module.fail_json(msg="service_enable_rcconf() requires rcconf_file, rcconf_key and rcconf_value")

        changed = None
        entry = '%s="%s"\n' % (self.rcconf_key, self.rcconf_value)
        RCFILE = open(self.rcconf_file, "r")
        new_rc_conf = []

321
        # Build a list containing the possibly modified file.
322
        for rcline in RCFILE:
323 324 325 326
            # Parse line removing whitespaces, quotes, etc.
            rcarray = shlex.split(rcline, comments=True)
            if len(rcarray) >= 1 and '=' in rcarray[0]:
                (key, value) = rcarray[0].split("=", 1)
327 328 329 330 331
                if key == self.rcconf_key:
                    if value == self.rcconf_value:
                        # Since the proper entry already exists we can stop iterating.
                        changed = False
                        break
332
                    else:
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
                        # We found the key but the value is wrong, replace with new entry.
                        rcline = entry
                        changed = True

            # Add line to the list.
            new_rc_conf.append(rcline)

        # We are done with reading the current rc.conf, close it.
        RCFILE.close()

        # If we did not see any trace of our entry we need to add it.
        if changed is None:
            new_rc_conf.append(entry)
            changed = True

        if changed is True:
349 350 351 352

            if self.module.check_mode:
                self.module.exit_json(changed=True, msg="changing service enablement")

353 354 355 356 357 358 359 360 361 362 363 364 365 366
            # Create a temporary file next to the current rc.conf (so we stay on the same filesystem).
            # This way the replacement operation is atomic.
            rcconf_dir = os.path.dirname(self.rcconf_file)
            rcconf_base = os.path.basename(self.rcconf_file)
            (TMP_RCCONF, tmp_rcconf_file) = tempfile.mkstemp(dir=rcconf_dir, prefix="%s-" % rcconf_base)

            # Write out the contents of the list into our temporary file.
            for rcline in new_rc_conf:
                os.write(TMP_RCCONF, rcline)

            # Close temporary file.
            os.close(TMP_RCCONF)

            # Replace previous rc.conf.
367
            self.module.atomic_move(tmp_rcconf_file, self.rcconf_file)
368

369 370 371 372 373 374 375 376 377 378 379 380 381 382
# ===========================================
# Subclass: Linux

class LinuxService(Service):
    """
    This is the Linux Service manipulation class - it is currently supporting
    a mixture of binaries and init scripts for controlling services started at
    boot, as well as for controlling the current state.
    """

    platform = 'Linux'
    distribution = None

    def get_service_tools(self):
383

384
        paths = [ '/sbin', '/usr/sbin', '/bin', '/usr/bin' ]
385
        binaries = [ 'service', 'chkconfig', 'update-rc.d', 'rc-service', 'rc-update', 'initctl', 'systemctl', 'start', 'stop', 'restart' ]
386 387 388 389 390 391 392 393
        initpaths = [ '/etc/init.d' ]
        location = dict()

        for binary in binaries:
            location[binary] = None
        for binary in binaries:
            location[binary] = self.module.get_bin_path(binary)

394 395 396 397 398
        def check_systemd(name):
            # verify service is managed by systemd
            if not location.get('systemctl', None):
                return False

399 400 401 402 403 404 405 406 407
            # default to .service if the unit type is not specified
            if name.find('.') > 0:
                unit_name, unit_type = name.rsplit('.', 1)
                if unit_type not in ("service", "socket", "device", "mount", "automount",
                                     "swap", "target", "path", "timer", "snapshot"):
                    name = "%s.service" % name
            else:
                name = "%s.service" % name

408 409 410 411 412
            rc, out, err = self.execute_command("%s list-unit-files" % (location['systemctl']))

            # adjust the service name to account for template service unit files
            index = name.find('@')
            if index != -1:
413
                name = name[:index+1]
414

415
            self.__systemd_unit = None
416
            for line in out.splitlines():
417 418
                if line.startswith(name):
                    self.__systemd_unit = name
419 420 421
                    return True
            return False

422
        # Locate a tool for enable options
423
        if location.get('chkconfig', None) and os.path.exists("/etc/init.d/%s" % self.name):
424 425 426 427 428 429
            if check_systemd(self.name):
                # service is managed by systemd
                self.enable_cmd = location['systemctl']
            else:
                # we are using a standard SysV service
                self.enable_cmd = location['chkconfig']
430 431 432 433
        elif location.get('update-rc.d', None):
            if check_systemd(self.name):
                # service is managed by systemd
                self.enable_cmd = location['systemctl']
434
            elif location['initctl'] and os.path.exists("/etc/init/%s.conf" % self.name):
435
                # service is managed by upstart
436
                self.enable_cmd = location['initctl']
437
            elif location['update-rc.d'] and os.path.exists("/etc/init.d/%s" % self.name):
438 439
                # service is managed by with SysV init scripts, but with update-rc.d
                self.enable_cmd = location['update-rc.d']
440
            else:
441
                self.module.fail_json(msg="service not found: %s" % self.name)
442 443 444 445 446
        elif location.get('rc-service', None) and not location.get('systemctl', None):
            # service is managed by OpenRC
            self.svc_cmd = location['rc-service']
            self.enable_cmd = location['rc-update']
            return
447 448 449
        elif check_systemd(self.name):
            # service is managed by systemd
            self.enable_cmd = location['systemctl']
450

451
        # Locate a tool for runtime service management (start, stop etc.)
452 453
        if location.get('service', None) and os.path.exists("/etc/init.d/%s" % self.name):
            # SysV init script
454
            self.svc_cmd = location['service']
455 456 457
        elif location.get('start', None) and os.path.exists("/etc/init/%s.conf" % self.name):
            # upstart -- rather than being managed by one command, start/stop/restart are actual commands
            self.svc_cmd = ''
458
        else:
459
            # still a SysV init script, but /sbin/service isn't installed
460 461 462 463 464
            for initdir in initpaths:
                initscript = "%s/%s" % (initdir,self.name)
                if os.path.isfile(initscript):
                    self.svc_initscript = initscript

465
        # couldn't find anything yet, assume systemd
466
        if self.svc_cmd is None and self.svc_initscript is None:
467 468 469
            if location.get('systemctl'):
                self.svc_cmd = location['systemctl']

470
        if self.svc_cmd is None and not self.svc_initscript:
Michael DeHaan committed
471
            self.module.fail_json(msg='cannot find \'service\' binary or init script for service,  possible typo in service name?, aborting')
472 473 474 475 476

        if location.get('initctl', None):
            self.svc_initctl = location['initctl']

    def get_service_status(self):
477 478
        self.action = "status"
        rc, status_stdout, status_stderr = self.service_control()
479

480
        # if we have decided the service is managed by upstart, we check for some additional output...
481
        if self.svc_initctl and self.running is None:
482 483 484 485 486 487 488
            # check the job status by upstart response
            initctl_rc, initctl_status_stdout, initctl_status_stderr = self.execute_command("%s status %s" % (self.svc_initctl, self.name))
            if initctl_status_stdout.find("stop/waiting") != -1:
                self.running = False
            elif initctl_status_stdout.find("start/running") != -1:
                self.running = True

489
        if self.svc_cmd and self.svc_cmd.endswith("rc-service") and self.running is None:
490 491 492 493
            openrc_rc, openrc_status_stdout, openrc_status_stderr = self.execute_command("%s %s status" % (self.svc_cmd, self.name))
            self.running = "started" in openrc_status_stdout
            self.crashed = "crashed" in openrc_status_stderr

494
        # if the job status is still not known check it by response code
495 496
        # For reference, see:
        # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
497
        if self.running is None:
498
            if rc in [1, 2, 3, 4, 69]:
499 500 501 502 503
                self.running = False
            elif rc == 0:
                self.running = True

        # if the job status is still not known check it by status output keywords
504
        if self.running is None:
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
            # first tranform the status output that could irritate keyword matching
            cleanout = status_stdout.lower().replace(self.name.lower(), '')
            if "stop" in cleanout:
                self.running = False
            elif "run" in cleanout and "not" in cleanout:
                self.running = False
            elif "run" in cleanout and "not" not in cleanout:
                self.running = True
            elif "start" in cleanout and "not" not in cleanout:
                self.running = True
            elif 'could not access pid file' in cleanout:
                self.running = False
            elif 'is dead and pid file exists' in cleanout:
                self.running = False
            elif 'dead but subsys locked' in cleanout:
                self.running = False
            elif 'dead but pid file exists' in cleanout:
                self.running = False

        # if the job status is still not known check it by special conditions
525
        if self.running is None:
526 527 528 529 530
            if self.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?
                self.running = True

531 532 533
        return self.running


534
    def service_enable(self):
535 536

        if self.enable_cmd is None:
537
            self.module.fail_json(msg='service name not recognized')
538

539 540 541
        # FIXME: we use chkconfig or systemctl
        # to decide whether to run the command here but need something
        # similar for upstart
542

543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
        if self.enable_cmd.endswith("initctl"):
            def write_to_override_file(file_name, file_contents, ):
                override_file = open(file_name, 'w')
                override_file.write(file_contents)
                override_file.close()

            initpath = '/etc/init'
            manreg = re.compile('^manual\s*$', re.M | re.I)
            conf_file_name = "%s/%s.conf" % (initpath, self.name)
            override_file_name = "%s/%s.override" % (initpath, self.name)

            # Check to see if files contain the manual line in .conf and fail if True
            if manreg.search(open(conf_file_name).read()):
                self.module.fail_json(msg="manual stanza not supported in a .conf file")

            if os.path.exists(override_file_name):
                override_file_contents = open(override_file_name).read()
                # Remove manual stanza if present and service enabled
                if self.enable and manreg.search(override_file_contents):
                    write_to_override_file(override_file_name, manreg.sub('', override_file_contents))
                # Add manual stanza if not present and service disabled
                elif not (self.enable) and not (manreg.search(override_file_contents)):
                    write_to_override_file(override_file_name, override_file_contents + '\nmanual\n')
                else:
                    return
            # Add file with manual stanza if service disabled
            elif not (self.enable):
                write_to_override_file(override_file_name, 'manual\n')
            else:
                return

574 575
        if self.enable_cmd.endswith("chkconfig"):
            (rc, out, err) = self.execute_command("%s --list %s" % (self.enable_cmd, self.name))
576 577 578
            if 'chkconfig --add %s' % self.name in err:
                self.execute_command("%s --add %s" % (self.enable_cmd, self.name))
                (rc, out, err) = self.execute_command("%s --list %s" % (self.enable_cmd, self.name))
579
            if not self.name in out:
580
                self.module.fail_json(msg="unknown service name")
581 582 583 584 585 586
            state = out.split()[-1]
            if self.enable and ( "3:on" in out and "5:on" in out ):
                return
            elif not self.enable and ( "3:off" in out and "5:off" in out ):
                return

587
        if self.enable_cmd.endswith("systemctl"):
588
            (rc, out, err) = self.execute_command("%s show %s" % (self.enable_cmd, self.__systemd_unit))
589 590 591 592 593 594 595 596

            d = dict(line.split('=', 1) for line in out.splitlines())
            if "UnitFileState" in d:
                if self.enable and d["UnitFileState"] == "enabled":
                    return
                elif not self.enable and d["UnitFileState"] == "disabled":
                    return
            elif not self.enable:
597 598
                return

599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
        if self.enable_cmd.endswith("rc-update"):
            (rc, out, err) = self.execute_command("%s show" % self.enable_cmd)
            for line in out.splitlines():
                service_name, runlevels = line.split('|')
                service_name = service_name.strip()
                if service_name != self.name:
                    continue
                runlevels = re.split(r'\s+', runlevels)
                # service already enabled for the runlevel
                if self.enable and self.runlevel in runlevels:
                    return
                # service already disabled for the runlevel
                elif not self.enable and self.runlevel not in runlevels:
                    return
                break
            else:
                # service already disabled altogether
                if not self.enable:
                    return

619
        if self.enable_cmd.endswith("update-rc.d"):
620 621 622 623
            if self.enable:
                action = 'enable'
            else:
                action = 'disable'
624

625 626 627 628 629 630 631
            (rc, out, err) = self.execute_command("%s -n %s %s" \
                                                  % (self.enable_cmd, self.name, action))
            self.changed = False
            for line in out.splitlines():
                if line.startswith('rename'):
                    self.changed = True
                    break
632 633 634 635 636 637
                elif self.enable and line.find('do not exist') != -1:
                    self.changed = True
                    break
                elif not self.enable and line.find('already exist') != -1:
                    self.changed = True
                    break
638

639 640 641 642 643 644
            # Debian compatibility
            for line in err.splitlines():
                if self.enable and line.find('no runlevel symlinks to modify') != -1:
                    self.changed = True
                    break

645
            if self.module.check_mode:
Brian Coca committed
646
                self.module.exit_json(changed=self.changed)
647 648 649 650

            if not self.changed:
                return

651
            if self.enable:
652
                # make sure the init.d symlinks are created
653 654 655 656 657 658 659 660 661
                # otherwise enable might not work
                (rc, out, err) = self.execute_command("%s %s defaults" \
                                                      % (self.enable_cmd, self.name))
                if rc != 0:
                    return (rc, out, err)

                return self.execute_command("%s %s enable" % (self.enable_cmd, self.name))
            else:
                return self.execute_command("%s -f %s remove" % (self.enable_cmd, self.name))
662

663 664 665 666 667
        # we change argument depending on real binary used:
        # - update-rc.d and systemctl wants enable/disable
        # - chkconfig wants on/off
        # - rc-update wants add/delete
        # also, rc-update and systemctl needs the argument order reversed
668 669 670
        if self.enable:
            on_off = "on"
            enable_disable = "enable"
671
            add_delete = "add"
672 673 674
        else:
            on_off = "off"
            enable_disable = "disable"
675
            add_delete = "delete"
676

677
        if self.enable_cmd.endswith("rc-update"):
678
            args = (self.enable_cmd, add_delete, self.name + " " + self.runlevel)
679
        elif self.enable_cmd.endswith("systemctl"):
680
            args = (self.enable_cmd, enable_disable, self.__systemd_unit)
681 682 683
        else:
            args = (self.enable_cmd, self.name, on_off)

684
        self.changed = True
685

686
        if self.module.check_mode and self.changed:
687
            self.module.exit_json(changed=True)
688

689
        return self.execute_command("%s %s %s" % args)
690

691

692 693 694
    def service_control(self):

        # Decide what command to run
695
        svc_cmd = ''
696
        arguments = self.arguments
697
        if self.svc_cmd:
698
            if not self.svc_cmd.endswith("systemctl"):
699
                # SysV and OpenRC take the form <cmd> <name> <action>
700 701 702 703
                svc_cmd = "%s %s" % (self.svc_cmd, self.name)
            else:
                # systemd commands take the form <cmd> <action> <name>
                svc_cmd = self.svc_cmd
704
                arguments = "%s %s" % (self.__systemd_unit, arguments)
705
        elif self.svc_initscript:
706
            # upstart
707 708
            svc_cmd = "%s" % self.svc_initscript

709 710
        # In OpenRC, if a service crashed, we need to reset its status to
        # stopped with the zap command, before we can start it back.
711
        if self.svc_cmd and self.svc_cmd.endswith('rc-service') and self.action == 'start' and self.crashed:
712 713
            self.execute_command("%s zap" % svc_cmd, daemonize=True)

714
        if self.action is not "restart":
715
            if svc_cmd != '':
716
                # upstart or systemd or OpenRC
717
                rc_state, stdout, stderr = self.execute_command("%s %s %s" % (svc_cmd, self.action, arguments), daemonize=True)
718
            else:
719 720
                # SysV
                rc_state, stdout, stderr = self.execute_command("%s %s %s" % (self.action, self.name, arguments), daemonize=True)
721
        elif self.svc_cmd and self.svc_cmd.endswith('rc-service'):
722 723
            # All services in OpenRC support restart.
            rc_state, stdout, stderr = self.execute_command("%s %s %s" % (svc_cmd, self.action, arguments), daemonize=True)
724
        else:
725
            # In other systems, not all services support restart. Do it the hard way.
726
            if svc_cmd != '':
727 728
                # upstart or systemd
                rc1, stdout1, stderr1 = self.execute_command("%s %s %s" % (svc_cmd, 'stop', arguments), daemonize=True)
729
            else:
730 731
                # SysV
                rc1, stdout1, stderr1 = self.execute_command("%s %s %s" % ('stop', self.name, arguments), daemonize=True)
732

733 734 735
            if self.sleep:
                time.sleep(self.sleep)

736
            if svc_cmd != '':
737 738
                # upstart or systemd
                rc2, stdout2, stderr2 = self.execute_command("%s %s %s" % (svc_cmd, 'start', arguments), daemonize=True)
739
            else:
740 741
                # SysV
                rc2, stdout2, stderr2 = self.execute_command("%s %s %s" % ('start', self.name, arguments), daemonize=True)
742 743

            # merge return information
744
            if rc1 != 0 and rc2 == 0:
745
                rc_state = rc2
746 747
                stdout = stdout2
                stderr = stderr2
748
            else:
749
                rc_state = rc1 + rc2
750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774
                stdout = stdout1 + stdout2
                stderr = stderr1 + stderr2

        return(rc_state, stdout, stderr)

# ===========================================
# Subclass: FreeBSD

class FreeBsdService(Service):
    """
    This is the FreeBSD Service manipulation class - it uses the /etc/rc.conf
    file for controlling services started at boot and the 'service' binary to
    check status and perform direct service manipulation.
    """

    platform = 'FreeBSD'
    distribution = None

    def get_service_tools(self):
        self.svc_cmd = self.module.get_bin_path('service', True)

        if not self.svc_cmd:
            self.module.fail_json(msg='unable to find service binary')

    def get_service_status(self):
775
        rc, stdout, stderr = self.execute_command("%s %s %s %s" % (self.svc_cmd, self.name, 'onestatus', self.arguments))
776 777
        if rc == 1:
            self.running = False
778
        elif rc == 0:
779 780 781 782
            self.running = True

    def service_enable(self):
        if self.enable:
783
            self.rcconf_value = "YES"
784
        else:
785
            self.rcconf_value = "NO"
786 787 788 789

        rcfiles = [ '/etc/rc.conf','/usr/local/etc/rc.conf' ]
        for rcfile in rcfiles:
            if os.path.isfile(rcfile):
790
                self.rcconf_file = rcfile
791

792
        rc, stdout, stderr = self.execute_command("%s %s %s %s" % (self.svc_cmd, self.name, 'rcvar', self.arguments))
793
        cmd = "%s %s %s %s" % (self.svc_cmd, self.name, 'rcvar', self.arguments)
794
        rcvars = shlex.split(stdout, comments=True)
795

796
        if not rcvars:
797
            self.module.fail_json(msg="unable to determine rcvar", stdout=stdout, stderr=stderr)
798 799

        # In rare cases, i.e. sendmail, rcvar can return several key=value pairs
800 801 802 803 804 805 806 807 808 809
        # Usually there is just one, however.  In other rare cases, i.e. uwsgi,
        # rcvar can return extra uncommented data that is not at all related to
        # the rcvar.  We will just take the first key=value pair we come across
        # and hope for the best.
        for rcvar in rcvars:
            if '=' in rcvar:
                self.rcconf_key = rcvar.split('=')[0]
                break

        if self.rcconf_key is None:
810
            self.module.fail_json(msg="unable to determine rcvar", stdout=stdout, stderr=stderr)
811 812

        return self.service_enable_rcconf()
813

814
    def service_control(self):
815

816 817 818 819 820 821 822
        if self.action is "start":
            self.action = "onestart"
        if self.action is "stop":
            self.action = "onestop"
        if self.action is "reload":
            self.action = "onereload"

823
        return self.execute_command("%s %s %s %s" % (self.svc_cmd, self.name, self.action, self.arguments))
824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854

# ===========================================
# Subclass: OpenBSD

class OpenBsdService(Service):
    """
    This is the OpenBSD Service manipulation class - it uses /etc/rc.d for
    service control. Enabling a service is currently not supported because the
    <service>_flags variable is not boolean, you should supply a rc.conf.local
    file in some other way.
    """

    platform = 'OpenBSD'
    distribution = None

    def get_service_tools(self):
        rcdir = '/etc/rc.d'

        rc_script = "%s/%s" % (rcdir, self.name)
        if os.path.isfile(rc_script):
            self.svc_cmd = rc_script

        if not self.svc_cmd:
            self.module.fail_json(msg='unable to find rc.d script')

    def get_service_status(self):
        rc, stdout, stderr = self.execute_command("%s %s" % (self.svc_cmd, 'check'))
        if rc == 1:
            self.running = False
        elif rc == 0:
            self.running = True
855

856 857
    def service_control(self):
        return self.execute_command("%s %s" % (self.svc_cmd, self.action))
858

859
# ===========================================
860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877
# Subclass: NetBSD

class NetBsdService(Service):
    """
    This is the NetBSD Service manipulation class - it uses the /etc/rc.conf
    file for controlling services started at boot, check status and perform
    direct service manipulation. Init scripts in /etc/rcd are used for
    controlling services (start/stop) as well as for controlling the current
    state.
    """

    platform = 'NetBSD'
    distribution = None

    def get_service_tools(self):
        initpaths = [ '/etc/rc.d' ]		# better: $rc_directories - how to get in here? Run: sh -c '. /etc/rc.conf ; echo $rc_directories'

        for initdir in initpaths:
878 879 880
            initscript = "%s/%s" % (initdir,self.name)
            if os.path.isfile(initscript):
                self.svc_initscript = initscript
881 882 883 884 885 886 887 888 889 890 891 892 893 894

        if not self.svc_initscript:
            self.module.fail_json(msg='unable to find rc.d script')

    def service_enable(self):
        if self.enable:
            self.rcconf_value = "YES"
        else:
            self.rcconf_value = "NO"

        rcfiles = [ '/etc/rc.conf' ]		# Overkill?
        for rcfile in rcfiles:
            if os.path.isfile(rcfile):
                self.rcconf_file = rcfile
895

Michael DeHaan committed
896
        self.rcconf_key = "%s" % string.replace(self.name,"-","_")
897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914

        return self.service_enable_rcconf()

    def get_service_status(self):
        self.svc_cmd = "%s" % self.svc_initscript
        rc, stdout, stderr = self.execute_command("%s %s" % (self.svc_cmd, 'onestatus'))
        if rc == 1:
            self.running = False
        elif rc == 0:
            self.running = True

    def service_control(self):
        if self.action is "start":
            self.action = "onestart"
        if self.action is "stop":
            self.action = "onestop"

        self.svc_cmd = "%s" % self.svc_initscript
915
        return self.execute_command("%s %s" % (self.svc_cmd, self.action), daemonize=True)
916

917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935
# ===========================================
# Subclass: SunOS
class SunOSService(Service):
    """
    This is the SunOS Service manipulation class - it uses the svcadm
    command for controlling services, and svcs command for checking status.
    It also tries to be smart about taking the service out of maintenance
    state if necessary.
    """
    platform = 'SunOS'
    distribution = None

    def get_service_tools(self):
        self.svcs_cmd = self.module.get_bin_path('svcs', True)

        if not self.svcs_cmd:
            self.module.fail_json(msg='unable to find svcs binary')

        self.svcadm_cmd = self.module.get_bin_path('svcadm', True)
936

937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967
        if not self.svcadm_cmd:
            self.module.fail_json(msg='unable to find svcadm binary')

    def get_service_status(self):
        status = self.get_sunos_svcs_status()
        # Only 'online' is considered properly running. Everything else is off
        # or has some sort of problem.
        if status == 'online':
            self.running = True
        else:
            self.running = False

    def get_sunos_svcs_status(self):
        rc, stdout, stderr = self.execute_command("%s %s" % (self.svcs_cmd, self.name))
        if rc == 1:
            if stderr:
                self.module.fail_json(msg=stderr)
            else:
                self.module.fail_json(msg=stdout)

        lines = stdout.rstrip("\n").split("\n")
        status = lines[-1].split(" ")[0]
        # status is one of: online, offline, degraded, disabled, maintenance, uninitialized
        # see man svcs(1)
        return status

    def service_enable(self):
        # Get current service enablement status
        rc, stdout, stderr = self.execute_command("%s -l %s" % (self.svcs_cmd, self.name))

        if rc != 0:
968
            if stderr:
969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986
                self.module.fail_json(msg=stderr)
            else:
                self.module.fail_json(msg=stdout)

        enabled = False
        temporary = False

        # look for enabled line, which could be one of:
        #    enabled   true (temporary)
        #    enabled   false (temporary)
        #    enabled   true
        #    enabled   false
        for line in stdout.split("\n"):
            if line.find("enabled") == 0:
                if line.find("true") != -1:
                    enabled = True
                if line.find("temporary") != -1:
                    temporary = True
987

988
        startup_enabled = (enabled and not temporary) or (not enabled and temporary)
989

990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010
        if self.enable and startup_enabled:
            return
        elif (not self.enable) and (not startup_enabled):
            return

        # Mark service as started or stopped (this will have the side effect of
        # actually stopping or starting the service)
        if self.enable:
            subcmd = "enable -rs"
        else:
            subcmd = "disable -s"

        rc, stdout, stderr = self.execute_command("%s %s %s" % (self.svcadm_cmd, subcmd, self.name))

        if rc != 0:
            if stderr:
                self.module.fail_json(msg=stderr)
            else:
                self.module.fail_json(msg=stdout)

        self.changed = True
1011 1012


1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
    def service_control(self):
        status = self.get_sunos_svcs_status()

        # if starting or reloading, clear maintenace states
        if self.action in ['start', 'reload', 'restart'] and status in ['maintenance', 'degraded']:
            rc, stdout, stderr = self.execute_command("%s clear %s" % (self.svcadm_cmd, self.name))
            if rc != 0:
                return rc, stdout, stderr
            status = self.get_sunos_svcs_status()

        if status in ['maintenance', 'degraded']:
            self.module.fail_json(msg="Failed to bring service out of %s status." % status)

        if self.action == 'start':
            subcmd = "enable -rst"
        elif self.action == 'stop':
            subcmd = "disable -st"
        elif self.action == 'reload':
            subcmd = "refresh"
        elif self.action == 'restart' and status == 'online':
            subcmd = "restart"
        elif self.action == 'restart' and status != 'online':
            subcmd = "enable -rst"
1036

1037 1038
        return self.execute_command("%s %s %s" % (self.svcadm_cmd, subcmd, self.name))

1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112
# ===========================================
# Subclass: AIX

class AIX(Service):
    """
    This is the AIX Service (SRC) manipulation class - it uses lssrc, startsrc, stopsrc
    and refresh for service control. Enabling a service is currently not supported.
    Would require to add an entry in the /etc/inittab file (mkitab, chitab and rmitab
    commands)
    """

    platform = 'AIX'
    distribution = None

    def get_service_tools(self):
        self.lssrc_cmd = self.module.get_bin_path('lssrc', True)

        if not self.lssrc_cmd:
            self.module.fail_json(msg='unable to find lssrc binary')

        self.startsrc_cmd = self.module.get_bin_path('startsrc', True)

        if not self.startsrc_cmd:
            self.module.fail_json(msg='unable to find startsrc binary')

        self.stopsrc_cmd = self.module.get_bin_path('stopsrc', True)

        if not self.stopsrc_cmd:
            self.module.fail_json(msg='unable to find stopsrc binary')

        self.refresh_cmd = self.module.get_bin_path('refresh', True)

        if not self.refresh_cmd:
            self.module.fail_json(msg='unable to find refresh binary')


    def get_service_status(self):
        status = self.get_aix_src_status()
        # Only 'active' is considered properly running. Everything else is off
        # or has some sort of problem.
        if status == 'active':
            self.running = True
        else:
            self.running = False

    def get_aix_src_status(self):
        rc, stdout, stderr = self.execute_command("%s -s %s" % (self.lssrc_cmd, self.name))
        if rc == 1:
            if stderr:
                self.module.fail_json(msg=stderr)
            else:
                self.module.fail_json(msg=stdout)

        lines = stdout.rstrip("\n").split("\n")
        status = lines[-1].split(" ")[-1]
        # status is one of: active, inoperative
        return status

    def service_control(self):
        if self.action == 'start':
            srccmd = self.startsrc_cmd
        elif self.action == 'stop':
            srccmd = self.stopsrc_cmd
        elif self.action == 'reload':
            srccmd = self.refresh_cmd
        elif self.action == 'restart':
            self.execute_command("%s -s %s" % (self.stopsrc_cmd, self.name))
            srccmd = self.startsrc_cmd

        if self.arguments and self.action == 'start':
            return self.execute_command("%s -a \"%s\" -s %s" % (srccmd, self.arguments, self.name))
        else:
            return self.execute_command("%s -s %s" % (srccmd, self.name))

1113 1114

# ===========================================
1115
# Main control flow
1116

Nikhil Singh committed
1117 1118 1119 1120
def main():
    module = AnsibleModule(
        argument_spec = dict(
            name = dict(required=True),
1121
            state = dict(choices=['running', 'started', 'stopped', 'restarted', 'reloaded']),
1122
            sleep = dict(required=False, type='int', default=None),
1123
            pattern = dict(required=False, default=None),
1124
            enabled = dict(type='bool'),
1125
            runlevel = dict(required=False, default='default'),
1126
            arguments = dict(aliases=['args'], default=''),
1127 1128
        ),
        supports_check_mode=True
Nikhil Singh committed
1129
    )
1130 1131
    if module.params['state'] is None and module.params['enabled'] is None:
        module.fail_json(msg="Neither 'state' nor 'enabled' set")
Nikhil Singh committed
1132

1133 1134 1135 1136 1137 1138 1139 1140
    service = Service(module)

    if service.syslogging:
        syslog.openlog('ansible-%s' % os.path.basename(__file__))
        syslog.syslog(syslog.LOG_NOTICE, 'Service instantiated - platform %s' % service.platform)
        if service.distribution:
            syslog.syslog(syslog.LOG_NOTICE, 'Service instantiated - distribution %s' % service.distribution)

1141
    rc = 0
Nikhil Singh committed
1142
    out = ''
1143 1144 1145
    err = ''
    result = {}
    result['name'] = service.name
1146

1147 1148 1149 1150
    # Find service management tools
    service.get_service_tools()

    # Enable/disable service startup at boot if requested
1151
    if service.module.params['enabled'] is not None:
1152 1153
        # FIXME: ideally this should detect if we need to toggle the enablement state, though
        # it's unlikely the changed handler would need to fire in this case so it's a minor thing.
1154
        service.service_enable()
1155 1156 1157 1158 1159 1160 1161 1162
        result['enabled'] = service.enable

    if module.params['state'] is None:
        # Not changing the running state, so bail out now.
        result['changed'] = service.changed
        module.exit_json(**result)

    result['state'] = service.state
1163 1164 1165 1166

    # Collect service status
    if service.pattern:
        service.check_ps()
1167 1168
    else:
        service.get_service_status()
1169 1170 1171 1172 1173 1174

    # Calculate if request will change service state
    service.check_service_changed()

    # Modify service state if necessary
    (rc, out, err) = service.modify_service_state()
1175

1176
    if rc != 0:
1177 1178 1179 1180 1181
        if err and err.find("is already") != -1:
            # upstart got confused, one such possibility is MySQL on Ubuntu 12.04
            # where status may report it has no start/stop links and we could
            # not get accurate status
            pass
1182
        else:
1183 1184 1185 1186
            if err:
                module.fail_json(msg=err)
            else:
                module.fail_json(msg=out)
1187

1188
    result['changed'] = service.changed | service.svc_change
1189
    if service.module.params['enabled'] is not None:
1190
        result['enabled'] = service.module.params['enabled']
1191

1192
    if not service.module.params['state']:
1193 1194
        status = service.get_service_status()
        if status is None:
1195
            result['state'] = 'absent'
1196 1197 1198 1199 1200 1201 1202
        elif status is False:
            result['state'] = 'started'
        else:
            result['state'] = 'stopped'
    else:
        # as we may have just bounced the service the service command may not
        # report accurate state at this moment so just show what we ran
1203
        if service.module.params['state'] in ['started','restarted','running','reloaded']:
1204 1205 1206
            result['state'] = 'started'
        else:
            result['state'] = 'stopped'
1207

1208
    module.exit_json(**result)
1209

1210
from ansible.module_utils.basic import *
Nikhil Singh committed
1211
main()