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: v18.2.1~326^2~88 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=f4138e33e6303efec34f8aeac7a2d999585b3a0d;p=ceph-ci.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 (cherry picked from commit 7a8bfb91af246e36242aee90c3f9245fcdf6a318) --- diff --git a/src/cephadm/cephadm.py b/src/cephadm/cephadm.py index c6f6b694afc..7a469e56ce8 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"""