forked from ansible/ansible-modules-core
-
Notifications
You must be signed in to change notification settings - Fork 0
/
file.py
416 lines (366 loc) · 16.5 KB
/
file.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (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 errno
import shutil
import stat
import grp
import pwd
try:
import selinux
HAVE_SELINUX=True
except ImportError:
HAVE_SELINUX=False
DOCUMENTATION = '''
---
module: file
version_added: "historical"
short_description: Sets attributes of files
extends_documentation_fragment: files
description:
- Sets attributes of files, symlinks, and directories, or removes
files/symlinks/directories. Many other modules support the same options as
the M(file) module - including M(copy), M(template), and M(assemble).
notes:
- See also M(copy), M(template), M(assemble)
requirements: [ ]
author:
- "Ansible Core Team"
- "Michael DeHaan"
options:
path:
description:
- 'path to the file being managed. Aliases: I(dest), I(name)'
required: true
default: []
aliases: ['dest', 'name']
state:
description:
- If C(directory), all immediate subdirectories will be created if they
do not exist, since 1.7 they will be created with the supplied permissions.
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. 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.
If C(touch) (new in 1.4), an empty file will be created if the C(path) does not
exist, while an existing file or directory will receive updated file access and
modification times (similar to the way `touch` works from the command line).
required: false
default: file
choices: [ file, link, directory, hard, touch, absent ]
src:
required: false
default: null
description:
- path of the file to link to (applies only to C(state=link)). Will accept absolute,
relative and nonexisting paths. Relative paths are not expanded.
recurse:
required: false
default: "no"
choices: [ "yes", "no" ]
version_added: "1.1"
description:
- recursively set the specified file attributes (applies only to state=directory)
force:
required: false
default: "no"
choices: [ "yes", "no" ]
description:
- 'force the creation of the symlinks in two cases: the source file does
not exist (but will appear later); the destination exists and is a file (so, we need to unlink the
"path" file and create symlink to the "src" file in place of it).'
'''
EXAMPLES = '''
# change file ownership, group and mode. When specifying mode using octal numbers, first digit should always be 0.
- file: path=/etc/foo.conf owner=foo group=foo mode=0644
- file: src=/file/to/link/to dest=/path/to/symlink owner=foo group=foo state=link
- file: src=/tmp/{{ item.path }} dest={{ item.dest }} state=link
with_items:
- { path: 'x', dest: 'y' }
- { path: 'z', dest: 'k' }
# touch a file, using symbolic modes to set the permissions (equivalent to 0644)
- file: path=/etc/foo.conf state=touch mode="u=rw,g=r,o=r"
# touch the same file, but add/remove some permissions
- file: path=/etc/foo.conf state=touch mode="u+rw,g-wx,o-rwx"
# create a directory if it doesn't exist
- file: path=/etc/some_directory state=directory mode=0755
'''
def get_state(path):
''' Find out current state '''
if os.path.lexists(path):
if os.path.islink(path):
return 'link'
elif os.path.isdir(path):
return 'directory'
elif os.stat(path).st_nlink > 1:
return 'hard'
else:
# could be many other things, but defaulting to file
return 'file'
return 'absent'
def recursive_set_attributes(module, path, follow, file_args):
changed = False
for root, dirs, files in os.walk(path):
for fsobj in dirs + files:
fsname = os.path.join(root, fsobj)
if not os.path.islink(fsname):
tmp_file_args = file_args.copy()
tmp_file_args['path']=fsname
changed |= module.set_fs_attributes_if_different(tmp_file_args, changed)
else:
tmp_file_args = file_args.copy()
tmp_file_args['path']=fsname
changed |= module.set_fs_attributes_if_different(tmp_file_args, changed)
if follow:
fsname = os.path.join(root, os.readlink(fsname))
if os.path.isdir(fsname):
changed |= recursive_set_attributes(module, fsname, follow, file_args)
tmp_file_args = file_args.copy()
tmp_file_args['path']=fsname
changed |= module.set_fs_attributes_if_different(tmp_file_args, changed)
return changed
def main():
module = AnsibleModule(
argument_spec = dict(
state = dict(choices=['file','directory','link','hard','touch','absent'], default=None),
path = dict(aliases=['dest', 'name'], required=True),
original_basename = dict(required=False), # Internal use only, for recursive ops
recurse = dict(default=False, type='bool'),
force = dict(required=False, default=False, type='bool'),
diff_peek = dict(default=None),
validate = dict(required=False, default=None),
src = dict(required=False, default=None),
),
add_file_common_args=True,
supports_check_mode=True
)
params = module.params
state = params['state']
force = params['force']
diff_peek = params['diff_peek']
src = params['src']
follow = params['follow']
# modify source as we later reload and pass, specially relevant when used by other modules.
params['path'] = path = os.path.expanduser(params['path'])
# short-circuit for diff_peek
if diff_peek is not None:
appears_binary = False
try:
f = open(path)
b = f.read(8192)
f.close()
if "\x00" in b:
appears_binary = True
except:
pass
module.exit_json(path=path, changed=False, appears_binary=appears_binary)
prev_state = get_state(path)
# state should default to file, but since that creates many conflicts,
# default to 'current' when it exists.
if state is None:
if prev_state != 'absent':
state = prev_state
else:
state = 'file'
# 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
if src is not None:
src = os.path.expanduser(src)
else:
if state in ['link','hard']:
if follow and state == 'link':
# use the current target of the link as the source
src = os.path.realpath(path)
else:
module.fail_json(msg='src and dest are required for creating links')
# original_basename is used by other modules that depend on file.
if os.path.isdir(path) and state not in ["link", "absent"]:
basename = None
if params['original_basename']:
basename = params['original_basename']
elif src is not None:
basename = os.path.basename(src)
if basename:
params['path'] = path = os.path.join(path, basename)
# make sure the target path is a directory when we're doing a recursive operation
recurse = params['recurse']
if recurse and state != 'directory':
module.fail_json(path=path, msg="recurse option requires state to be 'directory'")
file_args = module.load_file_common_arguments(params)
changed = False
if state == 'absent':
if state != prev_state:
if not module.check_mode:
if prev_state == 'directory':
try:
shutil.rmtree(path, ignore_errors=False)
except Exception, e:
module.fail_json(msg="rmtree failed: %s" % str(e))
else:
try:
os.unlink(path)
except Exception, e:
module.fail_json(path=path, msg="unlinking failed: %s " % str(e))
module.exit_json(path=path, changed=True)
else:
module.exit_json(path=path, changed=False)
elif state == 'file':
if state != prev_state:
if follow and prev_state == 'link':
# follow symlink and operate on original
path = os.path.realpath(path)
prev_state = get_state(path)
file_args['path'] = path
if prev_state not in ['file','hard']:
# file is not absent and any other state is a conflict
module.fail_json(path=path, msg='file (%s) is %s, cannot continue' % (path, prev_state))
changed = module.set_fs_attributes_if_different(file_args, changed)
module.exit_json(path=path, changed=changed)
elif state == 'directory':
if follow and prev_state == 'link':
path = os.path.realpath(path)
prev_state = get_state(path)
if prev_state == 'absent':
if module.check_mode:
module.exit_json(changed=True)
changed = True
curpath = ''
try:
# Split the path so we can apply filesystem attributes recursively
# from the root (/) directory for absolute paths or the base path
# of a relative path. We can then walk the appropriate directory
# path to apply attributes.
for dirname in path.strip('/').split('/'):
curpath = '/'.join([curpath, dirname])
# Remove leading slash if we're creating a relative path
if not os.path.isabs(path):
curpath = curpath.lstrip('/')
if not os.path.exists(curpath):
try:
os.mkdir(curpath)
except OSError, ex:
# Possibly something else created the dir since the os.path.exists
# check above. As long as it's a dir, we don't need to error out.
if not (ex.errno == errno.EEXISTS and os.isdir(curpath)):
raise
tmp_file_args = file_args.copy()
tmp_file_args['path']=curpath
changed = module.set_fs_attributes_if_different(tmp_file_args, changed)
except Exception, e:
module.fail_json(path=path, msg='There was an issue creating %s as requested: %s' % (curpath, str(e)))
# We already know prev_state is not 'absent', therefore it exists in some form.
elif prev_state != 'directory':
module.fail_json(path=path, msg='%s already exists as a %s' % (path, prev_state))
changed = module.set_fs_attributes_if_different(file_args, changed)
if recurse:
changed |= recursive_set_attributes(module, file_args['path'], follow, file_args)
module.exit_json(path=path, changed=changed)
elif state in ['link','hard']:
if os.path.isdir(path) and not os.path.islink(path):
relpath = path
else:
relpath = os.path.dirname(path)
absrc = os.path.join(relpath, src)
if not os.path.exists(absrc) and not force:
module.fail_json(path=path, src=src, msg='src file does not exist, use "force=yes" if you really want to create the link: %s' % absrc)
if state == 'hard':
if not os.path.isabs(src):
module.fail_json(msg="absolute paths are required")
elif prev_state == 'directory':
if not force:
module.fail_json(path=path, msg='refusing to convert between %s and %s for %s' % (prev_state, state, path))
elif len(os.listdir(path)) > 0:
# refuse to replace a directory that has files in it
module.fail_json(path=path, msg='the directory %s is not empty, refusing to convert it' % path)
elif prev_state in ['file', 'hard'] and not force:
module.fail_json(path=path, msg='refusing to convert between %s and %s for %s' % (prev_state, state, path))
if prev_state == 'absent':
changed = True
elif prev_state == 'link':
old_src = os.readlink(path)
if old_src != src:
changed = True
elif prev_state == 'hard':
if not (state == 'hard' and os.stat(path).st_ino == os.stat(src).st_ino):
changed = True
if not force:
module.fail_json(dest=path, src=src, msg='Cannot link, different hard link exists at destination')
elif prev_state in ['file', 'directory']:
changed = True
if not force:
module.fail_json(dest=path, src=src, msg='Cannot link, %s exists at destination' % prev_state)
else:
module.fail_json(dest=path, src=src, msg='unexpected position reached')
if changed and not module.check_mode:
if prev_state != 'absent':
# try to replace atomically
tmppath = '/'.join([os.path.dirname(path), ".%s.%s.tmp" % (os.getpid(),time.time())])
try:
if prev_state == 'directory' and (state == 'hard' or state == 'link'):
os.rmdir(path)
if state == 'hard':
os.link(src,tmppath)
else:
os.symlink(src, tmppath)
os.rename(tmppath, path)
except OSError, e:
if os.path.exists(tmppath):
os.unlink(tmppath)
module.fail_json(path=path, msg='Error while replacing: %s' % str(e))
else:
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))
if module.check_mode and not os.path.exists(path):
module.exit_json(dest=path, src=src, changed=changed)
changed = module.set_fs_attributes_if_different(file_args, changed)
module.exit_json(dest=path, src=src, changed=changed)
elif state == 'touch':
if not module.check_mode:
if prev_state == 'absent':
try:
open(path, 'w').close()
except OSError, e:
module.fail_json(path=path, msg='Error, could not touch target: %s' % str(e))
elif prev_state in ['file', 'directory', 'hard']:
try:
os.utime(path, None)
except OSError, e:
module.fail_json(path=path, msg='Error while touching existing target: %s' % str(e))
else:
module.fail_json(msg='Cannot touch other than files, directories, and hardlinks (%s is %s)' % (path, prev_state))
try:
module.set_fs_attributes_if_different(file_args, True)
except SystemExit, e:
if e.code:
# We take this to mean that fail_json() was called from
# somewhere in basic.py
if prev_state == 'absent':
# If we just created the file we can safely remove it
os.remove(path)
raise e
module.exit_json(dest=path, changed=True)
module.fail_json(path=path, msg='unexpected position reached')
# import module snippets
from ansible.module_utils.basic import *
if __name__ == '__main__':
main()