file 11.1 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 21 22 23 24

# (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/>.

import shutil
import stat
import grp
import pwd
25 26 27 28 29
try:
    import selinux
    HAVE_SELINUX=True
except ImportError:
    HAVE_SELINUX=False
30

31 32 33 34
def add_path_info(kwargs):
    path = kwargs['path']
    if os.path.exists(path):
        (user, group) = user_and_group(path)
35
        kwargs['owner']  = user
36 37
        kwargs['group'] = group
        st = os.stat(path)
38
        kwargs['mode']  = oct(stat.S_IMODE(st[stat.ST_MODE]))
39
        # secontext not yet supported
40 41 42
        if os.path.islink(path):
            kwargs['state'] = 'link'
        elif os.path.isfile(path):
43 44 45
            kwargs['state'] = 'file'
        else:
            kwargs['state'] = 'directory'
46
        if HAVE_SELINUX and selinux_enabled():
47
            kwargs['secontext'] = ':'.join(selinux_context(path))
48 49
    else:
        kwargs['state'] = 'absent'
50
    return kwargs
51

52 53 54 55 56 57 58 59
def module_exit_json(**kwargs):
    add_path_info(kwargs)
    module.exit_json(**kwargs)

def module_fail_json(**kwargs):
    add_path_info(kwargs)
    module.fail_json(**kwargs)

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
# 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:
        return True
    else:
        return False

def selinux_enabled():
    if not HAVE_SELINUX:
        return False
    if selinux.is_selinux_enabled() == 1:
        return True
    else:
        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

88 89
# If selinux fails to find a default, return an array of None
def selinux_default_context(path, mode=0):
90 91
    context = selinux_initial_context()
    if not HAVE_SELINUX or not selinux_enabled():
92 93 94 95 96 97 98 99 100 101
        return context
    try:
        ret = selinux.matchpathcon(path, mode)
    except OSError:
        return context
    if ret[0] == -1:
        return context
    context = ret[1].split(':')
    return context

Stephen Fromm committed
102
def selinux_context(path):
103 104
    context = selinux_initial_context()
    if not HAVE_SELINUX or not selinux_enabled():
Stephen Fromm committed
105 106 107 108
        return context
    try:
        ret = selinux.lgetfilecon(path)
    except:
109
        module_fail_json(path=path, msg='failed to retrieve selinux context')
Stephen Fromm committed
110 111 112 113 114
    if ret[0] == -1:
        return context
    context = ret[1].split(':')
    return context

115 116 117 118 119 120 121
# ===========================================
# support functions

def user_and_group(filename):
    st = os.stat(filename)
    uid = st.st_uid
    gid = st.st_gid
122 123 124
    try:
        user = pwd.getpwuid(uid)[0]
    except KeyError:
125
        user = str(uid)
126 127 128
    try:
        group = grp.getgrgid(gid)[0]
    except KeyError:
129
        group = str(gid)
130 131 132
    return (user, group)

def set_context_if_different(path, context, changed):
133
    if not HAVE_SELINUX or not selinux_enabled():
134 135
        return changed
    cur_context = selinux_context(path)
136
    new_context = list(cur_context)
137 138 139
    # Iterate over the current context instead of the
    # argument context, which may have selevel.
    for i in range(len(cur_context)):
140 141 142 143 144 145
        if context[i] is not None and context[i] != cur_context[i]:
            new_context[i] = context[i]
    if cur_context != new_context:
        try:
            rc = selinux.lsetfilecon(path, ':'.join(new_context))
        except OSError:
146
            module_fail_json(path=path, msg='invalid selinux context')
147
        if rc != 0:
148
            module_fail_json(path=path, msg='set selinux context failed')
149 150
        changed = True
    return changed
151

152
def set_owner_if_different(path, owner, changed):
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
    if owner is None:
        return changed
    user, group = user_and_group(path)
    if owner != user:
        try:
            uid = pwd.getpwnam(owner).pw_uid
        except KeyError:
            module_fail_json(path=path, msg='chown failed: failed to look up user %s' % owner)
        try:
            os.chown(path, uid, -1)
        except OSError:
            module_fail_json(path=path, msg='chown failed')
        return True

    return changed
168

169
def set_group_if_different(path, group, changed):
170 171 172 173 174 175 176 177 178 179 180 181 182 183
    if group is None:
        return changed
    old_user, old_group = user_and_group(path)
    if old_group != group:
        try:
            gid = grp.getgrnam(group).gr_gid
        except KeyError:
            module_fail_json(path=path, msg='chgrp failed: failed to look up group %s' % group)
        try:
            os.chown(path, -1, gid)
        except OSError:
            module_fail_json(path=path, msg='chgrp failed')
        return True
    return changed
184 185

def set_mode_if_different(path, mode, changed):
186 187 188 189 190 191 192
    if mode is None:
        return changed
    try:
        # FIXME: support English modes
        mode = int(mode, 8)
    except Exception, e:
        module_fail_json(path=path, msg='mode needs to be something octalish', details=str(e))
193

194 195
    st = os.stat(path)
    prev_mode = stat.S_IMODE(st[stat.ST_MODE])
196

197 198 199 200 201 202 203 204 205 206 207 208 209 210
    if prev_mode != mode:
        # FIXME: comparison against string above will cause this to be executed
        # every time
        try:
            os.chmod(path, mode)
        except Exception, e:
           module_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
211

212

213
def rmtree_error(func, path, exc_info):
214
    module_fail_json(path=path, msg='failed to remove directory')
215

216 217
def main():

218
    # FIXME: pass this around, should not use global
219
    global module
220

221 222
    module = AnsibleModule(
        check_invalid_arguments = False,
223 224 225 226 227 228 229 230 231 232
        argument_spec = dict(
            state = dict(choices=['file','directory','link','absent'], default='file'),
            path  = dict(aliases=['dest', 'name'], required=True),
            src   = dict(),
            mode  = dict(),
            owner = dict(),
            group = dict(),
            seuser = dict(),
            serole = dict(),
            selevel = dict(),
233
            setype = dict(),
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
        )
    )

    params = module.params
    state  = params['state']
    path   = os.path.expanduser(params['path'])
    src    = params.get('src', None)
    if src:
        src = os.path.expanduser(src)

    mode   = params.get('mode', None)
    owner  = params.get('owner', None)
    group  = params.get('group', None)

    # selinux related options
    seuser    = params.get('seuser', None)
    serole    = params.get('serole', None)
    setype    = params.get('setype', None)
    selevel   = params.get('serange', 's0')
    secontext = [seuser, serole, setype]
    if selinux_mls_enabled():
        secontext.append(selevel)
256

257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
    default_secontext = selinux_default_context(path)
    for i in range(len(default_secontext)):
        if i is not None and secontext[i] == '_default':
            secontext[i] = default_secontext[i]

    if state == 'link' and (src is None or path is None):
        module_fail_json(msg='src and dest are required for "link" state')
    elif path is None:
        module_fail_json(msg='path is required')

    changed = False

    prev_state = 'absent'
    if os.path.lexists(path):
        if os.path.islink(path):
            prev_state = 'link'
        elif os.path.isfile(path):
            prev_state = 'file'
275
        else:
276
            prev_state = 'directory'
277

278 279 280 281 282 283 284 285 286 287 288 289
    if prev_state != 'absent' and state == '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:
            module_fail_json(path=path, msg=str(e))
        module_exit_json(path=path, changed=True)
290

291 292
    if prev_state != 'absent' and prev_state != state:
        module_fail_json(path=path, msg='refusing to convert between %s and %s' % (prev_state, state))
293

294 295
    if prev_state == 'absent' and state == 'absent':
        module_exit_json(path=path, changed=False)
296

297
    if state == 'file':
298

299 300
        if prev_state == 'absent':
            module_fail_json(path=path, msg='file does not exist, use copy or template module to create')
301

302 303 304 305 306
        # 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)
307

308
        module_exit_json(path=path, changed=changed)
309

310
    elif state == 'directory':
311

312 313 314
        if prev_state == 'absent':
            os.makedirs(path)
            changed = True
315

316 317 318 319 320
        # 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)
321

322
        module_exit_json(path=path, changed=changed)
323

324
    elif state == 'link':
325

326 327 328
        if os.path.isabs(src):
            abs_src = src
        else:
329
            module.fail_json(msg="absolute paths are required")
330
        if not os.path.exists(abs_src):
331
            module_fail_json(path=path, src=src, msg='src file does not exist')
332

333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
        if prev_state == 'absent':
            os.symlink(src, path)
            changed = True
        elif prev_state == 'link':
            old_src = os.readlink(path)
            if not os.path.isabs(old_src):
                old_src = os.path.join(os.path.dirname(path), old_src)
            if old_src != src:
                os.unlink(path)
                os.symlink(src, path)
        else:
            module_fail_json(dest=path, src=src, msg='unexpected position reached')

        # 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)
351

352
        module.exit_json(dest=path, src=src, changed=changed)
353

354
    module_fail_json(path=path, msg='unexpected position reached')
355

356 357
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
358
main()
359