Initial refactor of the file module
* Separate the logic for each state into separate functions * Start the process of separating out initialization (pre-processing of parameters that cannot be done via arg spec) from the logic to implement each state. * Start the process of raising exceptions for errors and returning result values from each state implementing function Goal is for all fail_json's to be consolidated into exception handlers at the toplevel and for there to be only one exit_json() at the toplevel.
This commit is contained in:
parent
6b6c4914d2
commit
4e2876be9f
1 changed files with 424 additions and 268 deletions
|
@ -126,6 +126,7 @@ EXAMPLES = '''
|
||||||
import errno
|
import errno
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
@ -136,6 +137,50 @@ from ansible.module_utils._text import to_bytes, to_native
|
||||||
module = None
|
module = None
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleModuleError(Exception):
|
||||||
|
def __init__(self, results):
|
||||||
|
self.results = results
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
print('AnsibleModuleError({0})'.format(self.results))
|
||||||
|
|
||||||
|
|
||||||
|
class ParameterError(AnsibleModuleError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _ansible_excepthook(exc_type, exc_value, tb):
|
||||||
|
# Using an exception allows us to catch it if the calling code knows it can recover
|
||||||
|
if issubclass(exc_type, AnsibleModuleError):
|
||||||
|
module.fail_json(**exc_value.results)
|
||||||
|
else:
|
||||||
|
sys.__excepthook__(exc_type, exc_value, tb)
|
||||||
|
|
||||||
|
|
||||||
|
def additional_parameter_handling(params):
|
||||||
|
"""Additional parameter validation and reformatting"""
|
||||||
|
|
||||||
|
params['b_path'] = to_bytes(params['path'], errors='surrogate_or_strict')
|
||||||
|
params['b_src'] = to_bytes(params['src'], errors='surrogate_or_strict', nonstring='passthru')
|
||||||
|
|
||||||
|
# state should default to file, but since that creates many conflicts,
|
||||||
|
# default state to 'current' when it exists.
|
||||||
|
prev_state = get_state(params['b_path'])
|
||||||
|
|
||||||
|
if params['state'] is None:
|
||||||
|
if prev_state != 'absent':
|
||||||
|
params['state'] = prev_state
|
||||||
|
elif params['recurse']:
|
||||||
|
params['state'] = 'directory'
|
||||||
|
else:
|
||||||
|
params['state'] = 'file'
|
||||||
|
|
||||||
|
# make sure the target path is a directory when we're doing a recursive operation
|
||||||
|
if params['recurse'] and params['state'] != 'directory':
|
||||||
|
raise ParameterError(results={"path": params["path"],
|
||||||
|
"msg": "recurse option requires state to be 'directory'"})
|
||||||
|
|
||||||
|
|
||||||
def get_state(b_path):
|
def get_state(b_path):
|
||||||
''' Find out current state '''
|
''' Find out current state '''
|
||||||
|
|
||||||
|
@ -153,7 +198,8 @@ def get_state(b_path):
|
||||||
return 'absent'
|
return 'absent'
|
||||||
|
|
||||||
|
|
||||||
def recursive_set_attributes(module, b_path, follow, file_args):
|
# This should be moved into the common file utilities
|
||||||
|
def recursive_set_attributes(b_path, follow, file_args):
|
||||||
changed = False
|
changed = False
|
||||||
for b_root, b_dirs, b_files in os.walk(b_path):
|
for b_root, b_dirs, b_files in os.walk(b_path):
|
||||||
for b_fsobj in b_dirs + b_files:
|
for b_fsobj in b_dirs + b_files:
|
||||||
|
@ -174,7 +220,7 @@ def recursive_set_attributes(module, b_path, follow, file_args):
|
||||||
if os.path.exists(b_fsname):
|
if os.path.exists(b_fsname):
|
||||||
if os.path.isdir(b_fsname):
|
if os.path.isdir(b_fsname):
|
||||||
# Link is a directory so change perms on the directory's contents
|
# Link is a directory so change perms on the directory's contents
|
||||||
changed |= recursive_set_attributes(module, b_fsname, follow, file_args)
|
changed |= recursive_set_attributes(b_fsname, follow, file_args)
|
||||||
|
|
||||||
# Change perms on the file pointed to by the link
|
# Change perms on the file pointed to by the link
|
||||||
tmp_file_args = file_args.copy()
|
tmp_file_args = file_args.copy()
|
||||||
|
@ -183,6 +229,359 @@ def recursive_set_attributes(module, b_path, follow, file_args):
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def initial_diff(path, state, prev_state):
|
||||||
|
diff = {'before': {'path': path},
|
||||||
|
'after': {'path': path},
|
||||||
|
}
|
||||||
|
|
||||||
|
if prev_state != state:
|
||||||
|
diff['before']['state'] = prev_state
|
||||||
|
diff['after']['state'] = state
|
||||||
|
|
||||||
|
return diff
|
||||||
|
|
||||||
|
#
|
||||||
|
# States
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
def execute_diff_peek(b_path):
|
||||||
|
"""Take a guess as to whether a file is a binary file"""
|
||||||
|
appears_binary = False
|
||||||
|
try:
|
||||||
|
with open(b_path, 'rb') as f:
|
||||||
|
head = f.read(8192)
|
||||||
|
except Exception:
|
||||||
|
# If we can't read the file, we're okay assuming it's text
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if b"\x00" in head:
|
||||||
|
appears_binary = True
|
||||||
|
|
||||||
|
return appears_binary
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_absent(path, b_path, prev_state):
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if prev_state != 'absent':
|
||||||
|
if not module.check_mode:
|
||||||
|
if prev_state == 'directory':
|
||||||
|
try:
|
||||||
|
shutil.rmtree(b_path, ignore_errors=False)
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(msg="rmtree failed: %s" % to_native(e))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
os.unlink(b_path)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.ENOENT: # It may already have been removed
|
||||||
|
module.fail_json(path=path, msg="unlinking failed: %s " % to_native(e))
|
||||||
|
|
||||||
|
diff = initial_diff(path, 'absent', prev_state)
|
||||||
|
result.update({'path': path, 'changed': True, 'diff': diff})
|
||||||
|
else:
|
||||||
|
result.update({'path': path, 'changed': False})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def execute_touch(path, b_path, prev_state, follow):
|
||||||
|
if not module.check_mode:
|
||||||
|
if prev_state == 'absent':
|
||||||
|
# Create an empty file if the filename did not already exist
|
||||||
|
try:
|
||||||
|
open(b_path, 'wb').close()
|
||||||
|
except (OSError, IOError) as e:
|
||||||
|
raise AnsibleModuleError(results={'path': path,
|
||||||
|
'msg': 'Error, could not touch target: %s' % to_native(e, nonstring='simplerepr')})
|
||||||
|
|
||||||
|
elif prev_state in ('file', 'directory', 'hard'):
|
||||||
|
# Update the timestamp if the file already existed
|
||||||
|
try:
|
||||||
|
os.utime(b_path, None)
|
||||||
|
except OSError as e:
|
||||||
|
raise AnsibleModuleError(results={'path': path, 'msg': 'Error while touching existing target: %s' % to_native(e, nonstring='simplerepr')})
|
||||||
|
|
||||||
|
elif prev_state == 'link' and follow:
|
||||||
|
b_link_target = os.readlink(b_path)
|
||||||
|
try:
|
||||||
|
os.utime(b_link_target, None)
|
||||||
|
except OSError as e:
|
||||||
|
raise AnsibleModuleError(results={'path': path, 'msg': 'Error while touching existing target: %s' % to_native(e, nonstring='simplerepr')})
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise AnsibleModuleError(results={'msg': 'Can only touch files, directories, and hardlinks (%s is %s)' % (path, prev_state)})
|
||||||
|
|
||||||
|
# Update the attributes on the file
|
||||||
|
diff = initial_diff(path, 'absent', prev_state)
|
||||||
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
|
try:
|
||||||
|
module.set_fs_attributes_if_different(file_args, True, diff, expand=False)
|
||||||
|
except SystemExit as 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(b_path)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Unfortunately, touch always changes the file because it updates file's timestamp
|
||||||
|
return {'dest': path, 'changed': True}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_file_attributes(path, b_path, prev_state, follow):
|
||||||
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
|
if prev_state != 'file':
|
||||||
|
if follow and prev_state == 'link':
|
||||||
|
# follow symlink and operate on original
|
||||||
|
b_path = os.path.realpath(b_path)
|
||||||
|
path = to_native(b_path, errors='strict')
|
||||||
|
prev_state = get_state(b_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))
|
||||||
|
|
||||||
|
diff = initial_diff(path, 'file', prev_state)
|
||||||
|
changed = module.set_fs_attributes_if_different(file_args, False, diff, expand=False)
|
||||||
|
return {'path': path, 'changed': changed, 'diff': diff}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_directory(path, b_path, prev_state, follow, recurse):
|
||||||
|
if follow and prev_state == 'link':
|
||||||
|
b_path = os.path.realpath(b_path)
|
||||||
|
path = to_native(b_path, errors='strict')
|
||||||
|
prev_state = get_state(b_path)
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
|
diff = initial_diff(path, 'directory', prev_state)
|
||||||
|
|
||||||
|
if prev_state == 'absent':
|
||||||
|
if module.check_mode:
|
||||||
|
module.exit_json(changed=True, diff=diff)
|
||||||
|
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('/')
|
||||||
|
b_curpath = to_bytes(curpath, errors='surrogate_or_strict')
|
||||||
|
if not os.path.exists(b_curpath):
|
||||||
|
try:
|
||||||
|
os.mkdir(b_curpath)
|
||||||
|
changed = True
|
||||||
|
except OSError as 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.EEXIST and os.path.isdir(b_curpath)):
|
||||||
|
raise
|
||||||
|
tmp_file_args = file_args.copy()
|
||||||
|
tmp_file_args['path'] = curpath
|
||||||
|
changed = module.set_fs_attributes_if_different(tmp_file_args, changed, diff, expand=False)
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(path=path, msg='There was an issue creating %s as requested: %s' % (curpath, to_native(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, diff, expand=False)
|
||||||
|
|
||||||
|
if recurse:
|
||||||
|
changed |= recursive_set_attributes(to_bytes(file_args['path'], errors='surrogate_or_strict'), follow, file_args)
|
||||||
|
|
||||||
|
module.exit_json(path=path, changed=changed, diff=diff)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_symlink(path, b_path, src, b_src, prev_state, follow, force):
|
||||||
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
|
# 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 None:
|
||||||
|
if follow:
|
||||||
|
# use the current target of the link as the source
|
||||||
|
src = to_native(os.path.realpath(b_path), errors='strict')
|
||||||
|
b_src = to_bytes(os.path.realpath(b_path), errors='strict')
|
||||||
|
|
||||||
|
if not os.path.islink(b_path) and os.path.isdir(b_path):
|
||||||
|
relpath = path
|
||||||
|
else:
|
||||||
|
b_relpath = os.path.dirname(b_path)
|
||||||
|
relpath = to_native(b_relpath, errors='strict')
|
||||||
|
|
||||||
|
absrc = os.path.join(relpath, src)
|
||||||
|
b_absrc = to_bytes(absrc, errors='surrogate_or_strict')
|
||||||
|
if not force and not os.path.exists(b_absrc):
|
||||||
|
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 prev_state == 'directory':
|
||||||
|
if not force:
|
||||||
|
module.fail_json(path=path, msg='refusing to convert from %s to symlink for %s' % (prev_state, path))
|
||||||
|
elif os.listdir(b_path):
|
||||||
|
# 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 from %s to symlink for %s' % (prev_state, path))
|
||||||
|
|
||||||
|
diff = initial_diff(path, 'link', prev_state)
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if prev_state == 'absent':
|
||||||
|
changed = True
|
||||||
|
elif prev_state == 'link':
|
||||||
|
b_old_src = os.readlink(b_path)
|
||||||
|
if b_old_src != b_src:
|
||||||
|
diff['before']['src'] = to_native(b_old_src, errors='strict')
|
||||||
|
diff['after']['src'] = src
|
||||||
|
changed = True
|
||||||
|
elif prev_state == 'hard':
|
||||||
|
changed = True
|
||||||
|
if not force:
|
||||||
|
module.fail_json(dest=path, src=src, msg='Cannot link, different hard link exists at destination')
|
||||||
|
elif prev_state == 'file':
|
||||||
|
changed = True
|
||||||
|
if not force:
|
||||||
|
module.fail_json(dest=path, src=src, msg='Cannot link, %s exists at destination' % prev_state)
|
||||||
|
elif prev_state == 'directory':
|
||||||
|
changed = True
|
||||||
|
if os.path.exists(b_path):
|
||||||
|
if not force:
|
||||||
|
module.fail_json(dest=path, src=src, msg='Cannot link, different hard link exists at destination')
|
||||||
|
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
|
||||||
|
b_tmppath = to_bytes(os.path.sep).join(
|
||||||
|
[os.path.dirname(b_path), to_bytes(".%s.%s.tmp" % (os.getpid(), time.time()))]
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if prev_state == 'directory':
|
||||||
|
os.rmdir(b_path)
|
||||||
|
os.symlink(b_src, b_tmppath)
|
||||||
|
os.rename(b_tmppath, b_path)
|
||||||
|
except OSError as e:
|
||||||
|
if os.path.exists(b_tmppath):
|
||||||
|
os.unlink(b_tmppath)
|
||||||
|
module.fail_json(path=path, msg='Error while replacing: %s' % to_native(e, nonstring='simplerepr'))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
os.symlink(b_src, b_path)
|
||||||
|
except OSError as e:
|
||||||
|
module.fail_json(path=path, msg='Error while linking: %s' % to_native(e, nonstring='simplerepr'))
|
||||||
|
|
||||||
|
if module.check_mode and not os.path.exists(b_path):
|
||||||
|
module.exit_json(dest=path, src=src, changed=changed, diff=diff)
|
||||||
|
|
||||||
|
# Whenever we create a link to a nonexistent target we know that the nonexistent target
|
||||||
|
# cannot have any permissions set on it. Skip setting those and emit a warning (the user
|
||||||
|
# can set follow=False to remove the warning)
|
||||||
|
if follow and os.path.islink(b_path) and not os.path.exists(file_args['path']):
|
||||||
|
module.warn('Cannot set fs attributes on a non-existent symlink target. follow should be'
|
||||||
|
' set to False to avoid this.')
|
||||||
|
else:
|
||||||
|
changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
|
||||||
|
|
||||||
|
module.exit_json(dest=path, src=src, changed=changed, diff=diff)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_hardlink(path, b_path, src, b_src, prev_state, follow, force):
|
||||||
|
file_args = module.load_file_common_arguments(module.params)
|
||||||
|
# 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 None:
|
||||||
|
# Note: Bug: if hard link exists, we shouldn't need to check this
|
||||||
|
module.fail_json(msg='src and dest are required for creating hardlinks')
|
||||||
|
|
||||||
|
if not os.path.isabs(b_src):
|
||||||
|
module.fail_json(msg="absolute paths are required")
|
||||||
|
|
||||||
|
if not os.path.islink(b_path) and os.path.isdir(b_path):
|
||||||
|
relpath = path
|
||||||
|
else:
|
||||||
|
b_relpath = os.path.dirname(b_path)
|
||||||
|
relpath = to_native(b_relpath, errors='strict')
|
||||||
|
|
||||||
|
absrc = os.path.join(relpath, src)
|
||||||
|
b_absrc = to_bytes(absrc, errors='surrogate_or_strict')
|
||||||
|
if not force and not os.path.exists(b_absrc):
|
||||||
|
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)
|
||||||
|
|
||||||
|
diff = initial_diff(path, 'hard', prev_state)
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if prev_state == 'absent':
|
||||||
|
changed = True
|
||||||
|
elif prev_state == 'link':
|
||||||
|
b_old_src = os.readlink(b_path)
|
||||||
|
if b_old_src != b_src:
|
||||||
|
diff['before']['src'] = to_native(b_old_src, errors='strict')
|
||||||
|
diff['after']['src'] = src
|
||||||
|
changed = True
|
||||||
|
elif prev_state == 'hard':
|
||||||
|
if not os.stat(b_path).st_ino == os.stat(b_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 == 'file':
|
||||||
|
changed = True
|
||||||
|
if not force:
|
||||||
|
module.fail_json(dest=path, src=src, msg='Cannot link, %s exists at destination' % prev_state)
|
||||||
|
elif prev_state == 'directory':
|
||||||
|
changed = True
|
||||||
|
if os.path.exists(b_path):
|
||||||
|
if os.stat(b_path).st_ino == os.stat(b_src).st_ino:
|
||||||
|
module.exit_json(path=path, changed=False)
|
||||||
|
elif not force:
|
||||||
|
module.fail_json(dest=path, src=src, msg='Cannot link: different hard link exists at destination')
|
||||||
|
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
|
||||||
|
b_tmppath = to_bytes(os.path.sep).join(
|
||||||
|
[os.path.dirname(b_path), to_bytes(".%s.%s.tmp" % (os.getpid(), time.time()))]
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if prev_state == 'directory':
|
||||||
|
if os.path.exists(b_path):
|
||||||
|
try:
|
||||||
|
os.unlink(b_path)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.ENOENT: # It may already have been removed
|
||||||
|
raise
|
||||||
|
os.link(b_src, b_tmppath)
|
||||||
|
os.rename(b_tmppath, b_path)
|
||||||
|
except OSError as e:
|
||||||
|
if os.path.exists(b_tmppath):
|
||||||
|
os.unlink(b_tmppath)
|
||||||
|
module.fail_json(path=path, msg='Error while replacing: %s' % to_native(e, nonstring='simplerepr'))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
os.link(b_src, b_path)
|
||||||
|
except OSError as e:
|
||||||
|
module.fail_json(path=path, msg='Error while linking: %s' % to_native(e, nonstring='simplerepr'))
|
||||||
|
|
||||||
|
if module.check_mode and not os.path.exists(b_path):
|
||||||
|
module.exit_json(dest=path, src=src, changed=changed, diff=diff)
|
||||||
|
|
||||||
|
changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
|
||||||
|
|
||||||
|
module.exit_json(dest=path, src=src, changed=changed, diff=diff)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
global module
|
global module
|
||||||
|
@ -202,297 +601,54 @@ def main():
|
||||||
supports_check_mode=True
|
supports_check_mode=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# When we rewrite basic.py, we will do something similar to this on instantiating an AnsibleModule
|
||||||
|
sys.excepthook = _ansible_excepthook
|
||||||
|
additional_parameter_handling(module.params)
|
||||||
params = module.params
|
params = module.params
|
||||||
|
|
||||||
state = params['state']
|
state = params['state']
|
||||||
recurse = params['recurse']
|
recurse = params['recurse']
|
||||||
force = params['force']
|
force = params['force']
|
||||||
diff_peek = params['_diff_peek']
|
|
||||||
follow = params['follow']
|
follow = params['follow']
|
||||||
|
|
||||||
# modify paths as we later reload and pass, specially relevant when used by other modules.
|
|
||||||
path = params['path']
|
path = params['path']
|
||||||
b_path = to_bytes(path, errors='surrogate_or_strict')
|
b_path = params['b_path']
|
||||||
src = params['src']
|
src = params['src']
|
||||||
b_src = to_bytes(src, errors='surrogate_or_strict', nonstring='passthru')
|
b_src = params['b_src']
|
||||||
|
|
||||||
# short-circuit for diff_peek
|
|
||||||
if diff_peek is not None:
|
|
||||||
appears_binary = False
|
|
||||||
try:
|
|
||||||
f = open(b_path, 'rb')
|
|
||||||
head = f.read(8192)
|
|
||||||
f.close()
|
|
||||||
if b"\x00" in head:
|
|
||||||
appears_binary = True
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
module.exit_json(path=path, changed=False, appears_binary=appears_binary)
|
|
||||||
|
|
||||||
# state should default to file, but since that creates many conflicts,
|
|
||||||
# default state to 'current' when it exists.
|
|
||||||
prev_state = get_state(b_path)
|
prev_state = get_state(b_path)
|
||||||
|
|
||||||
if state is None:
|
# short-circuit for diff_peek
|
||||||
if prev_state != 'absent':
|
if params['_diff_peek'] is not None:
|
||||||
state = prev_state
|
appears_binary = execute_diff_peek(b_path)
|
||||||
elif recurse:
|
module.exit_json(path=path, changed=False, appears_binary=appears_binary)
|
||||||
state = 'directory'
|
|
||||||
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 None:
|
|
||||||
if state in ('link', 'hard'):
|
|
||||||
if follow and state == 'link':
|
|
||||||
# use the current target of the link as the source
|
|
||||||
src = to_native(os.path.realpath(b_path), errors='strict')
|
|
||||||
b_src = to_bytes(os.path.realpath(b_path), errors='strict')
|
|
||||||
else:
|
|
||||||
module.fail_json(msg='src and dest are required for creating links')
|
|
||||||
|
|
||||||
# original_basename is used by other modules that depend on file.
|
# original_basename is used by other modules that depend on file.
|
||||||
if state not in ("link", "absent") and os.path.isdir(b_path):
|
if state not in ("link", "absent") and os.path.isdir(b_path):
|
||||||
basename = None
|
basename = None
|
||||||
if params['original_basename']:
|
if params['original_basename']:
|
||||||
basename = params['original_basename']
|
basename = params['original_basename']
|
||||||
elif src is not None:
|
elif b_src is not None:
|
||||||
basename = os.path.basename(src)
|
basename = os.path.basename(b_src)
|
||||||
if basename:
|
if basename:
|
||||||
params['path'] = path = os.path.join(path, basename)
|
params['path'] = path = os.path.join(path, basename)
|
||||||
b_path = to_bytes(path, errors='surrogate_or_strict')
|
b_path = to_bytes(path, errors='surrogate_or_strict')
|
||||||
prev_state = get_state(b_path)
|
prev_state = get_state(b_path)
|
||||||
|
|
||||||
# make sure the target path is a directory when we're doing a recursive operation
|
if state == 'file':
|
||||||
if recurse and state != 'directory':
|
result = ensure_file_attributes(path, b_path, prev_state, follow)
|
||||||
module.fail_json(path=path, msg="recurse option requires state to be 'directory'")
|
|
||||||
|
|
||||||
file_args = module.load_file_common_arguments(params)
|
|
||||||
|
|
||||||
changed = False
|
|
||||||
diff = {'before': {'path': path},
|
|
||||||
'after': {'path': path},
|
|
||||||
}
|
|
||||||
|
|
||||||
state_change = False
|
|
||||||
if prev_state != state:
|
|
||||||
diff['before']['state'] = prev_state
|
|
||||||
diff['after']['state'] = state
|
|
||||||
state_change = True
|
|
||||||
|
|
||||||
if state == 'absent':
|
|
||||||
if state_change:
|
|
||||||
if not module.check_mode:
|
|
||||||
if prev_state == 'directory':
|
|
||||||
try:
|
|
||||||
shutil.rmtree(b_path, ignore_errors=False)
|
|
||||||
except Exception as e:
|
|
||||||
module.fail_json(msg="rmtree failed: %s" % to_native(e))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
os.unlink(b_path)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.ENOENT: # It may already have been removed
|
|
||||||
module.fail_json(path=path, msg="unlinking failed: %s " % to_native(e))
|
|
||||||
module.exit_json(path=path, changed=True, diff=diff)
|
|
||||||
else:
|
|
||||||
module.exit_json(path=path, changed=False)
|
|
||||||
|
|
||||||
elif state == 'file':
|
|
||||||
|
|
||||||
if state_change:
|
|
||||||
if follow and prev_state == 'link':
|
|
||||||
# follow symlink and operate on original
|
|
||||||
b_path = os.path.realpath(b_path)
|
|
||||||
path = to_native(b_path, errors='strict')
|
|
||||||
prev_state = get_state(b_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, diff, expand=False)
|
|
||||||
module.exit_json(path=path, changed=changed, diff=diff)
|
|
||||||
|
|
||||||
elif state == 'directory':
|
elif state == 'directory':
|
||||||
if follow and prev_state == 'link':
|
result = ensure_directory(path, b_path, prev_state, follow, recurse)
|
||||||
b_path = os.path.realpath(b_path)
|
elif state == 'link':
|
||||||
path = to_native(b_path, errors='strict')
|
result = ensure_symlink(path, b_path, src, b_src, prev_state, follow, force)
|
||||||
prev_state = get_state(b_path)
|
elif state == 'hard':
|
||||||
|
result = ensure_hardlink(path, b_path, src, b_src, prev_state, follow, force)
|
||||||
if prev_state == 'absent':
|
|
||||||
if module.check_mode:
|
|
||||||
module.exit_json(changed=True, diff=diff)
|
|
||||||
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('/')
|
|
||||||
b_curpath = to_bytes(curpath, errors='surrogate_or_strict')
|
|
||||||
if not os.path.exists(b_curpath):
|
|
||||||
try:
|
|
||||||
os.mkdir(b_curpath)
|
|
||||||
except OSError as 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.EEXIST and os.path.isdir(b_curpath)):
|
|
||||||
raise
|
|
||||||
tmp_file_args = file_args.copy()
|
|
||||||
tmp_file_args['path'] = curpath
|
|
||||||
changed = module.set_fs_attributes_if_different(tmp_file_args, changed, diff, expand=False)
|
|
||||||
except Exception as e:
|
|
||||||
module.fail_json(path=path, msg='There was an issue creating %s as requested: %s' % (curpath, to_native(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, diff, expand=False)
|
|
||||||
|
|
||||||
if recurse:
|
|
||||||
changed |= recursive_set_attributes(module, to_bytes(file_args['path'], errors='surrogate_or_strict'), follow, file_args)
|
|
||||||
|
|
||||||
module.exit_json(path=path, changed=changed, diff=diff)
|
|
||||||
|
|
||||||
elif state in ('link', 'hard'):
|
|
||||||
if not os.path.islink(b_path) and os.path.isdir(b_path):
|
|
||||||
relpath = path
|
|
||||||
else:
|
|
||||||
b_relpath = os.path.dirname(b_path)
|
|
||||||
relpath = to_native(b_relpath, errors='strict')
|
|
||||||
|
|
||||||
absrc = os.path.join(relpath, src)
|
|
||||||
b_absrc = to_bytes(absrc, errors='surrogate_or_strict')
|
|
||||||
if not force and not os.path.exists(b_absrc):
|
|
||||||
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(b_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 os.listdir(b_path):
|
|
||||||
# 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':
|
|
||||||
b_old_src = os.readlink(b_path)
|
|
||||||
if b_old_src != b_src:
|
|
||||||
diff['before']['src'] = to_native(b_old_src, errors='strict')
|
|
||||||
diff['after']['src'] = src
|
|
||||||
changed = True
|
|
||||||
elif prev_state == 'hard':
|
|
||||||
if not (state == 'hard' and os.stat(b_path).st_ino == os.stat(b_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 == 'file':
|
|
||||||
changed = True
|
|
||||||
if not force:
|
|
||||||
module.fail_json(dest=path, src=src, msg='Cannot link, %s exists at destination' % prev_state)
|
|
||||||
elif prev_state == 'directory':
|
|
||||||
changed = True
|
|
||||||
if os.path.exists(b_path):
|
|
||||||
if state == 'hard' and os.stat(b_path).st_ino == os.stat(b_src).st_ino:
|
|
||||||
module.exit_json(path=path, changed=False)
|
|
||||||
elif not force:
|
|
||||||
module.fail_json(dest=path, src=src, msg='Cannot link, different hard link exists at destination')
|
|
||||||
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
|
|
||||||
b_tmppath = to_bytes(os.path.sep).join(
|
|
||||||
[os.path.dirname(b_path), to_bytes(".%s.%s.tmp" % (os.getpid(), time.time()))]
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
if prev_state == 'directory' and state == 'link':
|
|
||||||
os.rmdir(b_path)
|
|
||||||
elif prev_state == 'directory' and state == 'hard':
|
|
||||||
if os.path.exists(b_path):
|
|
||||||
try:
|
|
||||||
os.unlink(b_path)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.ENOENT: # It may already have been removed
|
|
||||||
raise
|
|
||||||
if state == 'hard':
|
|
||||||
os.link(b_src, b_tmppath)
|
|
||||||
else:
|
|
||||||
os.symlink(b_src, b_tmppath)
|
|
||||||
os.rename(b_tmppath, b_path)
|
|
||||||
except OSError as e:
|
|
||||||
if os.path.exists(b_tmppath):
|
|
||||||
os.unlink(b_tmppath)
|
|
||||||
module.fail_json(path=path, msg='Error while replacing: %s' % to_native(e, nonstring='simplerepr'))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
if state == 'hard':
|
|
||||||
os.link(b_src, b_path)
|
|
||||||
else:
|
|
||||||
os.symlink(b_src, b_path)
|
|
||||||
except OSError as e:
|
|
||||||
module.fail_json(path=path, msg='Error while linking: %s' % to_native(e, nonstring='simplerepr'))
|
|
||||||
|
|
||||||
if module.check_mode and not os.path.exists(b_path):
|
|
||||||
module.exit_json(dest=path, src=src, changed=changed, diff=diff)
|
|
||||||
|
|
||||||
# Whenever we create a link to a nonexistent target we know that the nonexistent target
|
|
||||||
# cannot have any permissions set on it. Skip setting those and emit a warning (the user
|
|
||||||
# can set follow=False to remove the warning)
|
|
||||||
if (state == 'link' and params['follow'] and os.path.islink(params['path']) and
|
|
||||||
not os.path.exists(file_args['path'])):
|
|
||||||
module.warn('Cannot set fs attributes on a non-existent symlink target. follow should be'
|
|
||||||
' set to False to avoid this.')
|
|
||||||
else:
|
|
||||||
changed = module.set_fs_attributes_if_different(file_args, changed, diff, expand=False)
|
|
||||||
|
|
||||||
module.exit_json(dest=path, src=src, changed=changed, diff=diff)
|
|
||||||
|
|
||||||
elif state == 'touch':
|
elif state == 'touch':
|
||||||
if not module.check_mode:
|
result = execute_touch(path, b_path, prev_state, follow)
|
||||||
|
elif state == 'absent':
|
||||||
|
result = ensure_absent(path, b_path, prev_state)
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
if prev_state == 'absent':
|
module.exit_json(**result)
|
||||||
try:
|
|
||||||
open(b_path, 'wb').close()
|
|
||||||
except (OSError, IOError) as e:
|
|
||||||
module.fail_json(path=path, msg='Error, could not touch target: %s' % to_native(e, nonstring='simplerepr'))
|
|
||||||
elif prev_state in ('file', 'directory', 'hard'):
|
|
||||||
try:
|
|
||||||
os.utime(b_path, None)
|
|
||||||
except OSError as e:
|
|
||||||
module.fail_json(path=path, msg='Error while touching existing target: %s' % to_native(e, nonstring='simplerepr'))
|
|
||||||
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, diff, expand=False)
|
|
||||||
except SystemExit as 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(b_path)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
module.exit_json(dest=path, changed=True, diff=diff)
|
|
||||||
|
|
||||||
module.fail_json(path=path, msg='unexpected position reached')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
Loading…
Reference in a new issue