Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ensure_path_nonexistent race condition aware #429

Merged
merged 7 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 20 additions & 17 deletions src/batou/lib/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import os.path
import pwd
import random
import re
import shutil
import stat
Expand All @@ -19,18 +20,31 @@


def ensure_path_nonexistent(path):
"""Ensure that the given path does not exist.
elikoga marked this conversation as resolved.
Show resolved Hide resolved

strategy: rename the file to a temporary name, then remove it.
this allows us to fail in a useful way, since rename is atomic.
If we detect that the file exists after the rename, we know that
something else is going on and we can bail out.
"""
tmp_nonce = random.getrandbits(32)
tmp_filename = path + ".batou-ensure-path-nonexistent-tmp" + str(tmp_nonce)
elikoga marked this conversation as resolved.
Show resolved Hide resolved
if not os.path.lexists(path):
elikoga marked this conversation as resolved.
Show resolved Hide resolved
return
if os.path.islink(path):
os.unlink(path)
elif os.path.isdir(path):
shutil.rmtree(path)
os.rename(path, tmp_filename)
# if the rename did not work, we bail out
assert not os.path.lexists(
path
), f"{path} should not exist, if it does, it is a race condition."
if os.path.islink(tmp_filename):
os.unlink(tmp_filename)
elif os.path.isdir(tmp_filename):
shutil.rmtree(tmp_filename)
else:
os.unlink(path)
os.unlink(tmp_filename)


class File(Component):

namevar = "path"

ensure = "file" # or: directory, symlink
Expand Down Expand Up @@ -143,13 +157,11 @@ def last_updated(self, key="st_mtime"):


class BinaryFile(File):

is_template = False
encoding = None


class Presence(Component):

namevar = "path"
leading = False

Expand Down Expand Up @@ -183,7 +195,6 @@ def last_updated(self, key="st_mtime"):


class SyncDirectory(Component):

namevar = "path"
source = None
exclude = ()
Expand Down Expand Up @@ -238,7 +249,6 @@ def namevar_for_breadcrumb(self):


class Directory(Component):

namevar = "path"
leading = False
source = None
Expand Down Expand Up @@ -291,7 +301,6 @@ def namevar_for_breadcrumb(self):


class FileComponent(Component):

namevar = "path"
leading = False

Expand Down Expand Up @@ -534,7 +543,6 @@ def render(self):


class JSONContent(ManagedContentBase):

# Data to be used.
data = None

Expand Down Expand Up @@ -569,7 +577,6 @@ def render(self):


class YAMLContent(ManagedContentBase):

# Data to be used.
data = None

Expand All @@ -588,7 +595,6 @@ def render(self):


class Owner(FileComponent):

owner = None

def verify(self):
Expand All @@ -603,7 +609,6 @@ def update(self):


class Group(FileComponent):

group = None

def verify(self):
Expand Down Expand Up @@ -648,7 +653,6 @@ def convert_mode(string: str) -> int:


class Mode(FileComponent):

mode = Attribute(default=None)

def configure(self):
Expand Down Expand Up @@ -695,7 +699,6 @@ def _select_stat_implementation(self):


class Symlink(Component):

namevar = "target"
source = None

Expand Down
9 changes: 9 additions & 0 deletions src/batou/lib/tests/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ def test_ensure_path_does_not_fail_on_nonexisting_path():
assert not os.path.exists("missing")


def test_ensure_path_nonexistent_fails_if_os_rename_is_dummied(tmpdir):
os.chdir(str(tmpdir))
os.mkdir("dir")
with patch("os.rename", Mock(return_value=None)):
with pytest.raises(AssertionError) as e:
ensure_path_nonexistent("dir")
assert "race condition" in str(e.value)


def test_presence_creates_nonexisting_file(root):
p = Presence("path")
root.component += p
Expand Down
Loading