service 37 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
    pattern:
        required: false
44
        version_added: "0.7"
45
        description:
46 47 48
        - 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,
49
          the service will be assumed to be running.
50 51
    enabled:
        required: false
52
        choices: [ "yes", "no" ]
53
        description:
54 55 56
        - Whether the service should start on boot. At least one of state and
          enabled are required.

57 58 59 60
    arguments:
        description:
        - Additional arguments provided on the command line
        aliases: [ 'args' ]
61
examples:
62 63 64 65 66 67 68 69
    - description: Example action to start service httpd, if not running
      code: "service: name=httpd state=started"
    - description: Example action to stop service httpd, if running
      code: "service: name=httpd state=stopped"
    - description: Example action to restart service httpd, in all cases
      code: "service: name=httpd state=restarted"
    - description: Example action to reload service httpd, in all cases
      code: "service: name=httpd state=reloaded"
70 71
    - description: Example action to enable service httpd, and not touch the running state
      code: "service: name=httpd enabled=yes"
72 73 74 75
    - description: Example action to start service foo, based on running process /usr/bin/foo
      code: "service: name=foo pattern=/usr/bin/foo state=started"
    - description: Example action to restart network service for interface eth0
      code: "service: name=network state=restarted args=eth0"
76 77
'''

78
import platform
79
import os
80
import tempfile
81
import shlex
82
import select
83

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
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']
        self.pattern        = module.params['pattern']
109
        self.enable         = module.params['enabled']
110 111 112 113 114 115 116 117
        self.changed        = False
        self.running        = None
        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', '')
118 119 120
        self.rcconf_file    = None
        self.rcconf_key     = None
        self.rcconf_value   = None
121 122 123 124

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

125 126
    # ===========================================
    # Platform specific methods (must be replaced by subclass).
127 128 129 130 131 132 133 134 135 136 137 138 139

    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")

140 141
    # ===========================================
    # Generic methods that should be used on all platforms.
142

143
    def execute_command(self, cmd, daemonize=False):
144 145
        if self.syslogging:
            syslog.openlog('ansible-%s' % os.path.basename(__file__))
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
            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)
195
                    stdout += dat
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
                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)
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243

    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):
244
        if self.state and self.running is None:
245
            self.module.fail_json(msg="failed determining service state, possible typo of service name?")
246 247 248 249 250 251 252
        # Find out if state has changed
        if not self.running and self.state in ["started", "running"]:
            self.changed = True
        elif self.running and self.state in ["stopped","reloaded"]:
            self.changed = True
        elif self.state == "restarted":
            self.changed = True
253 254
        if self.module.check_mode and self.changed:
            self.module.exit_json(changed=True, msg='service state changed')
255 256

    def modify_service_state(self):
257

258 259 260 261 262 263 264 265 266 267 268 269
        # Only do something if state will change
        if self.changed:
            # Control service
            if self.state in ['started', 'running']:
                self.action = "start"
            elif self.state == 'stopped':
                self.action = "stop"
            elif self.state == 'reloaded':
                self.action = "reload"
            elif self.state == 'restarted':
                self.action = "restart"

270 271 272
            if self.module.check_mode:
                self.module.exit_json(changed=True, msg='changing service state')

273 274 275 276 277 278 279 280 281
            return self.service_control()

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

282 283 284 285 286 287 288 289 290
    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 = []

291
        # Build a list containing the possibly modified file.
292
        for rcline in RCFILE:
293 294 295 296
            # 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)
297 298 299 300 301
                if key == self.rcconf_key:
                    if value == self.rcconf_value:
                        # Since the proper entry already exists we can stop iterating.
                        changed = False
                        break
302
                    else:
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
                        # 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:
319 320 321 322

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

323 324 325 326 327 328 329 330 331 332 333 334 335 336
            # 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.
337
            self.module.atomic_move(tmp_rcconf_file, self.rcconf_file)
338

339 340 341 342 343 344 345 346 347 348 349 350 351 352
# ===========================================
# 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):
353

354
        paths = [ '/sbin', '/usr/sbin', '/bin', '/usr/bin' ]
355
        binaries = [ 'service', 'chkconfig', 'update-rc.d', 'initctl', 'systemctl', 'start', 'stop', 'restart' ]
356 357 358 359 360 361 362 363 364
        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)

        # Locate a tool for enable options
365 366
        if location.get('chkconfig', None) and os.path.exists("/etc/init.d/%s" % self.name):
            # we are using a standard SysV service
367
            self.enable_cmd = location['chkconfig']
368 369
        elif location.get('update-rc.d', None) and os.path.exists("/etc/init/%s.conf" % self.name):
            # service is managed by upstart
370
            self.enable_cmd = location['update-rc.d']
371 372 373
        elif location.get('update-rc.d', None) and os.path.exists("/etc/init.d/%s" % self.name):
            # service is managed by with SysV init scripts, but with update-rc.d
            self.enable_cmd = location['update-rc.d']
374
        elif location.get('systemctl', None):
375

376
            # verify service is managed by systemd
377 378 379 380 381 382 383 384 385 386 387
            rc, out, err = self.execute_command("%s list-unit-files" % (location['systemctl']))

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

            look_for = "%s.service" % name
            for line in out.splitlines():
388 389 390
                if line.startswith(look_for):
                    self.enable_cmd = location['systemctl']
                    break
391

392
        # Locate a tool for runtime service management (start, stop etc.)
393 394
        if location.get('service', None) and os.path.exists("/etc/init.d/%s" % self.name):
            # SysV init script
395
            self.svc_cmd = location['service']
396 397 398
        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 = ''
399
        else:
400
            # still a SysV init script, but /sbin/service isn't installed
401 402 403 404 405
            for initdir in initpaths:
                initscript = "%s/%s" % (initdir,self.name)
                if os.path.isfile(initscript):
                    self.svc_initscript = initscript

406
        # couldn't find anything yet, assume systemd
407
        if self.svc_cmd is None and self.svc_initscript is None:
408 409 410
            if location.get('systemctl'):
                self.svc_cmd = location['systemctl']

411
        if self.svc_cmd is None and not self.svc_initscript:
Michael DeHaan committed
412
            self.module.fail_json(msg='cannot find \'service\' binary or init script for service,  possible typo in service name?, aborting')
413 414 415 416 417

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

    def get_service_status(self):
418 419
        self.action = "status"
        rc, status_stdout, status_stderr = self.service_control()
420

421
        # if we have decided the service is managed by upstart, we check for some additional output...
422
        if self.svc_initctl and self.running is None:
423 424 425 426 427 428 429 430
            # 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

        # if the job status is still not known check it by response code
431 432
        # For reference, see:
        # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
433
        if self.running is None:
434
            if rc in [1, 2, 3, 4, 69]:
435 436 437 438 439
                self.running = False
            elif rc == 0:
                self.running = True

        # if the job status is still not known check it by status output keywords
440
        if self.running is None:
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
            # 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
461
        if self.running is None:
462 463 464 465 466
            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

467 468 469
        return self.running


470
    def service_enable(self):
471 472

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

475 476 477
        # FIXME: we use chkconfig or systemctl
        # to decide whether to run the command here but need something
        # similar for upstart
478 479 480

        if self.enable_cmd.endswith("chkconfig"):
            (rc, out, err) = self.execute_command("%s --list %s" % (self.enable_cmd, self.name))
481 482 483
            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))
484
            if not self.name in out:
485
                self.module.fail_json(msg="unknown service name")
486 487 488 489 490 491
            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

492
        if self.enable_cmd.endswith("systemctl"):
493 494 495 496 497 498 499 500 501
            (rc, out, err) = self.execute_command("%s show %s.service" % (self.enable_cmd, self.name))

            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:
502 503
                return

504 505 506
        # we change argument depending on real binary used
        # update-rc.d wants enable/disable while
        # chkconfig wants on/off
507
        # also, systemctl needs the argument order reversed
508 509 510 511 512 513 514 515 516 517 518 519 520 521
        if self.enable:
            on_off = "on"
            enable_disable = "enable"
        else:
            on_off = "off"
            enable_disable = "disable"

        if self.enable_cmd.endswith("update-rc.d"):
            args = (self.enable_cmd, self.name, enable_disable)
        elif self.enable_cmd.endswith("systemctl"):
            args = (self.enable_cmd, enable_disable, self.name + ".service")
        else:
            args = (self.enable_cmd, self.name, on_off)

522
        self.changed = True
523

524
        if self.module.check_mode and self.changed:
525
            self.module.exit_json(changed=True)
526

527
        return self.execute_command("%s %s %s" % args)
528

529

530 531 532
    def service_control(self):

        # Decide what command to run
533
        svc_cmd = ''
534
        arguments = self.arguments
535
        if self.svc_cmd:
536
            if not self.svc_cmd.endswith("systemctl"):
537
                # SysV take the form <cmd> <name> <action>
538 539 540 541
                svc_cmd = "%s %s" % (self.svc_cmd, self.name)
            else:
                # systemd commands take the form <cmd> <action> <name>
                svc_cmd = self.svc_cmd
542
                arguments = "%s %s" % (self.name, arguments)
543
        elif self.svc_initscript:
544
            # upstart
545 546 547
            svc_cmd = "%s" % self.svc_initscript

        if self.action is not "restart":
548
            if svc_cmd != '':
549 550
                # upstart or systemd
                rc_state, stdout, stderr = self.execute_command("%s %s %s" % (svc_cmd, self.action, arguments), daemonize=True)
551
            else:
552 553
                # SysV
                rc_state, stdout, stderr = self.execute_command("%s %s %s" % (self.action, self.name, arguments), daemonize=True)
554
        else:
555 556
            # not all services support restart. Do it the hard way.
            if svc_cmd != '':
557 558
                # upstart or systemd
                rc1, stdout1, stderr1 = self.execute_command("%s %s %s" % (svc_cmd, 'stop', arguments), daemonize=True)
559
            else:
560 561
                # SysV
                rc1, stdout1, stderr1 = self.execute_command("%s %s %s" % ('stop', self.name, arguments), daemonize=True)
562

563
            if svc_cmd != '':
564 565
                # upstart or systemd
                rc2, stdout2, stderr2 = self.execute_command("%s %s %s" % (svc_cmd, 'start', arguments), daemonize=True)
566
            else:
567 568
                # SysV
                rc2, stdout2, stderr2 = self.execute_command("%s %s %s" % ('start', self.name, arguments), daemonize=True)
569 570

            # merge return information
571
            if rc1 != 0 and rc2 == 0:
572
                rc_state = rc2
573 574
                stdout = stdout2
                stderr = stderr2
575
            else:
576
                rc_state = rc1 + rc2
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
                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):
602
        rc, stdout, stderr = self.execute_command("%s %s %s %s" % (self.svc_cmd, self.name, 'onestatus', self.arguments))
603 604
        if rc == 1:
            self.running = False
605
        elif rc == 0:
606 607 608 609
            self.running = True

    def service_enable(self):
        if self.enable:
610
            self.rcconf_value = "YES"
611
        else:
612
            self.rcconf_value = "NO"
613 614 615 616

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

619 620 621
        self.rcconf_key = "%s_enable" % self.name

        return self.service_enable_rcconf()
622

623
    def service_control(self):
624

625 626 627 628 629 630 631
        if self.action is "start":
            self.action = "onestart"
        if self.action is "stop":
            self.action = "onestop"
        if self.action is "reload":
            self.action = "onereload"

632
        return self.execute_command("%s %s %s %s" % (self.svc_cmd, self.name, self.action, self.arguments))
633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663

# ===========================================
# 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
664

665 666
    def service_control(self):
        return self.execute_command("%s %s" % (self.svc_cmd, self.action))
667

668
# ===========================================
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
# 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:
687 688 689
            initscript = "%s/%s" % (initdir,self.name)
            if os.path.isfile(initscript):
                self.svc_initscript = initscript
690 691 692 693 694 695 696 697 698 699 700 701 702 703

        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
704

705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
        self.rcconf_key = "%s" % self.name

        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
724
        return self.execute_command("%s %s" % (self.svc_cmd, self.action), daemonize=True)
725

726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
# ===========================================
# 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)
745

746 747 748 749 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 775 776
        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:
777
            if stderr:
778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795
                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
796

797
        startup_enabled = (enabled and not temporary) or (not enabled and temporary)
798

799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819
        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
820 821


822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844
    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"
845

846 847
        return self.execute_command("%s %s %s" % (self.svcadm_cmd, subcmd, self.name))

848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921
# ===========================================
# 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))

922 923

# ===========================================
924
# Main control flow
925

Nikhil Singh committed
926 927 928 929
def main():
    module = AnsibleModule(
        argument_spec = dict(
            name = dict(required=True),
930
            state = dict(choices=['running', 'started', 'stopped', 'restarted', 'reloaded']),
931
            pattern = dict(required=False, default=None),
932
            enabled = dict(choices=BOOLEANS, type='bool'),
933
            arguments = dict(aliases=['args'], default=''),
934 935
        ),
        supports_check_mode=True
Nikhil Singh committed
936
    )
937 938
    if module.params['state'] is None and module.params['enabled'] is None:
        module.fail_json(msg="Neither 'state' nor 'enabled' set")
Nikhil Singh committed
939

940 941 942 943 944 945 946 947
    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)

948
    rc = 0
Nikhil Singh committed
949
    out = ''
950 951 952
    err = ''
    result = {}
    result['name'] = service.name
953

954 955 956 957
    # Find service management tools
    service.get_service_tools()

    # Enable/disable service startup at boot if requested
958
    if service.module.params['enabled'] is not None:
959 960
        # 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.
961
        service.service_enable()
962 963 964 965 966 967 968 969
        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
970 971 972 973

    # Collect service status
    if service.pattern:
        service.check_ps()
974 975
    else:
        service.get_service_status()
976 977 978 979 980 981

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

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

983
    if rc != 0:
984 985 986 987 988
        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
989
        else:
990 991 992 993
            if err:
                module.fail_json(msg=err)
            else:
                module.fail_json(msg=out)
994

995
    result['changed'] = service.changed
996
    if service.module.params['enabled'] is not None:
997
        result['enabled'] = service.module.params['enabled']
998

999
    if not service.module.params['state']:
1000 1001
        status = service.get_service_status()
        if status is None:
1002
            result['state'] = 'absent'
1003 1004 1005 1006 1007 1008 1009
        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
1010
        if service.module.params['state'] in ['started','restarted','running']:
1011 1012 1013
            result['state'] = 'started'
        else:
            result['state'] = 'stopped'
1014

1015
    module.exit_json(**result)
1016 1017 1018

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

Nikhil Singh committed
1020
main()