]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
cephadm: add write_new function for robust file writes
authorJohn Mulligan <jmulligan@redhat.com>
Tue, 6 Jun 2023 00:12:10 +0000 (20:12 -0400)
committerAdam King <adking@redhat.com>
Thu, 31 Aug 2023 17:35:13 +0000 (13:35 -0400)
The cephadm code has a very common pattern made of at least one of
the three following steps:
* call fchown on the open file to set ownership
* call fchmod on the open file to set permissions
* rename the file from a temp name to final name

Add the write_new function to encapsulate these common actions.
If owner is not None then fchown will be called.
If perms is not None then fchmod will be called.
An optional encoding value may be passed.
It always uses a temporary file as a temporary file ensures that
there can never be a partially written file even in the event of
a power outage or system crash.
Encapsulating this all into a function also allows us to make
changes to this approach in the future without touching every
call site using `open(..., "w")` etc.

Signed-off-by: John Mulligan <jmulligan@redhat.com>
(cherry picked from commit 7a8bfb91af246e36242aee90c3f9245fcdf6a318)

src/cephadm/cephadm.py

index c6f6b694afcd5bbb6d194bc1976104286d5c67c4..7a469e56ce85ee49979db97747b9731c9f89d8ae 100755 (executable)
@@ -26,13 +26,13 @@ import errno
 import struct
 import ssl
 from enum import Enum
-from typing import Dict, List, Tuple, Optional, Union, Any, NoReturn, Callable, IO, Sequence, TypeVar, cast, Set, Iterable, TextIO
+from typing import Dict, List, Tuple, Optional, Union, Any, NoReturn, Callable, IO, Sequence, TypeVar, cast, Set, Iterable, TextIO, Generator
 
 import re
 import uuid
 
 from configparser import ConfigParser
-from contextlib import redirect_stdout
+from contextlib import redirect_stdout, contextmanager
 from functools import wraps
 from glob import glob
 from io import StringIO
@@ -657,6 +657,42 @@ class Monitoring(object):
 ##################################
 
 
+@contextmanager
+def write_new(
+    destination: Union[str, Path],
+    *,
+    owner: Optional[Tuple[int, int]] = None,
+    perms: Optional[int] = None,
+    encoding: Optional[str] = None,
+) -> Generator[IO, None, None]:
+    """Write a new file in a robust manner, optionally specifying the owner,
+    permissions, or encoding. This function takes care to never leave a file in
+    a partially-written state due to a crash or power outage by writing to
+    temporary file and then renaming that temp file over to the final
+    destination once all data is written.  Note that the temporary files can be
+    leaked but only for a "crash" or power outage - regular exceptions will
+    clean up the temporary file.
+    """
+    destination = os.path.abspath(destination)
+    tempname = f'{destination}.new'
+    open_kwargs: Dict[str, Any] = {}
+    if encoding:
+        open_kwargs['encoding'] = encoding
+    try:
+        with open(tempname, 'w', **open_kwargs) as fh:
+            yield fh
+            fh.flush()
+            os.fsync(fh.fileno())
+            if owner is not None:
+                os.fchown(fh.fileno(), *owner)
+            if perms is not None:
+                os.fchmod(fh.fileno(), perms)
+    except Exception:
+        os.unlink(tempname)
+        raise
+    os.rename(tempname, destination)
+
+
 def populate_files(config_dir, config_files, uid, gid):
     # type: (str, Dict, int, int) -> None
     """create config files for different services"""