#!/usr/bin/python # (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/>. try: import json except ImportError: import simplejson as json import os import sys import shlex import subprocess import shutil import stat import grp import pwd try: import selinux HAVE_SELINUX=True except ImportError: HAVE_SELINUX=False def debug(msg): # ansible ignores stderr, so it's safe to use for debug # print >>sys.stderr, msg pass def exit_json(rc=0, **kwargs): if 'path' in kwargs: debug("adding path info") add_path_info(kwargs) print json.dumps(kwargs) sys.exit(rc) def fail_json(**kwargs): kwargs['failed'] = True exit_json(rc=1, **kwargs) def add_path_info(kwargs): path = kwargs['path'] if os.path.exists(path): (user, group) = user_and_group(path) kwargs['user'] = user kwargs['group'] = group st = os.stat(path) kwargs['mode'] = oct(stat.S_IMODE(st[stat.ST_MODE])) # secontext not yet supported if os.path.islink(path): kwargs['state'] = 'link' elif os.path.isfile(path): kwargs['state'] = 'file' else: kwargs['state'] = 'directory' if HAVE_SELINUX and selinux_enabled(): kwargs['secontext'] = ':'.join(selinux_context(path)) else: kwargs['state'] = 'absent' return kwargs # Detect whether using selinux that is MLS-aware. # While this means you can set the level/range with # selinux.lsetfilecon(), it may or may not mean that you # will get the selevel as part of the context returned # by selinux.lgetfilecon(). def selinux_mls_enabled(): if not HAVE_SELINUX: return False if selinux.is_selinux_mls_enabled() == 1: debug('selinux mls is enabled') return True else: debug('selinux mls is disabled') return False def selinux_enabled(): if not HAVE_SELINUX: return False if selinux.is_selinux_enabled() == 1: debug('selinux is enabled') return True else: debug('selinux is disabled') return False # Determine whether we need a placeholder for selevel/mls def selinux_initial_context(): context = [None, None, None] if selinux_mls_enabled(): context.append(None) return context # If selinux fails to find a default, return an array of None def selinux_default_context(path, mode=0): context = selinux_initial_context() if not HAVE_SELINUX or not selinux_enabled(): return context try: ret = selinux.matchpathcon(path, mode) except OSError: debug("no default context available") return context if ret[0] == -1: debug("no default context available") return context context = ret[1].split(':') debug("got default secontext=%s" % ret[1]) return context def selinux_context(path): context = selinux_initial_context() if not HAVE_SELINUX or not selinux_enabled(): return context try: ret = selinux.lgetfilecon(path) except: fail_json(path=path, msg='failed to retrieve selinux context') if ret[0] == -1: return context context = ret[1].split(':') debug("got current secontext=%s" % ret[1]) return context # =========================================== argfile = sys.argv[1] args = open(argfile, 'r').read() items = shlex.split(args) if not len(items): fail_json(msg='the module requires arguments -a') sys.exit(1) params = {} for x in items: (k, v) = x.split("=") params[k] = v state = params.get('state','file') path = params.get('path', params.get('dest', params.get('name', None))) if path: path = os.path.expanduser(path) src = params.get('src', None) if src: src = os.path.expanduser(src) dest = params.get('dest', None) mode = params.get('mode', None) owner = params.get('owner', None) group = params.get('group', None) # presently unused, we always use -R (FIXME?) recurse = params.get('recurse', 'false') # selinux related options seuser = params.get('seuser', None) serole = params.get('serole', None) setype = params.get('setype', None) selevel = params.get('serange', 's0') context = params.get('context', None) secontext = [seuser, serole, setype] if selinux_mls_enabled(): secontext.append(selevel) if context is not None: if context != 'default': fail_json(msg='invalid context: %s' % context) if seuser is not None or serole is not None or setype is not None: fail_json(msg='cannot define context=default and seuser, serole or setype') secontext = selinux_default_context(path) if state not in [ 'file', 'directory', 'link', 'absent']: fail_json(msg='invalid state: %s' % state) if state == 'link' and (src is None or dest is None): fail_json(msg='src and dest are required for "link" state') elif path is None: fail_json(msg='path is required') changed = False # =========================================== # support functions def md5sum(filename): return os.popen("/usr/bin/md5sum %s" % f).read() def user_and_group(filename): st = os.stat(filename) uid = st.st_uid gid = st.st_gid user = pwd.getpwuid(uid)[0] group = grp.getgrgid(gid)[0] debug("got user=%s and group=%s" % (user, group)) return (user, group) def set_context_if_different(path, context, changed): if not HAVE_SELINUX or not selinux_enabled(): return changed cur_context = selinux_context(path) new_context = list(cur_context) debug("current secontext is %s" % ':'.join(cur_context)) # Iterate over the current context instead of the # argument context, which may have selevel. for i in range(len(cur_context)): if context[i] is not None and context[i] != cur_context[i]: new_context[i] = context[i] debug("new secontext is %s" % ':'.join(new_context)) if cur_context != new_context: try: rc = selinux.lsetfilecon(path, ':'.join(new_context)) except OSError: fail_json(path=path, msg='invalid selinux context') if rc != 0: fail_json(path=path, msg='set selinux context failed') changed = True return changed def set_owner_if_different(path, owner, changed): if owner is None: debug('not tweaking owner') return changed user, group = user_and_group(path) if owner != user: debug('setting owner') rc = os.system("/bin/chown -R %s %s" % (owner, path)) if rc != 0: fail_json(path=path, msg='chown failed') return True return changed def set_group_if_different(path, group, changed): if group is None: debug('not tweaking group') return changed old_user, old_group = user_and_group(path) if old_group != group: debug('setting group') rc = os.system("/bin/chgrp -R %s %s" % (group, path)) if rc != 0: fail_json(path=path, msg='chgrp failed') return True return changed def set_mode_if_different(path, mode, changed): if mode is None: debug('not tweaking mode') return changed try: # FIXME: support English modes mode = int(mode, 8) except Exception, e: fail_json(path=path, msg='mode needs to be something octalish', details=str(e)) st = os.stat(path) prev_mode = stat.S_IMODE(st[stat.ST_MODE]) if prev_mode != mode: # FIXME: comparison against string above will cause this to be executed # every time try: debug('setting mode') os.chmod(path, mode) except Exception, e: fail_json(path=path, msg='chmod failed', details=str(e)) st = os.stat(path) new_mode = stat.S_IMODE(st[stat.ST_MODE]) if new_mode != prev_mode: return True return changed def rmtree_error(func, path, exc_info): fail_json(path=path, msg='failed to remove directory') # =========================================== # go... prev_state = 'absent' if os.path.exists(path): if os.path.islink(path): prev_state = 'link' elif os.path.isfile(path): prev_state = 'file' else: prev_state = 'directory' if prev_state != 'absent' and state == 'absent': debug('requesting absent') try: if prev_state == 'directory': if os.path.islink(path): os.unlink(path) else: shutil.rmtree(path, ignore_errors=False, onerror=rmtree_error) else: os.unlink(path) except Exception, e: fail_json(path=path, msg=str(e)) exit_json(path=path, changed=True) sys.exit(0) if prev_state != 'absent' and prev_state != state: fail_json(path=path, msg='refusing to convert between %s and %s' % (prev_state, state)) if prev_state == 'absent' and state == 'absent': exit_json(path=path, changed=False) if state == 'file': debug('requesting file') if prev_state == 'absent': fail_json(path=path, msg='file does not exist, use copy or template module to create') # set modes owners and context as needed changed = set_context_if_different(path, secontext, changed) changed = set_owner_if_different(path, owner, changed) changed = set_group_if_different(path, group, changed) changed = set_mode_if_different(path, mode, changed) exit_json(path=path, changed=changed) elif state == 'directory': debug('requesting directory') if prev_state == 'absent': os.makedirs(path) changed = True # set modes owners and context as needed changed = set_context_if_different(path, secontext, changed) changed = set_owner_if_different(path, owner, changed) changed = set_group_if_different(path, group, changed) changed = set_mode_if_different(path, mode, changed) exit_json(path=path, changed=changed) elif state == 'link': if os.path.isabs(src): abs_src = src else: abs_src = os.path.join(os.path.dirname(dest), src) if not os.path.exists(abs_src): fail_json(dest=dest, src=src, msg='src file does not exist') if prev_state == 'absent': os.symlink(src, dest) changed = True elif prev_state == 'link': old_src = os.readlink(dest) if not os.path.isabs(old_src): old_src = os.path.join(os.path.dirname(dest), old_src) if old_src != src: os.unlink(dest) os.symlink(src, dest) else: fail_json(dest=dest, src=src, msg='unexpected position reached') # set modes owners and context as needed changed = set_context_if_different(dest, secontext, changed) changed = set_owner_if_different(dest, owner, changed) changed = set_group_if_different(dest, group, changed) changed = set_mode_if_different(dest, mode, changed) exit_json(dest=dest, src=src, changed=changed) fail_json(path=path, msg='unexpected position reached') sys.exit(0)