Skip to content

Commit

Permalink
fixes #62768 add atomic file operation for symlink changes
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasmhughes authored and Megan Wilhite committed Sep 29, 2022
1 parent 2d438cc commit 47aeb54
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 42 deletions.
1 change: 1 addition & 0 deletions changelog/62768.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add atomic file operation for symlink changes
37 changes: 29 additions & 8 deletions salt/modules/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -3709,7 +3709,7 @@ def is_link(path):
return os.path.islink(os.path.expanduser(path))


def symlink(src, path, force=False):
def symlink(src, path, force=False, atomic=False):
"""
Create a symbolic link (symlink, soft link) to a file
Expand All @@ -3723,8 +3723,12 @@ def symlink(src, path, force=False):
Overwrite an existing symlink with the same name
.. versionadded:: 3005
atomic (bool):
Use atomic file operations to create the symlink
.. versionadded:: 3006.0
Returns:
bool: True if successful, otherwise False
bool: ``True`` if successful, otherwise raises ``CommandExecutionError``
CLI Example:
Expand All @@ -3734,6 +3738,9 @@ def symlink(src, path, force=False):
"""
path = os.path.expanduser(path)

if not os.path.isabs(path):
raise SaltInvocationError("Link path must be absolute: {}".format(path))

if os.path.islink(path):
try:
if os.path.normpath(salt.utils.path.readlink(path)) == os.path.normpath(
Expand All @@ -3744,18 +3751,32 @@ def symlink(src, path, force=False):
except OSError:
pass

if force:
os.unlink(path)
else:
if not force and not atomic:
msg = "Found existing symlink: {}".format(path)
raise CommandExecutionError(msg)

if os.path.exists(path):
if os.path.exists(path) and not force and not atomic:
msg = "Existing path is not a symlink: {}".format(path)
raise CommandExecutionError(msg)

if not os.path.isabs(path):
raise SaltInvocationError("Link path must be absolute: {}".format(path))
if (os.path.islink(path) or os.path.exists(path)) and force and not atomic:
os.unlink(path)
elif atomic:
link_dir = os.path.dirname(path)
retry = 0
while retry < 5:
temp_link = tempfile.mktemp(dir=link_dir)
try:
os.symlink(src, temp_link)
break
except FileExistsError:
retry += 1
try:
os.replace(temp_link, path)
return True
except OSError:
os.remove(temp_link)
raise CommandExecutionError("Could not create '{}'".format(path))

try:
os.symlink(src, path)
Expand Down
62 changes: 28 additions & 34 deletions salt/states/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -1540,6 +1540,7 @@ def symlink(
win_perms=None,
win_deny_perms=None,
win_inheritance=None,
atomic=False,
**kwargs
):
"""
Expand Down Expand Up @@ -1618,17 +1619,22 @@ def symlink(
True to inherit permissions from parent, otherwise False
.. versionadded:: 2017.7.7
atomic
Use atomic file operation to create the symlink.
.. versionadded:: 3006.0
"""
name = os.path.expanduser(name)

# Make sure that leading zeros stripped by YAML loader are added back
mode = salt.utils.files.normalize_mode(mode)

user = _test_owner(kwargs, user=user)
ret = {"name": name, "changes": {}, "result": True, "comment": ""}
if not name:
return _error(ret, "Must provide name to file.symlink")

# Make sure that leading zeros stripped by YAML loader are added back
mode = salt.utils.files.normalize_mode(mode)

user = _test_owner(kwargs, user=user)
if user is None:
user = __opts__["user"]

Expand Down Expand Up @@ -1752,12 +1758,9 @@ def symlink(

if __salt__["file.is_link"](name):
# The link exists, verify that it matches the target
if os.path.normpath(__salt__["file.readlink"](name)) != os.path.normpath(
if os.path.normpath(__salt__["file.readlink"](name)) == os.path.normpath(
target
):
# The target is wrong, delete the link
os.remove(name)
else:
if _check_symlink_ownership(name, user, group, win_owner):
# The link looks good!
if salt.utils.platform.is_windows():
Expand Down Expand Up @@ -1837,14 +1840,7 @@ def symlink(
name, backupname, exc
),
)
elif force:
# Remove whatever is in the way
if __salt__["file.is_link"](name):
__salt__["file.remove"](name)
ret["changes"]["forced"] = "Symlink was forcibly replaced"
else:
__salt__["file.remove"](name)
else:
elif not force and not atomic:
# Otherwise throw an error
fs_entry_type = (
"File"
Expand All @@ -1858,26 +1854,24 @@ def symlink(
"{} exists where the symlink {} should be".format(fs_entry_type, name),
)

if not os.path.exists(name):
# The link is not present, make it
try:
__salt__["file.symlink"](target, name)
except OSError as exc:
try:
__salt__["file.symlink"](target, name, force=force, atomic=atomic)
except (CommandExecutionError, OSError) as exc:
ret["result"] = False
ret["comment"] = "Unable to create new symlink {} -> {}: {}".format(
name, target, exc
)
return ret
else:
ret["comment"] = "Created new symlink {} -> {}".format(name, target)
ret["changes"]["new"] = name

if not _check_symlink_ownership(name, user, group, win_owner):
if not _set_symlink_ownership(name, user, group, win_owner):
ret["result"] = False
ret["comment"] = "Unable to create new symlink {} -> {}: {}".format(
name, target, exc
ret["comment"] += ", but was unable to set ownership to {}:{}".format(
user, group
)
return ret
else:
ret["comment"] = "Created new symlink {} -> {}".format(name, target)
ret["changes"]["new"] = name

if not _check_symlink_ownership(name, user, group, win_owner):
if not _set_symlink_ownership(name, user, group, win_owner):
ret["result"] = False
ret["comment"] += ", but was unable to set ownership to {}:{}".format(
user, group
)
return ret


Expand Down
15 changes: 15 additions & 0 deletions tests/pytests/functional/modules/file/test_symlink.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,18 @@ def test_symlink_target_relative_path(file, source):
with pytest.raises(SaltInvocationError) as exc:
file.symlink(str(source), str(target))
assert "Link path must be absolute" in exc.value.message


def test_symlink_exists_different_atomic(file, source):
"""
Test symlink with an existing symlink to a different file with atomic=True
Should replace the existing symlink with a new one to the correct location
"""
dif_source = source.parent / "dif_source.txt"
target = source.parent / "symlink.lnk"
target.symlink_to(dif_source)
try:
file.symlink(str(source), str(target), atomic=True)
assert salt.utils.path.readlink(str(target)) == str(source)
finally:
target.unlink()

0 comments on commit 47aeb54

Please sign in to comment.