From: John Mulligan Date: Tue, 6 Jun 2023 00:12:10 +0000 (-0400) Subject: cephadm: add write_new function for robust file writes X-Git-Tag: v19.0.0~1023^2~8 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=7a8bfb91af246e36242aee90c3f9245fcdf6a318;p=ceph.git cephadm: add write_new function for robust file writes 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 --- diff --git a/src/cephadm/cephadm.py b/src/cephadm/cephadm.py index 3ca262de9ec9..dea3e238a210 100755 --- a/src/cephadm/cephadm.py +++ b/src/cephadm/cephadm.py @@ -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"""