1e5d34ba35
This adds the options: seuser, serole, setype, and serange to the file module. If the python selinux module doesn't exist, this will set HAVE_SELINUX to False and punt in the related modules. This takes the options the user provides and applies those to the default selinux context as provided from matchpathcon(). If there is no default context, this uses the value from the current context. This implies that if you set the setype and later remove it, the file module will rever the setype to the default if available.
342 lines
10 KiB
Python
Executable file
342 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
|
|
|
|
# ===========================================
|
|
|
|
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)))
|
|
src = params.get('src', None)
|
|
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)
|
|
serange = params.get('serange', 's0')
|
|
secontext = [seuser, serole, setype, serange]
|
|
|
|
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
|
|
|
|
# If selinux fails to find a default, return an array of None
|
|
def selinux_default_context(path, mode=0):
|
|
context = [None, None, None, None]
|
|
print >>sys.stderr, path
|
|
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
|
|
|
|
def set_context_if_different(path, context, changed):
|
|
if not HAVE_SELINUX:
|
|
return changed
|
|
cur_context = selinux_context(path)
|
|
new_context = selinux_default_context(path)
|
|
for i in range(len(context)):
|
|
if context[i] is not None and context[i] != cur_context[i]:
|
|
debug('new context was %s' % new_context[i])
|
|
new_context[i] = context[i]
|
|
debug('new context is %s' % new_context[i])
|
|
elif new_context[i] is None:
|
|
new_context[i] = cur_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)
|
|
|