mirror of
				https://github.com/ansible-collections/community.general.git
				synced 2024-09-14 20:13:21 +02:00 
			
		
		
		
	This adjusts behavior of file module such that removal of se* option does not revert the file's selinux context to the default. In order to go back to the default context according to the policy, you can use the context=default option.
		
			
				
	
	
		
			349 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			349 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
#!/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']  = 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:
 | 
						|
            kwargs['secontext'] = ':'.join(selinux_context(path))
 | 
						|
    else:
 | 
						|
        kwargs['state'] = 'absent'
 | 
						|
    return kwargs 
 | 
						|
 
 | 
						|
# If selinux fails to find a default, return an array of None
 | 
						|
def selinux_default_context(path, mode=0):
 | 
						|
    context = [None, None, None, None]
 | 
						|
    if not HAVE_SELINUX:
 | 
						|
        return context
 | 
						|
    try:
 | 
						|
        ret = selinux.matchpathcon(path, mode)
 | 
						|
    except OSError:
 | 
						|
        return context
 | 
						|
    if ret[0] == -1:
 | 
						|
        return context
 | 
						|
    context = ret[1].split(':')
 | 
						|
    debug("got default 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, 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 selinux_context(path):
 | 
						|
    context = [None, None, None, None]
 | 
						|
    if not HAVE_SELINUX:
 | 
						|
        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
 | 
						|
 | 
						|
def set_context_if_different(path, context, changed):
 | 
						|
    if not HAVE_SELINUX:
 | 
						|
        return changed
 | 
						|
    cur_context = selinux_context(path)
 | 
						|
    new_context = list(cur_context)
 | 
						|
    for i in range(len(context)):
 | 
						|
        if context[i] is not None and context[i] != cur_context[i]:
 | 
						|
            new_context[i] = context[i]
 | 
						|
    debug("current secontext is %s" % ':'.join(cur_context))
 | 
						|
    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))
 | 
						|
    if not os.path.exists(abssrc):
 | 
						|
        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)
 | 
						|
 |