# This code is part of Ansible, but is an independent component. # This particular file snippet, and this file snippet only, is BSD licensed. # Modules you write using this snippet, which is embedded dynamically by Ansible # still belong to the author of the module, and may assign their own license # to the complete work. # # Copyright (c) Ansible Inc, 2016 # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import (absolute_import, division, print_function) __metaclass__ = type import glob import os import pickle import platform import select import shlex import subprocess import traceback from ansible.module_utils.six import PY2, b from ansible.module_utils._text import to_bytes, to_text def sysv_is_enabled(name, runlevel=None): ''' This function will check if the service name supplied is enabled in any of the sysv runlevels :arg name: name of the service to test for :kw runlevel: runlevel to check (default: None) ''' if runlevel: if not os.path.isdir('/etc/rc0.d/'): return bool(glob.glob('/etc/init.d/rc%s.d/S??%s' % (runlevel, name))) return bool(glob.glob('/etc/rc%s.d/S??%s' % (runlevel, name))) else: if not os.path.isdir('/etc/rc0.d/'): return bool(glob.glob('/etc/init.d/rc?.d/S??%s' % name)) return bool(glob.glob('/etc/rc?.d/S??%s' % name)) def get_sysv_script(name): ''' This function will return the expected path for an init script corresponding to the service name supplied. :arg name: name or path of the service to test for ''' if name.startswith('/'): result = name else: result = '/etc/init.d/%s' % name return result def sysv_exists(name): ''' This function will return True or False depending on the existence of an init script corresponding to the service name supplied. :arg name: name of the service to test for ''' return os.path.exists(get_sysv_script(name)) def get_ps(module, pattern): ''' Last resort to find a service by trying to match pattern to programs in memory ''' found = False if platform.system() == 'SunOS': flags = '-ef' else: flags = 'auxww' psbin = module.get_bin_path('ps', True) (rc, psout, pserr) = module.run_command([psbin, flags]) if rc == 0: for line in psout.splitlines(): if pattern in line: # FIXME: should add logic to prevent matching 'self', though that should be extremely rare found = True break return found def fail_if_missing(module, found, service, msg=''): ''' This function will return an error or exit gracefully depending on check mode status and if the service is missing or not. :arg module: is an AnsibleModule object, used for it's utility methods :arg found: boolean indicating if services was found or not :arg service: name of service :kw msg: extra info to append to error/success msg when missing ''' if not found: module.fail_json(msg='Could not find the requested service %s: %s' % (service, msg)) def fork_process(): ''' This function performs the double fork process to detach from the parent process and execute. ''' pid = os.fork() if pid == 0: # Set stdin/stdout/stderr to /dev/null fd = os.open(os.devnull, os.O_RDWR) # clone stdin/out/err for num in range(3): if fd != num: os.dup2(fd, num) # close otherwise if fd not in range(3): os.close(fd) # Make us a daemon pid = os.fork() # end if not in child if pid > 0: os._exit(0) # get new process session and detach sid = os.setsid() if sid == -1: raise Exception("Unable to detach session while daemonizing") # avoid possible problems with cwd being removed os.chdir("/") pid = os.fork() if pid > 0: os._exit(0) return pid def daemonize(module, cmd): ''' Execute a command while detaching as a daemon, returns rc, stdout, and stderr. :arg module: is an AnsibleModule object, used for it's utility methods :arg cmd: is a list or string representing the command and options to run 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. ''' # init some vars chunk = 4096 # FIXME: pass in as arg? errors = 'surrogate_or_strict' # start it! try: pipe = os.pipe() pid = fork_process() except OSError: module.fail_json(msg="Error while attempting to fork: %s", exception=traceback.format_exc()) except Exception as exc: module.fail_json(msg=to_text(exc), exception=traceback.format_exc()) # we don't do any locking as this should be a unique module/process if pid == 0: os.close(pipe[0]) # if command is string deal with py2 vs py3 conversions for shlex if not isinstance(cmd, list): if PY2: cmd = shlex.split(to_bytes(cmd, errors=errors)) else: cmd = shlex.split(to_text(cmd, errors=errors)) # make sure we always use byte strings run_cmd = [] for c in cmd: run_cmd.append(to_bytes(c, errors=errors)) # execute the command in forked process p = subprocess.Popen(run_cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=lambda: os.close(pipe[1])) fds = [p.stdout, p.stderr] # loop reading output till its done output = {p.stdout: b(""), p.stderr: b("")} while fds: rfd, wfd, efd = select.select(fds, [], fds, 1) if (rfd + wfd + efd) or p.poll(): for out in list(fds): if out in rfd: data = os.read(out.fileno(), chunk) if not data: fds.remove(out) output[out] += b(data) # even after fds close, we might want to wait for pid to die p.wait() # Return a pickled data of parent return_data = pickle.dumps([p.returncode, to_text(output[p.stdout]), to_text(output[p.stderr])], protocol=pickle.HIGHEST_PROTOCOL) os.write(pipe[1], to_bytes(return_data, errors=errors)) # clean up os.close(pipe[1]) os._exit(0) elif pid == -1: module.fail_json(msg="Unable to fork, no exception thrown, probably due to lack of resources, check logs.") else: # in parent os.close(pipe[1]) os.waitpid(pid, 0) # Grab response data after child finishes return_data = b("") while True: rfd, wfd, efd = select.select([pipe[0]], [], [pipe[0]]) if pipe[0] in rfd: data = os.read(pipe[0], chunk) if not data: break return_data += b(data) # Note: no need to specify encoding on py3 as this module sends the # pickle to itself (thus same python interpreter so we aren't mixing # py2 and py3) return pickle.loads(to_bytes(return_data, errors=errors)) def check_ps(module, pattern): # Set ps flags if platform.system() == 'SunOS': psflags = '-ef' else: psflags = 'auxww' # Find ps binary psbin = module.get_bin_path('ps', True) (rc, out, err) = module.run_command('%s %s' % (psbin, psflags)) # If rc is 0, set running as appropriate if rc == 0: for line in out.split('\n'): if pattern in line: return True return False