file 11 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 35 36 37
DOCUMENTATION = '''
---
module: file
short_description: Sets attributes of files
description: 
     - Sets attributes of files, symlinks, and directories, or removes
       files/symlinks/directories. Many other modules support the same options as
38
       the M(file) module - including M(copy), M(template), and M(assemble).
39
options:
40
  path:
41
    description:
42
      - 'defines the file being managed, unless when used with C(state=link), and then sets the destination to create a symbolic link to using I(src). Aliases: I(dest), I(name)'
43 44
    required: true
    default: []
45
    aliases: ['dest', 'name'] 
46 47
  state:
    description:
48 49 50
      - If C(directory), all immediate subdirectories will be created if they
        do not exist. If C(file), the file will NOT be created if it does not
        exist, see the M(copy) or M(template) module if you want that behavior.
Brian Coca committed
51 52 53
        If C(link), the symbolic link will be created or changed. Use C(hard)
        for hardlinks. If C(absent), directories will be recursively deleted,
        and files or symlinks will be unlinked.
54 55
    required: false
    default: file
Brian Coca committed
56
    choices: [ file, link, directory, hard, absent ]
57
  mode:
58 59 60 61
    required: false
    default: null
    choices: []
    description:
Jan-Piet Mens committed
62
      - mode the file or directory should be, such as 0644 as would be fed to I(chmod)
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
  owner:
    required: false
    default: null
    choices: []
    description:
      - name of the user that should own the file/directory, as would be fed to I(chown)
  group:
    required: false
    default: null
    choices: []
    description:
      - name of the group that should own the file/directory, as would be fed to I(chown)
  src:
    required: false
    default: null
    choices: []
    description:
      - path of the file to link to (applies only to C(state=link)).
  seuser:
    required: false
    default: null
    choices: []
    description:
      - user part of SELinux file context. Will default to system policy, if
        applicable. If set to C(_default), it will use the C(user) portion of the
Jan-Piet Mens committed
88
        policy if available
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
  serole:
    required: false
    default: null
    choices: []
    description:
      - role part of SELinux file context, C(_default) feature works as for I(seuser).
  setype:
    required: false
    default: null
    choices: []
    description:
      - type part of SELinux file context, C(_default) feature works as for I(seuser).
  selevel:
    required: false
    default: "s0"
    choices: []
    description:
      - level part of the SELinux file context. This is the MLS/MCS attribute,
        sometimes known as the C(range). C(_default) feature works as for
        I(seuser).
109 110
  recurse:
    required: false
111
    default: "no"
112
    choices: [ "yes", "no" ]
113
    version_added: "1.1"
114 115
    description:
      - recursively set the specified file attributes (applies only to state=directory)
116 117 118
  force:
    required: false
    default: "no"
Mike Grozak committed
119
    choices: [ "yes", "no" ]
120
    description:
121
      - 'force the creation of the symlinks in two cases: the source file does 
122
        not exist (but will appear later); the destination exists and a file (so, we need to unlink the
123
        "path" file and create symlink to the "src" file in place of it).'
124 125 126
notes:
    - See also M(copy), M(template), M(assemble)
requirements: [ ]
127
author: Michael DeHaan
128 129
'''

130
EXAMPLES = '''
131
- file: path=/etc/foo.conf owner=foo group=foo mode=0644
132 133 134
- file: src=/file/to/link/to dest=/path/to/symlink owner=foo group=foo state=link
'''

135 136
def main():

137
    # FIXME: pass this around, should not use global
138
    global module
139

140
    module = AnsibleModule(
141
        argument_spec = dict(
Brian Coca committed
142
            state = dict(choices=['file','directory','link','hard','absent'], default='file'),
143
            path  = dict(aliases=['dest', 'name'], required=True),
Michael DeHaan committed
144
            recurse  = dict(default='no', type='bool'),
145
            force = dict(required=False,default=False,type='bool'),
146 147
            diff_peek = dict(default=None),
            validate = dict(required=False, default=None),
148
        ),
149 150
        add_file_common_args=True,
        supports_check_mode=True
151 152 153 154
    )

    params = module.params
    state  = params['state']
155
    force = params['force']
156
    params['path'] = path = os.path.expanduser(params['path'])
157

158 159 160 161
    # short-circuit for diff_peek
    if params.get('diff_peek', None) is not None:
        appears_binary = False
        try:
162
            f = open(path)
163 164 165 166 167 168
            b = f.read(8192)
            f.close()
            if b.find("\x00") != -1:
                appears_binary = True
        except:
            pass
169
        module.exit_json(path=path, changed=False, appears_binary=appears_binary)
170

171 172 173
    # source is both the source of a symlink or an informational passing of the src for a template module
    # or copy module, even if this module never uses it, it is needed to key off some things

174
    src = params.get('src', None)
175 176 177
    if src:
        src = os.path.expanduser(src)

178 179
    if src is not None and os.path.isdir(path) and state != "link":
        params['path'] = path = os.path.join(path, os.path.basename(src))
180

181
    file_args = module.load_file_common_arguments(params)
182

183
    if state in ['link','hard'] and (src is None or path is None):
Brian Coca committed
184
        module.fail_json(msg='src and dest are required for creating links')
185
    elif path is None:
186
        module.fail_json(msg='path is required')
187 188 189 190

    changed = False

    prev_state = 'absent'
191

192 193
    if os.path.lexists(path):
        if os.path.islink(path):
194
            prev_state = 'link'
195
        elif os.path.isdir(path):
196
            prev_state = 'directory'
197 198
        elif os.stat(path).st_nlink > 1:
            prev_state = 'hard'
199
        else:
200
            # could be many other tings, but defaulting to file
201
            prev_state = 'file'
202

203 204 205
    if prev_state != 'absent' and state == 'absent':
        try:
            if prev_state == 'directory':
206
                if os.path.islink(path):
207 208
                    if module.check_mode:
                        module.exit_json(changed=True)
209
                    os.unlink(path)
210
                else:
211
                    try:
212 213
                        if module.check_mode:
                            module.exit_json(changed=True)
214
                        shutil.rmtree(path, ignore_errors=False)
215 216
                    except:
                        module.exit_json(msg="rmtree failed")
217
            else:
218 219
                if module.check_mode:
                    module.exit_json(changed=True)
220
                os.unlink(path)
221
        except Exception, e:
222 223
            module.fail_json(path=path, msg=str(e))
        module.exit_json(path=path, changed=True)
224

225
    if prev_state != 'absent' and prev_state != state:
226
        if not (force and prev_state == 'file' and state == 'link'):
227
            module.fail_json(path=path, msg='refusing to convert between %s and %s for %s' % (prev_state, state, src))
228

229
    if prev_state == 'absent' and state == 'absent':
230
        module.exit_json(path=path, changed=False)
231

232
    if state == 'file':
233

234
        if prev_state != 'file':
235
            module.fail_json(path=path, msg='file (%s) does not exist, use copy or template module to create' % path)
236

237
        changed = module.set_file_attributes_if_different(file_args, changed)
238
        module.exit_json(path=path, changed=changed)
239

240 241
    elif state == 'directory':
        if prev_state == 'absent':
242 243
            if module.check_mode:
                module.exit_json(changed=True)
244
            os.makedirs(path)
245
            changed = True
246

247
        changed = module.set_directory_attributes_if_different(file_args, changed)
248
        recurse = params['recurse']
249
        if recurse:
250
            for root,dirs,files in os.walk( file_args['path'] ):
251 252 253
                for dir in dirs:
                    dirname=os.path.join(root,dir)
                    tmp_file_args = file_args.copy()
254
                    tmp_file_args['path']=dirname
255 256 257 258
                    changed = module.set_directory_attributes_if_different(tmp_file_args, changed)
                for file in files:
                    filename=os.path.join(root,file)
                    tmp_file_args = file_args.copy()
259
                    tmp_file_args['path']=filename
260
                    changed = module.set_file_attributes_if_different(tmp_file_args, changed)
261
        module.exit_json(path=path, changed=changed)
262

Brian Coca committed
263
    elif state in ['link','hard']:
264

265 266 267
        if os.path.isabs(src):
            abs_src = src
        else:
268
            module.fail_json(msg="absolute paths are required")
269 270

        if not os.path.exists(abs_src) and not force:
271
            module.fail_json(path=path, src=src, msg='src file does not exist')
272

273 274 275
        if prev_state == 'absent':
            changed = True
        elif prev_state == 'link':
276
            old_src = os.readlink(path)
277
            if not os.path.isabs(old_src):
278
                old_src = os.path.join(os.path.dirname(path), old_src)
279
            if old_src != src:
280 281 282 283 284
                changed = True
        elif prev_state == 'hard':
            if not (state == 'hard' and os.stat(path).st_ino == os.stat(src).st_ino):
                if not force:
                    module.fail_json(dest=path, src=src, msg='Cannot link, different hard link exists at destination')
285
                changed = True
Brian Coca committed
286
        elif prev_state == 'file':
287
            if not force:
288
                module.fail_json(dest=path, src=src, msg='Cannot link, file exists at destination')
289
            changed = True
290
        else:
291
            module.fail_json(dest=path, src=src, msg='unexpected position reached')
292

293 294 295 296 297 298 299 300 301 302 303 304 305
        if changed and not module.check_mode:
            if prev_state != 'absent':
                try:
                    os.unlink(path)
                except OSError, e:
                    module.fail_json(path=path, msg='Error while removing existing target: %s' % str(e))
            try:
                if state == 'hard':
                    os.link(src,path)
                else:
                    os.symlink(src, path)
            except OSError, e:
                module.fail_json(path=path, msg='Error while linking: %s' % str(e))
306

307
        changed = module.set_file_attributes_if_different(file_args, changed)
308
        module.exit_json(dest=path, src=src, changed=changed)
309

310 311
    else:
        module.fail_json(path=path, msg='unexpected position reached')
312

313 314
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
315
main()
316